"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LocalCache = void 0;
const metrics_1 = require("../metrics");
const util_1 = require("../util");
const metrics_2 = require("./metrics");
const logger = (0, util_1.makeLogger)('LocalCache');
/**
 * Local implementation of a Cache. It uses a DoubleLinkedList class for storing cache nodes.
 * Each LinkedListNode is storing entries with both value and an expiration timestamp.
 * Expired entries are deleted on reads (i.e. no background gc/upkeep).
 * @typeParam T - the type for the entries' values
 */
class LocalCache {
    constructor(capacity) {
        this.type = metrics_2.CacheTypes.Local;
        this.capacity = capacity;
        this.cache = new Map();
        this.list = new util_1.DoubleLinkedList();
    }
    async get(key) {
        (0, util_1.censorLogs)(() => logger.trace(`Getting key ${key}`));
        if (this.cache.has(key)) {
            const node = this.cache.get(key);
            const expired = node.data.expirationTimestamp <= Date.now();
            if (expired) {
                logger.debug('Entry in local cache expired, deleting and returning undefined');
                this.delete(key);
                return undefined;
            }
            else {
                logger.debug('Found valid entry in local cache, returning value');
                return node.data.value;
            }
        }
        else {
            (0, util_1.censorLogs)(() => logger.debug(`No entry in local cache for key "${key}", returning undefined`));
            return undefined;
        }
    }
    async delete(key) {
        (0, util_1.censorLogs)(() => logger.trace(`Deleting key ${key}`));
        if (this.cache.has(key)) {
            const node = this.cache.get(key);
            this.list.remove(node);
            this.cache.delete(key);
        }
    }
    async set(key, value, ttl) {
        (0, util_1.censorLogs)(() => logger.trace(`Setting key ${key} with ttl ${ttl}`));
        if (this.cache.has(key)) {
            (0, util_1.censorLogs)(() => logger.trace(`Found existing key ${key}. Updating value...`));
            const node = this.cache.get(key);
            const isCacheExpired = node.data.expirationTimestamp <= Date.now();
            // If the new value is an error, we don't want to overwrite the current active(not expired) 'successful' cache. If both cached and new values are errors or successful entries, the cache will be updated.
            if ('errorMessage' in value &&
                !isCacheExpired &&
                !('errorMessage' in node.data.value)) {
                (0, util_1.censorLogs)(() => logger.trace(`New value for key ${key} is an error, not updating existing value`));
            }
            else {
                node.data = {
                    value,
                    expirationTimestamp: Date.now() + ttl,
                };
                // When existing key is updated we move it to the end of the list as we keep recently updated entries there
                this.moveToTail(node);
            }
        }
        else {
            // For new cache entries check if we reached maximum size to delete least recently updated entry and free up space
            this.evictIfNeeded();
            const data = {
                value,
                expirationTimestamp: Date.now() + ttl,
            };
            const node = new util_1.LinkedListNode(key, data);
            // New entries are always added at the end of the list
            this.list.insertAtTail(node);
            this.cache.set(key, node);
        }
    }
    async setMany(entries, ttl) {
        logger.trace(`Setting a bunch of keys with ttl ${ttl}`);
        for (const { key, value } of entries) {
            this.set(key, value, ttl);
        }
    }
    async setTTL(key, ttl) {
        (0, util_1.censorLogs)(() => logger.trace(`Updating key ${key} with a new ttl ${ttl}`));
        if (this.cache.has(key)) {
            const node = this.cache.get(key);
            const isCacheExpired = node.data.expirationTimestamp <= Date.now();
            if (isCacheExpired) {
                return this.delete(key);
            }
            node.data = {
                ...node.data,
                expirationTimestamp: Date.now() + ttl,
            };
            // When ttl is updated, we move the node to the end of the list as we keep recently updated entries there
            this.moveToTail(node);
        }
    }
    evictIfNeeded() {
        if (this.list.size >= this.capacity) {
            logger.warn(`Cache list reached maximum capacity, evicting least recently updated entry`);
            const node = this.list.removeHead();
            if (node) {
                this.cache.delete(node.key);
                metrics_1.metrics.get('cacheOverflowCount').inc();
            }
        }
    }
    moveToTail(node) {
        this.list.remove(node);
        this.list.insertAtTail(node);
    }
}
exports.LocalCache = LocalCache;
//# sourceMappingURL=local.js.map