Add caching for travel forecasts so we don't hit APIs as hard
This commit is contained in:
parent
bb87c836b6
commit
c67809f62d
2 changed files with 38 additions and 25 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue