UNPKG

@gis-ag/oniyi-http-plugin-cache-redis

Version:

Plugin responsible for caching responses into redis db

173 lines (147 loc) 5.76 kB
'use strict'; // 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]; };