mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-11-04 21:56:12 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			918 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			918 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
let url = require("url");
 | 
						|
let MemoryCache = require("./memory-cache");
 | 
						|
 | 
						|
let t = {
 | 
						|
    ms: 1,
 | 
						|
    second: 1000,
 | 
						|
    minute: 60000,
 | 
						|
    hour: 3600000,
 | 
						|
    day: 3600000 * 24,
 | 
						|
    week: 3600000 * 24 * 7,
 | 
						|
    month: 3600000 * 24 * 30,
 | 
						|
};
 | 
						|
 | 
						|
let instances = [];
 | 
						|
 | 
						|
/**
 | 
						|
 * Does a === b
 | 
						|
 * @param {any} a
 | 
						|
 * @returns {function(any): boolean}
 | 
						|
 */
 | 
						|
let matches = function (a) {
 | 
						|
    return function (b) {
 | 
						|
        return a === b;
 | 
						|
    };
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Does a!==b
 | 
						|
 * @param {any} a
 | 
						|
 * @returns {function(any): boolean}
 | 
						|
 */
 | 
						|
let doesntMatch = function (a) {
 | 
						|
    return function (b) {
 | 
						|
        return !matches(a)(b);
 | 
						|
    };
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Get log duration
 | 
						|
 * @param {number} d Time in ms
 | 
						|
 * @param {string} prefix Prefix for log
 | 
						|
 * @returns {string} Coloured log string
 | 
						|
 */
 | 
						|
let logDuration = function (d, prefix) {
 | 
						|
    let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms";
 | 
						|
    return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m";
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Get safe headers
 | 
						|
 * @param {Object} res Express response object
 | 
						|
 * @returns {Object}
 | 
						|
 */
 | 
						|
function getSafeHeaders(res) {
 | 
						|
    return res.getHeaders ? res.getHeaders() : res._headers;
 | 
						|
}
 | 
						|
 | 
						|
/** Constructor for ApiCache instance */
 | 
						|
function ApiCache() {
 | 
						|
    let memCache = new MemoryCache();
 | 
						|
 | 
						|
    let globalOptions = {
 | 
						|
        debug: false,
 | 
						|
        defaultDuration: 3600000,
 | 
						|
        enabled: true,
 | 
						|
        appendKey: [],
 | 
						|
        jsonp: false,
 | 
						|
        redisClient: false,
 | 
						|
        headerBlacklist: [],
 | 
						|
        statusCodes: {
 | 
						|
            include: [],
 | 
						|
            exclude: [],
 | 
						|
        },
 | 
						|
        events: {
 | 
						|
            expire: undefined,
 | 
						|
        },
 | 
						|
        headers: {
 | 
						|
            // 'cache-control':  'no-cache' // example of header overwrite
 | 
						|
        },
 | 
						|
        trackPerformance: false,
 | 
						|
        respectCacheControl: false,
 | 
						|
    };
 | 
						|
 | 
						|
    let middlewareOptions = [];
 | 
						|
    let instance = this;
 | 
						|
    let index = null;
 | 
						|
    let timers = {};
 | 
						|
    let performanceArray = []; // for tracking cache hit rate
 | 
						|
 | 
						|
    instances.push(this);
 | 
						|
    this.id = instances.length;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Logs a message to the console if the `DEBUG` environment variable is set.
 | 
						|
     * @param {string} a The first argument to log.
 | 
						|
     * @param {string} b The second argument to log.
 | 
						|
     * @param {string} c The third argument to log.
 | 
						|
     * @param {string} d The fourth argument to log, and so on... (optional)
 | 
						|
     *
 | 
						|
     * Generated by Trelent
 | 
						|
     */
 | 
						|
    function debug(a, b, c, d) {
 | 
						|
        let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) {
 | 
						|
            return arg !== undefined;
 | 
						|
        });
 | 
						|
        let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1;
 | 
						|
 | 
						|
        return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns true if the given request and response should be logged.
 | 
						|
     * @param {Object} request The HTTP request object.
 | 
						|
     * @param {Object} response The HTTP response object.
 | 
						|
     * @param {function(Object, Object):boolean} toggle
 | 
						|
     * @returns {boolean}
 | 
						|
     */
 | 
						|
    function shouldCacheResponse(request, response, toggle) {
 | 
						|
        let opt = globalOptions;
 | 
						|
        let codes = opt.statusCodes;
 | 
						|
 | 
						|
        if (!response) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if (toggle && !toggle(request, response)) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if (codes.exclude && codes.exclude.length && codes.exclude.indexOf(response.statusCode) !== -1) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        if (codes.include && codes.include.length && codes.include.indexOf(response.statusCode) === -1) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Add key to index array
 | 
						|
     * @param {string} key Key to add
 | 
						|
     * @param {Object} req Express request object
 | 
						|
     */
 | 
						|
    function addIndexEntries(key, req) {
 | 
						|
        let groupName = req.apicacheGroup;
 | 
						|
 | 
						|
        if (groupName) {
 | 
						|
            debug("group detected \"" + groupName + "\"");
 | 
						|
            let group = (index.groups[groupName] = index.groups[groupName] || []);
 | 
						|
            group.unshift(key);
 | 
						|
        }
 | 
						|
 | 
						|
        index.all.unshift(key);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns a new object containing only the whitelisted headers.
 | 
						|
     * @param {Object} headers The original object of header names and
 | 
						|
     * values.
 | 
						|
     * @param {string[]} globalOptions.headerWhitelist An array of
 | 
						|
     * strings representing the whitelisted header names to keep in the
 | 
						|
     * output object.
 | 
						|
     *
 | 
						|
     * Generated by Trelent
 | 
						|
     */
 | 
						|
    function filterBlacklistedHeaders(headers) {
 | 
						|
        return Object.keys(headers)
 | 
						|
            .filter(function (key) {
 | 
						|
                return globalOptions.headerBlacklist.indexOf(key) === -1;
 | 
						|
            })
 | 
						|
            .reduce(function (acc, header) {
 | 
						|
                acc[header] = headers[header];
 | 
						|
                return acc;
 | 
						|
            }, {});
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Create a cache object
 | 
						|
     * @param {Object} headers The response headers to filter.
 | 
						|
     * @returns {Object} A new object containing only the whitelisted
 | 
						|
     * response headers.
 | 
						|
     *
 | 
						|
     * Generated by Trelent
 | 
						|
     */
 | 
						|
    function createCacheObject(status, headers, data, encoding) {
 | 
						|
        return {
 | 
						|
            status: status,
 | 
						|
            headers: filterBlacklistedHeaders(headers),
 | 
						|
            data: data,
 | 
						|
            encoding: encoding,
 | 
						|
            timestamp: new Date().getTime() / 1000, // seconds since epoch.  This is used to properly decrement max-age headers in cached responses.
 | 
						|
        };
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Sets a cache value for the given key.
 | 
						|
     * @param {string} key The cache key to set.
 | 
						|
     * @param {any} value The cache value to set.
 | 
						|
     * @param {number} duration How long in milliseconds the cached
 | 
						|
     * response should be valid for (defaults to 1 hour).
 | 
						|
     *
 | 
						|
     * Generated by Trelent
 | 
						|
     */
 | 
						|
    function cacheResponse(key, value, duration) {
 | 
						|
        let redis = globalOptions.redisClient;
 | 
						|
        let expireCallback = globalOptions.events.expire;
 | 
						|
 | 
						|
        if (redis && redis.connected) {
 | 
						|
            try {
 | 
						|
                redis.hset(key, "response", JSON.stringify(value));
 | 
						|
                redis.hset(key, "duration", duration);
 | 
						|
                redis.expire(key, duration / 1000, expireCallback || function () {});
 | 
						|
            } catch (err) {
 | 
						|
                debug("[apicache] error in redis.hset()");
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            memCache.add(key, value, duration, expireCallback);
 | 
						|
        }
 | 
						|
 | 
						|
        // add automatic cache clearing from duration, includes max limit on setTimeout
 | 
						|
        timers[key] = setTimeout(function () {
 | 
						|
            instance.clear(key, true);
 | 
						|
        }, Math.min(duration, 2147483647));
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Appends content to the response.
 | 
						|
     * @param {Object} res Express response object
 | 
						|
     * @param {(string|Buffer)} content The content to append.
 | 
						|
     *
 | 
						|
     * Generated by Trelent
 | 
						|
     */
 | 
						|
    function accumulateContent(res, content) {
 | 
						|
        if (content) {
 | 
						|
            if (typeof content == "string") {
 | 
						|
                res._apicache.content = (res._apicache.content || "") + content;
 | 
						|
            } else if (Buffer.isBuffer(content)) {
 | 
						|
                let oldContent = res._apicache.content;
 | 
						|
 | 
						|
                if (typeof oldContent === "string") {
 | 
						|
                    oldContent = !Buffer.from ? new Buffer(oldContent) : Buffer.from(oldContent);
 | 
						|
                }
 | 
						|
 | 
						|
                if (!oldContent) {
 | 
						|
                    oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0);
 | 
						|
                }
 | 
						|
 | 
						|
                res._apicache.content = Buffer.concat(
 | 
						|
                    [oldContent, content],
 | 
						|
                    oldContent.length + content.length
 | 
						|
                );
 | 
						|
            } else {
 | 
						|
                res._apicache.content = content;
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Monkeypatches the response object to add cache control headers
 | 
						|
     * and create a cache object.
 | 
						|
     * @param {Object} req Express request object
 | 
						|
     * @param {Object} res Express response object
 | 
						|
     * @param {function} next Function to call next
 | 
						|
     * @param {string} key Key to add response as
 | 
						|
     * @param {number} duration Time to cache response for
 | 
						|
     * @param {string} strDuration Duration in string form
 | 
						|
     * @param {function(Object, Object):boolean} toggle
 | 
						|
     */
 | 
						|
    function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
 | 
						|
    // monkeypatch res.end to create cache object
 | 
						|
        res._apicache = {
 | 
						|
            write: res.write,
 | 
						|
            writeHead: res.writeHead,
 | 
						|
            end: res.end,
 | 
						|
            cacheable: true,
 | 
						|
            content: undefined,
 | 
						|
        };
 | 
						|
 | 
						|
        // append header overwrites if applicable
 | 
						|
        Object.keys(globalOptions.headers).forEach(function (name) {
 | 
						|
            res.setHeader(name, globalOptions.headers[name]);
 | 
						|
        });
 | 
						|
 | 
						|
        res.writeHead = function () {
 | 
						|
            // add cache control headers
 | 
						|
            if (!globalOptions.headers["cache-control"]) {
 | 
						|
                if (shouldCacheResponse(req, res, toggle)) {
 | 
						|
                    res.setHeader("cache-control", "max-age=" + (duration / 1000).toFixed(0));
 | 
						|
                } else {
 | 
						|
                    res.setHeader("cache-control", "no-cache, no-store, must-revalidate");
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            res._apicache.headers = Object.assign({}, getSafeHeaders(res));
 | 
						|
            return res._apicache.writeHead.apply(this, arguments);
 | 
						|
        };
 | 
						|
 | 
						|
        // patch res.write
 | 
						|
        res.write = function (content) {
 | 
						|
            accumulateContent(res, content);
 | 
						|
            return res._apicache.write.apply(this, arguments);
 | 
						|
        };
 | 
						|
 | 
						|
        // patch res.end
 | 
						|
        res.end = function (content, encoding) {
 | 
						|
            if (shouldCacheResponse(req, res, toggle)) {
 | 
						|
                accumulateContent(res, content);
 | 
						|
 | 
						|
                if (res._apicache.cacheable && res._apicache.content) {
 | 
						|
                    addIndexEntries(key, req);
 | 
						|
                    let headers = res._apicache.headers || getSafeHeaders(res);
 | 
						|
                    let cacheObject = createCacheObject(
 | 
						|
                        res.statusCode,
 | 
						|
                        headers,
 | 
						|
                        res._apicache.content,
 | 
						|
                        encoding
 | 
						|
                    );
 | 
						|
                    cacheResponse(key, cacheObject, duration);
 | 
						|
 | 
						|
                    // display log entry
 | 
						|
                    let elapsed = new Date() - req.apicacheTimer;
 | 
						|
                    debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed));
 | 
						|
                    debug("_apicache.headers: ", res._apicache.headers);
 | 
						|
                    debug("res.getHeaders(): ", getSafeHeaders(res));
 | 
						|
                    debug("cacheObject: ", cacheObject);
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            return res._apicache.end.apply(this, arguments);
 | 
						|
        };
 | 
						|
 | 
						|
        next();
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Send a cached response to client
 | 
						|
     * @param {Request} request Express request object
 | 
						|
     * @param {Response} response Express response object
 | 
						|
     * @param {object} cacheObject Cache object to send
 | 
						|
     * @param {function(Object, Object):boolean} toggle
 | 
						|
     * @param {function} next Function to call next
 | 
						|
     * @param {number} duration Not used
 | 
						|
     * @returns {boolean|undefined} true if the request should be
 | 
						|
     * cached, false otherwise. If undefined, defaults to true.
 | 
						|
     */
 | 
						|
    function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
 | 
						|
        if (toggle && !toggle(request, response)) {
 | 
						|
            return next();
 | 
						|
        }
 | 
						|
 | 
						|
        let headers = getSafeHeaders(response);
 | 
						|
 | 
						|
        // Modified by @louislam, removed Cache-control, since I don't need client side cache!
 | 
						|
        // Original Source: https://github.com/kwhitley/apicache/blob/0d5686cc21fad353c6dddee646288c2fca3e4f50/src/apicache.js#L254
 | 
						|
        Object.assign(headers, filterBlacklistedHeaders(cacheObject.headers || {}));
 | 
						|
 | 
						|
        // only embed apicache headers when not in production environment
 | 
						|
        if (process.env.NODE_ENV !== "production") {
 | 
						|
            Object.assign(headers, {
 | 
						|
                "apicache-store": globalOptions.redisClient ? "redis" : "memory",
 | 
						|
                "apicache-version": "1.6.2-modified",
 | 
						|
            });
 | 
						|
        }
 | 
						|
 | 
						|
        // unstringify buffers
 | 
						|
        let data = cacheObject.data;
 | 
						|
        if (data && data.type === "Buffer") {
 | 
						|
            data =
 | 
						|
        typeof data.data === "number" ? new Buffer.alloc(data.data) : new Buffer.from(data.data);
 | 
						|
        }
 | 
						|
 | 
						|
        // test Etag against If-None-Match for 304
 | 
						|
        let cachedEtag = cacheObject.headers.etag;
 | 
						|
        let requestEtag = request.headers["if-none-match"];
 | 
						|
 | 
						|
        if (requestEtag && cachedEtag === requestEtag) {
 | 
						|
            response.writeHead(304, headers);
 | 
						|
            return response.end();
 | 
						|
        }
 | 
						|
 | 
						|
        response.writeHead(cacheObject.status || 200, headers);
 | 
						|
 | 
						|
        return response.end(data, cacheObject.encoding);
 | 
						|
    }
 | 
						|
 | 
						|
    /** Sync caching options */
 | 
						|
    function syncOptions() {
 | 
						|
        for (let i in middlewareOptions) {
 | 
						|
            Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Clear key from cache
 | 
						|
     * @param {string} target Key to clear
 | 
						|
     * @param {boolean} isAutomatic Is the key being cleared automatically
 | 
						|
     * @returns {number}
 | 
						|
     */
 | 
						|
    this.clear = function (target, isAutomatic) {
 | 
						|
        let group = index.groups[target];
 | 
						|
        let redis = globalOptions.redisClient;
 | 
						|
 | 
						|
        if (group) {
 | 
						|
            debug("clearing group \"" + target + "\"");
 | 
						|
 | 
						|
            group.forEach(function (key) {
 | 
						|
                debug("clearing cached entry for \"" + key + "\"");
 | 
						|
                clearTimeout(timers[key]);
 | 
						|
                delete timers[key];
 | 
						|
                if (!globalOptions.redisClient) {
 | 
						|
                    memCache.delete(key);
 | 
						|
                } else {
 | 
						|
                    try {
 | 
						|
                        redis.del(key);
 | 
						|
                    } catch (err) {
 | 
						|
                        console.log("[apicache] error in redis.del(\"" + key + "\")");
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                index.all = index.all.filter(doesntMatch(key));
 | 
						|
            });
 | 
						|
 | 
						|
            delete index.groups[target];
 | 
						|
        } else if (target) {
 | 
						|
            debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\"");
 | 
						|
            clearTimeout(timers[target]);
 | 
						|
            delete timers[target];
 | 
						|
            // clear actual cached entry
 | 
						|
            if (!redis) {
 | 
						|
                memCache.delete(target);
 | 
						|
            } else {
 | 
						|
                try {
 | 
						|
                    redis.del(target);
 | 
						|
                } catch (err) {
 | 
						|
                    console.log("[apicache] error in redis.del(\"" + target + "\")");
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            // remove from global index
 | 
						|
            index.all = index.all.filter(doesntMatch(target));
 | 
						|
 | 
						|
            // remove target from each group that it may exist in
 | 
						|
            Object.keys(index.groups).forEach(function (groupName) {
 | 
						|
                index.groups[groupName] = index.groups[groupName].filter(doesntMatch(target));
 | 
						|
 | 
						|
                // delete group if now empty
 | 
						|
                if (!index.groups[groupName].length) {
 | 
						|
                    delete index.groups[groupName];
 | 
						|
                }
 | 
						|
            });
 | 
						|
        } else {
 | 
						|
            debug("clearing entire index");
 | 
						|
 | 
						|
            if (!redis) {
 | 
						|
                memCache.clear();
 | 
						|
            } else {
 | 
						|
                // clear redis keys one by one from internal index to prevent clearing non-apicache entries
 | 
						|
                index.all.forEach(function (key) {
 | 
						|
                    clearTimeout(timers[key]);
 | 
						|
                    delete timers[key];
 | 
						|
                    try {
 | 
						|
                        redis.del(key);
 | 
						|
                    } catch (err) {
 | 
						|
                        console.log("[apicache] error in redis.del(\"" + key + "\")");
 | 
						|
                    }
 | 
						|
                });
 | 
						|
            }
 | 
						|
            this.resetIndex();
 | 
						|
        }
 | 
						|
 | 
						|
        return this.getIndex();
 | 
						|
    };
 | 
						|
 | 
						|
    /**
 | 
						|
     * Converts a duration string to an integer number of milliseconds.
 | 
						|
     * @param {(string|number)} duration The string to convert.
 | 
						|
     * @param {number} defaultDuration The default duration to return if
 | 
						|
     * can't parse duration
 | 
						|
     * @returns {number} The converted value in milliseconds, or the
 | 
						|
     * defaultDuration if it can't be parsed.
 | 
						|
     */
 | 
						|
    function parseDuration(duration, defaultDuration) {
 | 
						|
        if (typeof duration === "number") {
 | 
						|
            return duration;
 | 
						|
        }
 | 
						|
 | 
						|
        if (typeof duration === "string") {
 | 
						|
            let split = duration.match(/^([\d\.,]+)\s?(\w+)$/);
 | 
						|
 | 
						|
            if (split.length === 3) {
 | 
						|
                let len = parseFloat(split[1]);
 | 
						|
                let unit = split[2].replace(/s$/i, "").toLowerCase();
 | 
						|
                if (unit === "m") {
 | 
						|
                    unit = "ms";
 | 
						|
                }
 | 
						|
 | 
						|
                return (len || 1) * (t[unit] || 0);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return defaultDuration;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Parse duration
 | 
						|
     * @param {(number|string)} duration
 | 
						|
     * @returns {number} Duration parsed to a number
 | 
						|
     */
 | 
						|
    this.getDuration = function (duration) {
 | 
						|
        return parseDuration(duration, globalOptions.defaultDuration);
 | 
						|
    };
 | 
						|
 | 
						|
    /**
 | 
						|
   * Return cache performance statistics (hit rate).  Suitable for
 | 
						|
   * putting into a route:
 | 
						|
   * <code>
 | 
						|
   * app.get('/api/cache/performance', (req, res) => {
 | 
						|
   *    res.json(apicache.getPerformance())
 | 
						|
   * })
 | 
						|
   * </code>
 | 
						|
   * @returns {any[]}
 | 
						|
   */
 | 
						|
    this.getPerformance = function () {
 | 
						|
        return performanceArray.map(function (p) {
 | 
						|
            return p.report();
 | 
						|
        });
 | 
						|
    };
 | 
						|
 | 
						|
    /**
 | 
						|
     * Get index of a group
 | 
						|
     * @param {string} group 
 | 
						|
     * @returns {number}
 | 
						|
     */
 | 
						|
    this.getIndex = function (group) {
 | 
						|
        if (group) {
 | 
						|
            return index.groups[group];
 | 
						|
        } else {
 | 
						|
            return index;
 | 
						|
        }
 | 
						|
    };
 | 
						|
 | 
						|
    /**
 | 
						|
     * Express middleware
 | 
						|
     * @param {(string|number)} strDuration Duration to cache responses
 | 
						|
     * for.
 | 
						|
     * @param {function(Object, Object):boolean} middlewareToggle 
 | 
						|
     * @param {Object} localOptions Options for APICache
 | 
						|
     * @returns 
 | 
						|
     */
 | 
						|
    this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
 | 
						|
        let duration = instance.getDuration(strDuration);
 | 
						|
        let opt = {};
 | 
						|
 | 
						|
        middlewareOptions.push({
 | 
						|
            options: opt,
 | 
						|
        });
 | 
						|
 | 
						|
        let options = function (localOptions) {
 | 
						|
            if (localOptions) {
 | 
						|
                middlewareOptions.find(function (middleware) {
 | 
						|
                    return middleware.options === opt;
 | 
						|
                }).localOptions = localOptions;
 | 
						|
            }
 | 
						|
 | 
						|
            syncOptions();
 | 
						|
 | 
						|
            return opt;
 | 
						|
        };
 | 
						|
 | 
						|
        options(localOptions);
 | 
						|
 | 
						|
        /**
 | 
						|
         * A Function for non tracking performance
 | 
						|
         */
 | 
						|
        function NOOPCachePerformance() {
 | 
						|
            this.report = this.hit = this.miss = function () {}; // noop;
 | 
						|
        }
 | 
						|
 | 
						|
        /**
 | 
						|
         * A function for tracking and reporting hit rate.  These
 | 
						|
         * statistics are returned by the getPerformance() call above.
 | 
						|
         */
 | 
						|
        function CachePerformance() {
 | 
						|
            /**
 | 
						|
             * Tracks the hit rate for the last 100 requests. If there
 | 
						|
             * have been fewer than 100 requests, the hit rate just
 | 
						|
             * considers the requests that have happened.
 | 
						|
             */
 | 
						|
            this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
 | 
						|
 | 
						|
            /**
 | 
						|
             * Tracks the hit rate for the last 1000 requests. If there
 | 
						|
             * have been fewer than 1000 requests, the hit rate just
 | 
						|
             * considers the requests that have happened.
 | 
						|
             */
 | 
						|
            this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
 | 
						|
 | 
						|
            /**
 | 
						|
             * Tracks the hit rate for the last 10000 requests. If there
 | 
						|
             * have been fewer than 10000 requests, the hit rate just
 | 
						|
             * considers the requests that have happened.
 | 
						|
             */
 | 
						|
            this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
 | 
						|
 | 
						|
            /**
 | 
						|
             * Tracks the hit rate for the last 100000 requests. If
 | 
						|
             * there have been fewer than 100000 requests, the hit rate
 | 
						|
             * just considers the requests that have happened.
 | 
						|
             */
 | 
						|
            this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
 | 
						|
 | 
						|
            /**
 | 
						|
             * The number of calls that have passed through the
 | 
						|
             * middleware since the server started.
 | 
						|
             */
 | 
						|
            this.callCount = 0;
 | 
						|
 | 
						|
            /**
 | 
						|
             * The total number of hits since the server started
 | 
						|
             */
 | 
						|
            this.hitCount = 0;
 | 
						|
 | 
						|
            /**
 | 
						|
             * The key from the last cache hit.  This is useful in
 | 
						|
             * identifying which route these statistics apply to.
 | 
						|
             */
 | 
						|
            this.lastCacheHit = null;
 | 
						|
 | 
						|
            /**
 | 
						|
             * The key from the last cache miss.  This is useful in
 | 
						|
             * identifying which route these statistics apply to.
 | 
						|
             */
 | 
						|
            this.lastCacheMiss = null;
 | 
						|
 | 
						|
            /**
 | 
						|
             * Return performance statistics
 | 
						|
             * @returns {Object}
 | 
						|
             */
 | 
						|
            this.report = function () {
 | 
						|
                return {
 | 
						|
                    lastCacheHit: this.lastCacheHit,
 | 
						|
                    lastCacheMiss: this.lastCacheMiss,
 | 
						|
                    callCount: this.callCount,
 | 
						|
                    hitCount: this.hitCount,
 | 
						|
                    missCount: this.callCount - this.hitCount,
 | 
						|
                    hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount,
 | 
						|
                    hitRateLast100: this.hitRate(this.hitsLast100),
 | 
						|
                    hitRateLast1000: this.hitRate(this.hitsLast1000),
 | 
						|
                    hitRateLast10000: this.hitRate(this.hitsLast10000),
 | 
						|
                    hitRateLast100000: this.hitRate(this.hitsLast100000),
 | 
						|
                };
 | 
						|
            };
 | 
						|
 | 
						|
            /**
 | 
						|
             * Computes a cache hit rate from an array of hits and
 | 
						|
             * misses.
 | 
						|
             * @param {Uint8Array} array An array representing hits and
 | 
						|
             * misses.
 | 
						|
             * @returns {?number} a number between 0 and 1, or null if
 | 
						|
             * the array has no hits or misses
 | 
						|
             */
 | 
						|
            this.hitRate = function (array) {
 | 
						|
                let hits = 0;
 | 
						|
                let misses = 0;
 | 
						|
                for (let i = 0; i < array.length; i++) {
 | 
						|
                    let n8 = array[i];
 | 
						|
                    for (let j = 0; j < 4; j++) {
 | 
						|
                        switch (n8 & 3) {
 | 
						|
                            case 1:
 | 
						|
                                hits++;
 | 
						|
                                break;
 | 
						|
                            case 2:
 | 
						|
                                misses++;
 | 
						|
                                break;
 | 
						|
                        }
 | 
						|
                        n8 >>= 2;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                let total = hits + misses;
 | 
						|
                if (total == 0) {
 | 
						|
                    return null;
 | 
						|
                }
 | 
						|
                return hits / total;
 | 
						|
            };
 | 
						|
 | 
						|
            /**
 | 
						|
             * Record a hit or miss in the given array.  It will be
 | 
						|
             * recorded at a position determined by the current value of
 | 
						|
             * the callCount variable.
 | 
						|
             * @param {Uint8Array} array An array representing hits and
 | 
						|
             * misses.
 | 
						|
             * @param {boolean} hit true for a hit, false for a miss
 | 
						|
             * Each element in the array is 8 bits, and encodes 4
 | 
						|
             * hit/miss records. Each hit or miss is encoded as to bits
 | 
						|
             * as follows: 00 means no hit or miss has been recorded in
 | 
						|
             * these bits 01 encodes a hit 10 encodes a miss
 | 
						|
             */
 | 
						|
            this.recordHitInArray = function (array, hit) {
 | 
						|
                let arrayIndex = ~~(this.callCount / 4) % array.length;
 | 
						|
                let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
 | 
						|
                let clearMask = ~(3 << bitOffset);
 | 
						|
                let record = (hit ? 1 : 2) << bitOffset;
 | 
						|
                array[arrayIndex] = (array[arrayIndex] & clearMask) | record;
 | 
						|
            };
 | 
						|
 | 
						|
            /**
 | 
						|
             * Records the hit or miss in the tracking arrays and
 | 
						|
             * increments the call count.
 | 
						|
             * @param {boolean} hit true records a hit, false records a
 | 
						|
             * miss
 | 
						|
             */
 | 
						|
            this.recordHit = function (hit) {
 | 
						|
                this.recordHitInArray(this.hitsLast100, hit);
 | 
						|
                this.recordHitInArray(this.hitsLast1000, hit);
 | 
						|
                this.recordHitInArray(this.hitsLast10000, hit);
 | 
						|
                this.recordHitInArray(this.hitsLast100000, hit);
 | 
						|
                if (hit) {
 | 
						|
                    this.hitCount++;
 | 
						|
                }
 | 
						|
                this.callCount++;
 | 
						|
            };
 | 
						|
 | 
						|
            /**
 | 
						|
             * Records a hit event, setting lastCacheMiss to the given key
 | 
						|
             * @param {string} key The key that had the cache hit
 | 
						|
             */
 | 
						|
            this.hit = function (key) {
 | 
						|
                this.recordHit(true);
 | 
						|
                this.lastCacheHit = key;
 | 
						|
            };
 | 
						|
 | 
						|
            /**
 | 
						|
             * Records a miss event, setting lastCacheMiss to the given key
 | 
						|
             * @param {string} key The key that had the cache miss
 | 
						|
             */
 | 
						|
            this.miss = function (key) {
 | 
						|
                this.recordHit(false);
 | 
						|
                this.lastCacheMiss = key;
 | 
						|
            };
 | 
						|
        }
 | 
						|
 | 
						|
        let perf = globalOptions.trackPerformance ? new CachePerformance() : new NOOPCachePerformance();
 | 
						|
 | 
						|
        performanceArray.push(perf);
 | 
						|
 | 
						|
        /**
 | 
						|
         * Cache a request
 | 
						|
         * @param {Object} req Express request object
 | 
						|
         * @param {Object} res Express response object
 | 
						|
         * @param {function} next Function to call next
 | 
						|
         * @returns {any}
 | 
						|
         */
 | 
						|
        let cache = function (req, res, next) {
 | 
						|
            function bypass() {
 | 
						|
                debug("bypass detected, skipping cache.");
 | 
						|
                return next();
 | 
						|
            }
 | 
						|
 | 
						|
            // initial bypass chances
 | 
						|
            if (!opt.enabled) {
 | 
						|
                return bypass();
 | 
						|
            }
 | 
						|
            if (
 | 
						|
                req.headers["x-apicache-bypass"] ||
 | 
						|
        req.headers["x-apicache-force-fetch"] ||
 | 
						|
        (opt.respectCacheControl && req.headers["cache-control"] == "no-cache")
 | 
						|
            ) {
 | 
						|
                return bypass();
 | 
						|
            }
 | 
						|
 | 
						|
            // REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
 | 
						|
            // if (typeof middlewareToggle === 'function') {
 | 
						|
            //   if (!middlewareToggle(req, res)) return bypass()
 | 
						|
            // } else if (middlewareToggle !== undefined && !middlewareToggle) {
 | 
						|
            //   return bypass()
 | 
						|
            // }
 | 
						|
 | 
						|
            // embed timer
 | 
						|
            req.apicacheTimer = new Date();
 | 
						|
 | 
						|
            // In Express 4.x the url is ambigious based on where a router is mounted.  originalUrl will give the full Url
 | 
						|
            let key = req.originalUrl || req.url;
 | 
						|
 | 
						|
            // Remove querystring from key if jsonp option is enabled
 | 
						|
            if (opt.jsonp) {
 | 
						|
                key = url.parse(key).pathname;
 | 
						|
            }
 | 
						|
 | 
						|
            // add appendKey (either custom function or response path)
 | 
						|
            if (typeof opt.appendKey === "function") {
 | 
						|
                key += "$$appendKey=" + opt.appendKey(req, res);
 | 
						|
            } else if (opt.appendKey.length > 0) {
 | 
						|
                let appendKey = req;
 | 
						|
 | 
						|
                for (let i = 0; i < opt.appendKey.length; i++) {
 | 
						|
                    appendKey = appendKey[opt.appendKey[i]];
 | 
						|
                }
 | 
						|
                key += "$$appendKey=" + appendKey;
 | 
						|
            }
 | 
						|
 | 
						|
            // attempt cache hit
 | 
						|
            let redis = opt.redisClient;
 | 
						|
            let cached = !redis ? memCache.getValue(key) : null;
 | 
						|
 | 
						|
            // send if cache hit from memory-cache
 | 
						|
            if (cached) {
 | 
						|
                let elapsed = new Date() - req.apicacheTimer;
 | 
						|
                debug("sending cached (memory-cache) version of", key, logDuration(elapsed));
 | 
						|
 | 
						|
                perf.hit(key);
 | 
						|
                return sendCachedResponse(req, res, cached, middlewareToggle, next, duration);
 | 
						|
            }
 | 
						|
 | 
						|
            // send if cache hit from redis
 | 
						|
            if (redis && redis.connected) {
 | 
						|
                try {
 | 
						|
                    redis.hgetall(key, function (err, obj) {
 | 
						|
                        if (!err && obj && obj.response) {
 | 
						|
                            let elapsed = new Date() - req.apicacheTimer;
 | 
						|
                            debug("sending cached (redis) version of", key, logDuration(elapsed));
 | 
						|
 | 
						|
                            perf.hit(key);
 | 
						|
                            return sendCachedResponse(
 | 
						|
                                req,
 | 
						|
                                res,
 | 
						|
                                JSON.parse(obj.response),
 | 
						|
                                middlewareToggle,
 | 
						|
                                next,
 | 
						|
                                duration
 | 
						|
                            );
 | 
						|
                        } else {
 | 
						|
                            perf.miss(key);
 | 
						|
                            return makeResponseCacheable(
 | 
						|
                                req,
 | 
						|
                                res,
 | 
						|
                                next,
 | 
						|
                                key,
 | 
						|
                                duration,
 | 
						|
                                strDuration,
 | 
						|
                                middlewareToggle
 | 
						|
                            );
 | 
						|
                        }
 | 
						|
                    });
 | 
						|
                } catch (err) {
 | 
						|
                    // bypass redis on error
 | 
						|
                    perf.miss(key);
 | 
						|
                    return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
 | 
						|
                }
 | 
						|
            } else {
 | 
						|
                perf.miss(key);
 | 
						|
                return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
 | 
						|
            }
 | 
						|
        };
 | 
						|
 | 
						|
        cache.options = options;
 | 
						|
 | 
						|
        return cache;
 | 
						|
    };
 | 
						|
 | 
						|
    /**
 | 
						|
     * Process options
 | 
						|
     * @param {Object} options 
 | 
						|
     * @returns {Object}
 | 
						|
     */
 | 
						|
    this.options = function (options) {
 | 
						|
        if (options) {
 | 
						|
            Object.assign(globalOptions, options);
 | 
						|
            syncOptions();
 | 
						|
 | 
						|
            if ("defaultDuration" in options) {
 | 
						|
                // Convert the default duration to a number in milliseconds (if needed)
 | 
						|
                globalOptions.defaultDuration = parseDuration(globalOptions.defaultDuration, 3600000);
 | 
						|
            }
 | 
						|
 | 
						|
            if (globalOptions.trackPerformance) {
 | 
						|
                debug("WARNING: using trackPerformance flag can cause high memory usage!");
 | 
						|
            }
 | 
						|
 | 
						|
            return this;
 | 
						|
        } else {
 | 
						|
            return globalOptions;
 | 
						|
        }
 | 
						|
    };
 | 
						|
 | 
						|
    /** Reset the index */
 | 
						|
    this.resetIndex = function () {
 | 
						|
        index = {
 | 
						|
            all: [],
 | 
						|
            groups: {},
 | 
						|
        };
 | 
						|
    };
 | 
						|
 | 
						|
    /**
 | 
						|
     * Create a new instance of ApiCache
 | 
						|
     * @param {Object} config Config to pass
 | 
						|
     * @returns {ApiCache}
 | 
						|
     */
 | 
						|
    this.newInstance = function (config) {
 | 
						|
        let instance = new ApiCache();
 | 
						|
 | 
						|
        if (config) {
 | 
						|
            instance.options(config);
 | 
						|
        }
 | 
						|
 | 
						|
        return instance;
 | 
						|
    };
 | 
						|
 | 
						|
    /** Clone this instance */
 | 
						|
    this.clone = function () {
 | 
						|
        return this.newInstance(this.options());
 | 
						|
    };
 | 
						|
 | 
						|
    // initialize index
 | 
						|
    this.resetIndex();
 | 
						|
}
 | 
						|
 | 
						|
module.exports = new ApiCache();
 |