@gis-ag/oniyi-http-plugin-cache-redis
Version:
Plugin responsible for caching responses into redis db
154 lines (132 loc) • 5.81 kB
JavaScript
;
// node core modules
// 3rd party modules
const _ = require('lodash');
const debug = require('debug')('oniyi-http-plugin:cache:phase-lists:response:transform-response-data');
// internal modules
const serializeData = data => (_.isString(data) ? data : JSON.stringify(data));
const isTtlValid = (ttl, minTtl) => _.isFinite(ttl) && ttl >= minTtl; // ttl should not be less then provided min value
const defaultRegexParser = (regexResult, name) =>
(regexResult && regexResult[1] ? { [name]: parseInt(regexResult[1], 10) } : {});
/**
* Method that applies multiple Regex patterns on a single text source
*
* @param {String} textToMatch Text used for regex matching
* @param {Object} regexMap Map of regex patterns which are applied on textToMatch param
* @param {Function} regexParser Parser method that can be provided in order to handle matched Array response
*/
const multipleRegexCompiler = (textToMatch, regexMap, regexParser = defaultRegexParser) =>
_.reduce(
regexMap,
(result, regex, name) => {
const regexResult = textToMatch.match(regex);
return regexResult ? Object.assign(result, regexParser(regexResult, name)) : result;
},
{}
);
/**
* Method that calculates expiration time by validating different params in this exact order:
* 1. Check within the cache-control header param
* 2. Look into ttl param
* 3. Look into expires header param
* 4. Return null if 1-3 fails, which sets infinite expiration time for current data
* @param {*} data Data that requires caching
* @param {Boolean} isResponsePrivate Displays whether response is marked as private or not
* @param {Number} ttl Time to Live indicator, provided by either initial params,
* or by a single http request
* @param {Number} minTtl Minimum time to live, provided by plugin options
* @param {String} expires Date provided by response headers
* @param {String} cacheControl Value provided by response headers,
* where max-age and s-max-age are extracted from
* @return {Number|null}
*/
const getExpirationTime = (data, isResponsePrivate, ttl, minTtl, expires, cacheControl) => {
const now = Math.round(Date.now() / 1000);
if (_.isString(cacheControl)) {
// we have a cache-control header, check for s-maxage and maxage
const { sMaxAge, maxAge } = multipleRegexCompiler(cacheControl, {
sMaxAge: /s-max-age=([0-9]+)/,
maxAge: /max-age=([0-9]+)/,
});
// if we have a private cache-control, we are only interested in maxAge time.
if (isResponsePrivate && maxAge) {
return now + maxAge;
}
// otherwise get bot sMaxAge and maxAge (sMaxAge has higher priority)
if (sMaxAge || maxAge) {
return now + (sMaxAge || maxAge);
}
}
// validate ttl, if present
if (isTtlValid(ttl, minTtl)) {
return now + ttl;
}
// if all previous fails, look into "expires" param
if (_.isString(expires)) {
return Math.round(Date.parse(expires) / 1000);
}
debug('Expiration time not provided/found \nWARNING: This data will have no expiration time -> %o', data);
return null;
};
/**
*Method used for transforming response data into cacheable version.
*
* @param {*} data Response data which is being transformed
* @param {Boolean} storeMultiResponse Indicates that multiple responses were combined and
* response header should be aware of this information
* @param {Object} response Original http response received from origin server
* @param {Object} cachedResponse Cached version of an http response
* @param {String} hash Hashed key used for caching transformed response data
* @param {Boolean} isResponsePrivate Displays whether response is marked as private or not
* @param {Number} ttl Time to Live indicator, provided by either initial params,
* or by a single http request
* @param {Number} minTtl Minimum time to live, provided by plugin options
*
* @return {Object}
*/
const transformResponseData = ({
data,
storeMultiResponse,
response,
cachedResponse,
hash,
isResponsePrivate,
ttl,
minTtl,
}) => {
const { headers: { expires, 'cache-control': cacheControl } = {}, statusCode } = response;
const raw = serializeData(data);
const expireAt = getExpirationTime(data, isResponsePrivate, ttl, minTtl, expires, cacheControl);
// if we have received data that was build by combining multiple http responses,
// we want to handle it differently from storing single response data.
if (storeMultiResponse) {
return {
expireAt,
hash,
raw,
response: _.defaultsDeep({ headers: { storeMultiResponse } }, response),
};
}
const buildDataByStatusCode = {
// 200: OK -> indicates that we should build a full data object
200: {
expireAt,
hash,
raw,
response,
},
// 304: Not Modified -> Indicates that requested resource was not modified,
// we can use the cached version, and we should only update expireAt field
304: {
expireAt,
hash,
},
};
// only read statusCode when we have received the data from the cache,
// and if cacheControl requires re-validation
// otherwise force building of full data object by providing "200".
return _.isString(cacheControl) && !(cacheControl.match(/must-revalidate|no-cache/) && cachedResponse)
? buildDataByStatusCode[200]
: buildDataByStatusCode[statusCode];
};
module.exports = transformResponseData;