UNPKG

quick-lru

Version:

Simple “Least Recently Used” (LRU) cache

294 lines (243 loc) 5.93 kB
export default class QuickLRU extends Map { #size = 0; #cache = new Map(); #oldCache = new Map(); #maxSize; #maxAge; #onEviction; constructor(options = {}) { super(); if (!(options.maxSize && options.maxSize > 0)) { throw new TypeError('`maxSize` must be a number greater than 0'); } if (typeof options.maxAge === 'number' && options.maxAge === 0) { throw new TypeError('`maxAge` must be a number greater than 0'); } this.#maxSize = options.maxSize; this.#maxAge = options.maxAge || Number.POSITIVE_INFINITY; this.#onEviction = options.onEviction; } // For tests. get __oldCache() { return this.#oldCache; } #emitEvictions(cache) { if (typeof this.#onEviction !== 'function') { return; } for (const [key, item] of cache) { this.#onEviction(key, item.value); } } #deleteIfExpired(key, item) { if (typeof item.expiry === 'number' && item.expiry <= Date.now()) { if (typeof this.#onEviction === 'function') { this.#onEviction(key, item.value); } return this.delete(key); } return false; } #getOrDeleteIfExpired(key, item) { const deleted = this.#deleteIfExpired(key, item); if (deleted === false) { return item.value; } } #getItemValue(key, item) { return item.expiry ? this.#getOrDeleteIfExpired(key, item) : item.value; } #peek(key, cache) { const item = cache.get(key); return this.#getItemValue(key, item); } #set(key, value) { this.#cache.set(key, value); this.#size++; if (this.#size >= this.#maxSize) { this.#size = 0; this.#emitEvictions(this.#oldCache); this.#oldCache = this.#cache; this.#cache = new Map(); } } #moveToRecent(key, item) { this.#oldCache.delete(key); this.#set(key, item); } * #entriesAscending() { for (const item of this.#oldCache) { const [key, value] = item; if (!this.#cache.has(key)) { const deleted = this.#deleteIfExpired(key, value); if (deleted === false) { yield item; } } } for (const item of this.#cache) { const [key, value] = item; const deleted = this.#deleteIfExpired(key, value); if (deleted === false) { yield item; } } } get(key) { if (this.#cache.has(key)) { const item = this.#cache.get(key); return this.#getItemValue(key, item); } if (this.#oldCache.has(key)) { const item = this.#oldCache.get(key); if (this.#deleteIfExpired(key, item) === false) { this.#moveToRecent(key, item); return item.value; } } } set(key, value, {maxAge = this.#maxAge} = {}) { const expiry = typeof maxAge === 'number' && maxAge !== Number.POSITIVE_INFINITY ? (Date.now() + maxAge) : undefined; if (this.#cache.has(key)) { this.#cache.set(key, { value, expiry, }); } else { this.#set(key, {value, expiry}); } return this; } has(key) { if (this.#cache.has(key)) { return !this.#deleteIfExpired(key, this.#cache.get(key)); } if (this.#oldCache.has(key)) { return !this.#deleteIfExpired(key, this.#oldCache.get(key)); } return false; } peek(key) { if (this.#cache.has(key)) { return this.#peek(key, this.#cache); } if (this.#oldCache.has(key)) { return this.#peek(key, this.#oldCache); } } delete(key) { const deleted = this.#cache.delete(key); if (deleted) { this.#size--; } return this.#oldCache.delete(key) || deleted; } clear() { this.#cache.clear(); this.#oldCache.clear(); this.#size = 0; } resize(newSize) { if (!(newSize && newSize > 0)) { throw new TypeError('`maxSize` must be a number greater than 0'); } const items = [...this.#entriesAscending()]; const removeCount = items.length - newSize; if (removeCount < 0) { this.#cache = new Map(items); this.#oldCache = new Map(); this.#size = items.length; } else { if (removeCount > 0) { this.#emitEvictions(items.slice(0, removeCount)); } this.#oldCache = new Map(items.slice(removeCount)); this.#cache = new Map(); this.#size = 0; } this.#maxSize = newSize; } * keys() { for (const [key] of this) { yield key; } } * values() { for (const [, value] of this) { yield value; } } * [Symbol.iterator]() { for (const item of this.#cache) { const [key, value] = item; const deleted = this.#deleteIfExpired(key, value); if (deleted === false) { yield [key, value.value]; } } for (const item of this.#oldCache) { const [key, value] = item; if (!this.#cache.has(key)) { const deleted = this.#deleteIfExpired(key, value); if (deleted === false) { yield [key, value.value]; } } } } * entriesDescending() { let items = [...this.#cache]; for (let i = items.length - 1; i >= 0; --i) { const item = items[i]; const [key, value] = item; const deleted = this.#deleteIfExpired(key, value); if (deleted === false) { yield [key, value.value]; } } items = [...this.#oldCache]; for (let i = items.length - 1; i >= 0; --i) { const item = items[i]; const [key, value] = item; if (!this.#cache.has(key)) { const deleted = this.#deleteIfExpired(key, value); if (deleted === false) { yield [key, value.value]; } } } } * entriesAscending() { for (const [key, value] of this.#entriesAscending()) { yield [key, value.value]; } } get size() { if (!this.#size) { return this.#oldCache.size; } let oldCacheSize = 0; for (const key of this.#oldCache.keys()) { if (!this.#cache.has(key)) { oldCacheSize++; } } return Math.min(this.#size + oldCacheSize, this.#maxSize); } get maxSize() { return this.#maxSize; } entries() { return this.entriesAscending(); } forEach(callbackFunction, thisArgument = this) { for (const [key, value] of this.entriesAscending()) { callbackFunction.call(thisArgument, value, key, this); } } get [Symbol.toStringTag]() { return JSON.stringify([...this.entriesAscending()]); } }