UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

87 lines 3.85 kB
/** * A read-through cache for the dashboard's slow reads (#1028). * * The dashboard polls. Every session view asks for its branch's PR, twice (the worktree bar and * the handoff summary), on every navigation and again every ten seconds — and `gh pr view` costs * about 600ms against a local git read's ten. So the same answer was being bought over and over, * and the panel waited for it each time. * * Three behaviours, and each one is load-bearing: * - **single flight** — concurrent asks for the same key share one call, so two panels and a * poll tick do not become three subprocesses * - **stale while revalidate** — once a value exists it is returned immediately, and refreshed * in the background when it is older than `ttlMs`; nobody waits twice for the same answer * - **a budget on the cold ask** — the first ask waits only `budgetMs` for the answer before * reporting `pending`, so a slow lookup delays one panel's extra detail rather than the page * * `pending` is not "failed": it means the answer is on its way and the next read will have it. * A caller that must not act on a half-answer (offering to open a PR that may already exist) * uses it to hold off. */ const entries = new Map(); /** * Read `key` through the cache, calling `load` when it is missing or stale. * * A failed load is not cached: it leaves whatever was there (a panel keeps showing the last PR it * knew about rather than dropping it because gh hiccuped) and the next read tries again. */ export async function cachedRead(key, load, options = {}) { const { ttlMs = 60_000, budgetMs = 150, now = Date.now } = options; const entry = entries.get(key); // A known value answers straight away. Refresh it in the background when it has aged out, so // the cost of being current is never paid by the caller that happens to ask. if (entry?.has) { if (now() - entry.at >= ttlMs && !entry.inflight) void refresh(key, load, now); return { value: entry.value, pending: false }; } // Nothing known yet: join the running read, or start one, and wait a moment for it. const inflight = entry?.inflight ?? refresh(key, load, now); const settled = await withBudget(inflight, budgetMs); if (settled.done) return { value: settled.value, pending: false }; return { value: undefined, pending: true }; } /** Drop what is cached under `key`, so the next read is a fresh one. */ export function invalidate(key) { entries.delete(key); } /** Test seam: forget everything. */ export function clearCache() { entries.clear(); } function refresh(key, load, now) { const inflight = load() .then(value => { entries.set(key, { value, has: true, at: now() }); return value; }) .catch(error => { // Keep the last good value; only the in-flight marker goes. const current = entries.get(key); if (current?.has) entries.set(key, { value: current.value, has: true, at: current.at }); else entries.delete(key); throw error; }); const current = entries.get(key); entries.set(key, { ...(current ?? { has: false, at: 0 }), inflight }); // The rejection is reported to whoever awaited it; nothing else must see an unhandled one. inflight.catch(() => { }); return inflight; } /** Resolve as soon as `promise` does, or give up waiting after `ms`. */ async function withBudget(promise, ms) { let timer; const timeout = new Promise(resolve => { timer = setTimeout(() => resolve({ done: false }), ms); }); try { return await Promise.race([promise.then(value => ({ done: true, value })).catch(() => ({ done: false })), timeout]); } finally { clearTimeout(timer); } } //# sourceMappingURL=cache.js.map