UNPKG

least-recent

Version:

A cache object that deletes the least-recently-used items

364 lines (363 loc) 11.3 kB
/** * LRUCache * =================== * * JavaScript implementation of the LRU Cache data structure. To save up * memory and allocations this implementation represents its underlying * doubly-linked list as static arrays and pointers. Thus, memory is allocated * only once at instantiation and JS objects are never created to serve as * pointers. This also means this implementation does not trigger too many * garbage collections. * * Note that to save up memory, a LRU Cache can be implemented using a singly * linked list by storing predecessors' pointers as hashmap values. * However, this means more hashmap lookups and would probably slow the whole * thing down. What's more, pointers are not the things taking most space in * memory. */ import { createNanoEvents } from "nanoevents"; import { getPointerArray } from "./get-pointer-array.js"; function from(iterable, capacity = iterable.size || iterable.length || 0) { const cache = new LRUCache(capacity); for (const [key, value] of iterable) { cache.set(key, value); } return cache; } /** * LRUCache. * * @constructor * @param {function} Keys - Array class for storing keys. * @param {function} Values - Array class for storing values. * @param {number} capacity - Desired capacity. */ export class LRUCache { capacity; forward; backward; K; V; events; deleted; _size; head; tail; items; deletedSize; /** * Take an arbitrary iterable and convert it into a structure. */ static from = from; constructor(capacity) { this.capacity = capacity; if (!isFinite(this.capacity) || Math.floor(this.capacity) !== this.capacity || this.capacity <= 0) { throw new Error("Capacity should be a finite positive integer."); } const PointerArray = getPointerArray(capacity); this.forward = new PointerArray(capacity); this.backward = new PointerArray(capacity); this.K = new Array(capacity); this.V = new Array(capacity); this.events = createNanoEvents(); this._size = 0; this.head = 0; this.tail = 0; this.items = {}; this.deleted = new PointerArray(capacity); this.deletedSize = 0; } on(event, cb) { return this.events.on(event, cb); } get size() { return this._size; } /** * Clear the structure. */ clear() { this._size = 0; this.head = 0; this.tail = 0; this.items = {}; this.deletedSize = 0; } /** * Splay a value on top. * @param pointer Pointer of the value to splay on top. */ splayOnTop(pointer) { const oldHead = this.head; if (this.head === pointer) return; const previous = this.backward[pointer]; const next = this.forward[pointer]; if (this.tail === pointer) { this.tail = previous; } else { this.backward[next] = previous; } this.forward[previous] = next; this.backward[oldHead] = pointer; this.head = pointer; this.forward[pointer] = oldHead; } /** * Set the value for the given key in the cache. */ set(key, value) { let pointer = this.items[key]; // The key already exists, we just need to update the value and splay on top if (pointer !== undefined) { this.splayOnTop(pointer); this.V[pointer] = value; return; } // The cache is not yet full if (this._size < this.capacity) { if (this.deletedSize > 0) { // If there is a "hole" in the pointer list, reuse it pointer = this.deleted[--this.deletedSize]; } else { // otherwise append to the pointer list pointer = this._size; } this._size++; } // Cache is full, we need to drop the last value else { pointer = this.tail; this.tail = this.backward[pointer]; delete this.items[this.K[pointer]]; this.events.emit("evicted", this.K[pointer], this.V[pointer]); } this.storeKeyValue(pointer, key, value); } /** * Set the value for the given key in the cache * @param key * @param value * @return {{evicted: boolean, key: any, value: any}} An object containing the * key and value of an item that was overwritten or evicted in the set * operation, as well as a boolean indicating whether it was evicted due to * limited capacity. Return value is null if nothing was evicted or overwritten * during the set operation. */ setpop(key, value) { let oldValue; let oldKey; let pointer = this.items[key]; // The key already exists, we just need to update the value and splay on top if (pointer !== undefined) { this.splayOnTop(pointer); oldValue = this.V[pointer]; this.V[pointer] = value; return { evicted: false, key: key, value: oldValue }; } // The cache is not yet full if (this._size < this.capacity) { if (this.deletedSize > 0) { // If there is a "hole" in the pointer list, reuse it pointer = this.deleted[--this.deletedSize]; } else { // otherwise append to the pointer list pointer = this._size; } this._size++; } // Cache is full, we need to drop the last value else { pointer = this.tail; this.tail = this.backward[pointer]; oldValue = this.V[pointer]; oldKey = this.K[pointer]; delete this.items[this.K[pointer]]; this.events.emit("evicted", this.K[pointer], this.V[pointer]); } this.storeKeyValue(pointer, key, value); // Return object if eviction took place, otherwise return null if (oldKey) { return { evicted: true, key: oldKey, value: oldValue }; } else { return null; } } /** * Delete the entry for the given key in the cache. * Return `true` if the item was present. */ delete(key) { let pointer = this.items[key]; if (pointer === undefined) { return false; } delete this.items[key]; if (this._size === 1) { this._size = 0; this.head = 0; this.tail = 0; this.deletedSize = 0; return true; } const previous = this.backward[pointer]; const next = this.forward[pointer]; if (this.head === pointer) { this.head = next; } if (this.tail === pointer) { this.tail = previous; } this.forward[previous] = next; this.backward[next] = previous; this._size--; this.deleted[this.deletedSize++] = pointer; return true; } /** * Check whether the key exists in the cache. */ has(key) { return key in this.items; } /** * Get the value attached to the given key. Will move the * related key to the front of the underlying linked list. */ get(key) { const pointer = this.items[key]; if (pointer === undefined) return; this.splayOnTop(pointer); return this.V[pointer]; } /** * Method used to get the value attached to the given key. Does not modify * the ordering of the underlying linked list. */ peek(key) { const pointer = this.items[key]; if (pointer === undefined) return; return this.V[pointer]; } /** * Create an iterator over the cache's keys from most * recently used to least recently used. */ keys() { let i = 0; const l = this._size; let pointer = this.head; const keys = this.K; const forward = this.forward; return { [Symbol.iterator]() { return this; }, next() { if (i >= l) return { done: true, value: undefined }; const key = keys[pointer]; i++; if (i < l) { pointer = forward[pointer]; } return { done: false, value: key, }; }, }; } /** * Create an iterator over the cache's values from most * recently used to least recently used. */ values() { let i = 0; const l = this._size; let pointer = this.head; const values = this.V; const forward = this.forward; return { [Symbol.iterator]() { return this; }, next() { if (i >= l) return { done: true, value: undefined }; const value = values[pointer]; i++; if (i < l) pointer = forward[pointer]; return { done: false, value: value, }; }, }; } /** * Create an iterator over the cache's entries from most * recently used to least recently used. */ entries() { let i = 0; const l = this._size; let pointer = this.head; const keys = this.K; const values = this.V; const forward = this.forward; return { [Symbol.iterator]() { return this; }, next() { if (i >= l) return { done: true, value: undefined }; const key = keys[pointer]; const value = values[pointer]; i++; if (i < l) pointer = forward[pointer]; return { done: false, value: [key, value], }; }, }; } inspect() { const proxy = new Map(); let iterator = this.entries(), step; while (((step = iterator.next()), !step.done)) proxy.set(step.value[0], step.value[1]); // Trick so that node displays the name of the constructor Object.defineProperty(proxy, "constructor", { value: LRUCache, enumerable: false, }); return proxy; } storeKeyValue(pointer, key, value) { // Storing key & value this.items[key] = pointer; this.K[pointer] = key; this.V[pointer] = value; // Moving the item at the front of the list this.forward[pointer] = this.head; this.backward[this.head] = pointer; this.head = pointer; } [Symbol.iterator]() { return this.entries(); } [Symbol.for("nodejs.util.inspect.custom")]() { return this.inspect(); } }