Files
ridebus-board/server/weather.mjs
2026-10-08 22:56:41 +03:00

40 lines
3.0 KiB
JavaScript

import { ApiError } from './board.mjs';
import { isWeather } from '../src/lib/weather.mjs';
export function createWeatherClient({ fetchData = fetch, now = Date.now, apiKey = process.env.OPEN_METEO_API_KEY } = {}) {
const cache = new Map(), pending = new Map(), failures = new Map();
return {
async weather(cityId, stopId, coordinates) {
const latitude = Number(coordinates.latitude), longitude = Number(coordinates.longitude);
if (coordinates.latitude == null || coordinates.longitude == null || !Number.isFinite(latitude) || Math.abs(latitude) > 90 || !Number.isFinite(longitude) || Math.abs(longitude) > 180) throw new ApiError(503, 'coordinates_unavailable');
const key = `${cityId}:${stopId}:${latitude.toFixed(4)}:${longitude.toFixed(4)}`;
if (cache.get(key)?.expires > now()) return cache.get(key).data;
if (pending.has(key)) return pending.get(key);
if (failures.get(key) > now()) throw new ApiError(502, 'weather_unavailable');
const request = (async () => {
try {
const url = new URL(apiKey ? 'https://customer-api.open-meteo.com/v1/forecast' : 'https://api.open-meteo.com/v1/forecast');
url.search = new URLSearchParams({ latitude: String(latitude), longitude: String(longitude), timezone: 'Europe/Minsk', timeformat: 'unixtime',
current: 'temperature_2m,apparent_temperature,weather_code,is_day,wind_speed_10m',
hourly: 'temperature_2m,weather_code,precipitation_probability', forecast_days: '2', wind_speed_unit: 'ms', ...(apiKey ? { apikey: apiKey } : {}) }).toString();
// No transit credentials are sent to a weather provider.
const response = await fetchData(url, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(10000), redirect: 'error' });
if (!response.ok) throw new ApiError();
const raw = await response.json(), current = raw.current, hourly = raw.hourly;
if (!current || !Array.isArray(hourly?.time)) throw new ApiError();
const instant = seconds => typeof seconds === 'number' && Number.isFinite(seconds) ? new Date(seconds * 1000).toISOString() : '';
const data = { cityId, stopId, updatedAt: new Date(now()).toISOString(), observedAt: instant(current.time), temperature: current.temperature_2m,
apparentTemperature: current.apparent_temperature, code: current.weather_code, isDay: current.is_day === 1, windSpeed: current.wind_speed_10m,
hourly: hourly.time.map((time, index) => ({ at: instant(time), temperature: hourly.temperature_2m?.[index], code: hourly.weather_code?.[index], precipitationProbability: hourly.precipitation_probability?.[index] })) };
if (!isWeather(data, cityId, stopId)) throw new ApiError();
cache.set(key, { data, expires: now() + 600000 }); failures.delete(key);
return data;
} catch { failures.set(key, now() + 60000); throw new ApiError(502, 'weather_unavailable'); }
finally { pending.delete(key); }
})();
pending.set(key, request);
return request;
},
};
}