"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LRUCache = void 0;
const LRUCacheNode_1 = require("./LRUCacheNode");
/**
 * A key value cache that implements the LRU policy.
 *
 * @typeparam TKey The type of the keys in the cache. Defaults to `string`.
 * @typeparam TValue The type of the values in the cache. Defaults to `any`.
 *
 * @see {@link https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)}
 */
class LRUCache {
    /**
     * Creates a new instance of the LRUCache.
     *
     * @param options Additional configuration options for the LRUCache.
     *
     * @example
     * ```typescript
     * // No options.
     * const cache = new LRUCache();
     *
     * // With options.
     * const cache = new LRUCache({
     *  entryExpirationTimeInMS: 10000
     * });
     * ```
     */
    constructor(options) {
        this.lookupTable = new Map();
        this.head = null;
        this.tail = null;
        const { maxSize = 25, entryExpirationTimeInMS = null, onEntryEvicted, onEntryMarkedAsMostRecentlyUsed, cloneFn, clone } = options !== null && options !== void 0 ? options : {};
        if (Number.isNaN(maxSize) || maxSize <= 0) {
            throw new Error('maxSize must be greater than 0.');
        }
        if (typeof entryExpirationTimeInMS === 'number' &&
            (entryExpirationTimeInMS <= 0 || Number.isNaN(entryExpirationTimeInMS))) {
            throw new Error('entryExpirationTimeInMS must either be null (no expiry) or greater than 0');
        }
        this.maxSizeInternal = maxSize;
        this.entryExpirationTimeInMS = entryExpirationTimeInMS;
        this.onEntryEvicted = onEntryEvicted;
        this.onEntryMarkedAsMostRecentlyUsed = onEntryMarkedAsMostRecentlyUsed;
        this.clone = clone;
        this.cloneFn = cloneFn;
    }
    /**
     * Returns the number of entries in the LRUCache object.
     * If the cache has entryExpirationTimeInMS set, expired entries will be removed before the size is returned.
     *
     * @returns The number of entries in the cache.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache();
     *
     * cache.set('testKey', 'testValue');
     *
     * const size = cache.size;
     *
     * // Will log 1
     * console.log(size);
     * ```
     */
    get size() {
        this.cleanCache();
        return this.lookupTable.size;
    }
    /**
     * Returns the number of entries that can still be added to the LRUCache without evicting existing entries.
     *
     * @returns The number of entries that can still be added without evicting existing entries.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache({ maxSize: 10 });
     *
     * cache.set('testKey', 'testValue');
     *
     * const remainingSize = cache.remainingSize;
     *
     * // Will log 9 due to 9 spots remaining before reaching maxSize of 10.
     * console.log(remainingSize);
     * ```
     */
    get remainingSize() {
        return this.maxSizeInternal - this.size;
    }
    /**
     * Returns the most recently used (newest) entry in the cache.
     * This will not mark the entry as recently used.
     * If the newest node is expired, it will be removed.
     *
     * @returns The most recently used (newest) entry in the cache.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache({ maxSize: 10 });
     *
     * cache.set('testKey', 'testValue');
     *
     * const newest = cache.newest;
     *
     * // Will log testValue
     * console.log(newest.value);
     *
     * // Will log testKey
     * console.log(newest.key);
     * ```
     */
    get newest() {
        if (!this.head) {
            return null;
        }
        if (this.head.isExpired) {
            this.removeNodeFromListAndLookupTable(this.head);
            return this.newest;
        }
        return this.mapNodeToEntry(this.head);
    }
    /**
     * Returns the least recently used (oldest) entry in the cache.
     * This will not mark the entry as recently used.
     * If the oldest node is expired, it will be removed.
     *
     * @returns The least recently used (oldest) entry in the cache.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache({ maxSize: 10 });
     *
     * cache.set('testKey', 'testValue');
     *
     * const oldest = cache.oldest;
     *
     * // Will log testValue
     * console.log(oldest.value);
     *
     * // Will log testKey
     * console.log(oldest.key);
     * ```
     */
    get oldest() {
        if (!this.tail) {
            return null;
        }
        if (this.tail.isExpired) {
            this.removeNodeFromListAndLookupTable(this.tail);
            return this.oldest;
        }
        return this.mapNodeToEntry(this.tail);
    }
    /**
     * Gets or sets the maxSize of the cache.
     * This will evict the least recently used entries if needed to reach new maxSize.
     *
     * @param value The new value for maxSize. Must be greater than 0.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache({ maxSize: 10 });
     *
     * cache.set('testKey', 'testValue');
     *
     * // Will be 10
     * const maxSize = cache.maxSize;
     *
     * // Set new maxSize to 5. If there are more than 5 items in the cache, the least recently used entries will be removed until cache size is 5.
     * cache.maxSize = 5;
     * ```
     */
    get maxSize() {
        return this.maxSizeInternal;
    }
    set maxSize(value) {
        if (Number.isNaN(value) || value <= 0) {
            throw new Error('maxSize must be greater than 0.');
        }
        this.maxSizeInternal = value;
        this.enforceSizeLimit();
    }
    /**
     * Sets the value for the key in the LRUCache object. Returns the LRUCache object.
     * This marks the newly added entry as the most recently used entry.
     * If adding the new entry makes the cache size go above maxSize,
     * this will evict the least recently used entries until size is equal to maxSize.
     *
     * @param key The key of the entry.
     * @param value The value to set for the key.
     * @param entryOptions Additional configuration options for the cache entry.
     *
     * @returns The LRUCache instance.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache();
     *
     * // Set the key testKey to value testValue
     * cache.set('testKey', 'testValue');
     *
     * // Set the key key2 to value value2. Pass in optional options.
     * cache.set('key2', 'value2', { entryExpirationTimeInMS: 10 });
     * ```
     */
    set(key, value, entryOptions) {
        const currentNodeForKey = this.lookupTable.get(key);
        if (currentNodeForKey) {
            this.removeNodeFromListAndLookupTable(currentNodeForKey);
        }
        const node = new LRUCacheNode_1.LRUCacheNode(key, value, {
            entryExpirationTimeInMS: this.entryExpirationTimeInMS,
            onEntryEvicted: this.onEntryEvicted,
            onEntryMarkedAsMostRecentlyUsed: this.onEntryMarkedAsMostRecentlyUsed,
            clone: this.clone,
            cloneFn: this.cloneFn,
            ...entryOptions
        });
        this.setNodeAsHead(node);
        this.lookupTable.set(key, node);
        this.enforceSizeLimit();
        return this;
    }
    /**
     * Returns the value associated to the key, or null if there is none or if the entry is expired.
     * If an entry is returned, this marks the returned entry as the most recently used entry.
     *
     * @param key The key of the entry to get.
     *
     * @returns The cached value or null.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache();
     *
     * // Set the key testKey to value testValue
     * cache.set('testKey', 'testValue');
     *
     * // Will be 'testValue'. Entry will now be most recently used.
     * const item1 = cache.get('testKey');
     *
     * // Will be null
     * const item2 = cache.get('keyNotInCache');
     * ```
     */
    get(key) {
        const node = this.lookupTable.get(key);
        if (!node) {
            return null;
        }
        if (node.isExpired) {
            this.removeNodeFromListAndLookupTable(node);
            return null;
        }
        this.setNodeAsHead(node);
        return node.value;
    }
    /**
     * Returns the value associated to the key, or null if there is none or if the entry is expired.
     * If an entry is returned, this will not mark the entry as most recently accessed.
     * Useful if a value is needed but the order of the cache should not be changed.
     *
     * @param key The key of the entry to get.
     *
     * @returns The cached value or null.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache();
     *
     * // Set the key testKey to value testValue
     * cache.set('testKey', 'testValue');
     *
     * // Will be 'testValue'
     * const item1 = cache.peek('testKey');
     *
     * // Will be null
     * const item2 = cache.peek('keyNotInCache');
     * ```
     */
    peek(key) {
        const node = this.lookupTable.get(key);
        if (!node) {
            return null;
        }
        if (node.isExpired) {
            this.removeNodeFromListAndLookupTable(node);
            return null;
        }
        return node.value;
    }
    /**
     * Deletes the entry for the passed in key.
     *
     * @param key The key of the entry to delete
     *
     * @returns True if an element in the LRUCache object existed and has been removed,
     * or false if the element does not exist.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache();
     *
     * // Set the key testKey to value testValue
     * cache.set('testKey', 'testValue');
     *
     * // Will be true
     * const wasDeleted = cache.delete('testKey');
     *
     * // Will be false
     * const wasDeleted2 = cache.delete('keyNotInCache');
     * ```
     */
    delete(key) {
        const node = this.lookupTable.get(key);
        if (!node) {
            return false;
        }
        return this.removeNodeFromListAndLookupTable(node);
    }
    /**
     * Returns a boolean asserting whether a value has been associated to the key in the LRUCache object or not.
     * This does not mark the entry as recently used.
     * If the cache has a key but the entry is expired, it will be removed and false will be returned.
     *
     * @param key The key of the entry to check if exists
     *
     * @returns true if the cache contains the supplied key. False if not.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache();
     *
     * // Set the key testKey to value testValue
     * cache.set('testKey', 'testValue');
     *
     * // Will be true
     * const wasDeleted = cache.has('testKey');
     *
     * // Will be false
     * const wasDeleted2 = cache.has('keyNotInCache');
     * ```
     */
    has(key) {
        const node = this.lookupTable.get(key);
        if (!node) {
            return false;
        }
        if (node.isExpired) {
            this.removeNodeFromListAndLookupTable(node);
            return false;
        }
        return true;
    }
    /**
     * Removes all entries in the cache.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache();
     *
     * // Set the key testKey to value testValue
     * cache.set('testKey', 'testValue');
     *
     * // Clear cache.
     * cache.clear();
     * ```
     */
    clear() {
        this.head = null;
        this.tail = null;
        this.lookupTable.clear();
    }
    /**
     * Searches the cache for an entry matching the passed in condition.
     * Expired entries will be skipped (and removed).
     * If multiply entries in the cache match the condition, the most recently used entry will be returned.
     * If an entry is returned, this marks the returned entry as the most recently used entry.
     *
     * @param condition The condition to apply to each entry in the
     *
     * @returns The first cache entry to match the condition. Null if none match.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache();
     *
     * // Set the key testKey to value testValue
     * cache.set('testKey', 'testValue');
     *
     * // item will be { key: 'testKey', value: 'testValue }
     * const item = cache.find(entry => {
     *   const { key, value } = entry;
     *
     *   if (key === 'testKey' || value === 'something') {
     *     return true;
     *   }
     *
     *   return false;
     * });
     *
     * // item2 will be null
     * const item2 = cache.find(entry => entry.key === 'notInCache');
     * ```
     */
    find(condition) {
        let node = this.head;
        while (node) {
            if (node.isExpired) {
                const next = node.next;
                this.removeNodeFromListAndLookupTable(node);
                node = next;
                continue;
            }
            const entry = this.mapNodeToEntry(node);
            if (condition(entry)) {
                this.setNodeAsHead(node);
                return entry;
            }
            node = node.next;
        }
        return null;
    }
    /**
     * Iterates over and applies the callback function to each entry in the cache.
     * Iterates in order from most recently accessed entry to least recently.
     * Expired entries will be skipped (and removed).
     * No entry will be marked as recently used.
     *
     * @param callback the callback function to apply to the entry
     *
     * @example
     * ```typescript
     * const cache = new LRUCache();
     *
     * // Set the key testKey to value testValue
     * cache.set('testKey', 'testValue');
     *
     * cache.forEach((key, value, index) => {
     *   // do something with key, value, and/or index
     * });
     * ```
     */
    forEach(callback) {
        let node = this.head;
        let index = 0;
        while (node) {
            if (node.isExpired) {
                const next = node.next;
                this.removeNodeFromListAndLookupTable(node);
                node = next;
                continue;
            }
            callback(node.value, node.key, index);
            node = node.next;
            index++;
        }
    }
    /**
     * Creates a Generator which can be used with for ... of ... to iterate over the cache values.
     * Iterates in order from most recently accessed entry to least recently.
     * Expired entries will be skipped (and removed).
     * No entry will be marked as accessed.
     *
     * @returns A Generator for the cache values.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache();
     *
     * // Set the key testKey to value testValue
     * cache.set('testKey', 'testValue');
     *
     * for (const value of cache.values()) {
     *   // do something with the value
     * }
     * ```
     */
    *values() {
        let node = this.head;
        while (node) {
            if (node.isExpired) {
                const next = node.next;
                this.removeNodeFromListAndLookupTable(node);
                node = next;
                continue;
            }
            yield node.value;
            node = node.next;
        }
    }
    /**
     * Creates a Generator which can be used with for ... of ... to iterate over the cache keys.
     * Iterates in order from most recently accessed entry to least recently.
     * Expired entries will be skipped (and removed).
     * No entry will be marked as accessed.
     *
     * @returns A Generator for the cache keys.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache();
     *
     * // Set the key testKey to value testValue
     * cache.set('testKey', 'testValue');
     *
     * for (const key of cache.keys()) {
     *   // do something with the key
     * }
     * ```
     */
    *keys() {
        let node = this.head;
        while (node) {
            if (node.isExpired) {
                const next = node.next;
                this.removeNodeFromListAndLookupTable(node);
                node = next;
                continue;
            }
            yield node.key;
            node = node.next;
        }
    }
    /**
     * Creates a Generator which can be used with for ... of ... to iterate over the cache entries.
     * Iterates in order from most recently accessed entry to least recently.
     * Expired entries will be skipped (and removed).
     * No entry will be marked as accessed.
     *
     * @returns A Generator for the cache entries.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache();
     *
     * // Set the key testKey to value testValue
     * cache.set('testKey', 'testValue');
     *
     * for (const entry of cache.entries()) {
     *   const { key, value } = entry;
     *   // do something with the entry
     * }
     * ```
     */
    *entries() {
        let node = this.head;
        while (node) {
            if (node.isExpired) {
                const next = node.next;
                this.removeNodeFromListAndLookupTable(node);
                node = next;
                continue;
            }
            yield this.mapNodeToEntry(node);
            node = node.next;
        }
    }
    /**
     * Creates a Generator which can be used with for ... of ... to iterate over the cache entries.
     * Iterates in order from most recently accessed entry to least recently.
     * Expired entries will be skipped (and removed).
     * No entry will be marked as accessed.
     *
     * @returns A Generator for the cache entries.
     *
     * @example
     * ```typescript
     * const cache = new LRUCache();
     *
     * // Set the key testKey to value testValue
     * cache.set('testKey', 'testValue');
     *
     * for (const entry of cache) {
     *   const { key, value } = entry;
     *   // do something with the entry
     * }
     * ```
     */
    *[Symbol.iterator]() {
        let node = this.head;
        while (node) {
            if (node.isExpired) {
                const next = node.next;
                this.removeNodeFromListAndLookupTable(node);
                node = next;
                continue;
            }
            yield this.mapNodeToEntry(node);
            node = node.next;
        }
    }
    enforceSizeLimit() {
        let node = this.tail;
        while (node !== null && this.size > this.maxSizeInternal) {
            const prev = node.prev;
            this.removeNodeFromListAndLookupTable(node);
            node = prev;
        }
    }
    mapNodeToEntry({ key, value }) {
        return {
            key,
            value
        };
    }
    setNodeAsHead(node) {
        this.removeNodeFromList(node);
        if (!this.head) {
            this.head = node;
            this.tail = node;
        }
        else {
            node.next = this.head;
            this.head.prev = node;
            this.head = node;
        }
        node.invokeOnEntryMarkedAsMostRecentlyUsed();
    }
    removeNodeFromList(node) {
        if (node.prev !== null) {
            node.prev.next = node.next;
        }
        if (node.next !== null) {
            node.next.prev = node.prev;
        }
        if (this.head === node) {
            this.head = node.next;
        }
        if (this.tail === node) {
            this.tail = node.prev;
        }
        node.next = null;
        node.prev = null;
    }
    removeNodeFromListAndLookupTable(node) {
        node.invokeOnEvicted();
        this.removeNodeFromList(node);
        return this.lookupTable.delete(node.key);
    }
    cleanCache() {
        // Don't spend time cleaning if entries don't expire.
        if (!this.entryExpirationTimeInMS) {
            return;
        }
        const expiredNodes = [];
        for (const node of this.lookupTable.values()) {
            if (node.isExpired) {
                expiredNodes.push(node);
            }
        }
        expiredNodes.forEach(node => this.removeNodeFromListAndLookupTable(node));
    }
}
exports.LRUCache = LRUCache;
//# sourceMappingURL=LRUCache.js.map