|
|
|
@ -13,27 +13,49 @@ let t = {
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
@ -70,10 +92,10 @@ function ApiCache() {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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)
|
|
|
|
|
* @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
|
|
|
|
|
*/
|
|
|
|
@ -90,8 +112,8 @@ function ApiCache() {
|
|
|
|
|
* 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.
|
|
|
|
|
*
|
|
|
|
|
* Generated by Trelent
|
|
|
|
|
* @param {function(Object, Object):boolean} toggle
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
|
|
|
|
function shouldCacheResponse(request, response, toggle) {
|
|
|
|
|
let opt = globalOptions;
|
|
|
|
@ -116,10 +138,9 @@ function ApiCache() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Adds a key to the index.
|
|
|
|
|
* @param {string} key The key to add.
|
|
|
|
|
*
|
|
|
|
|
* Generated by Trelent
|
|
|
|
|
* 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;
|
|
|
|
@ -135,8 +156,11 @@ function ApiCache() {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns a new object containing only the whitelisted headers.
|
|
|
|
|
* @param {Object} headers The original object of header names and values.
|
|
|
|
|
* @param {Array.<string>} globalOptions.headerWhitelist An array of strings representing the whitelisted header names to keep in the output object.
|
|
|
|
|
* @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
|
|
|
|
|
*/
|
|
|
|
@ -152,8 +176,10 @@ function ApiCache() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a cache object
|
|
|
|
|
* @param {Object} headers The response headers to filter.
|
|
|
|
|
* @returns {Object} A new object containing only the whitelisted response headers.
|
|
|
|
|
* @returns {Object} A new object containing only the whitelisted
|
|
|
|
|
* response headers.
|
|
|
|
|
*
|
|
|
|
|
* Generated by Trelent
|
|
|
|
|
*/
|
|
|
|
@ -170,8 +196,9 @@ function ApiCache() {
|
|
|
|
|
/**
|
|
|
|
|
* Sets a cache value for the given key.
|
|
|
|
|
* @param {string} key The cache key to set.
|
|
|
|
|
* @param {*} value The cache value to set.
|
|
|
|
|
* @param {number} duration How long in milliseconds the cached response should be valid for (defaults to 1 hour).
|
|
|
|
|
* @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
|
|
|
|
|
*/
|
|
|
|
@ -199,7 +226,8 @@ function ApiCache() {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Appends content to the response.
|
|
|
|
|
* @param {string|Buffer} content The content to append.
|
|
|
|
|
* @param {Object} res Express response object
|
|
|
|
|
* @param {(string|Buffer)} content The content to append.
|
|
|
|
|
*
|
|
|
|
|
* Generated by Trelent
|
|
|
|
|
*/
|
|
|
|
@ -229,11 +257,15 @@ function ApiCache() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Monkeypatches the response object to add cache control headers and create a cache object.
|
|
|
|
|
* @param {Object} req - The request object.
|
|
|
|
|
* @param {Object} res - The response object.
|
|
|
|
|
*
|
|
|
|
|
* Generated by Trelent
|
|
|
|
|
* 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
|
|
|
|
@ -302,11 +334,15 @@ function ApiCache() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Request} request
|
|
|
|
|
* @param {Response} response
|
|
|
|
|
* @returns {boolean|undefined} true if the request should be cached, false otherwise. If undefined, defaults to true.
|
|
|
|
|
*
|
|
|
|
|
* Generated by Trelent
|
|
|
|
|
* 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)) {
|
|
|
|
@ -348,12 +384,19 @@ function ApiCache() {
|
|
|
|
|
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;
|
|
|
|
@ -430,10 +473,11 @@ function ApiCache() {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Converts a duration string to an integer number of milliseconds.
|
|
|
|
|
* @param {string} duration - The string to convert.
|
|
|
|
|
* @returns {number} The converted value in milliseconds, or the defaultDuration if it can't be parsed.
|
|
|
|
|
*
|
|
|
|
|
* Generated by Trelent
|
|
|
|
|
* @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") {
|
|
|
|
@ -457,17 +501,24 @@ function ApiCache() {
|
|
|
|
|
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:
|
|
|
|
|
* 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) {
|
|
|
|
@ -475,6 +526,11 @@ function ApiCache() {
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get index of a group
|
|
|
|
|
* @param {string} group
|
|
|
|
|
* @returns {number}
|
|
|
|
|
*/
|
|
|
|
|
this.getIndex = function (group) {
|
|
|
|
|
if (group) {
|
|
|
|
|
return index.groups[group];
|
|
|
|
@ -483,6 +539,14 @@ function ApiCache() {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 = {};
|
|
|
|
@ -513,35 +577,41 @@ function ApiCache() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
|
|
|
|
|
* 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.
|
|
|
|
|
* 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.
|
|
|
|
|
* 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.
|
|
|
|
|
* 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.
|
|
|
|
|
* 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.
|
|
|
|
|
* The number of calls that have passed through the
|
|
|
|
|
* middleware since the server started.
|
|
|
|
|
*/
|
|
|
|
|
this.callCount = 0;
|
|
|
|
|
|
|
|
|
@ -551,17 +621,20 @@ function ApiCache() {
|
|
|
|
|
this.hitCount = 0;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The key from the last cache hit. This is useful in identifying which route these statistics apply to.
|
|
|
|
|
* 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.
|
|
|
|
|
* 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 {
|
|
|
|
@ -579,9 +652,12 @@ function ApiCache() {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Computes a cache hit rate from an array of hits and misses.
|
|
|
|
|
* @param {Uint8Array} array An array representing hits and misses.
|
|
|
|
|
* @returns a number between 0 and 1, or null if the array has no hits or misses
|
|
|
|
|
* 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;
|
|
|
|
@ -608,15 +684,16 @@ function ApiCache() {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
* 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
|
|
|
|
|
* 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;
|
|
|
|
@ -627,8 +704,10 @@ function ApiCache() {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
* 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);
|
|
|
|
@ -664,6 +743,13 @@ function ApiCache() {
|
|
|
|
|
|
|
|
|
|
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.");
|
|
|
|
@ -771,6 +857,11 @@ function ApiCache() {
|
|
|
|
|
return cache;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Process options
|
|
|
|
|
* @param {Object} options
|
|
|
|
|
* @returns {Object}
|
|
|
|
|
*/
|
|
|
|
|
this.options = function (options) {
|
|
|
|
|
if (options) {
|
|
|
|
|
Object.assign(globalOptions, options);
|
|
|
|
@ -791,6 +882,7 @@ function ApiCache() {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Reset the index */
|
|
|
|
|
this.resetIndex = function () {
|
|
|
|
|
index = {
|
|
|
|
|
all: [],
|
|
|
|
@ -798,6 +890,11 @@ function ApiCache() {
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a new instance of ApiCache
|
|
|
|
|
* @param {Object} config Config to pass
|
|
|
|
|
* @returns {ApiCache}
|
|
|
|
|
*/
|
|
|
|
|
this.newInstance = function (config) {
|
|
|
|
|
let instance = new ApiCache();
|
|
|
|
|
|
|
|
|
@ -808,6 +905,7 @@ function ApiCache() {
|
|
|
|
|
return instance;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Clone this instance */
|
|
|
|
|
this.clone = function () {
|
|
|
|
|
return this.newInstance(this.options());
|
|
|
|
|
};
|
|
|
|
|