UNPKG

@kensingtontech/recacheman

Version:

Small and efficient cache provider for Node.JS with In-memory, Redis and MongoDB engines

447 lines (360 loc) 9.45 kB
'use strict'; /** * Module dependencies. */ // import ms from 'ms'; const ms = require('ms'); /** * Module constants. */ const engines = ['memory', 'redis', 'mongo', 'file']; /** * Cacheman base error class. * * @constructor * @param {String} message * @api private */ class CachemanError extends Error { constructor(message) { super(message); this.name = this.constructor.name; this.message = message; Error.captureStackTrace(this, this.constructor); } } /** * Helper to allow all async methods to support both callbacks and promises */ function maybePromised(_this, callback, wrapped) { if ('function' === typeof callback) { // Call wrapped with unmodified callback wrapped(callback); // Return `this` to keep the same behaviour Cacheman had before promises were added return _this; } else { let _Promise = _this.options.Promise; if ('function' !== typeof _Promise) { throw new CachemanError('Promises not available: Please polyfill native Promise before creating a Cacheman object, pass a Promise library as a Cacheman option, or use the callback interface') } if (_Promise.fromCallback) { // Bluebird's fromCallback, this is faster than new Promise return _Promise.fromCallback(wrapped) } // Standard new Promise based wrapper for native Promises return new _Promise(function(resolve, reject) { wrapped(function(err, value) { if (err) { reject(err); } else { resolve(value); } }); }); } } /** * Cacheman constructor. * * @param {String} name * @param {Object} options * @api public */ class Cacheman { /** * Class constructor method. * * @param {String} name * @param {Object} [options] * @return {Cacheman} this * @api public */ constructor(name, options = {}) { if (name && 'object' === typeof name) { options = name; name = null; } const _Promise = options.Promise || (function() { try { return Promise; } catch (e) {} })(); let { prefix = 'cacheman', engine = 'memory', delimiter = ':', ttl = 60 } = options; if ('string' === typeof ttl) { ttl = Math.round(ms(ttl)/1000); } prefix = [prefix, name || 'cache', ''].join(delimiter); this.options = { ...options, Promise: _Promise, delimiter, prefix: engine === 'redis' ? '' : prefix, ttl, count: 1000 }; this._prefix = prefix; this._ttl = ttl; this._fns = []; this.engine(engine); } /** * Set get engine. * * @param {String} engine * @param {Object} options * @return {Cacheman} this * @api public */ engine(engine, options) { if (!arguments.length) return this._engine; const type = typeof engine; if (! /string|function|object/.test(type)) { throw new CachemanError('Invalid engine format, engine must be a String, Function or a valid engine instance'); } if ('string' === type) { let Engine; if (~Cacheman.engines.indexOf(engine)) { engine = `recacheman-${engine}`; } try { Engine = require(`${engine.endsWith('redis') ? '@kensingtontech/' : ''}${engine}`); } catch(e) { if (e.code === 'MODULE_NOT_FOUND') { throw new CachemanError(`Missing required npm module ${engine}`); } else { throw e; } } this._engine = new Engine(options || this.options, this); } else if ('object' === type) { ['get', 'set', 'del', 'clear'].forEach(key => { if ('function' !== typeof engine[key]) { throw new CachemanError('Invalid engine format, must be a valid engine instance'); } }) this._engine = engine; } else { this._engine = engine(options || this.options, this); } return this; } /** * Wrap key with prefix. * * @param {String} key * @return {String} * @api private */ key(key) { if ( Array.isArray(key) ) { key = key.join(this.options.delimiter); } return (this.options.engine === 'redis') ? key : this._prefix + key; } /** * Sets up namespace middleware. * * @return {Cacheman} this * @api public */ use(fn) { this._fns.push(fn); return this; } /** * Executes the cache middleware. * * @param {String} key * @param {Mixed} data * @param {Number} ttl * @param {Function} fn * @api private */ run(key, data, ttl, fn) { const fns = this._fns.slice(0); if (!fns.length) return fn(null); const go = i => { fns[i](key, data, ttl, (err, _data, _ttl, _force) => { // upon error, short-circuit if (err) return fn(err); // if no middleware left, summon callback if (!fns[i + 1]) return fn(null, _data, _ttl, _force); // go on to next go(i + 1); }); } go(0); } /** * Set an entry. * * @param {String} key * @param {Mixed} data * @param {Number} ttl * @param {Function} [fn] * @return {Cacheman} this * @api public */ cache(key, data, ttl, fn) { if ('function' === typeof ttl) { fn = ttl; ttl = null; } return maybePromised(this, fn, (fn) => { this.get(key, (err, res) => { this.run(key, res, ttl, (_err, _data, _ttl, _force) => { if (err || _err) return fn(err || _err); let force = false; if ('undefined' !== typeof _data) { force = true; data = _data; } if ('undefined' !== typeof _ttl) { force = true; ttl = _ttl; } if ('undefined' === typeof res || force) { return this.set(key, data, ttl, fn); } fn(null, res); }); }); }); } /** * Get an entry. * * @param {String} key * @param {Function} [fn] * @return {Cacheman} this * @api public */ get(key, fn) { return maybePromised(this, fn, (fn) => this._engine.get(this.key(key), fn)); } /** * Set an entry. * * @param {String} key * @param {Mixed} data * @param {Number} ttl * @param {Function} [fn] * @return {Cacheman} this * @api public */ set(key, data, ttl, fn) { if ('function' === typeof ttl) { fn = ttl; ttl = null; } if ('string' === typeof ttl) { ttl = Math.round(ms(ttl)/1000); } return maybePromised(this, fn, (fn) => { if ('string' !== typeof key && !Array.isArray(key)) { return process.nextTick(() => { fn(new CachemanError('Invalid key, key must be a string or array.')); }); } if ('undefined' === typeof data) { return process.nextTick(fn); } return this._engine.set(this.key(key), data, ttl || this._ttl, fn); }); } /** * Delete an entry. * * @param {String} key * @param {Function} [fn] * @return {Cacheman} this * @api public */ del(key, fn) { if ('function' === typeof key) { fn = key; key = ''; } return maybePromised(this, fn, (fn) => this._engine.del(this.key(key), fn)); } /** * Clear all entries. * * @param {String} key * @param {Function} [fn] * @return {Cacheman} this * @api public */ clear(fn) { return maybePromised(this, fn, (fn) => this._engine.clear(fn)); } /** * Wraps a function in cache. I.e., the first time the function is run, * its results are stored in cache so subsequent calls retrieve from cache * instead of calling the function. * * @param {String} key * @param {Function} work * @param {Number} ttl * @param {Function} [fn] * @api public */ wrap(key, work, ttl, fn) { // Allow work and ttl to be passed in the oposite order to make promises nicer if ('function' !== typeof work && 'function' === typeof ttl) { [ttl, work] = [work, ttl]; } if ('function' === typeof ttl) { fn = ttl; ttl = null; } return maybePromised(this, fn, (fn) => { this.get(key, (err, res) => { if (err || res) return fn(err, res); let next = (err, data) => { if (err) return fn(err); this.set(key, data, ttl, err => { fn(err, data); }); // Don't allow callbacks to be called twice next = () => { process.nextTick(() => { throw new CachemanError('callback called twice'); }); }; } if ( work.length >= 1 ) { const result = work((err, data) => next(err, data)); if ('undefined' !== typeof result) { process.nextTick(() => { throw new CachemanError('return value cannot be used when callback argument is used'); }); } } else { try { const result = work(); if ('object' === typeof result && 'function' === typeof result.then) { result .then((value) => next(null, value)) .then(null, (err) => next(err)); } else { next(null, result); } } catch (err) { next(err); } } }); }); } } Cacheman.engines = engines; module.exports = Cacheman;