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; }, }; }