Skip to content

Commit cae8f6a

Browse files
committed
feature: allow setting, getting and deleting multiple keys
feat(multiple keys): fix comments PR Remove yarn.lock feat(multi keys): refactor caching "wrap" adding a wrapMultiple feat(multi keys): refactor multi_caching "warp" adding a multiWrapping
1 parent 4c5eda1 commit cae8f6a

10 files changed

+1488
-102
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ coverage
33
.idea
44
*.iml
55
out
6+
.vscode

README.md

+74-3
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ See the [Express.js cache-manager example app](https://github.com/BryanDonovan/n
5151

5252
## Overview
5353

54-
First, it includes a `wrap` function that lets you wrap any function in cache.
54+
**First**, it includes a `wrap` function that lets you wrap any function in cache.
5555
(Note, this was inspired by [node-caching](https://github.com/mape/node-caching).)
5656
This is probably the feature you're looking for. As an example, where you might have to do this:
5757

@@ -82,20 +82,22 @@ function getCachedUser(id, cb) {
8282
}
8383
```
8484

85-
Second, node-cache-manager features a built-in memory cache (using [node-lru-cache](https://github.com/isaacs/node-lru-cache)),
85+
**Second**, node-cache-manager features a built-in memory cache (using [node-lru-cache](https://github.com/isaacs/node-lru-cache)),
8686
with the standard functions you'd expect in most caches:
8787

8888
set(key, val, {ttl: ttl}, cb) // * see note below
8989
get(key, cb)
9090
del(key, cb)
91+
mset(key1, val1, key2, val2, {ttl: ttl}, cb) // set several keys at once
92+
mget(key1, key2, key3, cb) // get several keys at once
9193

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

98-
Third, node-cache-manager lets you set up a tiered cache strategy. This may be of
100+
**Third**, node-cache-manager lets you set up a tiered cache strategy. This may be of
99101
limited use in most cases, but imagine a scenario where you expect tons of
100102
traffic, and don't want to hit your primary cache (like Redis) for every request.
101103
You decide to store the most commonly-requested data in an in-memory cache,
@@ -105,6 +107,8 @@ aren't as common as the ones you want to store in memory. This is something
105107
node-cache-manager handles easily and transparently.
106108

107109

110+
**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.
111+
108112
## Usage Examples
109113

110114
See examples below and in the examples directory. See ``examples/redis_example`` for an example of how to implement a
@@ -178,6 +182,41 @@ memoryCache.wrap(key, function(cb) {
178182
}
179183
```
180184
185+
You can get several keys at once. E.g.
186+
187+
```js
188+
189+
var key1 = 'user_1';
190+
var key2 = 'user_1';
191+
192+
memoryCache.wrap(key1, key2, function (cb) {
193+
getManyUser([key1, key2], cb);
194+
}, function (err, users) {
195+
console.log(users[0]);
196+
console.log(users[1]);
197+
});
198+
```
199+
200+
#### Example setting/getting several keys with mset() and mget()
201+
202+
```js
203+
memoryCache.mset('foo', 'bar', 'foo2', 'bar2' {ttl: ttl}, function(err) {
204+
if (err) { throw err; }
205+
206+
memoryCache.mget('foo', 'foo2', function(err, result) {
207+
console.log(result);
208+
// >> ['bar', 'bar2']
209+
210+
// Delete keys with del() passing arguments...
211+
memoryCache.del('foo', 'foo2', function(err) {});
212+
213+
// ...passing an Array of keys
214+
memoryCache.del(['foo', 'foo2'], function(err) {});
215+
});
216+
});
217+
218+
```
219+
181220
#### Example Using Promises
182221
183222
```javascript
@@ -275,6 +314,30 @@ multiCache.set('foo2', 'bar2', {ttl: ttl}, function(err) {
275314
});
276315
});
277316

317+
// Sets multiple keys in all caches.
318+
// You can pass as many key,value pair as you want
319+
multiCache.mset('key', 'value', 'key2', 'value2', {ttl: ttl}, function(err) {
320+
if (err) { throw err; }
321+
322+
// mget() fetches from highest priority cache.
323+
// If the first cache does not return all the keys,
324+
// the next cache is fetched with the keys that were not found.
325+
// This is done recursively until either:
326+
// - all have been found
327+
// - all caches has been fetched
328+
multiCache.mget('key', 'key2', function(err, result) {
329+
console.log(result[0]);
330+
console.log(result[1]);
331+
// >> 'bar2'
332+
// >> 'bar3'
333+
334+
// Delete from all caches
335+
multiCache.del('key', 'key2');
336+
// ...or with an Array
337+
multiCache.del(['key', 'key2']);
338+
});
339+
});
340+
278341
// Note: options with ttl are optional in wrap()
279342
multiCache.wrap(key2, function (cb) {
280343
getUser(userId2, cb);
@@ -290,6 +353,14 @@ multiCache.wrap(key2, function (cb) {
290353
console.log(user);
291354
});
292355
});
356+
357+
// Multiple keys
358+
multiCache.wrap('key1', 'key2', function (cb) {
359+
getManyUser(['key1', 'key2'], cb);
360+
}, {ttl: ttl}, function (err, users) {
361+
console.log(users[0]);
362+
console.log(users[1]);
363+
});
293364
```
294365
295366
### Specifying What to Cache in `wrap` Function

lib/caching.js

+146-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/** @module cacheManager/caching */
2-
/*jshint maxcomplexity:15*/
2+
/*jshint maxcomplexity:16*/
33
var CallbackFiller = require('./callback_filler');
4+
var utils = require('./utils');
5+
var parseWrapArguments = utils.parseWrapArguments;
46

57
/**
68
* Generic caching interface that wraps any caching library with a compatible interface.
@@ -47,12 +49,12 @@ var caching = function(args) {
4749
return new Promise(function(resolve, reject) {
4850
self.wrap(key, function(cb) {
4951
Promise.resolve()
50-
.then(promise)
51-
.then(function(result) {
52-
cb(null, result);
53-
return null;
54-
})
55-
.catch(cb);
52+
.then(promise)
53+
.then(function(result) {
54+
cb(null, result);
55+
return null;
56+
})
57+
.catch(cb);
5658
}, options, function(err, result) {
5759
if (err) {
5860
return reject(err);
@@ -66,33 +68,57 @@ var caching = function(args) {
6668
* Wraps a function in cache. I.e., the first time the function is run,
6769
* its results are stored in cache so subsequent calls retrieve from cache
6870
* instead of calling the function.
71+
* You can pass any number of keys as long as the wrapped function returns
72+
* an array with the same number of values and in the same order.
6973
*
7074
* @function
7175
* @name wrap
7276
*
73-
* @param {string} key - The cache key to use in cache operations
77+
* @param {string} key - The cache key to use in cache operations. Can be one or many.
7478
* @param {function} work - The function to wrap
7579
* @param {object} [options] - options passed to `set` function
7680
* @param {function} cb
7781
*
7882
* @example
79-
* var key = 'user_' + userId;
80-
* cache.wrap(key, function(cb) {
81-
* User.get(userId, cb);
82-
* }, function(err, user) {
83-
* console.log(user);
84-
* });
83+
* var key = 'user_' + userId;
84+
* cache.wrap(key, function(cb) {
85+
* User.get(userId, cb);
86+
* }, function(err, user) {
87+
* console.log(user);
88+
* });
89+
*
90+
* // Multiple keys
91+
* var key = 'user_' + userId;
92+
* var key2 = 'user_' + userId2;
93+
* cache.wrap(key, key2, function(cb) {
94+
* User.getMany([userId, userId2], cb);
95+
* }, function(err, users) {
96+
* console.log(users[0]);
97+
* console.log(users[1]);
98+
* });
8599
*/
86-
self.wrap = function(key, work, options, cb) {
87-
if (typeof options === 'function') {
88-
cb = options;
89-
options = {};
90-
}
100+
self.wrap = function() {
101+
var parsedArgs = parseWrapArguments(Array.prototype.slice.apply(arguments));
102+
var keys = parsedArgs.keys;
103+
var work = parsedArgs.work;
104+
var options = parsedArgs.options;
105+
var cb = parsedArgs.cb;
91106

92107
if (!cb) {
93-
return wrapPromise(key, work, options);
108+
keys.push(work);
109+
keys.push(options);
110+
return wrapPromise.apply(this, keys);
111+
}
112+
113+
if (keys.length > 1) {
114+
/**
115+
* Handle more than 1 key
116+
*/
117+
return wrapMultiple(keys, work, options, cb);
94118
}
95119

120+
var key = keys[0];
121+
96122
var hasKey = callbackFiller.has(key);
97123
callbackFiller.add(key, {cb: cb});
98124
if (hasKey) { return; }
@@ -130,20 +156,120 @@ var caching = function(args) {
130156
});
131157
};
132158

159+
function wrapMultiple(keys, work, options, cb) {
160+
/**
161+
* We create a unique key for the multiple keys
162+
* by concatenating them
163+
*/
164+
var combinedKey = keys.reduce(function(acc, k) {
165+
return acc + k;
166+
}, '');
167+
168+
var hasKey = callbackFiller.has(combinedKey);
169+
callbackFiller.add(combinedKey, {cb: cb});
170+
if (hasKey) { return; }
171+
172+
keys.push(options);
173+
keys.push(onResult);
174+
175+
self.store.mget.apply(self.store, keys);
176+
177+
function onResult(err, result) {
178+
if (err && (!self.ignoreCacheErrors)) {
179+
return callbackFiller.fill(combinedKey, err);
180+
}
181+
182+
/**
183+
* If all the values returned are cacheable we don't need
184+
* to call our "work" method and the values returned by the cache
185+
* are valid. If one or more of the values is not cacheable
186+
* the cache result is not valid.
187+
*/
188+
var cacheOK = Array.isArray(result) && result.filter(function(_result) {
189+
return self._isCacheableValue(_result);
190+
}).length === result.length;
191+
192+
if (cacheOK) {
193+
return callbackFiller.fill(combinedKey, null, result);
194+
}
195+
196+
return work(function(err, data) {
197+
if (err) {
198+
return done(err);
199+
}
200+
201+
var _args = [];
202+
data.forEach(function(value, i) {
203+
/**
204+
* Add the {key, value} pair to the args
205+
* array that we will send to mset()
206+
*/
207+
if (self._isCacheableValue(value)) {
208+
_args.push(keys[i]);
209+
_args.push(value);
210+
}
211+
});
212+
213+
// If no key|value, exit
214+
if (_args.length === 0) {
215+
return done(null);
216+
}
217+
218+
if (options && typeof options.ttl === 'function') {
219+
options.ttl = options.ttl(data);
220+
}
221+
222+
_args.push(options);
223+
_args.push(done);
224+
225+
self.store.mset.apply(self.store, _args);
226+
227+
function done(err) {
228+
if (err && (!self.ignoreCacheErrors)) {
229+
callbackFiller.fill(combinedKey, err);
230+
} else {
231+
callbackFiller.fill(combinedKey, null, data);
232+
}
233+
}
234+
});
235+
}
236+
}
237+
133238
/**
134239
* Binds to the underlying store's `get` function.
135240
* @function
136241
* @name get
137242
*/
138243
self.get = self.store.get.bind(self.store);
139244

245+
/**
246+
* Get multiple keys at once.
247+
* Binds to the underlying store's `mget` function.
248+
* @function
249+
* @name mget
250+
*/
251+
if (typeof self.store.mget === 'function') {
252+
self.mget = self.store.mget.bind(self.store);
253+
}
254+
140255
/**
141256
* Binds to the underlying store's `set` function.
142257
* @function
143258
* @name set
144259
*/
145260
self.set = self.store.set.bind(self.store);
146261

262+
/**
263+
* Set multiple keys at once.
264+
* It accepts any number of {key, value} pair
265+
* Binds to the underlying store's `mset` function.
266+
* @function
267+
* @name mset
268+
*/
269+
if (typeof self.store.mset === 'function') {
270+
self.mset = self.store.mset.bind(self.store);
271+
}
272+
147273
/**
148274
* Binds to the underlying store's `del` function if it exists.
149275
* @function

0 commit comments

Comments
 (0)