CentralPing/request-cache

View on GitHub
index.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict';

const crypto = require('crypto');
const request = require('request');
const _ = require('lodash');

/**
 * @module request-cache
 * @example
```js
const requestCache = require('request-cache');
const rCache = requestCache(REDIS_CLIENT);
const cacheKey;

setInterval(function () {
  const key = rCache('http://www.somethingawesome.com', function (err, resp, body) {
    // Do something with the awesomeness
  });

  // First call - cacheKey is undefined; resp and body are cached and returned
  // Second call - cacheKey === key; cached resp and body are returned
  // Third call - cacheKey === key; cached resp and body are returned
  // ...
  // Three-thousand-six-hundredth call - cacheKey === key; new resp and body are cached and returned

  cacheKey = key;
}, 1000);
```
*/

/**
 * @param {object} redisClient - an instance of a [redis client](https://github.com/mranney/node_redis)
 * @param {object} [options] - Options for cache management
 * @param {string} [options.algorithm=md5] - Any available system hashing algorithm to generate cache key ([more info](https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm))
 * @param {string} [options.encoding=hex] - Encoding algorithm to use for encoding hashed cache key
 * @param {number} [options.ttl=3600] - Time in seconds for cache time-to-live
 * @param {number} [options.refresh=0] - Time in seconds to refresh time-to-live (this does not initiate a new request)
 * @param {array} [options.queryCacheKeys] - Query param keys to use for generating cache key
 * @param {string} [options.keyPrefix] - Prefix for generated cache keys
 * @return {function}
 */
module.exports = function requestCacheInitialization(redisClient, options) {
  options = _.assign({
    algorithm: 'md5',
    encoding: 'hex',
    refresh: 0, // seconds
    ttl: 3600, // seconds
    queryCacheKeys: [],
    keyPrefix: ''
  }, options || {});

  /**
   * Caches successful request calls for subsequent requests. If original request results in an error the response will not be cached.
   * @example
     ```js
     const cacheKey = rCache(req_obj[, key], next);
     ```
   * @param {object|string} req_obj - A request object or URL ([more info](https://github.com/request/request#requestoptions-callback))
   * @param {string} [key] - Cache key to use in place of the generated cache key
   * @param {function} next - callback function `next(err, resp, body)`
   * @return {string} - Generated cache key or provided key value
   */
  return function requestCache(reqObj, key, next) {
    if (arguments.length === 2) {
      // requestCache(reqObj, next);
      next = key;
      key = undefined;
    }

    if (_.isString(reqObj)) {
      // requestCache(URI[, key], next);
      reqObj = {uri: reqObj};
    }

    if (_.isEmpty(reqObj) || (_.isEmpty(reqObj.uri) && _.isEmpty(reqObj.url))) {
      return next ?
        process.nextTick(next, new TypeError('A URI or URL is required.')) :
        new TypeError('A URI or URL and a callback are required.');
    }

    if (key === undefined) {
      // Generate a key
      let hash = crypto.createHash(options.algorithm);

      hash.update(reqObj.uri || reqObj.url);

      options.queryCacheKeys.forEach(function addQeuryToHash(key) {
        if (_.has(reqObj.qs, key)) {
          hash.update(`${key}${reqObj.qs[key]}`);
        }
      });

      key = `${options.keyPrefix}${hash.digest(options.encoding)}`;
    }

    redisClient.get(key, function fetchObj(err, obj) {
      if (err) { return next(err); }

      if (obj) {
        // refresh expiration
        if (options.refresh) { redisClient.expire(key, options.refresh); }

        // reconstitute resp object and body
        return next.apply(null, [null].concat(JSON.parse(obj)));
      }

      // Fetch external API response if no cache or error
      request(reqObj, function response(err, resp, body) {
        if (err === null) {
          // Cache in redis
          redisClient.set(key, JSON.stringify([resp, body]));

          if (options.ttl) { redisClient.expire(key, options.ttl); }
        }

        return next(err, resp, body);
      });
    });

    // return `key` immediately
    return key;
  };
};