/** * A doubly linked list-based Least Recently Used (LRU) cache. Will keep most * recently used items while discarding least recently used items when its limit * is reached. * * Licensed under MIT. Copyright (c) 2010 Rasmus Andersson * See README.md for details. * * Illustration of the design: * * entry entry entry entry * ______ ______ ______ ______ * | head |.newer => | |.newer => | |.newer => | tail | * | A | | B | | C | | D | * |______| <= older.|______| <= older.|______| <= older.|______| * * removed <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- added */ /* eslint-disable */ const NEWER = Symbol('newer') const OLDER = Symbol('older') export function LRUMap(limit: number, entries?: any) { if (typeof limit !== 'number') { // called as (entries) entries = limit limit = 0 } this.size = 0 this.limit = limit this.oldest = this.newest = undefined this._keymap = new Map() if (entries) { this.assign(entries) if (limit < 1) { this.limit = this.size } } } function Entry(key: any, value: any) { this.key = key this.value = value this[NEWER] = undefined this[OLDER] = undefined } LRUMap.prototype._markEntryAsUsed = function(entry: any) { if (entry === this.newest) { // Already the most recenlty used entry, so no need to update the list return } // HEAD--------------TAIL // <.older .newer> // <--- add direction -- // A B C E if (entry[NEWER]) { if (entry === this.oldest) { this.oldest = entry[NEWER] } entry[NEWER][OLDER] = entry[OLDER] // C <-- E. } if (entry[OLDER]) { entry[OLDER][NEWER] = entry[NEWER] // C. --> E } entry[NEWER] = undefined // D --x entry[OLDER] = this.newest // D. --> E if (this.newest) { this.newest[NEWER] = entry // E. <-- D } this.newest = entry } LRUMap.prototype.assign = function(entries: any) { let entry: any let limit = this.limit || Number.MAX_VALUE this._keymap.clear() const it = entries[Symbol.iterator]() for (let itv = it.next(); !itv.done; itv = it.next()) { const e = new Entry(itv.value[0], itv.value[1]) this._keymap.set(e.key, e) if (!entry) { this.oldest = e } else { entry[NEWER] = e e[OLDER] = entry } entry = e if (limit-- === 0) { throw new Error('overflow') } } this.newest = entry this.size = this._keymap.size } LRUMap.prototype.get = function(key: any) { // First, find our cache entry const entry = this._keymap.get(key) if (!entry) { return } // Not cached. Sorry. // As was found in the cache, register it as being requested recently this._markEntryAsUsed(entry) return entry.value } LRUMap.prototype.set = function(key: any, value: any) { let entry = this._keymap.get(key) if (entry) { // update existing entry.value = value this._markEntryAsUsed(entry) return this } // new entry this._keymap.set(key, (entry = new Entry(key, value))) if (this.newest) { // link previous tail to the new tail (entry) this.newest[NEWER] = entry entry[OLDER] = this.newest } else { // we're first in -- yay this.oldest = entry } // add new entry to the end of the linked list -- it's now the freshest entry. this.newest = entry ++this.size if (this.size > this.limit) { // we hit the limit -- remove the head this.shift() } return this } LRUMap.prototype.shift = function() { // todo: handle special case when limit == 1 const entry = this.oldest if (entry) { if (this.oldest[NEWER]) { // advance the list this.oldest = this.oldest[NEWER] this.oldest[OLDER] = undefined } else { // the cache is exhausted this.oldest = undefined this.newest = undefined } // Remove last strong reference to and remove links from the purged // entry being returned: entry[NEWER] = entry[OLDER] = undefined this._keymap.delete(entry.key) --this.size return [entry.key, entry.value] } } // ---------------------------------------------------------------------------- // Following code is optional and can be removed without breaking the core // functionality. LRUMap.prototype.find = function(key: any) { const e = this._keymap.get(key) return e ? e.value : undefined } LRUMap.prototype.has = function(key: any) { return this._keymap.has(key) } LRUMap.prototype.delete = function(key: any) { const entry = this._keymap.get(key) if (!entry) { return } this._keymap.delete(entry.key) if (entry[NEWER] && entry[OLDER]) { // relink the older entry with the newer entry entry[OLDER][NEWER] = entry[NEWER] entry[NEWER][OLDER] = entry[OLDER] } else if (entry[NEWER]) { // remove the link to us entry[NEWER][OLDER] = undefined // link the newer entry to head this.oldest = entry[NEWER] } else if (entry[OLDER]) { // remove the link to us entry[OLDER][NEWER] = undefined // link the newer entry to head this.newest = entry[OLDER] } else { // if(entry[OLDER] === undefined && entry.newer === undefined) { this.oldest = this.newest = undefined } this.size-- return entry.value } LRUMap.prototype.clear = function() { // Not clearing links should be safe, as we don't expose live links to user this.oldest = this.newest = undefined this.size = 0 this._keymap.clear() } function EntryIterator(oldestEntry: any) { this.entry = oldestEntry } EntryIterator.prototype[Symbol.iterator] = function() { return this } EntryIterator.prototype.next = function() { const ent = this.entry if (ent) { this.entry = ent[NEWER] return { done: false, value: [ent.key, ent.value] } } else { return { done: true, value: undefined } } } function KeyIterator(oldestEntry) { this.entry = oldestEntry } KeyIterator.prototype[Symbol.iterator] = function() { return this } KeyIterator.prototype.next = function() { const ent = this.entry if (ent) { this.entry = ent[NEWER] return { done: false, value: ent.key } } else { return { done: true, value: undefined } } } function ValueIterator(oldestEntry) { this.entry = oldestEntry } ValueIterator.prototype[Symbol.iterator] = function() { return this } ValueIterator.prototype.next = function() { const ent = this.entry if (ent) { this.entry = ent[NEWER] return { done: false, value: ent.value } } else { return { done: true, value: undefined } } } LRUMap.prototype.keys = function() { return new KeyIterator(this.oldest) } LRUMap.prototype.values = function() { return new ValueIterator(this.oldest) } LRUMap.prototype.entries = function() { return this } LRUMap.prototype[Symbol.iterator] = function() { return new EntryIterator(this.oldest) } LRUMap.prototype.forEach = function( fun: (value: any, key: any, ctx: object) => void, thisObj: any ) { if (typeof thisObj !== 'object') { thisObj = this } let entry = this.oldest while (entry) { fun.call(thisObj, entry.value, entry.key, this) entry = entry[NEWER] } } /** Returns a JSON (array) representation */ LRUMap.prototype.toJSON = function() { const s = new Array(this.size) let i = 0 let entry = this.oldest while (entry) { s[i++] = { key: entry.key, value: entry.value } entry = entry[NEWER] } return s } /** Returns a String representation */ LRUMap.prototype.toString = function() { let s = '' let entry = this.oldest while (entry) { s += String(entry.key) + ':' + entry.value entry = entry[NEWER] if (entry) { s += ' < ' } } return s }