@unruggable/gateways
Version:
Trustless Ethereum Multichain CCIP-Read Gateway
274 lines (273 loc) • 7.53 kB
JavaScript
// 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));
}
}