quick-lru
Version:
Simple “Least Recently Used” (LRU) cache
294 lines (243 loc) • 5.93 kB
JavaScript
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()]);
}
}