UNPKG

@akashbabu/lfu-cache

Version:

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

538 lines (531 loc) 16.1 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.LFUCache = factory()); }(this, (function () { 'use strict'; class DLLItem { constructor(data, prev = null, next = null) { this.data = data; this.prev = prev; this.next = next; } } class DLLItemAccessRestrictor { /** * Grants all the access on the given dllItem * * @param accessRestrictedDllItem dll item whose access is restricted * * @returns all access granted dll item */ grantAccess(accessRestrictedDllItem) { return accessRestrictedDllItem.__dllItem__; } /** * Revokes write access to `prev` & `next` properties * * @param dllItem a node in the dll chain * * @returns Access restricted dll item */ revokeAccess(dllItem) { return (dllItem ? new AccessRestrictedDLLItem(dllItem) : null); } } class AccessRestrictedDLLItem { constructor(dllItem) { this.dllItem = dllItem; this.dllItemAccessRestrictor = new DLLItemAccessRestrictor(); this.__dllItem__ = dllItem; } get data() { return this.dllItem.data; } set data(dt) { this.dllItem.data = dt; } get prev() { return this.dllItemAccessRestrictor.revokeAccess(this.dllItem.prev); } get next() { return this.dllItemAccessRestrictor.revokeAccess(this.dllItem.next); } } class DLL { constructor() { this.state = this.getFreshState(); this.dllItemAccessRestrictor = new DLLItemAccessRestrictor(); } get head() { return this.dllItemAccessRestrictor.revokeAccess(this.state.head); } get tail() { return this.dllItemAccessRestrictor.revokeAccess(this.state.tail); } get length() { return this.state.length; } /** * Removes and returns the first * item in the list * * @returns Same data that was * used to append to this list */ shift() { let dllItem = this.state.head; if (!(dllItem instanceof DLLItem)) return undefined; this.remove(dllItem); const value = dllItem.data; // for gc dllItem = null; return value; } /** * Add the given item to the head of * DLL chain * * In other words the new item would * be the new head of the chain * * @returns Same data that was * used to append to this list */ unshift(data) { const currHead = this.state.head; const dllItem = new DLLItem(data, null, currHead); this.state.head = dllItem; if (currHead instanceof DLLItem) { currHead.prev = dllItem; // if HEAD is not set // then its an empty list } else { this.state.tail = dllItem; } this.state.length++; } /** * Iterate through the entire DLL chain * just like Array.forEach() * * @param cb iterator callback */ forEach(cb) { this.iterate((dllItem, i) => { cb(dllItem.data, i); }); } /** * Iterates through the entire DLL chain * and returns the result array, just * like Array.map() * * @param cb iterator callback * * @returns the result array just like Array.map() */ map(cb) { const mapped = []; this.forEach((value, i) => { mapped.push(cb(value, i)); }); return mapped; } /** * Adds the given item the tail of DLL * * @param data Data to be appended to the list * * @returns {DLLItem} dllItem, the same * can be used to remove this item from * DLL */ push(data) { return this.appendAfter(this.state.tail, data); } /** * Appends the given value after the * specified node * * @param node Node to append the new item * @param data Value for the new node * * @returns the newly appended node */ appendAfter(accessRestrictedNode, data) { let node; if (accessRestrictedNode === null && this.state.length > 0) { throw new Error('Invalid Node `null`: DLL is not empty, hence can\'t append to the given node'); } else if (accessRestrictedNode instanceof AccessRestrictedDLLItem) { node = this.dllItemAccessRestrictor.grantAccess(accessRestrictedNode); } else { node = accessRestrictedNode; } const dllItem = new DLLItem(data); // if node is null, then it means // that the list is empty if (node === null) { this.state.head = this.state.tail = dllItem; } else { dllItem.prev = node; dllItem.next = node.next; node.next = dllItem; // if the node was a tail node // then reset the tail node if (node === this.state.tail) { this.state.tail = dllItem; } } this.state.length++; return this.dllItemAccessRestrictor.revokeAccess(dllItem); } /** * Removes the given item from DLL * * @param dllItem */ remove(accessRestrictedDllItem) { let dllItem; if (accessRestrictedDllItem instanceof AccessRestrictedDLLItem) { dllItem = this.dllItemAccessRestrictor.grantAccess(accessRestrictedDllItem); } else if (accessRestrictedDllItem instanceof DLLItem) { dllItem = accessRestrictedDllItem; } else { return false; } // Isolate the node from the chain if (dllItem.prev) { dllItem.prev.next = dllItem.next; } else { // If it's HEAD node this.state.head = dllItem.next; } if (dllItem.next) { dllItem.next.prev = dllItem.prev; } else { // if it's also a TAIL node this.state.tail = dllItem.prev; } this.state.length--; return true; } /** * Clears the DLL chain */ clear() { // unlink all the items in the DLL chain // such it will be garbage collected // as no reference between them is found this.iterate((dllItem) => { dllItem.prev = dllItem.next = null; }); this.state = this.getFreshState(); } getFreshState() { return { length: 0, head: null, tail: null, }; } iterate(cb) { let dllItem = this.state.head; let i = 0; while (dllItem) { cb(dllItem, i++); dllItem = dllItem.next; } } } 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 DLL(), nodeList: new 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'); } return LFUCache; })));