UNPKG

@evolplus/evo-utils

Version:
262 lines (261 loc) 8.37 kB
"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;