284 lines
5.7 KiB
JavaScript
284 lines
5.7 KiB
JavaScript
export default class QuickLRU extends Map {
|
|
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');
|
|
}
|
|
|
|
// TODO: Use private class fields when ESLint supports them.
|
|
this.maxSize = options.maxSize;
|
|
this.maxAge = options.maxAge || Number.POSITIVE_INFINITY;
|
|
this.onEviction = options.onEviction;
|
|
this.cache = new Map();
|
|
this.oldCache = new Map();
|
|
this._size = 0;
|
|
}
|
|
|
|
// TODO: Use private class methods when targeting Node.js 16.
|
|
_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);
|
|
}
|
|
|
|
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()]);
|
|
}
|
|
}
|