@evolplus/evo-utils
Version:
Simple utilities solution.
262 lines (261 loc) • 8.37 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DecayCache = exports.LRUCache = void 0;
/**
* Abstract class representing the basic structure and functionality of a cache.
* Provides foundational methods and properties for specific cache implementations.
*
* @abstract
* @template T The type of items that the cache will store.
*/
class BasicCache {
/**
* Creates an instance of the BasicCache class.
* Initializes the cache with a specified capacity and sets up head and tail pointers for cache item order.
*
* @param {number} capacity - The maximum number of items the cache can hold.
*/
constructor(capacity) {
this.map = {};
this._capacity = capacity;
this._count = 0;
this.head = {
lastUpdate: 0
};
this.tail = {
prev: this.head,
lastUpdate: 0
};
this.head.next = this.tail;
}
/**
* Adds an item to the cache or updates an existing item's value.
* If the cache reaches its capacity, it evicts an item based on the specific cache's eviction policy.
*
* @param {string} key - The key associated with the item.
* @param {T} value - The value to be stored in the cache.
* @param {number} [expiry] - Optional expiry time for the item.
*/
put(key, value, expiry) {
let item = this.map[key];
if (!item) {
item = {
key: key,
value: value,
lastUpdate: Date.now()
};
if (this._count == this._capacity) {
this.remove(this.tail.prev);
}
else {
this._count++;
}
this.map[key] = item;
this.attachItem(item);
}
else {
item.value = value;
item.lastUpdate = Date.now();
}
if (expiry) {
item.expiry = Date.now() + expiry;
}
else {
item.expiry = 0;
}
}
/**
* Retrieves an item from the cache based on its key.
*
* @param {string} key - The key associated with the item to be retrieved.
* @returns {T | undefined} The value of the item if it exists, or undefined if it doesn't.
*/
get(key) {
let item = this.map[key];
if (item) {
if (item.key && item.expiry && item.expiry < Date.now()) {
delete this.map[key];
this.remove(item);
}
else {
this.hitItem(item);
return item.value;
}
}
}
/**
* Check a key is in the cache or not.
*
* @param {string} key - The key associated with the item to be checked.
* @returns {boolean} True if the key is in the cache, otherwise false.
*/
contains(key) {
let item = this.map[key];
if (item) {
if (item.key && item.expiry && item.expiry < Date.now()) {
delete this.map[key];
this.remove(item);
}
else {
return true;
}
}
return false;
}
/**
* Removes an item from the cache based on its key.
*
* @param {string} key - The key associated with the item to be removed.
*/
delete(key) {
let item = this.map[key];
if (item) {
this.remove(item);
delete this.map[key];
}
}
/**
* Protected method that remove a cache item from the linked list.
* @param item Item being to be removed.
*/
remove(item) {
let prev = item.prev;
if (prev && item.next) {
prev.next = item.next;
item.next.prev = prev;
}
}
/**
* Protected method that insert a cache item just after another item in the linked list.
* @param item The item which already is in the list. The new item will be inserted at the position right after this item.
* @param newItem The item that will be inserted into the list.
*/
insertAfter(item, newItem) {
newItem.next = item.next;
newItem.prev = item;
item.next.prev = newItem;
item.next = newItem;
}
/**
* Protected function that allow caller to query a cache item by its key.
* @param key The key for querying.
* @returns The cache item if it is found, otherwise returns undefined.
*/
getItem(key) {
return this.map[key];
}
/**
* Returns the maximum capacity of the cache.
*
* @returns {number} The maximum number of items the cache can hold.
*/
capacity() {
return this._capacity;
}
/**
* Returns the current number of items in the cache.
*
* @returns {number} The number of items currently stored in the cache.
*/
count() {
return this._count;
}
}
/**
* LRUCache class that extends BasicCache to implement the Least Recently Used (LRU) cache eviction policy.
* Items that are accessed recently are moved to the front,
* and the least recently used items are removed when the cache reaches its capacity.
*
* @extends {BasicCache<T>}
* @template T The type of items that the cache will store.
*/
class LRUCache extends BasicCache {
/**
* Creates an instance of the LRUCache class with a specified capacity.
*
* @param {number} capacity - The maximum number of items the cache can hold.
*/
constructor(capacity) {
super(capacity);
}
attachItem(item) {
this.insertAfter(this.head, item); // always insert to the head
}
hitItem(item) {
this.remove(item);
this.insertAfter(this.head, item); // always insert to the head
}
}
exports.LRUCache = LRUCache;
/**
* DecayCache class that extends BasicCache to implement a decay-based cache eviction policy.
* Cache items have a score which decays over time. The score increases with each access,
* and the item's position in the cache is adjusted based on its score relative to other items.
*
* @extends {BasicCache<T>}
* @template T The type of items that the cache will store.
*/
class DecayCache extends BasicCache {
/**
* Creates an instance of the DecayCache class with a specified capacity and half-life.
*
* @param {number} capacity - The maximum number of items the cache can hold.
* @param {number} [halfLife=300000] - The time (in milliseconds) after which the score of a cache item is halved.
*/
constructor(capacity, halfLife = 300000) {
super(capacity);
this.halfLife = halfLife;
}
/**
*
* @param item Calculate decay of an item.
* @param ts Timestamp of the time when this item is calculated.
*/
calculate(item, ts) {
let delta = (item.lastUpdate - ts) / this.halfLife;
item.lastUpdate = ts;
item.score = item.score ? item.score * Math.pow(2, delta) : 0;
}
attachItem(item) {
// calculate the decay score of the item and find a right position for it to make the list in order.
item.score = 1;
let next = this.head.next, now = Date.now();
while (next != this.tail) {
this.calculate(next, now);
if (next.score < 1) {
break;
}
next = next.next;
}
this.insertAfter(next.prev, item);
}
hitItem(item) {
// calculate the decay score of the item and items before it, then move it to the right postion to make the list in order.
let now = Date.now();
this.calculate(item, now);
item.score += 1;
let prev = item.prev;
while (prev != this.head && prev.score < item.score) {
prev = prev.prev;
}
if (prev != item.prev) {
this.remove(item);
this.insertAfter(prev, item);
}
}
/**
* A special method for DecayCache class.
* @param key The key to be hit
* @returns The decay score of the key.
*/
hit(key) {
let item = this.getItem(key), now = Date.now();
if (item) {
this.calculate(item, now);
item.score += 1;
return item.score;
}
return 0;
}
}
exports.DecayCache = DecayCache;