Source: components/client-request-cache.js

/**
 * Caches requests in memory and handles expiring them.
 */
class ClientRequestCache {
    /**
     * @param ttl {Number} How old a result can be before it gets expired.
     * @param size {Number} How many results to store before we trim.
     * @param requestFunc The function to use on cache miss.
     */
    constructor (ttl, size, requestFunc) {
        if (!Number.isInteger(ttl) || ttl <= 0) {
            throw Error("'ttl' must be greater than 0");
        }
        if (!Number.isInteger(size) || ttl <= 0) {
            throw Error("'size' must be greater than 0");
        }
        if (typeof(requestFunc) !== "function") {
            throw Error("'requestFunc' must be a function");
        }
        this._requestContent = new Map(); // key => {ts, content}
        this.requestFunc = requestFunc;
        this.ttl = ttl;
        this.maxSize = size;
    }

    /**
     * Gets a result of a request from the cache, or otherwise
     * tries to fetch the the result with this.requestFunc
     *
     * @param {string}} key Key of the item to get/store.
     * @param {any[]} args A set of arguments to pass to the request func.
     * @returns {Promise} The request, or undefined if not retrievable.
     * @throws {Error} If the key is not a string.
     */
    get(key, ...args) {
        if (typeof(key) !== "string") {
            throw Error("'key' must be a string");
        }
        const cachedResult = this._requestContent.get(key);
        if (cachedResult !== undefined && cachedResult.ts >= Date.now() - this.ttl) {
            return cachedResult.content;
        }
        // Delete the old req.
        this._requestContent.delete(key);
        return new Promise((resolve, reject) => {
            resolve(this.requestFunc.apply(null, [key].concat(args)))
        }).then((result) => {
            if (result !== undefined) {
                this._requestContent.set(key, {
                    ts: Date.now(),
                    content: result,
                });
                if (this._requestContent.size > this.maxSize) {
                    const oldKey = this._requestContent.keys().next().value;
                    this._requestContent.delete(oldKey);
                }
            }
            return result;
        });
        // Not catching here because we want to pass 
        // through any failures.
    }

    /**
     * Clone the current request result cache, mapping keys to their cache records.
     * @returns {Map<string,any>}
     */
    getCachedResults() {
        return new Map(this._requestContent);
    }

    /**
     * @callback requestFunc
     * @param {any[]} args A set of arguments passed from get().
     * @param {string} key The key for the cached item.
     */
}

module.exports = ClientRequestCache;