// In-memory TTL cache with single-flight, shared across all browser sessions, // to collapse repeated MCMS bulk fetches into one upstream request. // // Keyed by "|": operators pointed at the SAME MCMS // share a snapshot (the inventory is identical regardless of which // authenticated user fetched it), while different MCMS hosts never cross. // // Only slow-changing bulk *reads* are cached (ONU/OLT config + state lists, // firmware inventory). Per-ONU reads used for read-modify-write (getOnuConfig // before a PUT) and the FEC pre-flight are deliberately NOT cached — they must // be live. Writes invalidate the affected datasets so the next read refetches. class TtlCache { constructor(ttlMs) { this.ttl = ttlMs; this.entries = new Map(); // key -> { value, at } this.inflight = new Map(); // key -> Promise (single-flight) this.hits = 0; this.misses = 0; this.coalesced = 0; } enabled() { return this.ttl > 0; } /** * Return the cached value for `key` if fresh; otherwise call `producer()` * once (coalescing concurrent callers) and cache the result. * @param {object} [opts] * @param {boolean} [opts.fresh] bypass a still-fresh cached entry */ async get(key, producer, { fresh = false } = {}) { if (!this.enabled()) return producer(); const now = Date.now(); const e = this.entries.get(key); if (!fresh && e && (now - e.at) < this.ttl) { this.hits++; return e.value; } // Single-flight: if a fetch for this key is already running, join it // (even a `fresh` request — it's fetching right now anyway). const existing = this.inflight.get(key); if (existing) { this.coalesced++; return existing; } this.misses++; const p = Promise.resolve() .then(producer) .then( (value) => { this.entries.set(key, { value, at: Date.now() }); this.inflight.delete(key); return value; }, (err) => { this.inflight.delete(key); throw err; }, ); this.inflight.set(key, p); return p; } /** Drop every entry whose key starts with `prefix` (e.g. one MCMS host's * dataset). Returns how many were removed. */ invalidate(prefix) { let n = 0; for (const k of [...this.entries.keys()]) { if (!prefix || k.startsWith(prefix)) { this.entries.delete(k); n++; } } return n; } /** Free memory: drop entries far older than the TTL (idle datasets). */ sweep(maxAgeMs) { const cutoff = Date.now() - maxAgeMs; for (const [k, e] of [...this.entries]) if (e.at < cutoff) this.entries.delete(k); } stats() { return { ttlMs: this.ttl, enabled: this.enabled(), size: this.entries.size, hits: this.hits, misses: this.misses, coalesced: this.coalesced, keys: [...this.entries.keys()], }; } } module.exports = { TtlCache };