Files

81 lines
2.6 KiB
TypeScript

/**
* Lightweight in-memory SWR cache for API data.
*
* Lives at module scope — persists across React renders and page navigations
* within the same browser tab, but clears on hard-refresh / tab close.
*
* Usage:
* const data = await swrFetch('posts-500', 3 * 60_000, () => getPosts(500))
*
* On first call → fetches, caches, returns.
* On subsequent calls within TTL → returns cached data immediately.
* On calls after TTL → returns stale data immediately AND triggers a
* background revalidation; the `onUpdate` callback fires when fresh
* data arrives so the component can re-render without a loading flash.
*/
interface CacheEntry<T> {
data: T
ts: number
revalidating: boolean
/** onUpdate callbacks of every caller that hit this key while a background
* revalidation was in flight (M9 fix). All are notified when the fetch
* resolves — previously only the caller that STARTED the revalidation got
* fresh data; everyone else silently kept the stale copy. */
subscribers: Array<(fresh: T) => void>
}
const store = new Map<string, CacheEntry<unknown>>()
export async function swrFetch<T>(
key: string,
ttlMs: number,
fetcher: () => Promise<T>,
onUpdate?: (fresh: T) => void,
): Promise<T> {
const now = Date.now()
const hit = store.get(key) as CacheEntry<T> | undefined
if (hit) {
const age = now - hit.ts
if (age < ttlMs) {
// Still fresh — return immediately.
return hit.data
}
// Stale — return immediately AND revalidate in background. Every caller's
// onUpdate joins the subscriber list; the in-flight fetch notifies all.
if (onUpdate) hit.subscribers.push(onUpdate)
if (!hit.revalidating) {
hit.revalidating = true
fetcher()
.then(fresh => {
const subs = hit.subscribers
store.set(key, { data: fresh, ts: Date.now(), revalidating: false, subscribers: [] })
for (const notify of subs) {
try { notify(fresh) } catch { /* one bad subscriber must not starve the rest */ }
}
})
.catch(() => {
hit.revalidating = false
hit.subscribers = []
})
}
return hit.data
}
// Cache miss — must wait for the first fetch.
const data = await fetcher()
store.set(key, { data, ts: Date.now(), revalidating: false, subscribers: [] })
return data
}
/** Manually invalidate a cache key (e.g. after a write action). */
export function invalidate(key: string) {
store.delete(key)
}
/** Peek at whether a key has any cached data (stale or fresh). */
export function hasCached(key: string): boolean {
return store.has(key)
}