@gabrielpotter/lru-lfu-cache
Version:
386 lines (385 loc) • 11.9 kB
JavaScript
"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;