@gis-ag/oniyi-http-plugin-cache-redis
Version:
Plugin responsible for caching responses into redis db
173 lines (147 loc) • 5.76 kB
JavaScript
;
// node core modules
// 3rd party modules
const _ = require('lodash');
const debug = require('debug')('oniyi-http-plugin:cache:phase-lists:request');
const async = require('async');
// internal modules
const { updateContext } = require('../utils');
const {
tryJsonParse,
buildIdsAndEvaluator,
constants: { PHASE_NAME, HOOK_STATE, REQUEST_OPTIONS },
} = require('./utils');
module.exports = (cache, pluginOptions) => {
const loadFromCache = {
phaseName: PHASE_NAME,
handler: (ctx, next) => {
const { options } = ctx;
const {
phasesToSkip: { requestPhases = [] } = {},
} = options;
let cachedData;
const { hashedId, privateHashedId, evaluator } = buildIdsAndEvaluator(cache, pluginOptions, options);
const listOfIds = _.values({ hashedId, privateHashedId }).filter(Boolean);
const contextUpdater = updateContext(ctx, debug);
const defaultHookStateUpdates = {
hashedId,
privateHashedId,
evaluator,
};
// we might want to skip this phase handler
if (requestPhases.includes(PHASE_NAME)) {
debug(
'Unable to retrieve data from cache for the request options -> %o\n' +
'Reason: Phase [%s] marked for skipping',
options,
PHASE_NAME
);
contextUpdater(HOOK_STATE, {
cache: defaultHookStateUpdates,
});
next();
return;
}
const isRetrievable = evaluator.isRetrievable(options);
// if the provided requestOptions is marked us "not retrievable",
// our job is done here, and we can pass the execution to the next handler
if (!isRetrievable) {
debug(
'Unable to retrieve data from cache for the request options -> %o\n' +
'Reason: Provided options are not retrievable',
options
);
contextUpdater(HOOK_STATE, {
cache: defaultHookStateUpdates,
});
next();
return;
}
// since we might have multiple items in the "listOfIds" array, async flow stops if it got
// an error, or a first positive result
async.findSeries(
listOfIds,
(id, callback) => {
cache.get(id, (cacheError, data) => {
if (cacheError) {
debug('Unable to load the cached data with id [%s]', id);
callback(cacheError);
return;
}
cachedData = Object.assign({}, data);
callback(null, !!data);
});
},
(findCacheError) => {
// if server has refused the connection or something else got wrong,
// code should proceed with the execution and simply log the error
if (findCacheError) {
debug('An error has occured while trying to load the data from the cache', findCacheError);
}
// validate cachedData and make sure that we received the required cached data
if (!(cachedData && cachedData.response && _.isString(cachedData.raw))) {
debug(
'Unable to retrieve data from cache for the request options -> %o\n' +
'Reason: Data has not been found or invalid for keys %s',
options,
listOfIds
);
contextUpdater(HOOK_STATE, {
cache: defaultHookStateUpdates,
});
next();
return;
}
const { response, raw } = cachedData;
const {
headers: {
'cache-control': cacheControl,
'last-modified': lastModified,
storeMultiResponse,
etag: eTag,
} = {},
} = response;
const parsedCachedData = {
body: tryJsonParse(options, raw),
response,
};
// if "must-revalidate/no-cache" is not present, we can use the cached version,
// and we don't have to double-check with origin server if our cached data is stale(out-of-date).
// also, if we've got cached data that is build by combining multiple http responses,
// we can't revalidate it, and so we return that cached data as well .
if (!cacheControl.match(/must-revalidate|no-cache/) || storeMultiResponse) {
debug('Retrieving data from the cache -> %o', parsedCachedData);
// make sure that "storeMultiResponse" flag does not get sent to the caller
next(_.omit(parsedCachedData, ['response.headers.storeMultiResponse']));
return;
}
// eTag has a higher priority since it is considered stronger validator,
// and append it to the request options headers if present
if (eTag) {
contextUpdater(REQUEST_OPTIONS, {
headers: {
'if-none-match': eTag,
},
});
}
// check for lastModified as well, and append it to the request options headers if present
if (lastModified && _.isDate(new Date(lastModified))) {
contextUpdater(REQUEST_OPTIONS, {
headers: {
'if-modified-since': lastModified,
},
});
}
// at the end, we want to pass our cached data to response phase list via hookState.
// response phase list is responsible for response validation, and this
// cached data might still be used (if we received "response.statusCode: 304" for e.x.)
contextUpdater(HOOK_STATE, {
cache: _.merge({}, defaultHookStateUpdates, parsedCachedData),
});
next();
}
);
},
};
return [loadFromCache];
};