main
import { peekCache, readCache, writeCache } from '@app/services/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<string, Promise<unknown>>()
/**
* 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<T>(cacheKey: string, fn: () => Promise<T>): Promise<T> {
const existing = inflight.get(cacheKey)
if (existing) return existing as Promise<T>
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<T> {
/** 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<T>(
cacheKey: string,
fn: () => Promise<T>,
opts?: PrefetchOptions<T>,
): Promise<T | undefined> {
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<T>).catch(() => undefined)
const hit = peekCache<T>(cacheKey) ?? (await readCache<T>(cacheKey))
if (hit && Date.now() - hit.at < freshMs) return hit.data
try {
const fresh = await runShared(cacheKey, fn)
const cached = peekCache<T>(cacheKey)?.data
const merged = cached !== undefined && opts?.merge ? opts.merge(cached, fresh) : fresh
writeCache(cacheKey, merged)
return merged
} catch {
return undefined
}
}