@trifrost/core
Version:
Blazingly fast, runtime-agnostic server framework for modern edge and node environments
135 lines (134 loc) • 4.42 kB
JavaScript
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),
});
}
}