least-recent
Version:
A cache object that deletes the least-recently-used items
364 lines (363 loc) • 11.3 kB
JavaScript
/**
* LRUCache
* ===================
*
* JavaScript implementation of the LRU Cache data structure. To save up
* memory and allocations this implementation represents its underlying
* doubly-linked list as static arrays and pointers. Thus, memory is allocated
* only once at instantiation and JS objects are never created to serve as
* pointers. This also means this implementation does not trigger too many
* garbage collections.
*
* Note that to save up memory, a LRU Cache can be implemented using a singly
* linked list by storing predecessors' pointers as hashmap values.
* However, this means more hashmap lookups and would probably slow the whole
* thing down. What's more, pointers are not the things taking most space in
* memory.
*/
import { createNanoEvents } from "nanoevents";
import { getPointerArray } from "./get-pointer-array.js";
function from(iterable, capacity = iterable.size || iterable.length || 0) {
const cache = new LRUCache(capacity);
for (const [key, value] of iterable) {
cache.set(key, value);
}
return cache;
}
/**
* LRUCache.
*
* @constructor
* @param {function} Keys - Array class for storing keys.
* @param {function} Values - Array class for storing values.
* @param {number} capacity - Desired capacity.
*/
export class LRUCache {
capacity;
forward;
backward;
K;
V;
events;
deleted;
_size;
head;
tail;
items;
deletedSize;
/**
* Take an arbitrary iterable and convert it into a structure.
*/
static from = from;
constructor(capacity) {
this.capacity = capacity;
if (!isFinite(this.capacity) || Math.floor(this.capacity) !== this.capacity || this.capacity <= 0) {
throw new Error("Capacity should be a finite positive integer.");
}
const PointerArray = getPointerArray(capacity);
this.forward = new PointerArray(capacity);
this.backward = new PointerArray(capacity);
this.K = new Array(capacity);
this.V = new Array(capacity);
this.events = createNanoEvents();
this._size = 0;
this.head = 0;
this.tail = 0;
this.items = {};
this.deleted = new PointerArray(capacity);
this.deletedSize = 0;
}
on(event, cb) {
return this.events.on(event, cb);
}
get size() {
return this._size;
}
/**
* Clear the structure.
*/
clear() {
this._size = 0;
this.head = 0;
this.tail = 0;
this.items = {};
this.deletedSize = 0;
}
/**
* Splay a value on top.
* @param pointer Pointer of the value to splay on top.
*/
splayOnTop(pointer) {
const oldHead = this.head;
if (this.head === pointer)
return;
const previous = this.backward[pointer];
const next = this.forward[pointer];
if (this.tail === pointer) {
this.tail = previous;
}
else {
this.backward[next] = previous;
}
this.forward[previous] = next;
this.backward[oldHead] = pointer;
this.head = pointer;
this.forward[pointer] = oldHead;
}
/**
* Set the value for the given key in the cache.
*/
set(key, value) {
let pointer = this.items[key];
// The key already exists, we just need to update the value and splay on top
if (pointer !== undefined) {
this.splayOnTop(pointer);
this.V[pointer] = value;
return;
}
// The cache is not yet full
if (this._size < this.capacity) {
if (this.deletedSize > 0) {
// If there is a "hole" in the pointer list, reuse it
pointer = this.deleted[--this.deletedSize];
}
else {
// otherwise append to the pointer list
pointer = this._size;
}
this._size++;
}
// Cache is full, we need to drop the last value
else {
pointer = this.tail;
this.tail = this.backward[pointer];
delete this.items[this.K[pointer]];
this.events.emit("evicted", this.K[pointer], this.V[pointer]);
}
this.storeKeyValue(pointer, key, value);
}
/**
* Set the value for the given key in the cache
* @param key
* @param value
* @return {{evicted: boolean, key: any, value: any}} An object containing the
* key and value of an item that was overwritten or evicted in the set
* operation, as well as a boolean indicating whether it was evicted due to
* limited capacity. Return value is null if nothing was evicted or overwritten
* during the set operation.
*/
setpop(key, value) {
let oldValue;
let oldKey;
let pointer = this.items[key];
// The key already exists, we just need to update the value and splay on top
if (pointer !== undefined) {
this.splayOnTop(pointer);
oldValue = this.V[pointer];
this.V[pointer] = value;
return { evicted: false, key: key, value: oldValue };
}
// The cache is not yet full
if (this._size < this.capacity) {
if (this.deletedSize > 0) {
// If there is a "hole" in the pointer list, reuse it
pointer = this.deleted[--this.deletedSize];
}
else {
// otherwise append to the pointer list
pointer = this._size;
}
this._size++;
}
// Cache is full, we need to drop the last value
else {
pointer = this.tail;
this.tail = this.backward[pointer];
oldValue = this.V[pointer];
oldKey = this.K[pointer];
delete this.items[this.K[pointer]];
this.events.emit("evicted", this.K[pointer], this.V[pointer]);
}
this.storeKeyValue(pointer, key, value);
// Return object if eviction took place, otherwise return null
if (oldKey) {
return { evicted: true, key: oldKey, value: oldValue };
}
else {
return null;
}
}
/**
* Delete the entry for the given key in the cache.
* Return `true` if the item was present.
*/
delete(key) {
let pointer = this.items[key];
if (pointer === undefined) {
return false;
}
delete this.items[key];
if (this._size === 1) {
this._size = 0;
this.head = 0;
this.tail = 0;
this.deletedSize = 0;
return true;
}
const previous = this.backward[pointer];
const next = this.forward[pointer];
if (this.head === pointer) {
this.head = next;
}
if (this.tail === pointer) {
this.tail = previous;
}
this.forward[previous] = next;
this.backward[next] = previous;
this._size--;
this.deleted[this.deletedSize++] = pointer;
return true;
}
/**
* Check whether the key exists in the cache.
*/
has(key) {
return key in this.items;
}
/**
* Get the value attached to the given key. Will move the
* related key to the front of the underlying linked list.
*/
get(key) {
const pointer = this.items[key];
if (pointer === undefined)
return;
this.splayOnTop(pointer);
return this.V[pointer];
}
/**
* Method used to get the value attached to the given key. Does not modify
* the ordering of the underlying linked list.
*/
peek(key) {
const pointer = this.items[key];
if (pointer === undefined)
return;
return this.V[pointer];
}
/**
* Create an iterator over the cache's keys from most
* recently used to least recently used.
*/
keys() {
let i = 0;
const l = this._size;
let pointer = this.head;
const keys = this.K;
const forward = this.forward;
return {
[Symbol.iterator]() {
return this;
},
next() {
if (i >= l)
return { done: true, value: undefined };
const key = keys[pointer];
i++;
if (i < l) {
pointer = forward[pointer];
}
return {
done: false,
value: key,
};
},
};
}
/**
* Create an iterator over the cache's values from most
* recently used to least recently used.
*/
values() {
let i = 0;
const l = this._size;
let pointer = this.head;
const values = this.V;
const forward = this.forward;
return {
[Symbol.iterator]() {
return this;
},
next() {
if (i >= l)
return { done: true, value: undefined };
const value = values[pointer];
i++;
if (i < l)
pointer = forward[pointer];
return {
done: false,
value: value,
};
},
};
}
/**
* Create an iterator over the cache's entries from most
* recently used to least recently used.
*/
entries() {
let i = 0;
const l = this._size;
let pointer = this.head;
const keys = this.K;
const values = this.V;
const forward = this.forward;
return {
[Symbol.iterator]() {
return this;
},
next() {
if (i >= l)
return { done: true, value: undefined };
const key = keys[pointer];
const value = values[pointer];
i++;
if (i < l)
pointer = forward[pointer];
return {
done: false,
value: [key, value],
};
},
};
}
inspect() {
const proxy = new Map();
let iterator = this.entries(), step;
while (((step = iterator.next()), !step.done))
proxy.set(step.value[0], step.value[1]);
// Trick so that node displays the name of the constructor
Object.defineProperty(proxy, "constructor", {
value: LRUCache,
enumerable: false,
});
return proxy;
}
storeKeyValue(pointer, key, value) {
// Storing key & value
this.items[key] = pointer;
this.K[pointer] = key;
this.V[pointer] = value;
// Moving the item at the front of the list
this.forward[pointer] = this.head;
this.backward[this.head] = pointer;
this.head = pointer;
}
[Symbol.iterator]() {
return this.entries();
}
[Symbol.for("nodejs.util.inspect.custom")]() {
return this.inspect();
}
}