cache-manager
Advanced tools
Comparing version 2.6.0 to 2.7.0
@@ -0,1 +1,5 @@ | ||
- 2.7.0 2018-02-13 | ||
- allow setting, getting and deleting multiple keys (#114) - @sebelga | ||
- allow passing in a function to determine TTL based on store - @sebelga | ||
- 2.6.0 2017-12-08 | ||
@@ -2,0 +6,0 @@ - fix multicaching when result is not cacheable (#106) - @gswalden |
/** @module cacheManager/caching */ | ||
/*jshint maxcomplexity:15*/ | ||
/*jshint maxcomplexity:16*/ | ||
var CallbackFiller = require('./callback_filler'); | ||
var utils = require('./utils'); | ||
var parseWrapArguments = utils.parseWrapArguments; | ||
@@ -50,8 +52,8 @@ /** | ||
Promise.resolve() | ||
.then(promise) | ||
.then(function(result) { | ||
cb(null, result); | ||
return null; | ||
}) | ||
.catch(cb); | ||
.then(promise) | ||
.then(function(result) { | ||
cb(null, result); | ||
return null; | ||
}) | ||
.catch(cb); | ||
}, options, function(err, result) { | ||
@@ -70,2 +72,4 @@ if (err) { | ||
* instead of calling the function. | ||
* You can pass any number of keys as long as the wrapped function returns | ||
* an array with the same number of values and in the same order. | ||
* | ||
@@ -75,3 +79,3 @@ * @function | ||
* | ||
* @param {string} key - The cache key to use in cache operations | ||
* @param {string} key - The cache key to use in cache operations. Can be one or many. | ||
* @param {function} work - The function to wrap | ||
@@ -82,19 +86,41 @@ * @param {object} [options] - options passed to `set` function | ||
* @example | ||
* var key = 'user_' + userId; | ||
* cache.wrap(key, function(cb) { | ||
* User.get(userId, cb); | ||
* }, function(err, user) { | ||
* console.log(user); | ||
* }); | ||
* var key = 'user_' + userId; | ||
* cache.wrap(key, function(cb) { | ||
* User.get(userId, cb); | ||
* }, function(err, user) { | ||
* console.log(user); | ||
* }); | ||
* | ||
* // Multiple keys | ||
* var key = 'user_' + userId; | ||
* var key2 = 'user_' + userId2; | ||
* cache.wrap(key, key2, function(cb) { | ||
* User.getMany([userId, userId2], cb); | ||
* }, function(err, users) { | ||
* console.log(users[0]); | ||
* console.log(users[1]); | ||
* }); | ||
*/ | ||
self.wrap = function(key, work, options, cb) { | ||
if (typeof options === 'function') { | ||
cb = options; | ||
options = {}; | ||
} | ||
self.wrap = function() { | ||
var parsedArgs = parseWrapArguments(Array.prototype.slice.apply(arguments)); | ||
var keys = parsedArgs.keys; | ||
var work = parsedArgs.work; | ||
var options = parsedArgs.options; | ||
var cb = parsedArgs.cb; | ||
if (!cb) { | ||
return wrapPromise(key, work, options); | ||
keys.push(work); | ||
keys.push(options); | ||
return wrapPromise.apply(this, keys); | ||
} | ||
if (keys.length > 1) { | ||
/** | ||
* Handle more than 1 key | ||
*/ | ||
return wrapMultiple(keys, work, options, cb); | ||
} | ||
var key = keys[0]; | ||
var hasKey = callbackFiller.has(key); | ||
@@ -137,2 +163,81 @@ callbackFiller.add(key, {cb: cb}); | ||
function wrapMultiple(keys, work, options, cb) { | ||
/** | ||
* We create a unique key for the multiple keys | ||
* by concatenating them | ||
*/ | ||
var combinedKey = keys.reduce(function(acc, k) { | ||
return acc + k; | ||
}, ''); | ||
var hasKey = callbackFiller.has(combinedKey); | ||
callbackFiller.add(combinedKey, {cb: cb}); | ||
if (hasKey) { return; } | ||
keys.push(options); | ||
keys.push(onResult); | ||
self.store.mget.apply(self.store, keys); | ||
function onResult(err, result) { | ||
if (err && (!self.ignoreCacheErrors)) { | ||
return callbackFiller.fill(combinedKey, err); | ||
} | ||
/** | ||
* If all the values returned are cacheable we don't need | ||
* to call our "work" method and the values returned by the cache | ||
* are valid. If one or more of the values is not cacheable | ||
* the cache result is not valid. | ||
*/ | ||
var cacheOK = Array.isArray(result) && result.filter(function(_result) { | ||
return self._isCacheableValue(_result); | ||
}).length === result.length; | ||
if (cacheOK) { | ||
return callbackFiller.fill(combinedKey, null, result); | ||
} | ||
return work(function(err, data) { | ||
if (err) { | ||
return done(err); | ||
} | ||
var _args = []; | ||
data.forEach(function(value, i) { | ||
/** | ||
* Add the {key, value} pair to the args | ||
* array that we will send to mset() | ||
*/ | ||
if (self._isCacheableValue(value)) { | ||
_args.push(keys[i]); | ||
_args.push(value); | ||
} | ||
}); | ||
// If no key|value, exit | ||
if (_args.length === 0) { | ||
return done(null); | ||
} | ||
if (options && typeof options.ttl === 'function') { | ||
options.ttl = options.ttl(data); | ||
} | ||
_args.push(options); | ||
_args.push(done); | ||
self.store.mset.apply(self.store, _args); | ||
function done(err) { | ||
if (err && (!self.ignoreCacheErrors)) { | ||
callbackFiller.fill(combinedKey, err); | ||
} else { | ||
callbackFiller.fill(combinedKey, null, data); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
/** | ||
@@ -146,2 +251,12 @@ * Binds to the underlying store's `get` function. | ||
/** | ||
* Get multiple keys at once. | ||
* Binds to the underlying store's `mget` function. | ||
* @function | ||
* @name mget | ||
*/ | ||
if (typeof self.store.mget === 'function') { | ||
self.mget = self.store.mget.bind(self.store); | ||
} | ||
/** | ||
* Binds to the underlying store's `set` function. | ||
@@ -154,2 +269,13 @@ * @function | ||
/** | ||
* Set multiple keys at once. | ||
* It accepts any number of {key, value} pair | ||
* Binds to the underlying store's `mset` function. | ||
* @function | ||
* @name mset | ||
*/ | ||
if (typeof self.store.mset === 'function') { | ||
self.mset = self.store.mset.bind(self.store); | ||
} | ||
/** | ||
* Binds to the underlying store's `del` function if it exists. | ||
@@ -156,0 +282,0 @@ * @function |
/** @module cacheManager/multiCaching */ | ||
var async = require('async'); | ||
var CallbackFiller = require('./callback_filler'); | ||
var utils = require('./utils'); | ||
var isObject = utils.isObject; | ||
var parseWrapArguments = utils.parseWrapArguments; | ||
@@ -51,5 +54,9 @@ /** | ||
function getFromHighestPriorityCachePromise(key, options) { | ||
function getFromHighestPriorityCachePromise() { | ||
var args = Array.prototype.slice.apply(arguments).filter(function(v) { | ||
return typeof v !== 'undefined'; | ||
}); | ||
return new Promise(function(resolve, reject) { | ||
getFromHighestPriorityCache(key, options, function(err, result) { | ||
var cb = function(err, result) { | ||
if (err) { | ||
@@ -59,16 +66,50 @@ return reject(err); | ||
resolve(result); | ||
}); | ||
}; | ||
args.push(cb); | ||
getFromHighestPriorityCache.apply(null, args); | ||
}); | ||
} | ||
function getFromHighestPriorityCache(key, options, cb) { | ||
if (typeof options === 'function') { | ||
cb = options; | ||
options = {}; | ||
function getFromHighestPriorityCache() { | ||
var args = Array.prototype.slice.apply(arguments).filter(function(v) { | ||
return typeof v !== 'undefined'; | ||
}); | ||
var cb; | ||
var options = {}; | ||
if (typeof args[args.length - 1] === 'function') { | ||
cb = args.pop(); | ||
} | ||
if (!cb) { | ||
return getFromHighestPriorityCachePromise(key, options); | ||
return getFromHighestPriorityCachePromise.apply(this, args); | ||
} | ||
if (isObject(args[args.length - 1])) { | ||
options = args.pop(); | ||
} | ||
/** | ||
* Keep a copy of the keys to retrieve | ||
*/ | ||
var keys = Array.prototype.slice.apply(args); | ||
var multi = keys.length > 1; | ||
/** | ||
* Then put back the options in the args Array | ||
*/ | ||
args.push(options); | ||
if (multi) { | ||
/** | ||
* Keep track of the keys left to fetch accross the caches | ||
*/ | ||
var keysToFetch = Array.prototype.slice.apply(keys); | ||
/** | ||
* Hash to save our multi keys result | ||
*/ | ||
var mapResult = {}; | ||
} | ||
var i = 0; | ||
@@ -83,3 +124,12 @@ async.eachSeries(caches, function(cache, next) { | ||
if (_isCacheableValue(result)) { | ||
if (multi) { | ||
addResultToMap(result, _isCacheableValue); | ||
if (keysToFetch.length === 0 || i === caches.length - 1) { | ||
// Return an Array with the values merged from all the caches | ||
return cb(null, keys.map(function(k) { | ||
return mapResult[k] || undefined; | ||
}), i); | ||
} | ||
} else if (_isCacheableValue(result)) { | ||
// break out of async loop. | ||
@@ -93,11 +143,49 @@ return cb(err, result, i); | ||
cache.store.get(key, options, callback); | ||
if (multi) { | ||
if (typeof cache.store.mget !== 'function') { | ||
/** | ||
* Silently fail for store that don't support mget() | ||
*/ | ||
return callback(null, []); | ||
} | ||
var _args = Array.prototype.slice.apply(keysToFetch); | ||
_args.push(options); | ||
_args.push(callback); | ||
cache.store.mget.apply(cache.store, _args); | ||
} else { | ||
cache.store.get(args[0], options, callback); | ||
} | ||
}, function(err, result) { | ||
return cb(err, result); | ||
}); | ||
function addResultToMap(result, isCacheable) { | ||
var key; | ||
var diff = 0; | ||
/** | ||
* We loop through the result and if the value | ||
* is cacheable we add it to the mapResult hash | ||
* and remove the key to fetch from the "keysToFetch" array | ||
*/ | ||
result.forEach(function(res, i) { | ||
if (isCacheable(res)) { | ||
key = keysToFetch[i - diff]; | ||
// Add the result to our map | ||
mapResult[key] = res; | ||
// delete key from our keysToFetch array | ||
keysToFetch.splice(i, 1); | ||
diff += 1; | ||
} | ||
}); | ||
} | ||
} | ||
function setInMultipleCachesPromise(caches, opts) { | ||
function setInMultipleCachesPromise() { | ||
var args = Array.prototype.slice.apply(arguments); | ||
return new Promise(function(resolve, reject) { | ||
setInMultipleCaches(caches, opts, function(err, result) { | ||
var cb = function(err, result) { | ||
if (err) { | ||
@@ -107,20 +195,70 @@ return reject(err); | ||
resolve(result); | ||
}); | ||
}; | ||
args.push(cb); | ||
setInMultipleCaches.apply(null, args); | ||
}); | ||
} | ||
function setInMultipleCaches(caches, opts, cb) { | ||
opts.options = opts.options || {}; | ||
function setInMultipleCaches() { | ||
var args = Array.prototype.slice.apply(arguments); | ||
var _caches = Array.isArray(args[0]) ? args.shift() : caches; | ||
var cb; | ||
var options = {}; | ||
if (typeof args[args.length - 1] === 'function') { | ||
cb = args.pop(); | ||
} | ||
if (!cb) { | ||
return setInMultipleCachesPromise(caches, opts); | ||
return setInMultipleCachesPromise.apply(this, args); | ||
} | ||
async.each(caches, function(cache, next) { | ||
if (isObject(args[args.length - 1])) { | ||
options = args.pop(); | ||
} | ||
var length = args.length; | ||
var multi = length > 2; | ||
var i; | ||
async.each(_caches, function(cache, next) { | ||
var _isCacheableValue = getIsCacheableValueFunction(cache); | ||
var keysValues = Array.prototype.slice.apply(args); | ||
if (_isCacheableValue(opts.value)) { | ||
cache.store.set(opts.key, opts.value, opts.options, next); | ||
/** | ||
* We filter out the keys *not* cacheable | ||
*/ | ||
for (i = 0; i < length; i += 2) { | ||
if (!_isCacheableValue(keysValues[i + 1])) { | ||
keysValues.splice(i, 2); | ||
} | ||
} | ||
if (keysValues.length === 0) { | ||
return next(); | ||
} | ||
var cacheOptions = options; | ||
if (typeof options.ttl === 'function') { | ||
/** | ||
* Dynamically set the ttl by context depending of the store | ||
*/ | ||
cacheOptions = {}; | ||
cacheOptions.ttl = options.ttl(keysValues, cache.store.name); | ||
} | ||
if (multi) { | ||
if (typeof cache.store.mset !== 'function') { | ||
/** | ||
* Silently fail for store that don't support mset() | ||
*/ | ||
return next(); | ||
} | ||
keysValues.push(cacheOptions); | ||
keysValues.push(next); | ||
cache.store.mset.apply(cache.store, keysValues); | ||
} else { | ||
next(); | ||
cache.store.set(keysValues[0], keysValues[1], cacheOptions, next); | ||
} | ||
@@ -202,4 +340,9 @@ }, function(err, result) { | ||
* cache, it gets set in all higher-priority caches. | ||
* You can pass any number of keys as long as the wrapped function returns | ||
* an array with the same number of values and in the same order. | ||
* | ||
* @param {string} key - The cache key to use in cache operations | ||
* @function | ||
* @name wrap | ||
* | ||
* @param {string} key - The cache key to use in cache operations. Can be one or many. | ||
* @param {function} work - The function to wrap | ||
@@ -209,20 +352,24 @@ * @param {object} [options] - options passed to `set` function | ||
*/ | ||
self.wrap = function(key, work, options, cb) { | ||
if (typeof options === 'function') { | ||
cb = options; | ||
options = {}; | ||
} | ||
self.wrap = function() { | ||
var parsedArgs = parseWrapArguments(Array.prototype.slice.apply(arguments)); | ||
var keys = parsedArgs.keys; | ||
var work = parsedArgs.work; | ||
var options = parsedArgs.options; | ||
var cb = parsedArgs.cb; | ||
function getOptsForSet(value) { | ||
return { | ||
key: key, | ||
value: value, | ||
options: options | ||
}; | ||
if (!cb) { | ||
keys.push(work); | ||
keys.push(options); | ||
return wrapPromise.apply(this, keys); | ||
} | ||
if (!cb) { | ||
return wrapPromise(key, work, options); | ||
if (keys.length > 1) { | ||
/** | ||
* Handle more than 1 key | ||
*/ | ||
return wrapMultiple(keys, work, options, cb); | ||
} | ||
var key = keys[0]; | ||
var hasKey = callbackFiller.has(key); | ||
@@ -237,7 +384,7 @@ callbackFiller.add(key, {cb: cb}); | ||
var cachesToUpdate = caches.slice(0, index); | ||
var opts = getOptsForSet(result); | ||
var args = [cachesToUpdate, key, result, options, function(err) { | ||
callbackFiller.fill(key, err, result); | ||
}]; | ||
setInMultipleCaches(cachesToUpdate, opts, function(err) { | ||
callbackFiller.fill(key, err, result); | ||
}); | ||
setInMultipleCaches.apply(null, args); | ||
} else { | ||
@@ -253,7 +400,7 @@ work(function(err, data) { | ||
var opts = getOptsForSet(data); | ||
var args = [caches, key, data, options, function(err) { | ||
callbackFiller.fill(key, err, data); | ||
}]; | ||
setInMultipleCaches(caches, opts, function(err) { | ||
callbackFiller.fill(key, err, data); | ||
}); | ||
setInMultipleCaches.apply(null, args); | ||
}); | ||
@@ -264,2 +411,121 @@ } | ||
function wrapMultiple(keys, work, options, cb) { | ||
/** | ||
* We create a unique key for the multiple keys | ||
* by concatenating them | ||
*/ | ||
var combinedKey = keys.reduce(function(acc, k) { | ||
return acc + k; | ||
}, ''); | ||
var hasKey = callbackFiller.has(combinedKey); | ||
callbackFiller.add(combinedKey, {cb: cb}); | ||
if (hasKey) { return; } | ||
keys.push(options); | ||
keys.push(onResult); | ||
/** | ||
* Get from all the caches. If multiple keys have been passed, | ||
* we'll go through all the caches and merge the result | ||
*/ | ||
getFromHighestPriorityCache.apply(this, keys); | ||
function onResult(err, result, index) { | ||
if (err) { | ||
return done(err); | ||
} | ||
/** | ||
* If all the values returned are cacheable we don't need | ||
* to call our "work" method and the values returned by the cache | ||
* are valid. If one or more of the values is not cacheable | ||
* the cache result is not valid. | ||
*/ | ||
var cacheOK = result.filter(function(_result) { | ||
return self._isCacheableValue(_result); | ||
}).length === result.length; | ||
if (!cacheOK) { | ||
/** | ||
* We need to fetch the data first | ||
*/ | ||
return work(workCallback); | ||
} | ||
var cachesToUpdate = caches.slice(0, index); | ||
/** | ||
* Prepare arguments to set the values in | ||
* higher priority caches | ||
*/ | ||
var _args = [cachesToUpdate]; | ||
/** | ||
* Add the {key, value} pair | ||
*/ | ||
result.forEach(function(value, i) { | ||
_args.push(keys[i]); | ||
_args.push(value); | ||
}); | ||
/** | ||
* Add options and final callback | ||
*/ | ||
_args.push(options); | ||
_args.push(function(err) { | ||
done(err, result); | ||
}); | ||
return setInMultipleCaches.apply(null, _args); | ||
/** | ||
* Wrapped function callback | ||
*/ | ||
function workCallback(err, data) { | ||
if (err) { | ||
return done(err); | ||
} | ||
/** | ||
* Prepare arguments for "setInMultipleCaches" | ||
*/ | ||
var _args; | ||
_args = []; | ||
data.forEach(function(value, i) { | ||
/** | ||
* Add the {key, value} pair to the args | ||
* array that we will send to mset() | ||
*/ | ||
if (self._isCacheableValue(value)) { | ||
_args.push(keys[i]); | ||
_args.push(value); | ||
} | ||
}); | ||
// If no key,value --> exit | ||
if (_args.length === 0) { | ||
return done(null); | ||
} | ||
/** | ||
* Add options and final callback | ||
*/ | ||
_args.push(options); | ||
_args.push(function(err) { | ||
done(err, data); | ||
}); | ||
setInMultipleCaches.apply(null, _args); | ||
} | ||
/** | ||
* Final callback | ||
*/ | ||
function done(err, data) { | ||
callbackFiller.fill(combinedKey, err, data); | ||
} | ||
} | ||
} | ||
/** | ||
@@ -276,17 +542,20 @@ * Set value in all caches | ||
*/ | ||
self.set = function(key, value, options, cb) { | ||
if (typeof options === 'function') { | ||
cb = options; | ||
options = {}; | ||
} | ||
self.set = setInMultipleCaches; | ||
var opts = { | ||
key: key, | ||
value: value, | ||
options: options | ||
}; | ||
/** | ||
* Set multiple values in all caches | ||
* Accepts an unlimited pair of {key, value} | ||
* | ||
* @function | ||
* @name mset | ||
* | ||
* @param {string} key | ||
* @param {*} value | ||
* @param {string} [key2] | ||
* @param {*} [value2] | ||
* @param {object} [options] to pass to underlying set function. | ||
* @param {function} [cb] | ||
*/ | ||
self.mset = setInMultipleCaches; | ||
return setInMultipleCaches(caches, opts, cb); | ||
}; | ||
/** | ||
@@ -302,10 +571,18 @@ * Get value from highest level cache that has stored it. | ||
*/ | ||
self.get = function(key, options, cb) { | ||
if (typeof options === 'function') { | ||
cb = options; | ||
options = {}; | ||
} | ||
self.get = getFromHighestPriorityCache; | ||
return getFromHighestPriorityCache(key, options, cb); | ||
}; | ||
/** | ||
* Get multiple value from highest level cache that has stored it. | ||
* If some values are not found, the next highest cache is used | ||
* until either all keys are found or all caches have been fetched. | ||
* Accepts an unlimited number of keys. | ||
* | ||
* @function | ||
* @name mget | ||
* | ||
* @param {string} key key to get (any number) | ||
* @param {object} [options] to pass to underlying get function. | ||
* @param {function} cb optional callback | ||
*/ | ||
self.mget = getFromHighestPriorityCache; | ||
@@ -322,10 +599,20 @@ /** | ||
*/ | ||
self.del = function(key, options, cb) { | ||
if (typeof options === 'function') { | ||
cb = options; | ||
options = {}; | ||
self.del = function() { | ||
var args = Array.prototype.slice.apply(arguments); | ||
var cb; | ||
var options = {}; | ||
if (typeof args[args.length - 1] === 'function') { | ||
cb = args.pop(); | ||
} | ||
if (isObject(args[args.length - 1])) { | ||
options = args.pop(); | ||
} | ||
args.push(options); | ||
async.each(caches, function(cache, next) { | ||
cache.store.del(key, options, next); | ||
var _args = Array.prototype.slice.apply(args); | ||
_args.push(next); | ||
cache.store.del.apply(cache.store, _args); | ||
}, cb); | ||
@@ -332,0 +619,0 @@ }; |
var Lru = require("lru-cache"); | ||
var utils = require('../utils'); | ||
var isObject = utils.isObject; | ||
@@ -21,2 +23,12 @@ var memoryStore = function(args) { | ||
var setMultipleKeys = function setMultipleKeys(keysValues, maxAge) { | ||
var length = keysValues.length; | ||
var values = []; | ||
for (var i = 0; i < length; i += 2) { | ||
lruCache.set(keysValues[i], keysValues[i + 1], maxAge); | ||
values.push(keysValues[i + 1]); | ||
} | ||
return values; | ||
}; | ||
self.set = function(key, value, options, cb) { | ||
@@ -39,2 +51,26 @@ if (typeof options === 'function') { | ||
self.mset = function() { | ||
var args = Array.prototype.slice.apply(arguments); | ||
var cb; | ||
var options = {}; | ||
if (typeof args[args.length - 1] === 'function') { | ||
cb = args.pop(); | ||
} | ||
if (args.length % 2 > 0 && isObject(args[args.length - 1])) { | ||
options = args.pop(); | ||
} | ||
var maxAge = (options.ttl || options.ttl === 0) ? options.ttl * 1000 : lruOpts.maxAge; | ||
var values = setMultipleKeys(args, maxAge); | ||
if (cb) { | ||
process.nextTick(cb.bind(null, null)); | ||
} else if (self.usePromises) { | ||
return Promise.resolve(values); | ||
} | ||
}; | ||
self.get = function(key, options, cb) { | ||
@@ -55,10 +91,50 @@ if (typeof options === 'function') { | ||
self.del = function(key, options, cb) { | ||
if (typeof options === 'function') { | ||
cb = options; | ||
self.mget = function() { | ||
var args = Array.prototype.slice.apply(arguments); | ||
var cb; | ||
var options = {}; | ||
if (typeof args[args.length - 1] === 'function') { | ||
cb = args.pop(); | ||
} | ||
lruCache.del(key); | ||
if (isObject(args[args.length - 1])) { | ||
options = args.pop(); | ||
} | ||
var values = args.map(function(key) { | ||
return lruCache.get(key); | ||
}); | ||
if (cb) { | ||
process.nextTick(cb.bind(null, null, values)); | ||
} else if (self.usePromises) { | ||
return Promise.resolve(values); | ||
} else { | ||
return values; | ||
} | ||
}; | ||
self.del = function() { | ||
var args = Array.prototype.slice.apply(arguments); | ||
var cb; | ||
var options = {}; | ||
if (typeof args[args.length - 1] === 'function') { | ||
cb = args.pop(); | ||
} | ||
if (isObject(args[args.length - 1])) { | ||
options = args.pop(); | ||
} | ||
if (Array.isArray(args[0])) { | ||
args = args[0]; | ||
} | ||
args.forEach(function(key) { | ||
lruCache.del(key); | ||
}); | ||
if (cb) { | ||
process.nextTick(cb.bind(null, null)); | ||
@@ -65,0 +141,0 @@ } else if (self.usePromises) { |
{ | ||
"name": "cache-manager", | ||
"version": "2.6.0", | ||
"version": "2.7.0", | ||
"description": "Cache module for Node.js", | ||
@@ -31,3 +31,3 @@ "main": "index.js", | ||
"jscs": "2.11.0", | ||
"jsdoc": "3.3.0", | ||
"jsdoc": "3.5.5", | ||
"jshint": "2.9.1", | ||
@@ -34,0 +34,0 @@ "mocha": "2.4.5", |
102
README.md
@@ -54,3 +54,3 @@ [![build status](https://secure.travis-ci.org/BryanDonovan/node-cache-manager.svg)](http://travis-ci.org/BryanDonovan/node-cache-manager) | ||
First, it includes a `wrap` function that lets you wrap any function in cache. | ||
**First**, it includes a `wrap` function that lets you wrap any function in cache. | ||
(Note, this was inspired by [node-caching](https://github.com/mape/node-caching).) | ||
@@ -86,3 +86,3 @@ This is probably the feature you're looking for. As an example, where you might have to do this: | ||
Second, node-cache-manager features a built-in memory cache (using [node-lru-cache](https://github.com/isaacs/node-lru-cache)), | ||
**Second**, node-cache-manager features a built-in memory cache (using [node-lru-cache](https://github.com/isaacs/node-lru-cache)), | ||
with the standard functions you'd expect in most caches: | ||
@@ -93,2 +93,4 @@ | ||
del(key, cb) | ||
mset(key1, val1, key2, val2, {ttl: ttl}, cb) // set several keys at once | ||
mget(key1, key2, key3, cb) // get several keys at once | ||
@@ -101,3 +103,3 @@ // * Note that depending on the underlying store, you may be able to pass the | ||
Third, node-cache-manager lets you set up a tiered cache strategy. This may be of | ||
**Third**, node-cache-manager lets you set up a tiered cache strategy. This may be of | ||
limited use in most cases, but imagine a scenario where you expect tons of | ||
@@ -112,2 +114,4 @@ traffic, and don't want to hit your primary cache (like Redis) for every request. | ||
**Fourth**, it allows you to get and set multiple keys at once for caching store that support it. This means that when getting muliple keys it will go through the different caches starting from the highest priority one (see multi store below) and merge the values it finds at each level. | ||
## Usage Examples | ||
@@ -186,2 +190,37 @@ | ||
You can get several keys at once. E.g. | ||
```js | ||
var key1 = 'user_1'; | ||
var key2 = 'user_1'; | ||
memoryCache.wrap(key1, key2, function (cb) { | ||
getManyUser([key1, key2], cb); | ||
}, function (err, users) { | ||
console.log(users[0]); | ||
console.log(users[1]); | ||
}); | ||
``` | ||
#### Example setting/getting several keys with mset() and mget() | ||
```js | ||
memoryCache.mset('foo', 'bar', 'foo2', 'bar2' {ttl: ttl}, function(err) { | ||
if (err) { throw err; } | ||
memoryCache.mget('foo', 'foo2', function(err, result) { | ||
console.log(result); | ||
// >> ['bar', 'bar2'] | ||
// Delete keys with del() passing arguments... | ||
memoryCache.del('foo', 'foo2', function(err) {}); | ||
// ...passing an Array of keys | ||
memoryCache.del(['foo', 'foo2'], function(err) {}); | ||
}); | ||
}); | ||
``` | ||
#### Example Using Promises | ||
@@ -209,2 +248,18 @@ | ||
#### Example Using async/await | ||
```javascript | ||
try { | ||
let user = await memoryCache.wrap(key, function() { | ||
return getUserPromise(userId); | ||
}); | ||
} catch (err) { | ||
// error handling | ||
} | ||
``` | ||
Hint: should wrap `await` call with `try` - `catch` to handle `promise` error. | ||
#### Example Express App Usage | ||
@@ -256,2 +311,3 @@ | ||
// Sets in all caches. | ||
// The "ttl" option can also be a function (see example below) | ||
multiCache.set('foo2', 'bar2', {ttl: ttl}, function(err) { | ||
@@ -270,2 +326,34 @@ if (err) { throw err; } | ||
// Set the ttl value by context depending on the store. | ||
function getTTL(data, store) { | ||
if (store === 'redis') { | ||
return 6000; | ||
} | ||
return 3000; | ||
} | ||
// Sets multiple keys in all caches. | ||
// You can pass as many key,value pair as you want | ||
multiCache.mset('key', 'value', 'key2', 'value2', {ttl: getTTL}, function(err) { | ||
if (err) { throw err; } | ||
// mget() fetches from highest priority cache. | ||
// If the first cache does not return all the keys, | ||
// the next cache is fetched with the keys that were not found. | ||
// This is done recursively until either: | ||
// - all have been found | ||
// - all caches has been fetched | ||
multiCache.mget('key', 'key2', function(err, result) { | ||
console.log(result[0]); | ||
console.log(result[1]); | ||
// >> 'bar2' | ||
// >> 'bar3' | ||
// Delete from all caches | ||
multiCache.del('key', 'key2'); | ||
// ...or with an Array | ||
multiCache.del(['key', 'key2']); | ||
}); | ||
}); | ||
// Note: options with ttl are optional in wrap() | ||
@@ -286,2 +374,10 @@ multiCache.wrap(key2, function (cb) { | ||
}); | ||
// Multiple keys | ||
multiCache.wrap('key1', 'key2', function (cb) { | ||
getManyUser(['key1', 'key2'], cb); | ||
}, {ttl: ttl}, function (err, users) { | ||
console.log(users[0]); | ||
console.log(users[1]); | ||
}); | ||
``` | ||
@@ -288,0 +384,0 @@ |
@@ -18,3 +18,8 @@ // TODO: These are really a mix of unit and integration tests. | ||
}); | ||
} | ||
}, | ||
getMultiWidget: function(names, cb) { | ||
process.nextTick(function() { | ||
cb(null, names.map(function(name) { return {name: name}; })); | ||
}); | ||
}, | ||
}; | ||
@@ -86,2 +91,78 @@ | ||
describe("mget() and mset()", function() { | ||
var key2; | ||
var value2; | ||
var store = 'memory'; | ||
beforeEach(function() { | ||
key = support.random.string(20); | ||
value = support.random.string(); | ||
key2 = support.random.string(20); | ||
value2 = support.random.string(); | ||
cache = caching({store: store, ttl: defaultTtl, ignoreCacheErrors: false}); | ||
}); | ||
it("lets us set and get several keys and data in cache", function(done) { | ||
cache.mset(key, value, key2, value2, {ttl: defaultTtl}, function(err) { | ||
checkErr(err); | ||
cache.mget(key, key2, function(err, result) { | ||
checkErr(err); | ||
assert.equal(result[0], value); | ||
assert.equal(result[1], value2); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it("lets us set and get data without a callback", function(done) { | ||
cache = caching({store: memoryStore.create({noPromises: true})}); | ||
cache.mset(key, value, key2, value2, {ttl: defaultTtl}); | ||
setTimeout(function() { | ||
var result = cache.mget(key, key2); | ||
assert.equal(result[0], value); | ||
assert.equal(result[1], value2); | ||
done(); | ||
}, 20); | ||
}); | ||
it("lets us set and get data without a callback, returning a promise", function(done) { | ||
cache.mset(key, value, key2, value2, {ttl: defaultTtl}); | ||
setTimeout(function() { | ||
cache.mget(key, key2) | ||
.then(function(result) { | ||
assert.equal(result[0], value); | ||
assert.equal(result[1], value2); | ||
done(); | ||
}); | ||
}, 20); | ||
}); | ||
it("lets us set and get data without options object or callback", function(done) { | ||
cache = caching({store: memoryStore.create({noPromises: true})}); | ||
cache.mset(key, value, key2, value2); | ||
setTimeout(function() { | ||
var result = cache.mget(key, key2); | ||
assert.equal(result[0], value); | ||
assert.equal(result[1], value2); | ||
done(); | ||
}, 20); | ||
}); | ||
it("lets us pass an 'options' object", function(done) { | ||
cache = caching({store: memoryStore.create({noPromises: true})}); | ||
cache.mset(key, value, key2, value2); | ||
setTimeout(function() { | ||
var result = cache.mget(key, key2, {someConfig: true}); | ||
assert.equal(result[0], value); | ||
assert.equal(result[1], value2); | ||
done(); | ||
}, 20); | ||
}); | ||
}); | ||
describe("del()", function() { | ||
@@ -148,2 +229,51 @@ ['memory'].forEach(function(store) { | ||
}); | ||
describe('with multiple keys', function() { | ||
var key2; | ||
var value2; | ||
beforeEach(function(done) { | ||
cache = caching({store: store}); | ||
key2 = support.random.string(20); | ||
value2 = support.random.string(); | ||
cache.mset(key, value, key2, value2, {ttl: defaultTtl}, function(err) { | ||
checkErr(err); | ||
done(); | ||
}); | ||
}); | ||
it('deletes an unlimited number of key arguments', function(done) { | ||
cache.mget(key, key2, function(err, result) { | ||
assert.equal(result[0], value); | ||
assert.equal(result[1], value2); | ||
cache.del(key, key2, function(err) { | ||
checkErr(err); | ||
cache.mget(key, key2, function(err, result) { | ||
assert.ok(!result[0]); | ||
assert.ok(!result[1]); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
it('deletes an array of keys', function(done) { | ||
cache.mget(key, key2, function(err, result) { | ||
assert.equal(result[0], value); | ||
assert.equal(result[1], value2); | ||
cache.del([key, key2], function(err) { | ||
checkErr(err); | ||
cache.mget(key, key2, function(err, result) { | ||
assert.ok(!result[0]); | ||
assert.ok(!result[1]); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -200,3 +330,3 @@ }); | ||
context("when store has no del() method", function() { | ||
context("when store has no del(), mget() or mset() method", function() { | ||
var fakeStore; | ||
@@ -777,2 +907,156 @@ | ||
}); | ||
context("when passing multiple keys", function() { | ||
var key2; | ||
var name2; | ||
beforeEach(function() { | ||
key2 = support.random.string(20); | ||
name2 = support.random.string(); | ||
sinon.spy(memoryStoreStub, 'mset'); | ||
}); | ||
afterEach(function() { | ||
memoryStoreStub.mset.restore(); | ||
}); | ||
context("when result is already cached", function() { | ||
it("retrieves data from cache", function(done) { | ||
var funcCalled = false; | ||
sinon.stub(memoryStoreStub, 'mget', function() { | ||
var args = Array.prototype.slice.apply(arguments); | ||
var cb = args.pop(); | ||
cb(null, [{name: name}, {name: name2}]); | ||
}); | ||
cache.wrap(key, key2, function(cb) { | ||
funcCalled = true; | ||
cb(); | ||
}, function(err, widgets) { | ||
checkErr(err); | ||
assert.deepEqual(widgets[0], {name: name}); | ||
assert.deepEqual(widgets[1], {name: name2}); | ||
assert.ok(memoryStoreStub.mget.calledWith(key, key2)); | ||
assert.ok(!funcCalled); | ||
memoryStoreStub.mget.restore(); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it("when a ttl is passed in", function(done) { | ||
cache.wrap(key, key2, function(cb) { | ||
methods.getMultiWidget([name, name2], cb); | ||
}, opts, function(err, widgets) { | ||
checkErr(err); | ||
assert.deepEqual(widgets[0], {name: name}); | ||
assert.deepEqual(widgets[1], {name: name2}); | ||
sinon.assert.calledWith(memoryStoreStub.mset, key, {name: name}, key2, {name: name2}, opts); | ||
done(); | ||
}); | ||
}); | ||
it("when a ttl is passed in (function)", function(done) { | ||
var ttlFunc = function() { return 1234; }; | ||
opts = {ttl: ttlFunc}; | ||
cache.wrap(key, key2, function(cb) { | ||
methods.getMultiWidget([name, name2], cb); | ||
}, opts, function(err) { | ||
checkErr(err); | ||
sinon.assert.calledWith( | ||
memoryStoreStub.mset, key, {name: name}, key2, {name: name2}, {ttl: 1234} | ||
); | ||
done(); | ||
}); | ||
}); | ||
it("when a ttl is not passed in", function(done) { | ||
cache.wrap(key, key2, function(cb) { | ||
methods.getMultiWidget([name, name2], cb); | ||
}, function(err, widgets) { | ||
checkErr(err); | ||
assert.deepEqual(widgets[0], {name: name}); | ||
assert.deepEqual(widgets[1], {name: name2}); | ||
sinon.assert.calledWith(memoryStoreStub.mset, key, {name: name}, key2, {name: name2}, {}); | ||
done(); | ||
}); | ||
}); | ||
it("does not store non-allowed values", function(done) { | ||
var name = 'bar'; | ||
cache = caching({store: 'memory', isCacheableValue: function() { return false; }}); | ||
cache.wrap(key, key2, function(cb) { | ||
methods.getMultiWidget([name, name2], cb); | ||
}, function(err) { | ||
checkErr(err); | ||
assert.ok(memoryStoreStub.mset.notCalled); | ||
done(); | ||
}); | ||
}); | ||
context("when store.mget() calls back with an error", function() { | ||
context("and ignoreCacheErrors is not set (default is false)", function() { | ||
it("bubbles up that error", function(done) { | ||
var fakeError = new Error(support.random.string()); | ||
sinon.stub(memoryStoreStub, 'mget', function() { | ||
var args = Array.prototype.slice.apply(arguments); | ||
var cb = args.pop(); | ||
cb(fakeError); | ||
}); | ||
cache.wrap(key, key2, function(cb) { | ||
methods.getMultiWidget([name, name2], cb); | ||
}, function(err) { | ||
assert.equal(err, fakeError); | ||
memoryStoreStub.mget.restore(); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
context("and ignoreCacheErrors is set to true", function() { | ||
it("does not bubble up that error", function(done) { | ||
cache = caching({store: 'memory', ttl: defaultTtl, ignoreCacheErrors: true}); | ||
var fakeError = new Error(support.random.string()); | ||
sinon.stub(memoryStoreStub, 'mget', function() { | ||
var args = Array.prototype.slice.apply(arguments); | ||
var cb = args.pop(); | ||
cb(fakeError); | ||
}); | ||
cache.wrap(key, key2, function(cb) { | ||
methods.getMultiWidget([name, name2], cb); | ||
}, function(err) { | ||
assert.equal(err, null); | ||
memoryStoreStub.mget.restore(); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
context("when an error is thrown in the work function", function() { | ||
var fakeError; | ||
beforeEach(function() { | ||
fakeError = new Error(support.random.string()); | ||
}); | ||
it("does catch the error ", function(done) { | ||
cache = caching({store: 'memory', ttl: defaultTtl, ignoreCacheErrors: false}); | ||
cache.wrap(key, key2, function(cb) { | ||
return cb(fakeError); | ||
}, function(err) { | ||
assert.equal(err, fakeError); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -782,2 +1066,3 @@ | ||
var construct; | ||
var constructMultiple; | ||
@@ -797,2 +1082,9 @@ beforeEach(function() { | ||
}); | ||
constructMultiple = sinon.spy(function(val, cb) { | ||
var timeout = support.random.number(100); | ||
setTimeout(function() { | ||
cb(null, ['value', 'value2']); | ||
}, timeout); | ||
}); | ||
}); | ||
@@ -819,2 +1111,23 @@ | ||
}); | ||
it("calls the multiWrapped function once", function(done) { | ||
var values = []; | ||
for (var i = 0; i < 2; i++) { | ||
values.push(i); | ||
} | ||
async.each(values, function(val, next) { | ||
cache.wrap('key', 'key2', function(cb) { | ||
constructMultiple(val, cb); | ||
}, function(err, results) { | ||
assert.equal(results[0], 'value'); | ||
assert.equal(results[1], 'value2'); | ||
next(err); | ||
}); | ||
}, function(err) { | ||
checkErr(err); | ||
assert.equal(constructMultiple.callCount, 1); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
@@ -821,0 +1134,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
195337
4135
438