/** * Short-lived cache for cutover list APIs. * Switching biz-state / compare / migration / templates remounts each page; * without cache every click re-waits on Promise.all network round-trips. */ type Entry = { at: number; data: unknown }; const store = new Map(); const inFlight = new Map>(); const DEFAULT_TTL_MS = 45_000; export async function cutoverCachedGet( key: string, fetcher: () => Promise, opts?: { force?: boolean; ttlMs?: number }, ): Promise { const ttl = opts?.ttlMs ?? DEFAULT_TTL_MS; if (!opts?.force) { const hit = store.get(key); if (hit && Date.now() - hit.at < ttl) { return hit.data as T; } } const existing = inFlight.get(key); if (!opts?.force && existing) return existing as Promise; const request = Promise.resolve().then(fetcher).then((data) => { // An invalidation or forced refresh may supersede this request. if (inFlight.get(key) === request) store.set(key, { at: Date.now(), data }); return data; }).finally(() => { if (inFlight.get(key) === request) inFlight.delete(key); }); inFlight.set(key, request); return request; } /** Return stale immediately (if any), refresh in background and notify. */ export async function cutoverCachedGetSWR( key: string, fetcher: () => Promise, onFresh?: (data: T) => void, ): Promise { const hit = store.get(key); if (hit) { void cutoverCachedGet(key, fetcher, { ttlMs: 0 }) .then((data) => { if (store.get(key)?.data === data) onFresh?.(data); }) .catch(() => { /* keep stale */ }); return hit.data as T; } return cutoverCachedGet(key, fetcher); } export function invalidateCutoverCache(prefix = ""): void { if (!prefix) { store.clear(); inFlight.clear(); return; } for (const k of [...store.keys()]) { if (k.startsWith(prefix)) store.delete(k); } for (const k of [...inFlight.keys()]) { if (k.startsWith(prefix)) inFlight.delete(k); } }