|
'use strict'; |
|
|
|
class QuickLRU { |
|
constructor(options = {}) { |
|
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 || Infinity; |
|
this.onEviction = options.onEviction; |
|
this.cache = new Map(); |
|
this.oldCache = new Map(); |
|
this._size = 0; |
|
} |
|
|
|
_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 === Infinity ? undefined : Date.now() + this.maxAge} = {}) { |
|
if (this.cache.has(key)) { |
|
this.cache.set(key, { |
|
value, |
|
maxAge |
|
}); |
|
} else { |
|
this._set(key, {value, expiry: maxAge}); |
|
} |
|
} |
|
|
|
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); |
|
} |
|
} |
|
|
|
module.exports = QuickLRU; |
|
|