// Generic in-memory stale-while-revalidate cache: TTL + inflight de-duplication. // Extracted + decoupled from WC26's ESPN data service (the real caching layer when the // upstream sends `Cache-Control: max-age=1` and no ETag, so there's no 304 path). // Dependency-free; wraps any async fetcher. // fresh -> serve cached // stale -> serve stale immediately + refresh in the background // cold -> await the first fetch // error -> serve the last good value if we have one export interface SwrEntry { data: T; at: number; inflight?: Promise; } export interface SwrCache { /** SWR read: fresh -> cached; stale -> stale + bg refresh; cold -> await. */ get(key: string): Promise; /** Always fetch (warm the cache + dedupe concurrent calls) — e.g. a live poller. */ fresh(key: string): Promise; /** Drop a key (or the whole cache). */ invalidate(key?: string): void; } export function createSwrCache(fetcher: (key: string) => Promise, ttlMs: number): SwrCache { const cache = new Map>(); const load = (key: string) => fetcher(key).then((data) => { cache.set(key, { data, at: Date.now() }); return data; }); return { async get(key) { const now = Date.now(); const e = cache.get(key); if (e && now - e.at < ttlMs) return e.data; // fresh if (e && e.inflight) return e.data; // already revalidating -> serve stale const revalidate = load(key) .catch((err) => { if (e) return e.data; // serve stale on error throw err; }) .finally(() => { const cur = cache.get(key); if (cur) cur.inflight = undefined; }); if (e) { e.inflight = revalidate; return e.data; // stale-while-revalidate } return revalidate; // cold }, async fresh(key) { const e = cache.get(key); if (e?.inflight) return e.inflight; // dedupe concurrent forced fetches const p = load(key).finally(() => { const cur = cache.get(key); if (cur) cur.inflight = undefined; }); cache.set(key, { ...(e ?? { data: undefined as unknown as T, at: 0 }), inflight: p }); return p; }, invalidate(key) { if (key) cache.delete(key); else cache.clear(); }, }; }