UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

71 lines (70 loc) • 2.93 kB
import { BridgeError } from "./protocol.js"; /** Bounded Hook lookups. Invalidated/expired refreshes may finish, but cannot * repopulate the cache. Failed refreshes retain successes only until maxAge. */ export class LookupCache { ttl; maxAge; limit; available; pendingTimeout; revokes; entries = new Map(); constructor(ttl, maxAge = 300_000, limit = 64, available = () => true, pendingTimeout = 45_000, /** A result that revokes what was cached (lost access), so the old success is never served again. */ revokes = () => false) { this.ttl = ttl; this.maxAge = maxAge; this.limit = limit; this.available = available; this.pendingTimeout = pendingTimeout; this.revokes = revokes; } invalidate(matches) { for (const key of this.entries.keys()) if (matches(key)) this.entries.delete(key); } get(key, read) { let entry = this.entries.get(key); if (!entry) { entry = {}; this.entries.set(key, entry); if (this.entries.size > this.limit) this.entries.delete(this.entries.keys().next().value); } const cached = entry.value; const age = cached ? Date.now() - cached.at : Infinity; const available = cached && this.available(cached.data); const ttl = available ? this.ttl : 5_000; const maxAge = available ? this.maxAge : 5_000; if (cached && age < ttl) return Promise.resolve(cached.data); if (!entry.pending) { const current = entry; let timer; const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new BridgeError(504, "Lookup did not finish within the request time limit.", { code: "lookup-timeout" })), this.pendingTimeout); }); const pending = Promise.race([Promise.resolve().then(read), timeout]).then(data => { if (this.entries.get(key) === current && current.pending === pending) { const previous = current.value; const keepSuccess = previous && this.available(previous.data) && !this.available(data) && !this.revokes(data) && Date.now() - previous.at < this.maxAge; if (!keepSuccess) current.value = { data, at: Date.now() }; } return data; }).finally(() => { clearTimeout(timer); if (current.pending === pending) current.pending = undefined; }); entry.pending = pending; } if (cached && age < maxAge && !this.revokes(cached.data)) { void entry.pending.catch(() => { }); return Promise.resolve(cached.data); } return entry.pending; } }