summaryrefslogtreecommitdiffstats
path: root/server/modules/apicache/apicache.js
diff options
context:
space:
mode:
Diffstat (limited to 'server/modules/apicache/apicache.js')
-rw-r--r--server/modules/apicache/apicache.js917
1 files changed, 917 insertions, 0 deletions
diff --git a/server/modules/apicache/apicache.js b/server/modules/apicache/apicache.js
new file mode 100644
index 0000000..41930b2
--- /dev/null
+++ b/server/modules/apicache/apicache.js
@@ -0,0 +1,917 @@
+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();