Add caching for travel forecasts so we don't hit APIs as hard

This commit is contained in:
mrkmntal 2026-04-07 19:04:10 -04:00
commit c67809f62d
2 changed files with 38 additions and 25 deletions

View file

@ -8,16 +8,15 @@ import { registerDisplay } from './navigation.mjs';
import calculateScrollTiming from './utils/scroll-timing.mjs';
import { debugFlag } from './utils/debug.mjs';
import { temperature } from './utils/units.mjs';
import { getAggregatedOpenMeteoForecast } from './utils/weather.mjs';
import { getCachedAggregatedOpenMeteoForecast } from './utils/weather.mjs';
const MIN_TRAVEL_CITIES = 5;
class TravelForecast extends WeatherDisplay {
constructor(navId, elemId, defaultActive) {
// special height and width for scrolling
super(navId, elemId, 'Travel Forecast', defaultActive);
// add previous data cache
this.previousData = [];
// cache for scroll calculations
// This cache is essential because baseCountChange() is called 25 times per second (every 40ms)
// during scrolling. Travel forecast scroll duration varies based on the number of cities configured.
@ -45,30 +44,13 @@ class TravelForecast extends WeatherDisplay {
// super checks for enabled
if (!super.getData(weatherParameters, refresh)) return;
// clear stored data if not refresh
if (!refresh) {
this.previousData = [];
}
const temperatureConverter = temperature();
const selectedTravelCities = getTravelCitiesForLocation(this.weatherParameters);
const forecastPromises = selectedTravelCities.map(async (city, index) => {
const forecastPromises = selectedTravelCities.map(async (city) => {
try {
let forecast;
forecast = await getAggregatedOpenMeteoForecast(city.Latitude, city.Longitude);
if (forecast) {
// store for the next run
this.previousData[index] = forecast;
} else if (this.previousData?.[index]) {
// if there's previous data use it
if (debugFlag('travelforecast')) {
console.warn(`Using previous forecast data for ${city.Name} travel forecast`);
}
forecast = this.previousData?.[index];
} else {
// no current data and no previous data available
const forecast = await getCachedAggregatedOpenMeteoForecast(city.Latitude, city.Longitude);
if (!forecast) {
if (debugFlag('verbose-failures')) {
console.warn(`No travel forecast for ${city.Name} available`);
}
@ -100,7 +82,11 @@ class TravelForecast extends WeatherDisplay {
// wait for all forecasts using centralized safe Promise handling
const forecasts = await safePromiseAll(forecastPromises);
this.data = forecasts;
const validForecasts = forecasts.filter((forecast) => forecast && !forecast.error && forecast.high !== undefined);
const invalidForecasts = forecasts.filter((forecast) => forecast && forecast.error);
this.data = validForecasts.length >= MIN_TRAVEL_CITIES
? validForecasts
: [...validForecasts, ...invalidForecasts].slice(0, Math.max(validForecasts.length, MIN_TRAVEL_CITIES));
// test for some data available in at least one forecast
const hasData = this.data.some((forecast) => forecast.high);

View file

@ -16,7 +16,9 @@ const OPEN_METEO_RADAR_OBSERVATION_PARAMETERS = [
].join('&');
const OPEN_METEO_OBSERVATION_CACHE_TTL_MS = 10 * 60 * 1000;
const OPEN_METEO_TRAVEL_FORECAST_CACHE_TTL_MS = 30 * 60 * 1000;
const openMeteoObservationCache = new Map();
const openMeteoTravelForecastCache = new Map();
const getPoint = async (lat, lon) => {
const point = await safeJson(`https://api.weather.gov/points/${lat.toFixed(4)},${lon.toFixed(4)}`);
@ -58,6 +60,30 @@ const getAggregatedOpenMeteoForecast = async (lat, lon) => {
};
};
const getCachedAggregatedOpenMeteoForecast = async (lat, lon) => {
const cacheKey = `${lat.toFixed(4)},${lon.toFixed(4)}`;
const cachedEntry = openMeteoTravelForecastCache.get(cacheKey);
const now = Date.now();
if (cachedEntry && (now - cachedEntry.fetchedAt) < OPEN_METEO_TRAVEL_FORECAST_CACHE_TTL_MS) {
return cachedEntry.data;
}
const forecast = await getAggregatedOpenMeteoForecast(lat, lon);
if (forecast) {
openMeteoTravelForecastCache.set(cacheKey, {
data: forecast,
fetchedAt: now,
});
return forecast;
}
if (cachedEntry) {
return cachedEntry.data;
}
return false;
};
const getOpenMeteoObservationSnapshot = async (lat, lon) => {
const cacheKey = `${lat.toFixed(4)},${lon.toFixed(4)}`;
const cachedEntry = openMeteoObservationCache.get(cacheKey);
@ -199,6 +225,7 @@ export {
getPoint,
getOpenMeteoForecast,
getAggregatedOpenMeteoForecast,
getCachedAggregatedOpenMeteoForecast,
getOpenMeteoObservationSnapshot,
aggregateWeatherForecastData,
getConditionText,