import { AppContext } from "./app-context.js"; interface MutableLRUParam { evict: (param: LRUParam, newItem: T, map: LRUMap) => boolean; // is called if the params are changed // default it removes the least recently accessed refresh: (param: LRUParam, map: LRUMap) => void; maxEntries: number; maxAge: number; // not implemented } export type LRUParam = Readonly>; /** * A Set implementation with Least Recently Used (LRU) eviction policy. * * LRUSet maintains a set of values with automatic eviction of least recently * accessed items when capacity limits are reached. Items are moved to the end * of the access order when accessed. * * @template T - The type of values in the set * * @example * ```typescript * const cache = new LRUSet({ maxEntries: 3 }); * cache.add('a'); * cache.add('b'); * cache.add('c'); * cache.add('d'); // 'a' is evicted * console.log(cache.has('a')); // false * ``` */ export class LRUSet { readonly #lruMap: LRUMap; constructor(param: Partial> = {}) { this.#lruMap = new LRUMap(param); } setParam(param: Partial> = {}): void { this.#lruMap.setParam(param); } get size(): number { return this.#lruMap.size; } has(key: T): boolean { return this.#lruMap.has(key); } add(key: T): void { this.#lruMap.set(key, key); } delete(key: T): void { this.#lruMap.delete(key); } clear(): void { this.#lruMap.clear(); } forEach(callbackfn: (value: T, key: T) => void): void { this.#lruMap.forEach((value) => callbackfn(value, value)); } entries(): IterableIterator<[T, T, LRUCtx]> { return this.#lruMap.entries(); } } export type LRUWithIdx = T & { readonly idx: number }; export interface LRUCtx { readonly update: boolean; readonly ref: LRUMap; readonly stats: LRUMap["stats"]; readonly item: LRUItem; } export interface LRUItem { readonly value: V; ctx?: AppContext; } export type LRUMapFn = (value: T, key: K, meta: LRUCtx) => void; export type UnregFn = () => void; function defaultRefresh(param: LRUParam, map: LRUMap): void { if (param.maxEntries > 0 && map.size > param.maxEntries) { const toDelete: K[] = []; let cacheSize = map.size; for (const key of map.keys()) { if (cacheSize > param.maxEntries) { toDelete.push(key); cacheSize--; } else { break; } } for (const key of toDelete) { map.delete(key); } } } /** * A Map implementation with Least Recently Used (LRU) eviction policy. * * LRUMap maintains key-value pairs with automatic eviction of least recently * accessed items when capacity limits are reached. Provides configurable * eviction and refresh strategies, event callbacks, and access statistics. * * @template K - The type of keys in the map * @template V - The type of values in the map * * @example * ```typescript * const cache = new LRUMap({ maxEntries: 100 }); * * // Add items * cache.set('key1', 42); * cache.set('key2', 100); * * // Get items (updates access order) * const value = cache.get('key1'); * * // Listen for evictions * const unregister = cache.onDelete((key, value) => { * console.log(`Evicted: ${key} = ${value}`); * }); * ``` */ export class LRUMap { private _map: Map> = new Map>(); private param: MutableLRUParam; readonly stats = { gets: 0, puts: 0, deletes: 0, }; constructor(c: Partial> = {}) { this.param = { maxEntries: c.maxEntries || 100, maxAge: c.maxAge || 0, evict: c.evict || ((param, _newItem, map): boolean => param.maxEntries > 0 && map.size >= param.maxEntries), refresh: c.refresh || ((param: LRUParam, map: LRUMap): void => defaultRefresh(param, map)), }; } private _onSetFns: Map> = new Map>(); onSet(fn: LRUMapFn): UnregFn { const id = Math.random().toString(36); this._onSetFns.set(id, fn); return () => { this._onSetFns.delete(id); }; } private _onDeleteFns: Map> = new Map>(); onDelete(fn: LRUMapFn): UnregFn { const id = Math.random().toString(36); this._onDeleteFns.set(id, fn); return () => { this._onDeleteFns.delete(id); }; } private touch(key: K): LRUItem { if (!this._map.has(key)) { throw new Error(`key not found in cache: ${key as unknown as string}`); } const value = this._map.get(key) as LRUItem; this._map.delete(key); this._map.set(key, value); return value; } setParam(param: Partial> = {}): void { if (param.evict) { this.param.evict = param.evict; } if (param.refresh) { this.param.refresh = param.refresh; } if (typeof param.maxEntries === "number") { this.param.maxEntries = param.maxEntries; } if (typeof param.maxAge === "number") { this.param.maxAge = param.maxAge; } this.param.refresh(this.param, this); } keys(): IterableIterator { return this._map.keys(); } has(key: K): boolean { return this._map.has(key); } get size(): number { return this._map.size; } async getSet(key: K, createFN: (key: K) => Promise): Promise { const val = this.get(key); if (val) { return val; } else { const val = await createFN(key); this.set(key, val as V); return val; } } get(key: K): V | undefined { return this.getItem(key)?.value; } getItem(key: K): LRUItem | undefined { if (this._map.has(key)) { this.stats.gets++; return this.touch(key); } return undefined; } private buildItem(item: LRUItem | undefined, value: V): LRUItem { return { ...item, value, }; } set(key: K, value: V): void { const update = this._map.has(key); let item = this._map.get(key); if (update) { if (item?.value === value) { return; } // simulate touch this._map.delete(key); } item = this.buildItem(item, value); if (this.param.evict(this.param, value, this)) { // delete the least recently accessed // const key = Array.from(this.cache.keys())[0]; // this.cache.delete(key) or const k = this._map.keys().next(); if (!k.done) { this._map.delete(k.value); } } this._map.set(key, item); this.stats.puts++; this._onSetFns.forEach((fn) => fn(key, item?.value, this.buildItemCtx(item, update))); } private buildItemCtx(item: LRUItem, update: boolean): LRUCtx { return { update, ref: this, stats: this.stats, item, }; } delete(key: K): void { if (this._map.has(key)) { const item = this._map.get(key) as LRUItem; this._onDeleteFns.forEach((fn) => fn(key, item?.value, this.buildItemCtx(item, true))); this._map.delete(key); this.stats.deletes++; } } clear(): void { this._map.forEach((value, key) => { const item = this.buildItemCtx(value, true); this._onDeleteFns.forEach((fn) => fn(key, item.item.value, item)); this.stats.deletes++; }); this._map.clear(); } forEach(fn: (value: V, key: K, ctx: LRUWithIdx>) => void): void { let idx = 0; this._map.forEach((v, k) => { // not really efficient but ok for now fn(v.value, k, { ...this.buildItemCtx(v, false), idx: idx++ }); }); } *entries(): IterableIterator<[K, V, LRUCtx]> { for (const [key, value] of this._map.entries()) { yield [key, value.value, this.buildItemCtx(value, true)]; } } // *entries(): IterableIterator<[T, K]> { // for (const x of this._cache.entries()) { // yield x; // } // } // getLeastRecent(): K { // return Array.from(this.cache)[0]; // } // getMostRecent(): K { // return Array.from(this.cache)[this.cache.size - 1]; // } }