const NEXT = '@@next'; const PREV = '@@prev'; export class CacheItem { key: K; value: V; [NEXT]: CacheItem | undefined; [PREV]: CacheItem | undefined; isValid!: boolean; expireTimerId: number | undefined; lastUpdateAt: number | undefined; constructor(key: K, value: V) { this.key = key; this.value = value; Object.defineProperties(this, { [NEXT]: { value: undefined, writable: true, }, [PREV]: { value: undefined, writable: true, }, isValid: { value: false, writable: true, }, expireTimerId: { value: undefined, writable: true, }, lastUpdateAt: { value: undefined, writable: true, }, }); } destroy(): void { delete this.key; delete this.value; this[NEXT] = undefined; this[PREV] = undefined; this.isValid = false; clearTimeout(this.expireTimerId); this.expireTimerId = undefined; this.lastUpdateAt = undefined; } } export interface SerializableCacheItem { key: K; value: V; } const REDUCED = '@@reduced'; interface ReducedType { [REDUCED]: true; value: ValueType; } function reduced(value: ValueType): ReducedType { return {[REDUCED]: true, value}; } function isReduced(value: any): value is ReducedType { return value && REDUCED in value; } export function reduce( cache: Cache, accumulator: ReturnType, iteratee: (result: ReturnType, item: CacheItem) => ReturnType | ReducedType, ) { let item = cache.head; let result: ReturnType | ReducedType = accumulator; while (item) { result = iteratee(result, item); if (isReduced(result)) { return result.value; } item = item[NEXT]; } return result; } export class Cache { head: CacheItem | undefined; tail: CacheItem | undefined; size = 0; constructor(items: (CacheItem | SerializableCacheItem)[]) { const now = Date.now(); for (let i = items.length; i > 0; i--) { const item = items[i - 1]; const cacheItem: CacheItem = item instanceof CacheItem ? item : new CacheItem(item.key, item.value); cacheItem.isValid = true; cacheItem.lastUpdateAt = now; this.prepend(cacheItem); } } prepend(item: CacheItem): void { item[NEXT] = this.head; if (this.head) { this.head[PREV] = item; } this.head = item; if (!this.tail) { this.tail = item; } this.size += 1; } remove(item: CacheItem): boolean { if (!item.key) { return false; } const next = item[NEXT]; const prev = item[PREV]; if (prev) { prev[NEXT] = next; } else { this.head = next; } if (next) { next[PREV] = prev; } else { this.tail = prev; } this.size -= 1; return true; } findBy(predicate: (item: CacheItem) => boolean): CacheItem | undefined { return reduce | undefined, K, V>(this, undefined, (acc, item) => predicate(item) ? reduced(item) : acc, ); } }