/** * SWR 缓存:始终返回热数据,后台定时刷新。 * * 工作机制: * - 首次请求:阻塞等待 loader(cold start,3-4s 不可避免) * - 之后:每个 key 自调度刷新(TTL 到期前 5s),用户永远命中热缓存 * - 闲置 IDLE_TIMEOUT_MS 后取消调度(避免浪费 DB 资源) * - 同一 key 并发请求只触发一次 loader * - force=true:手动强制刷新,绕过缓存(但仍参与 inflight 复用) */ interface Entry { value: T; freshAt: number; expiresAt: number; loader: () => Promise; lastAccess: number; timer?: NodeJS.Timeout; } const TTL_MS = 60 * 1000; const REFRESH_LEAD_MS = 5 * 1000; // TTL 到期前多久触发刷新 const IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 分钟无访问则停止调度 const RETRY_BACKOFF_MS = 10 * 1000; // loader 失败时重试间隔 const cache = new Map>(); const inflight = new Map>(); function scheduleRefresh(key: string, entry: Entry) { if (entry.timer) clearTimeout(entry.timer); const delay = Math.max(0, entry.freshAt + TTL_MS - Date.now() - REFRESH_LEAD_MS); entry.timer = setTimeout(() => { void runRefresh(key); }, delay); entry.timer.unref?.(); } async function runRefresh(key: string) { const entry = cache.get(key) as Entry | undefined; if (!entry) return; // 闲置超时:停止调度 if (Date.now() - entry.lastAccess > IDLE_TIMEOUT_MS) { if (entry.timer) clearTimeout(entry.timer); return; } if (inflight.has(key)) return; const p = entry.loader() .then(value => { const now = Date.now(); const next: Entry = { value, freshAt: now, expiresAt: now + TTL_MS, loader: entry.loader, lastAccess: entry.lastAccess, }; cache.set(key, next); scheduleRefresh(key, next); return value; }) .catch(e => { console.error(`[energy/cache] refresh failed for "${key}":`, e instanceof Error ? e.message : e); // 保留旧值,10s 后重试 const retry: Entry = { ...entry }; retry.timer = setTimeout(() => { void runRefresh(key); }, RETRY_BACKOFF_MS); retry.timer.unref?.(); cache.set(key, retry); }) .finally(() => inflight.delete(key)); inflight.set(key, p); } export interface CachedOpts { force?: boolean; } export async function cached(key: string, loader: () => Promise, opts: CachedOpts = {}): Promise { const now = Date.now(); const hit = cache.get(key) as Entry | undefined; if (hit) { hit.lastAccess = now; hit.loader = loader; } // 强制刷新:等待 loader 完成 if (opts.force) { const ongoing = inflight.get(key) as Promise | undefined; if (ongoing) return ongoing; const p = loader() .then(value => { const t = Date.now(); const next: Entry = { value, freshAt: t, expiresAt: t + TTL_MS, loader, lastAccess: t }; cache.set(key, next); scheduleRefresh(key, next); return value; }) .finally(() => inflight.delete(key)); inflight.set(key, p as Promise); return p; } // 命中且未过期 → 立即返回 if (hit && hit.expiresAt > now) { return hit.value; } // 命中但过期 → 返回 stale,后台刷新 if (hit) { if (!inflight.has(key)) void runRefresh(key); return hit.value; } // 完全未命中 → 阻塞等待 const ongoing = inflight.get(key) as Promise | undefined; if (ongoing) return ongoing; const p = loader() .then(value => { const t = Date.now(); const entry: Entry = { value, freshAt: t, expiresAt: t + TTL_MS, loader, lastAccess: t }; cache.set(key, entry); scheduleRefresh(key, entry); return value; }) .finally(() => inflight.delete(key)); inflight.set(key, p as Promise); return p; } /** 仅用于测试或调试:清空所有缓存与定时器 */ export function _clearEnergyCache() { for (const e of cache.values()) { if (e.timer) clearTimeout(e.timer); } cache.clear(); inflight.clear(); }