UNPKG

@gabrielpotter/lru-lfu-cache

Version:
386 lines (385 loc) 11.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UnifiedCache = void 0; const async_mutex_1 = require("async-mutex"); class CacheNode { constructor(key, value, size, meta = {}) { this.prev = null; this.next = null; this.key = key; this.value = value; this.freq = 1; this.size = size; this.meta = meta; } } class DoublyLinkedList { constructor() { this.head = null; this.tail = null; } addToFront(node) { node.next = this.head; node.prev = null; if (this.head) { this.head.prev = node; } this.head = node; if (!this.tail) { this.tail = node; } } removeNode(node) { if (node.prev) { node.prev.next = node.next; } else { this.head = node.next; } if (node.next) { node.next.prev = node.prev; } else { this.tail = node.prev; } node.prev = null; node.next = null; } moveToFront(node) { this.removeNode(node); this.addToFront(node); } removeTail() { if (!this.tail) return null; const tailNode = this.tail; this.removeNode(tailNode); return tailNode; } } class UnifiedCache { constructor(capacity, maxMemory, strategy = "LRU", hitReset = 1000) { this.capacity = capacity; this.maxMemory = maxMemory; this.strategy = strategy; this.cacheMap = new Map(); this.lruList = new DoublyLinkedList(); this.freqMap = new Map(); this.minFreq = 0; this.mutex = new async_mutex_1.Mutex(); this.currentMemory = 0; this.absHit = 0; this.absReq = 0; this.relHit = 0; this.relReq = 0; this.hitResetCounter = hitReset; } async clear(params) { return this.mutex.runExclusive(() => { if (params?.capacity) { this.capacity = params.capacity; } if (params?.maxMemory) { this.maxMemory = params.maxMemory; } if (params?.strategy) { this.strategy = params.strategy; } if (params?.hitReset) { this.hitResetCounter = params.hitReset; } this.cacheMap = new Map(); this.lruList = new DoublyLinkedList(); this.freqMap = new Map(); this.minFreq = 0; this.mutex = new async_mutex_1.Mutex(); this.currentMemory = 0; this.absHit = 0; this.absReq = 0; this.relHit = 0; this.relReq = 0; }); } async get(key) { return this.mutex.runExclusive(() => { this.absReq++; this.relReq++; if (this.relReq > this.hitResetCounter) { this.relReq = 1; this.relHit = 0; } const node = this.cacheMap.get(key); if (!node) return undefined; this.absHit++; this.relHit++; if (this.strategy === "LRU") { this.lruList.moveToFront(node); } else { this.updateFrequency(node); } return node.value; }); } async getValueAndMeta(key) { return this.mutex.runExclusive(() => { this.absReq++; this.relReq++; if (this.relReq > this.hitResetCounter) { this.relReq = 1; this.relHit = 0; } const node = this.cacheMap.get(key); if (!node) return undefined; this.absHit++; this.relHit++; if (this.strategy === "LRU") { this.lruList.moveToFront(node); } else { this.updateFrequency(node); } return { value: node.value, meta: node.meta }; }); } async getByMeta(callback) { return this.mutex.runExclusive(() => { const res = []; for (const [key, node] of this.cacheMap) { this.absReq++; this.relReq++; if (this.relReq > this.hitResetCounter) { this.relReq = 1; this.relHit = 0; } if (callback(node.meta)) { this.absHit++; this.relHit++; if (this.strategy === "LRU") { this.lruList.moveToFront(node); } else { this.updateFrequency(node); } res.push(node.value); } } return res; }); } async getMeta(key) { return this.mutex.runExclusive(() => { this.absReq++; this.relReq++; if (this.relReq > this.hitResetCounter) { this.relReq = 1; this.relHit = 0; } const node = this.cacheMap.get(key); if (!node) return undefined; this.absHit++; this.relHit++; if (this.strategy === "LRU") { this.lruList.moveToFront(node); } else { this.updateFrequency(node); } return node.meta; }); } async test(key) { return this.mutex.runExclusive(() => { return this.cacheMap.has(key); }); } async set(key, value, meta = {}) { return this.mutex.runExclusive(() => { const existingNode = this.cacheMap.get(key); const size = this.roughSizeOfObject(value) + this.roughSizeOfObject(meta); if (existingNode) { this.currentMemory -= existingNode.size; existingNode.value = value; existingNode.size = size; existingNode.meta = meta; this.currentMemory += size; if (this.strategy === "LRU") { this.lruList.moveToFront(existingNode); } else { this.updateFrequency(existingNode); } } else { while (this.cacheMap.size >= this.capacity || this.currentMemory + size > this.maxMemory) { this.evict(); } const newNode = new CacheNode(key, value, size, meta); this.cacheMap.set(key, newNode); this.currentMemory += size; if (this.strategy === "LRU") { this.lruList.addToFront(newNode); } else { this.minFreq = 1; this.addToFreqMap(newNode); } } }); } async remove(key) { return this.mutex.runExclusive(() => { const node = this.cacheMap.get(key); if (node) { if (this.strategy === "LRU") { this.lruList.removeNode(node); } else { this.removeFreqMap(node); } this.cacheMap.delete(node.key); this.currentMemory -= node.size; } }); } async removeByMeta(callback) { return this.mutex.runExclusive(() => { for (const [key, node] of this.cacheMap) { if (callback(node.meta)) { if (this.strategy === "LRU") { this.lruList.removeNode(node); } else { this.removeFreqMap(node); } this.cacheMap.delete(node.key); this.currentMemory -= node.size; } } }); } evict() { let node = null; if (this.strategy === "LRU") { node = this.lruList.removeTail(); } else { const nodes = this.freqMap.get(this.minFreq); if (nodes) { node = nodes.values().next().value; if (node) { nodes.delete(node); } if (nodes.size === 0) { this.freqMap.delete(this.minFreq); } } } if (node) { this.cacheMap.delete(node.key); this.currentMemory -= node.size; } } updateFrequency(node) { this.removeFreqMap(node); node.freq++; this.addToFreqMap(node); } addToFreqMap(node) { const freq = node.freq; if (!this.freqMap.has(freq)) { this.freqMap.set(freq, new Set()); } this.freqMap.get(freq).add(node); } removeFreqMap(node) { const freq = node.freq; if (this.freqMap.has(freq)) { const nodes = this.freqMap.get(freq); if (nodes) { nodes.delete(node); if (nodes.size === 0) { this.freqMap.delete(freq); if (this.minFreq === freq) { this.minFreq++; } } } } } roughSizeOfObject(object) { const objectList = []; const stack = [object]; let bytes = 0; while (stack.length) { const value = stack.pop(); if (typeof value === "boolean") { bytes += 4; } else if (typeof value === "string") { bytes += value.length * 2; } else if (typeof value === "number") { bytes += 8; } else if (typeof value === "object" && value !== null) { if (objectList.indexOf(value) === -1) { objectList.push(value); for (const i in value) { stack.push(value[i]); } } } } return bytes; } getStats() { return { strategy: this.strategy, currentNodes: this.cacheMap.size, maxNodes: this.capacity, currentMemory: this.currentMemory, maxMemory: this.maxMemory, absHitRate: (() => { if (this.absReq == 0) { return 0; } else { return Number((this.absHit / this.absReq).toFixed(2)); } })(), relHitRate: (() => { if (this.relReq == 0) { return 0; } else { return Number((this.relHit / this.relReq).toFixed(2)); } })(), }; } sizeSuppressorReplacer(key, value) { if (Array.isArray(value) && value.length > 100) { return `[ ... ${value.length} items ]`; } return value; } dump() { const plainObj = Object.fromEntries([...this.cacheMap.entries()].map(([key, node]) => [ key, { key: node.key, value: node.value, freq: node.freq, size: node.size, prev: node.prev ? node.prev.key : null, next: node.next ? node.next.key : null, }, ])); return JSON.stringify(plainObj, this.sizeSuppressorReplacer, 2); } } exports.UnifiedCache = UnifiedCache;