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 calculateScrollTiming from './utils/scroll-timing.mjs';
|
||||||
import { debugFlag } from './utils/debug.mjs';
|
import { debugFlag } from './utils/debug.mjs';
|
||||||
import { temperature } from './utils/units.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 {
|
class TravelForecast extends WeatherDisplay {
|
||||||
constructor(navId, elemId, defaultActive) {
|
constructor(navId, elemId, defaultActive) {
|
||||||
// special height and width for scrolling
|
// special height and width for scrolling
|
||||||
super(navId, elemId, 'Travel Forecast', defaultActive);
|
super(navId, elemId, 'Travel Forecast', defaultActive);
|
||||||
|
|
||||||
// add previous data cache
|
|
||||||
this.previousData = [];
|
|
||||||
|
|
||||||
// cache for scroll calculations
|
// cache for scroll calculations
|
||||||
// This cache is essential because baseCountChange() is called 25 times per second (every 40ms)
|
// 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.
|
// 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
|
// super checks for enabled
|
||||||
if (!super.getData(weatherParameters, refresh)) return;
|
if (!super.getData(weatherParameters, refresh)) return;
|
||||||
|
|
||||||
// clear stored data if not refresh
|
|
||||||
if (!refresh) {
|
|
||||||
this.previousData = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const temperatureConverter = temperature();
|
const temperatureConverter = temperature();
|
||||||
const selectedTravelCities = getTravelCitiesForLocation(this.weatherParameters);
|
const selectedTravelCities = getTravelCitiesForLocation(this.weatherParameters);
|
||||||
|
|
||||||
const forecastPromises = selectedTravelCities.map(async (city, index) => {
|
const forecastPromises = selectedTravelCities.map(async (city) => {
|
||||||
try {
|
try {
|
||||||
let forecast;
|
const forecast = await getCachedAggregatedOpenMeteoForecast(city.Latitude, city.Longitude);
|
||||||
forecast = await getAggregatedOpenMeteoForecast(city.Latitude, city.Longitude);
|
if (!forecast) {
|
||||||
|
|
||||||
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
|
|
||||||
if (debugFlag('verbose-failures')) {
|
if (debugFlag('verbose-failures')) {
|
||||||
console.warn(`No travel forecast for ${city.Name} available`);
|
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
|
// wait for all forecasts using centralized safe Promise handling
|
||||||
const forecasts = await safePromiseAll(forecastPromises);
|
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
|
// test for some data available in at least one forecast
|
||||||
const hasData = this.data.some((forecast) => forecast.high);
|
const hasData = this.data.some((forecast) => forecast.high);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ const OPEN_METEO_RADAR_OBSERVATION_PARAMETERS = [
|
||||||
].join('&');
|
].join('&');
|
||||||
|
|
||||||
const OPEN_METEO_OBSERVATION_CACHE_TTL_MS = 10 * 60 * 1000;
|
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 openMeteoObservationCache = new Map();
|
||||||
|
const openMeteoTravelForecastCache = new Map();
|
||||||
|
|
||||||
const getPoint = async (lat, lon) => {
|
const getPoint = async (lat, lon) => {
|
||||||
const point = await safeJson(`https://api.weather.gov/points/${lat.toFixed(4)},${lon.toFixed(4)}`);
|
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 getOpenMeteoObservationSnapshot = async (lat, lon) => {
|
||||||
const cacheKey = `${lat.toFixed(4)},${lon.toFixed(4)}`;
|
const cacheKey = `${lat.toFixed(4)},${lon.toFixed(4)}`;
|
||||||
const cachedEntry = openMeteoObservationCache.get(cacheKey);
|
const cachedEntry = openMeteoObservationCache.get(cacheKey);
|
||||||
|
|
@ -199,6 +225,7 @@ export {
|
||||||
getPoint,
|
getPoint,
|
||||||
getOpenMeteoForecast,
|
getOpenMeteoForecast,
|
||||||
getAggregatedOpenMeteoForecast,
|
getAggregatedOpenMeteoForecast,
|
||||||
|
getCachedAggregatedOpenMeteoForecast,
|
||||||
getOpenMeteoObservationSnapshot,
|
getOpenMeteoObservationSnapshot,
|
||||||
aggregateWeatherForecastData,
|
aggregateWeatherForecastData,
|
||||||
getConditionText,
|
getConditionText,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue