Skip to content

Commit

Permalink
Merge 4ec6a44 into 4c5eda1
Browse files Browse the repository at this point in the history
  • Loading branch information
sebelga committed Feb 12, 2018
2 parents 4c5eda1 + 4ec6a44 commit 66cd112
Show file tree
Hide file tree
Showing 10 changed files with 1,536 additions and 102 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -3,3 +3,4 @@ coverage
.idea
*.iml
out
.vscode
86 changes: 83 additions & 3 deletions README.md
Expand Up @@ -51,7 +51,7 @@ See the [Express.js cache-manager example app](https://github.com/BryanDonovan/n

## Overview

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).)
This is probably the feature you're looking for. As an example, where you might have to do this:

Expand Down Expand Up @@ -82,20 +82,22 @@ function getCachedUser(id, cb) {
}
```

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:

set(key, val, {ttl: ttl}, cb) // * see note below
get(key, cb)
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

// * Note that depending on the underlying store, you may be able to pass the
// ttl as the third param, like this:
set(key, val, ttl, cb)
// ... or pass no ttl at all:
set(key, val, cb)

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
traffic, and don't want to hit your primary cache (like Redis) for every request.
You decide to store the most commonly-requested data in an in-memory cache,
Expand All @@ -105,6 +107,8 @@ aren't as common as the ones you want to store in memory. This is something
node-cache-manager handles easily and transparently.


**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

See examples below and in the examples directory. See ``examples/redis_example`` for an example of how to implement a
Expand Down Expand Up @@ -178,6 +182,41 @@ memoryCache.wrap(key, function(cb) {
}
```
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
```javascript
Expand Down Expand Up @@ -262,6 +301,7 @@ key2 = 'user_' + userId;
ttl = 5;

// Sets in all caches.
// The "ttl" option can also be a function (see example below)
multiCache.set('foo2', 'bar2', {ttl: ttl}, function(err) {
if (err) { throw err; }

Expand All @@ -275,6 +315,38 @@ multiCache.set('foo2', 'bar2', {ttl: ttl}, function(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()
multiCache.wrap(key2, function (cb) {
getUser(userId2, cb);
Expand All @@ -290,6 +362,14 @@ multiCache.wrap(key2, function (cb) {
console.log(user);
});
});

// 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]);
});
```
### Specifying What to Cache in `wrap` Function
Expand Down
166 changes: 146 additions & 20 deletions lib/caching.js
@@ -1,6 +1,8 @@
/** @module cacheManager/caching */
/*jshint maxcomplexity:15*/
/*jshint maxcomplexity:16*/
var CallbackFiller = require('./callback_filler');
var utils = require('./utils');
var parseWrapArguments = utils.parseWrapArguments;

/**
* Generic caching interface that wraps any caching library with a compatible interface.
Expand Down Expand Up @@ -47,12 +49,12 @@ var caching = function(args) {
return new Promise(function(resolve, reject) {
self.wrap(key, function(cb) {
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) {
if (err) {
return reject(err);
Expand All @@ -66,33 +68,57 @@ var caching = function(args) {
* 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.
* 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.
*
* @function
* @name wrap
*
* @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
* @param {object} [options] - options passed to `set` function
* @param {function} cb
*
* @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);
callbackFiller.add(key, {cb: cb});
if (hasKey) { return; }
Expand Down Expand Up @@ -130,20 +156,120 @@ var caching = function(args) {
});
};

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);
}
}
});
}
}

/**
* Binds to the underlying store's `get` function.
* @function
* @name get
*/
self.get = self.store.get.bind(self.store);

/**
* 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.
* @function
* @name set
*/
self.set = self.store.set.bind(self.store);

/**
* 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.
* @function
Expand Down

0 comments on commit 66cd112

Please sign in to comment.