weatherService.ts
Purpose
This module is the central weather data layer. It provides a single cached-read entry point for getting a weather point for any coordinate and time, and a bulk pre-cache entry point for warming the cache across a set of work-area locations before they are needed.
Its responsibilities include:
- Lazy-initializing the weather cache database once across the process lifetime
- Resolving a weather point through a four-step fallback chain (exact cache → next-hour cache → on-demand fetch → nearby fallback)
- Persisting fetched NDFD forecast series to the SQLite cache with LRU eviction
- Pre-caching a 24-hour forecast horizon for a set of geographic locations with bounded concurrency
- Analyzing precache coverage to accept either the requested or next-hour forecast start
- Validating all coordinate inputs before any operation
- Logging all cache hits, misses, fetches, and errors with structured context
Invariants
Single Initialization
ensureWeatherServiceInitialized runs initWeatherCacheDb at most once per process lifetime using a module-level promise.
If initialization fails, the promise is cleared so the next call retries. Callers await this before any cache read or write.
Four-Step Fallback Chain
getWeatherPointCached attempts point resolution in this exact order:
- Exact cache — non-stale row for the requested cell and hour
- Next-hour cache — non-stale row for the next hour (only when the requested hour is the current forecast hour)
- On-demand NDFD fetch — live fetch + immediate cache write + LRU eviction
- Nearby fallback — any non-stale row for the same hour within 5 km
If all four fail, WeatherCacheMissError is thrown with a message describing the first significant failure.
Staleness Definition
A cache row is stale when Date.now() >= validTo.
Stale exact rows are deleted from the cache before the on-demand fetch is attempted.
Cache Size Enforcement
LRU eviction is run after every successful persist to keep the cache below WEATHER_CACHE_MAX_ROWS = 12,000 rows.
During a bulk precache run, eviction is deferred until all locations have been fetched, then run once at the end.
Precache Concurrency Limit
precacheWeatherForLocations fans out fetches using an internal worker pool capped at WEATHER_PRECACHE_MAX_CONCURRENT_FETCHES = 8.
This prevents flooding the NDFD endpoint or the native fetch stack when pre-caching large work areas.
Location Deduplication
precacheWeatherForLocations deduplicates input locations by their cache cell key before fetching.
Invalid coordinates are skipped with a WARN log and counted toward failed in the summary.
Precache Coverage Acceptance
A fetched series is accepted if it covers a full DEFAULT_FORECAST_HOURS = 24 horizon starting from either:
- the requested forecast hour, or
- the next forecast hour (one hour later)
Series that do not cover either horizon are persisted as-is (best effort) but counted as failed.
Constants
| Constant | Value | Description |
|---|---|---|
WEATHER_CACHE_MAX_ROWS | 12,000 | Maximum rows before LRU eviction |
WEATHER_PRECACHE_MAX_CONCURRENT_FETCHES | 8 | Max parallel NDFD fetches during precache |
NEARBY_RADIUS_KM | 5 | Max distance for nearby fallback points |
DEFAULT_FORECAST_HOURS | 24 | Forecast horizon for both on-demand and precache |
DEFAULT_FETCH_TIMEOUT_MS | 12,000 | NDFD fetch network timeout |
Exports
WeatherCacheMissError
class WeatherCacheMissError extends Error {
name: "WeatherCacheMissError";
cause?: unknown;
}
Thrown when no usable weather point can be found after all fallback paths are exhausted. The message describes which step failed.
WeatherLocationInput
type WeatherLocationInput = {
lat: number;
lon: number;
};
WeatherPrecacheSummary
type WeatherPrecacheSummary = {
attempted: number;
succeeded: number;
failed: number;
};
initWeatherService()
async function initWeatherService(): Promise<void>
Ensures the weather cache database is initialized. Safe to call multiple times.
getWeatherPointCached(...)
async function getWeatherPointCached(
lat: number,
lon: number,
targetTime?: Date | string,
): Promise<WeatherPoint>
Returns a weather point for the requested coordinate and hour using the four-step fallback chain. Throws WeatherCacheMissError when all paths fail.
precacheWeatherForLocations(...)
async function precacheWeatherForLocations(
locations: WeatherLocationInput[],
targetTime?: Date | string,
): Promise<WeatherPrecacheSummary>
Fetches and stores a 24-hour forecast for each unique cache cell in locations, using bounded concurrency. Returns a summary of attempted, succeeded, and failed fetches.