import { peekCache, readCache, writeCache } from '@/shared/storage' /** * Shared in-flight registry + prefetch primitive for the local-first cache. * * One promise per cache key, shared by every consumer: a hover prefetch that * is still running when the user clicks becomes the very request the mounting * `useAsync` waits on (no duplicate roundtrip), and two components mounting * onto the same key (e.g. member lists in three panels) collapse into one * fetch even when the underlying alova methods differ. */ const inflight = new Map>() /** * Run `fn` at most once per key while a request is in flight; later callers * receive the same promise (and the same error, if it rejects). */ export function runShared(cacheKey: string, fn: () => Promise): Promise { const existing = inflight.get(cacheKey) if (existing) return existing as Promise const promise = fn().finally(() => { if (inflight.get(cacheKey) === promise) inflight.delete(cacheKey) }) inflight.set(cacheKey, promise) return promise } /** A cache entry younger than this does not justify another prefetch request. */ export const PREFETCH_FRESH_MS = 30_000 export interface PrefetchOptions { /** Throttle window: skip while the cached entry is younger (default 30s). */ freshMs?: number /** Same merge semantics as `useAsync` — kept so hover prefill and mount agree. */ merge?: (cached: T, fresh: T) => T } /** * Warm the cache for a key the user is about to open (nav hover, idle * top-N, pagination lookahead). Failures are silent by design: a prefetch * is an optimization, and the real `useAsync` on mount will surface any * genuine error through its own state. */ export async function prefetch( cacheKey: string, fn: () => Promise, opts?: PrefetchOptions, ): Promise { const freshMs = opts?.freshMs ?? PREFETCH_FRESH_MS const running = inflight.get(cacheKey) // Someone (another hover, a mounting hook) is already fetching this key — // ride along, but keep the prefetch contract of never rejecting. if (running) return (running as Promise).catch(() => undefined) const hit = peekCache(cacheKey) ?? (await readCache(cacheKey)) if (hit && Date.now() - hit.at < freshMs) return hit.data try { const fresh = await runShared(cacheKey, fn) const cached = peekCache(cacheKey)?.data const merged = cached !== undefined && opts?.merge ? opts.merge(cached, fresh) : fresh writeCache(cacheKey, merged) return merged } catch { return undefined } }