UNPKG

@trifrost/core

Version:

Blazingly fast, runtime-agnostic server framework for modern edge and node environments

135 lines (134 loc) 4.42 kB
import { isNeArray } from '@valkyriestudios/utils/array'; import { isIntGt } from '@valkyriestudios/utils/number'; import { TriFrostCache } from '../modules/Cache/_Cache'; import { TriFrostRateLimit } from '../modules/RateLimit/_RateLimit'; import { Store } from './_Storage'; export class MemoryStoreAdapter { #store = new Map(); /* Garbage collection interval */ #gc = null; /* Used for lru (least-recently-used) tracking */ #lru = new Set(); /* Set to the max amount of items allowed in our store if configured */ #lruMax = null; constructor(opts) { /* Configure garbage collection interval */ const filter = typeof opts?.gc_filter === 'function' ? opts.gc_filter : (_key, _v, _now, _exp) => _exp <= _now; if (isIntGt(opts?.gc_interval, 0)) { this.#gc = setInterval(() => { const now = Date.now(); for (const [key, entry] of this.#store.entries()) { if (filter(key, entry.value, now, entry.expires)) { this.#store.delete(key); if (this.isLRU) this.#lru.delete(key); } } }, opts.gc_interval); } /* Set max usage value if max entries is provided */ if (isIntGt(opts?.max_items, 0)) this.#lruMax = opts.max_items; } get isLRU() { return this.#lruMax !== null; } async get(key) { const val = this.#store.get(key); if (!val) return null; if (this.isLRU) this.#lru.delete(key); if (Date.now() > val.expires) { this.#store.delete(key); return null; } else { if (this.isLRU) this.#lru.add(key); return val.value; } } async set(key, value, ttl) { if (this.isLRU) { /* Mark as most recently used in LRU */ this.#lru.delete(key); this.#lru.add(key); /* Evict if above size */ if (this.#lru.size > this.#lruMax) { const to_evict = this.#lru.values().next().value; if (to_evict) { this.#store.delete(to_evict); this.#lru.delete(to_evict); } } } this.#store.set(key, { value, expires: Date.now() + ttl * 1000 }); } async del(key) { this.#store.delete(key); if (this.isLRU) this.#lru.delete(key); } async delPrefixed(prefix) { for (const k of this.#store.keys()) { if (k.startsWith(prefix)) { this.#store.delete(k); if (this.isLRU) this.#lru.delete(k); } } } async stop() { if (!this.#gc) return; clearInterval(this.#gc); this.#gc = null; } } /** * MARK: Store */ export class MemoryStore extends Store { constructor(opts) { super('MemoryStore', new MemoryStoreAdapter(opts)); } } /** * MARK: Cache */ export class MemoryCache extends TriFrostCache { constructor(cfg) { super({ store: new Store('MemoryCache', new MemoryStoreAdapter({ gc_interval: isIntGt(cfg?.gc_interval, 0) ? cfg?.gc_interval : 60_000, ...(cfg?.max_items !== null && { max_items: isIntGt(cfg?.max_items, 0) ? cfg.max_items : 1_000 }), })), }); } } /** * MARK: RateLimit */ export class MemoryRateLimit extends TriFrostRateLimit { constructor(cfg) { const window = isIntGt(cfg?.window, 0) ? cfg.window : 60_000; let adapter; if (cfg?.strategy === 'sliding') { adapter = new MemoryStoreAdapter({ gc_interval: isIntGt(cfg?.gc_interval, 0) ? cfg.gc_interval : 60_000, gc_filter: (_, timestamps, now) => isNeArray(timestamps) && timestamps[timestamps.length - 1] < now - window, }); } else { adapter = new MemoryStoreAdapter({ gc_interval: isIntGt(cfg?.gc_interval, 0) ? cfg.gc_interval : 60_000, gc_filter: (_, value, now) => value.reset <= now, }); } super({ ...(cfg || {}), store: new Store('MemoryRateLimit', adapter), }); } }