UNPKG

@unruggable/gateways

Version:

Trustless Ethereum Multichain CCIP-Read Gateway

274 lines (273 loc) 7.53 kB
// CachedMap maintains 2 maps: // 1) pending promises by key // 2) settled promises by key + expiration // requests for the same key return the same promise // which may be from (1) or (2) // too many pending {maxPending} are errors // too many cached {maxCached} purge the oldest // resolved promises are cached for {cacheMs} // rejected promises are cached for {errorMs} // CachedValue does the same for a single value // using an init-time generator const ERR = Symbol(); function clock() { return performance.now(); } export class CachedValue { fn; cacheMs; #exp = 0; #value; errorMs = 250; constructor(fn, cacheMs = 60000) { this.fn = fn; this.cacheMs = cacheMs; } clear() { this.#value = undefined; } set(value) { this.#value = Promise.resolve(value); this.#exp = clock() + this.cacheMs; } get value() { return this.#value; } get isCached() { return this.#exp > clock(); } get cachedRemainingMs() { return Math.max(0, clock() - this.#exp); } async get() { if (this.#value) { if (this.isCached) return this.#value; this.#value = undefined; } const p = (this.#value = this.fn()); return p .catch(() => ERR) .then((x) => { if (this.#value === p) { this.#exp = clock() + (x === ERR ? this.errorMs : this.cacheMs); } return p; }); } force() { this.clear(); return this.get(); } } export class CachedMap { cacheMs; maxCached; cached = new Map(); pending = new Map(); timer; timer_t = Infinity; errorMs = 250; // how long to cache a rejected promise slopMs = 50; // reschedule precision constructor(cacheMs = 60000, // how long to cache a resolved promise maxCached = 10000 // overflow clears oldest items ) { this.cacheMs = cacheMs; this.maxCached = maxCached; } schedule(exp) { const now = clock(); const t = Math.max(now + this.slopMs, exp); if (this.timer_t < t) return; // scheduled and shorter clearTimeout(this.timer); // kill old this.timer_t = t; // remember fire time if (t === Infinity) return; this.timer = setTimeout(() => { const now = clock(); let min = Infinity; for (const [key, [exp]] of this.cached) { if (exp < now) { this.cached.delete(key); } else { min = Math.min(min, exp); // find next } } this.timer_t = Infinity; if (this.cached.size && min < Infinity) { this.schedule(min); // schedule for next } else { clearTimeout(this.timer); } }, t - now).unref(); // schedule } get pendingSize() { return this.pending.size; } get cachedSize() { return this.cached.size; } get nextExpirationMs() { return this.timer_t; } clear() { this.cached.clear(); this.pending.clear(); clearTimeout(this.timer); this.timer_t = Infinity; } // async resolvePending() { // await Promise.all(Array.from(this.pending.values())); // } set(key, value, ms) { this.delete(key); ms ??= this.cacheMs; if (this.maxCached > 0 && ms > 0) { if (this.cached.size >= this.maxCached) { // we need room // TODO: this needs a heap for (const [key] of Array.from(this.cached) .sort((a, b) => a[1][0] - b[1][0]) .slice(-Math.ceil(this.maxCached / 16))) { // remove batch this.cached.delete(key); } } const exp = clock() + ms; this.cached.set(key, [exp, Promise.resolve(value)]); // add cache entry this.schedule(exp); } } delete(key) { this.cached.delete(key); this.pending.delete(key); } cachedRemainingMs(key) { const c = this.cached.get(key); if (c) { const rem = c[0] - clock(); if (rem > 0) return rem; } return 0; } cachedValue(key) { const c = this.cached.get(key); if (c) { const [exp, q] = c; if (exp > clock()) return q; // still valid this.cached.delete(key); // expired } return; // ree } cachedKeys() { return this.cached.keys(); } peek(key) { return this.cachedValue(key) ?? this.pending.get(key); } setPending(key, value, ms) { const p = value .catch(() => ERR) .then((x) => { // we got an answer if (this.pending.get(key) === p) { // remove from pending this.set(key, value, x === ERR ? this.errorMs : ms); // add original to cache if existed } return value; // resolve to original }); this.pending.set(key, p); // remember in-flight return p; } get(key, fn, ms) { return this.peek(key) ?? this.setPending(key, fn(key), ms); } } // keep the last n promises // setValue(), setPending(), cache(), touch() refresh the key // awaited pending that are still in the cache refresh the key // replaced/removed pending values do not overwrite new values export class LRU { #map = new Map(); #max; constructor(max = 8192) { this.max = max; } get size() { return this.#map.size; } get max() { return this.#max; } set max(n) { if (n < 0) throw new TypeError('expected size'); this.#max = n; this.deleteOldest(this.#map.size - n); } #set(key, promise) { if (this.#max) { this.#map.delete(key); if (this.#map.size == this.#max) this.deleteOldest(1); this.#map.set(key, promise); } } deleteOldest(n) { if (n < 1) return; for (const key of this.#map.keys()) { this.#map.delete(key); if (--n < 1) break; } } keys() { return this.#map.keys(); } entries() { return this.#map.entries(); } clear() { this.#map.clear(); } delete(key) { this.#map.delete(key); } setValue(key, value) { this.#set(key, Promise.resolve(value)); } setPending(key, promise) { const p = promise.then((x) => { if (this.#map.get(key) === p) this.setValue(key, x); return x; }, (x) => { if (this.#map.get(key) === p) this.#map.delete(key); throw x; }); this.#set(key, p); return p; } setFuture(key, promise) { this.setPending(key, promise).catch(() => { }); } peek(key) { return this.#map.get(key); } touch(key) { const p = this.#map.get(key); if (p) this.#set(key, p); return p; } cache(key, fn) { return this.touch(key) ?? this.setPending(key, fn(key)); } }