SWR Cache
Generic in-memory stale-while-revalidate cache (TTL + inflight de-duplication) that wraps any async fetcher: serve fresh, serve stale + refresh in the background, await cold, serve last-good on error. Decoupled from WC26's ESPN service.
// 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<T> {
data: T;
at: number;
inflight?: Promise<T>;
}
export interface SwrCache<T> {
/** SWR read: fresh -> cached; stale -> stale + bg refresh; cold -> await. */
get(key: string): Promise<T>;
/** Always fetch (warm the cache + dedupe concurrent calls) — e.g. a live poller. */
fresh(key: string): Promise<T>;
/** Drop a key (or the whole cache). */
invalidate(key?: string): void;
}
export function createSwrCache<T>(fetcher: (key: string) => Promise<T>, ttlMs: number): SwrCache<T> {
const cache = new Map<string, SwrEntry<T>>();
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();
},
};
}