@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
71 lines (70 loc) • 2.93 kB
JavaScript
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;
}
}