Skip to content

Commit

Permalink
Merge 211b7c1 into 4c5eda1
Browse files Browse the repository at this point in the history
  • Loading branch information
sebelga committed Feb 6, 2018
2 parents 4c5eda1 + 211b7c1 commit cc742a6
Show file tree
Hide file tree
Showing 11 changed files with 3,347 additions and 129 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -3,3 +3,4 @@ coverage
.idea
*.iml
out
.vscode
77 changes: 74 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 @@ -275,6 +314,30 @@ multiCache.set('foo2', 'bar2', {ttl: ttl}, function(err) {
});
});

// Sets multiple keys in all caches.
// You can pass as many key,value pair as you want
multiCache.mset('key', 'value', 'key2', 'value2', {ttl: ttl}, 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 +353,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
200 changes: 163 additions & 37 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 isObject = utils.isObject;

/**
* Generic caching interface that wraps any caching library with a compatible interface.
Expand Down Expand Up @@ -66,68 +68,171 @@ 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 args = Array.prototype.slice.apply(arguments);
var length = args.length;
var work;
var options = {};
var cb;

/**
* As we can receive an unlimited number of keys
* we find the index of the first function which is
* the "work" handler to fetch the keys.
*/
for (var i = 0; i < length; i += 1) {
if (typeof args[i] === 'function') {
if (typeof args[i + 2] === 'function') {
cb = args.pop();
} else if (typeof args[i + 1] === 'function') {
cb = args.pop();
}
if (isObject(args[i + 1])) {
options = args.pop();
}
work = args.pop();
break;
}
}

if (!cb) {
return wrapPromise(key, work, options);
args.push(work);
args.push(options);
return wrapPromise.apply(this, args);
}

var isMultiple = args.length > 1;
var key = isMultiple ? args : args[0];

/**
* Keep a copy of the keys passed
*/
var keys = Array.prototype.slice.apply(args);

if (isMultiple) {
/**
* We create a unique key for the multiple keys
* by concatenating them
*/
key = args.reduce(function(acc, k) {
return acc + k;
}, '');
}

var hasKey = callbackFiller.has(key);
callbackFiller.add(key, {cb: cb});
if (hasKey) { return; }

self.store.get(key, options, function(err, result) {
if (isMultiple) {
args.push(options);
args.push(onResult);
self.store.mget.apply(self.store, args);
} else {
self.store.get(key, options, onResult);
}

function onResult(err, result) {
if (err && (!self.ignoreCacheErrors)) {
callbackFiller.fill(key, err);
} else if (self._isCacheableValue(result)) {
callbackFiller.fill(key, null, result);
return callbackFiller.fill(key, err);
}
var cacheOK;
if (isMultiple) {
/**
* 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.
*/
cacheOK = result.filter(function(_result) {
return self._isCacheableValue(_result);
}).length === result.length;
} else {
work(function(err, data) {
if (err) {
callbackFiller.fill(key, err);
return;
}
cacheOK = self._isCacheableValue(result);
}

if (!self._isCacheableValue(data)) {
callbackFiller.fill(key, null, data);
return;
}
if (cacheOK) {
return callbackFiller.fill(key, null, result);
}

if (options && typeof options.ttl === 'function') {
options.ttl = options.ttl(data);
}
return work(function(err, data) {
if (err) {
callbackFiller.fill(key, err);
return;
}

self.store.set(key, data, options, function(err) {
if (err && (!self.ignoreCacheErrors)) {
callbackFiller.fill(key, err);
} else {
callbackFiller.fill(key, null, data);
var _args = [];
if (isMultiple) {
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);
}
} else {
if (!self._isCacheableValue(data)) {
// callbackFiller.fill(key, null, data);
return done(null);
}
}

if (options && typeof options.ttl === 'function') {
options.ttl = options.ttl(data);
}

if (isMultiple) {
_args.push(options);
_args.push(done);
self.store.mset.apply(self.store, _args);
} else {
self.store.set(key, data, options, done);
}

function done(err) {
if (err && (!self.ignoreCacheErrors)) {
callbackFiller.fill(key, err);
} else {
callbackFiller.fill(key, null, data);
}
}
});
}
};

/**
Expand All @@ -137,13 +242,34 @@ var caching = function(args) {
*/
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 cc742a6

Please sign in to comment.