LRU.js

/**
 * A Simple LRU Map
 * This maintains 2 maps internally and swaps them when one becomes full
 * NOTE: At any time size of the map will be from 0 to 2 * maxItems
 *
 * @example
 * const lru = new LRU({maxItems: 1000});
 * lru.set('hello', 'world');
 * lru.get('hello');
 * lru.delete('hello');
 */
class LRU {
	/**
	 * @param {object} [options={}]
	 * @param {number} [options.maxItems] max items in the lru map
	 */
	constructor(options = {}) {
		if (!(options.maxItems && options.maxItems > 0)) {
			throw new TypeError('`maxItems` must be a number greater than 0');
		}

		this.maxItems = options.maxItems;
		this.cache = new Map();
		this.oldCache = new Map();
		this._size = 0;
	}

	_set(key, value) {
		this.cache.set(key, value);

		if (this.cache.size >= this.maxItems) {
			this._size = this.cache.size;
			this.oldCache = this.cache;
			this.cache = new Map();
		}
	}

	/**
	 * gets a value from the lru map
	 * @param {any} key
	 * @returns {any}
	 */
	get(key) {
		const value = this.cache.get(key);
		if (value !== undefined) return value;

		const oldValue = this.oldCache.get(key);
		if (oldValue) {
			this._set(key, oldValue);
			return oldValue;
		}

		return undefined;
	}

	/**
	 * sets a value in the lru map
	 * @param {any} key
	 * @param {any} value
	 */
	set(key, value) {
		if (this.cache.has(key)) {
			this.cache.set(key, value);
		}
		else {
			this._size++;
			this._set(key, value);
		}

		return this;
	}

	/**
	 * returns whether a value exists in the lru map
	 * @param {any} key
	 * @returns {boolean}
	 */
	has(key) {
		return this.cache.has(key) || this.oldCache.has(key);
	}

	/**
	 * gets a value from the lru map without touching the lru sequence
	 * @param {any} key
	 * @returns {any}
	 */
	peek(key) {
		const value = this.cache.get(key);
		if (value !== undefined) return value;

		const oldValue = this.oldCache.get(key);
		if (oldValue) return oldValue;

		return undefined;
	}

	/**
	 * deletes a value from the lru map
	 * @param {any} key
	 * @returns {boolean} whether any key was deleted
	 */
	delete(key) {
		const newDeleted = this.cache.delete(key);
		const oldDeleted = this.oldCache.delete(key);
		const deleted = oldDeleted || newDeleted;
		if (deleted) {
			this._size--;
		}

		return deleted;
	}

	/**
	 * removes all values from the lru map
	 */
	clear() {
		this.cache.clear();
		this.oldCache.clear();
		this._size = 0;
	}

	/**
	 * return an iterator over the keys of the lru map
	 */
	* keys() {
		for (const [key] of this) {
			yield key;
		}
	}

	/**
	 * return an iterator over the values of the lru map
	 */
	* values() {
		for (const [, value] of this) {
			yield value;
		}
	}

	/**
	 * return an iterator over the entries (key, value) of the lru map
	 */
	* [Symbol.iterator]() {
		for (const item of this.cache) {
			yield item;
		}

		for (const item of this.oldCache) {
			const [key] = item;
			if (!this.cache.has(key)) {
				yield item;
			}
		}
	}

	/**
	 * returns the size of the lru map (number of items in the map)
	 * @returns {number}
	 */
	get size() {
		return this._size;
	}

	/**
	 * Total size (including old + new) of the LRU cache
	 * @returns {number}
	 */
	totalSize() {
		return this.cache.size + this.oldCache.size;
	}
}

module.exports = LRU;