UNPKG

@akashbabu/lfu-cache

Version:

LFU cache implementation with a complexity of `O(1)` for all transactions

286 lines (282 loc) 8.99 kB
'use strict'; var nodeDll = require('@akashbabu/node-dll'); class LFUCache { // private dummy: any[] = []; constructor(options = {}) { this.state = this.getFreshState(); if (options.max && !options.evictCount) { // choose between 10% of max count and 1 as evictCount options.evictCount = Math.max(1, options.max * 0.1); } this.options = { max: options.max || 100, evictCount: options.evictCount || 10, maxAge: options.maxAge, }; } /** * Returns the current size of the cache */ get size() { return this.state.size; } /** * Caches the given key-value pair * * **Example** * ```TS * const lfu = new LFUCache<string>(); * * lfu.set('key', 'value'); * lfu.set('key', 'value1', true); * ``` * * @param key Key * @param value Value to be caches against the provided key * @param force Replaces the existing value if any, else will add the value to cache */ set(key, value, force) { const existingValue = this.state.byKey.get(key); // for duplicate entry / updating // remove the current entry // and create a new entry in the cache if (existingValue) { if (force) { // if set by force, then replace only the existing value // with the new one without touching size, frequency or nodeList existingValue.data.value = value; return value; } else { return existingValue.data.value; } } this.state.size++; const freqListItem = this.addToFreqList(key); // Add the new node to nodeList const nodeItem = this.state.nodeList.push({ key, value, utime: Date.now(), parent: freqListItem, }); // create a mapping btw new member // and the created nodeItem this.state.byKey.set(key, nodeItem); if (this.state.size > this.options.max) { this.evict(this.options.evictCount); } return value; } /** * Returns cached value for the provided key * if one exists, else returns undefined * * **Example** * ```TS * const lfu = new LFUCache<string>(); * * lfu.set('key', 'value'); * lfu.get('key') // => 'value' * ``` * * @param key Key whose value is needed * * @returns Value for the given key if one exists, else * return undefined * */ get(key) { const nodeListItem = this.state.byKey.get(key); if (!nodeListItem) return undefined; if (this.options.maxAge) { if (nodeListItem.data.utime + this.options.maxAge < Date.now()) { this.delete(key); return; } } // Get the current parent frequency item const freqListItem = nodeListItem.data.parent; // get next freq item let nextFreqListItem = freqListItem.next; const nextFreqValue = freqListItem.data.value + 1; // if the next freq item value is not as expected, // then create a new one with the expected freq value if (!nextFreqListItem || nextFreqListItem.data.value !== nextFreqValue) { // create a new freq item list and append it // after the curr freq item and add the // requested key to freq items list nextFreqListItem = this.state.freqList.appendAfter(freqListItem, { value: freqListItem.data.value + 1, items: new Set([key]), }); } else { // add requested key to the next freq list item nextFreqListItem.data.items.add(key); } this.removeKeyFromFreqItem(freqListItem, key); // Set the new parent nodeListItem.data.parent = nextFreqListItem; // Update utime/accessed time of the node nodeListItem.data.utime = Date.now(); return nodeListItem.data.value; } /** * Removes the given key from cache * * **Example** * ```TS * const lfu = new LFUCache<string>(); * * lfu.set('key', 'value') * lfu.get('key') // => value * * lfu.delete('key') * lfu.get('key') // => undefined * ``` * * @param key Key to be removed from cache */ delete(key) { // get the current node item const nodeListItem = this.state.byKey.get(key); if (nodeListItem) { // Remove the key from frequency node item list const freqListItem = nodeListItem.data.parent; this.removeKeyFromFreqItem(freqListItem, key); // remove the corresponding node from nodelist this.state.nodeList.remove(nodeListItem); // remove the key from byKey Map this.state.byKey.delete(key); // Since a node is removed, // decrease the size of the cache this.state.size--; return true; } return false; } /** * Returns the value for the provided key * without increasing the accessed frequency * of the requested key * * **Example** * ```TS * const lfu = new LFUCache<string>(); * * lfu.set('key', 'value'); * lfu.peek('key') // => value * * // but the access frequency of 'key' remain untouched * ``` * * @param key Key whose value has to be peeked * * @returns Value for the provided key */ peek(key) { return this.state.byKey.get(key) ? this.state.byKey.get(key).data.value : undefined; } /** * Iterates over the entire cache * in the form of cb([key, val], i) * * @param cb Iterator callback */ forEach(cb) { this.state.nodeList.forEach((nodeListItem, i) => { cb([nodeListItem.key, nodeListItem.value], i); }); } /** * Iterates over the entire cache * in the form of cb([key, val], i) * and returns the resultant array * * @param cb Iterator callback */ map(cb) { const result = []; this.forEach((data, i) => { result.push(cb(data, i)); }); return result; } /** * Clears all the data in the cache */ clear() { this.state = this.getFreshState(); } /** * Returns the internal state of the cache. * MUST BE USED ONLY FOR TESTING PURPOSE. * This must NOT BE tampered for any reasons, * if not, the integrity of the functionality * CANNOT BE PROMISED. */ dangerously_getState() { return this.state; } /********************* * PRIVATE METHODS ********************/ getFreshState() { return { freqList: new nodeDll.DLL(), nodeList: new nodeDll.DLL(), byKey: new Map(), size: 0, }; } removeKeyFromFreqItem(freqListItem, key) { // remove the requested key from the current parent freqListItem.data.items.delete(key); // if the curr freq item does not have any items // after removing the requested key, then // remove the item from freqListItem DLL if (freqListItem.data.items.size === 0) { this.state.freqList.remove(freqListItem); } } evict(count) { while (count--) { const freqListHead = this.state.freqList.head; if (freqListHead) { // remove the first key from frequency List head item const key = freqListHead.data.items.keys().next().value; this.delete(key); } } } addToFreqList(key) { const freqListHead = this.state.freqList.head; if (!freqListHead || freqListHead.data.value !== 1) { this.state.freqList.unshift({ value: 1, items: new Set([key]), }); } else { freqListHead.data.items.add(key); } return this.state.freqList.head; } } if (require.main === module) { const SIZE = 3; const lfu = new LFUCache({ max: SIZE }); new Array(SIZE).fill(0).forEach((_, i) => { lfu.set(`foo_${i + 1}`, `bar_${i + 1}`); }); // Increment the access frequency lfu.get('foo_1'); lfu.set('foo_1', 'bar_1_2', true); lfu.set('foo_4', 'bar_4'); lfu.set('foo_5', 'bar_5'); } module.exports = LFUCache;