Browse Source

Merge branch 'release/1.0.0'

feature/fix-isCacheableValue-usage
Bryan Donovan 10 years ago
parent
commit
11873719f5
  1. 3
      .gitignore
  2. 12
      History.md
  3. 5
      Makefile
  4. 51
      README.md
  5. 4
      examples/example.js
  6. 5
      examples/redis_example/redis_store.js
  7. 8
      index.js
  8. 74
      lib/caching.js
  9. 7
      lib/index.js
  10. 117
      lib/multi_caching.js
  11. 3
      lib/stores/memory.js
  12. 3
      package.json
  13. 193
      test/caching.unit.js
  14. 236
      test/multi_caching.unit.js
  15. 209
      test/stores/options.unit.js
  16. 3
      test/support.js

3
.gitignore

@ -1,2 +1,5 @@
node_modules
coverage
.idea
*.iml
out

12
History.md

@ -1,3 +1,15 @@
- 1.0.0 2015-05-23
- Added JSDOC generation (`make docs`)
- (Breaking change) By default, cache falsey values like `false`, `0`, and `null`, but not `undefined` (#25).
- Allow users to pass in callback function `isCacheableValue` to specify what to cache.
- (Breaking change) Removed deprecated lower-case `multi_caching` export (use `multiCaching` instead).
- (Breaking change) Removed `multiCaching#get_and_pass_up` (use `getAndPassUp` instead).
- (Breaking change) Cache store methods must accept an `options` param (which can be ignored). Eg.,
`function set(key, val, options, cb) { }`
- (Breaking change) caching/multicaching methods no longer accept a `ttl` param. You must instead pass
in an options object which will be passed to the cache store's `set` method.
- (Breaking change) caching.js no longer accepts a path to cache store. Pass in an object or 'memory' instead.
- 0.19.0 2015-03-29
- Pass dispose, length & stale options to lru-cache (#22). - @gmaclennan

5
Makefile

@ -3,7 +3,7 @@ BASE = .
ISTANBUL = ./node_modules/.bin/istanbul
COVERAGE_OPTS = --lines 99 --statements 95 --branches 90 --functions 95
main: lint test
main: lint test docs
cover:
$(ISTANBUL) cover test/run.js
@ -28,5 +28,8 @@ lint:
./node_modules/.bin/jshint ./test --config $(BASE)/.jshintrc
./node_modules/.bin/jshint ./examples --config $(BASE)/.jshintrc
docs:
./node_modules/.bin/jsdoc lib --recurse --readme README.md --package package.json
echo docs available in ./out/index.html
.PHONY: test

51
README.md

@ -57,7 +57,7 @@ function getCachedUser(id, cb) {
function getCachedUser(id, cb) {
memoryCache.wrap(id, function (cacheCallback) {
getUser(id, cacheCallback);
}, ttl, cb);
}, {ttl: ttl}, cb);
}
```
@ -147,7 +147,7 @@ app.get('/foo/bar', function(req, res) {
var ttl = 10;
memoryCache.wrap(cacheKey, function(cacheCallback) {
DB.find(req.query, cacheCallback);
}, ttl, function(err, result) {
}, {ttl: ttl}, function(err, result) {
respond(res, err, result);
});
});
@ -177,7 +177,7 @@ key2 = 'user_' + userId;
ttl = 5;
// Sets in all caches.
multiCache.set('foo2', 'bar2', ttl, function(err) {
multiCache.set('foo2', 'bar2', {ttl: ttl}, function(err) {
if (err) { throw err; }
// Fetches from highest priority cache that has the key.
@ -190,10 +190,10 @@ multiCache.set('foo2', 'bar2', ttl, function(err) {
});
});
// Note: ttl is optional in wrap()
// Note: options with ttl are optional in wrap()
multiCache.wrap(key2, function (cb) {
getUser(userId2, cb);
}, ttl, function (err, user) {
}, {ttl: ttl}, function (err, user) {
console.log(user);
// Second time fetches user from memoryCache, since it's highest priority.
@ -207,6 +207,47 @@ multiCache.wrap(key2, function (cb) {
});
```
### Specifying What to Cache
Both the `caching` and `multicaching` modules allow you to pass in a callback function called
`isCacheableValue` which is called with every value returned from cache or from a wrapped function.
This lets you specify which values should and should not be cached. If the function returns true, it will be
stored in cache. By default the caches cache everything except `undefined`.
For example, if you don't want to cache `false` and `null`, you can pass in a function like this:
```javascript
var isCacheableValue = function(value) {
return value !== null && value !== false && value !== undefined;
};
```
Then pass it to `caching` like this:
```javascript
var memoryCache = cacheManager.caching({store: 'memory', isCacheableValue: isCacheableValue};
```
And pass it to `multicaching` like this:
```javascript
var multiCache = cacheManager.multiCaching([memoryCache, someOtherCache], {
isCacheableValue: isCacheableValue
});
```
## Docs
To generate JSDOC 3 documentation:
make docs
## Tests
To run tests, first run:

4
examples/example.js

@ -8,7 +8,7 @@ var ttl; //Can't use a different ttl per set() call with memory cache
//
// Basic usage
//
memoryCache.set('foo', 'bar', ttl, function(err) {
memoryCache.set('foo', 'bar', function(err) {
if (err) { throw err; }
memoryCache.get('foo', function(err, result) {
@ -112,7 +112,7 @@ multiCache.wrap(key2, function(cb) {
});
// Sets in all caches.
multiCache.set('foo2', 'bar2', ttl2, function(err) {
multiCache.set('foo2', 'bar2', {ttl: ttl2}, function(err) {
if (err) { throw err; }
// Fetches from highest priority cache that has the key.

5
examples/redis_example/redis_store.js

@ -81,7 +81,10 @@ function redisStore(args) {
});
};
self.del = function(key, cb) {
self.del = function(key, options, cb) {
if (typeof options === 'function') {
cb = options;
}
connect(function(err, conn) {
if (err) { return cb(err); }
conn.del(key, handleResponse(conn, cb));

8
index.js

@ -1,7 +1 @@
var cache = {
caching: require('./lib/caching'),
multi_caching: require('./lib/multi_caching'), //backward compat
multiCaching: require('./lib/multi_caching')
};
module.exports = cache;
module.exports = require('./lib');

74
lib/caching.js

@ -1,7 +1,18 @@
/** @module cacheManager/caching */
/*jshint maxcomplexity:15*/
var domain = require('domain');
var CallbackFiller = require('./callback_filler');
/**
* Generic caching interface that wraps any caching library with a compatible interface.
*
* @param {object} args
* @param {object|string} args.store - The store must at least have `set` and a `get` functions.
* @param {function} [args.isCacheableValue] - A callback function which is called
* with every value returned from cache or from a wrapped function. This lets you specify
* which values should and should not be cached. If the function returns true, it will be
* stored in cache. By default it caches everything except undefined.
*/
var caching = function(args) {
args = args || {};
var self = {};
@ -11,8 +22,6 @@ var caching = function(args) {
} else {
self.store = args.store;
}
} else if (typeof args.store === 'string' && args.store.match(/\//)) {
self.store = require(args.store).create(args);
} else {
var storeName = args.store || 'memory';
self.store = require('./stores/' + storeName).create(args);
@ -23,13 +32,28 @@ var caching = function(args) {
var callbackFiller = new CallbackFiller();
if (typeof args.isCacheableValue === 'function') {
self._isCacheableValue = args.isCacheableValue;
} else {
self._isCacheableValue = function(value) {
return value !== undefined;
};
}
/**
* 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.
*
* @example
* @function
* @name wrap
*
* @param {string} key - The cache key to use in cache operations
* @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);
@ -40,7 +64,7 @@ var caching = function(args) {
self.wrap = function(key, work, options, cb) {
if (typeof options === 'function') {
cb = options;
options = undefined;
options = {};
}
var hasKey = callbackFiller.has(key);
@ -50,7 +74,7 @@ var caching = function(args) {
self.store.get(key, options, function(err, result) {
if (err && (!self.ignoreCacheErrors)) {
callbackFiller.fill(key, err);
} else if (result) {
} else if (self._isCacheableValue(result)) {
callbackFiller.fill(key, null, result);
} else {
domain
@ -63,6 +87,11 @@ var caching = function(args) {
callbackFiller.fill(key, err);
return;
}
if (!self._isCacheableValue(data)) {
return cb();
}
self.store.set(key, data, options, function(err) {
if (err && (!self.ignoreCacheErrors)) {
callbackFiller.fill(key, err);
@ -75,26 +104,61 @@ var caching = function(args) {
});
};
/**
* Binds to the underlying store's `get` function.
* @function
* @name get
*/
self.get = self.store.get.bind(self.store);
/**
* Binds to the underlying store's `set` function.
* @function
* @name set
*/
self.set = self.store.set.bind(self.store);
/**
* Binds to the underlying store's `del` function if it exists.
* @function
* @name del
*/
if (typeof self.store.del === 'function') {
self.del = self.store.del.bind(self.store);
}
/**
* Binds to the underlying store's `setex` function if it exists.
* @function
* @name setex
*/
if (typeof self.store.setex === 'function') {
self.setex = self.store.setex.bind(self.store);
}
/**
* Binds to the underlying store's `reset` function if it exists.
* @function
* @name reset
*/
if (typeof self.store.reset === 'function') {
self.reset = self.store.reset.bind(self.store);
}
/**
* Binds to the underlying store's `keys` function if it exists.
* @function
* @name keys
*/
if (typeof self.store.keys === 'function') {
self.keys = self.store.keys.bind(self.store);
}
/**
* Binds to the underlying store's `ttl` function if it exists.
* @function
* @name ttl
*/
if (typeof self.store.ttl === 'function') {
self.ttl = self.store.ttl.bind(self.store);
}

7
lib/index.js

@ -0,0 +1,7 @@
/** @namespace cacheManager */
var cacheManager = {
caching: require('./caching'),
multiCaching: require('./multi_caching')
};
module.exports = cacheManager;

117
lib/multi_caching.js

@ -1,22 +1,40 @@
/** @module cacheManager/multiCaching */
var async = require('async');
var domain = require('domain');
var CallbackFiller = require('./callback_filler');
/**
* Module that lets you specify a hierarchy of caches.
*
* @param {array} caches - Array of caching objects.
* @param {object} [options]
* @param {function} [options.isCacheableValue] - A callback function which is called
* with every value returned from cache or from a wrapped function. This lets you specify
* which values should and should not be cached. If the function returns true, it will be
* stored in cache. By default it caches everything except undefined.
*/
var multiCaching = function(caches) {
var multiCaching = function(caches, options) {
var self = {};
options = options || {};
if (!Array.isArray(caches)) {
throw new Error('multiCaching requires an array of caches');
}
var callbackFiller = new CallbackFiller();
if (typeof options.isCacheableValue === 'function') {
self._isCacheableValue = options.isCacheableValue;
} else {
self._isCacheableValue = function(value) {
return value !== undefined;
};
}
function getFromHighestPriorityCache(key, options, cb) {
if (typeof options === 'function') {
cb = options;
options = undefined;
options = {};
}
var i = 0;
@ -34,28 +52,23 @@ var multiCaching = function(caches) {
next();
};
if (typeof options === 'object') {
cache.store.get(key, options, callback);
} else {
cache.store.get(key, callback);
}
}, cb);
}
function setInMultipleCaches(caches, opts, cb) {
opts.options = opts.options || {};
async.each(caches, function(cache, next) {
if (typeof opts.options === 'object') {
cache.store.set(opts.key, opts.value, opts.options, next);
} else {
cache.store.set(opts.key, opts.value, opts.ttl, next);
}
}, cb);
}
/**
* Looks for an item in cache tiers.
* When a key is found in a lower cache, all higher levels are updated.
*
* When a key is found in a lower cache, all higher levels are updated
* @param {string} key
* @param {function} cb
*/
self.getAndPassUp = function(key, cb) {
getFromHighestPriorityCache(key, function(err, result, index) {
@ -75,13 +88,6 @@ var multiCaching = function(caches) {
});
};
/**
* This is for backward-compatibility
*/
//jscs:disable requireCamelCaseOrUpperCaseIdentifiers
self.get_and_pass_up = self.getAndPassUp;
//jscs:enable requireCamelCaseOrUpperCaseIdentifiers
/**
* Wraps a function in one or more caches.
* Has same API as regular caching module.
@ -91,25 +97,24 @@ var multiCaching = function(caches) {
* without getting set in other lower-priority caches.
* If a key doesn't exist in a higher-priority cache but exists in a lower-priority
* cache, it gets set in all higher-priority caches.
*
* @param {string} key - The cache key to use in cache operations
* @param {function} work - The function to wrap
* @param {object} [options] - options passed to `set` function
* @param {function} cb
*/
self.wrap = function(key, work, options, cb) {
if (typeof options === 'function') {
cb = options;
options = undefined;
options = {};
}
function getOptsForSet(result) {
var opts = {
function getOptsForSet(value) {
return {
key: key,
value: result,
value: value,
options: options
};
if (typeof options !== 'object') {
opts.ttl = options;
}
return opts;
}
var hasKey = callbackFiller.has(key);
@ -119,7 +124,7 @@ var multiCaching = function(caches) {
getFromHighestPriorityCache(key, function(err, result, index) {
if (err) {
return callbackFiller.fill(key, err);
} else if (result) {
} else if (self._isCacheableValue(result)) {
var cachesToUpdate = caches.slice(0, index);
var opts = getOptsForSet(result);
@ -137,6 +142,10 @@ var multiCaching = function(caches) {
return callbackFiller.fill(key, err);
}
if (!self._isCacheableValue(data)) {
return cb();
}
var opts = getOptsForSet(data);
setInMultipleCaches(caches, opts, function(err) {
@ -147,37 +156,69 @@ var multiCaching = function(caches) {
});
};
/**
* Set value in all caches
*
* @function
* @name set
*
* @param {string} key
* @param {*} value
* @param {object} [options] to pass to underlying set function.
* @param {function} [cb]
*/
self.set = function(key, value, options, cb) {
if (typeof options === 'function') {
cb = options;
options = {};
}
var opts = {
key: key,
value: value,
options: options
};
if (typeof options !== 'object') {
opts.ttl = options;
}
setInMultipleCaches(caches, opts, cb);
};
/**
* Get value from highest level cache that has stored it.
*
* @function
* @name get
*
* @param {string} key
* @param {object} [options] to pass to underlying get function.
* @param {function} cb
*/
self.get = function(key, options, cb) {
if (typeof options === 'function') {
cb = options;
options = false;
options = {};
}
getFromHighestPriorityCache(key, options, cb);
};
/**
* Delete value from all caches.
*
* @function
* @name del
*
* @param {string} key
* @param {object} [options] to pass to underlying del function.
* @param {function} cb
*/
self.del = function(key, options, cb) {
if (typeof options === 'function') {
cb = options;
options = false;
options = {};
}
async.each(caches, function(cache, next) {
if (typeof options === 'object') {
cache.store.del(key, options, next);
} else {
cache.store.del(key, next);
}
}, cb);
};

3
lib/stores/memory.js

@ -16,6 +16,9 @@ var memoryStore = function(args) {
var lruCache = new Lru(lruOpts);
self.set = function(key, value, options, cb) {
if (typeof options === 'function') {
cb = options;
}
lruCache.set(key, value);
if (cb) {
process.nextTick(cb);

3
package.json

@ -1,6 +1,6 @@
{
"name": "cache-manager",
"version": "0.19.0",
"version": "1.0.0",
"description": "Cache module for Node.js",
"main": "index.js",
"scripts": {
@ -27,6 +27,7 @@
"coveralls": "^2.3.0",
"istanbul": "^0.2.11",
"jscs": "^1.9.0",
"jsdoc": "^3.3.0",
"jshint": "^2.5.4",
"mocha": "^1.20.1",
"optimist": "^0.6.1",

193
test/caching.unit.js

@ -10,14 +10,16 @@ var memoryStore = require('../lib/stores/memory');
var methods = {
getWidget: function(name, cb) {
process.nextTick(function() {
cb(null, {name: name});
});
}
};
describe("caching", function() {
var cache;
var key;
var ttl = 1;
var defaultTtl = 1;
var name;
var value;
@ -31,9 +33,11 @@ describe("caching", function() {
});
it("lets us set and get data in cache", function(done) {
cache.set(key, value, ttl, function(err) {
cache.set(key, value, {ttl: defaultTtl}, function(err) {
checkErr(err);
cache.get(key, function(err, result) {
checkErr(err);
assert.equal(result, value);
done();
});
@ -41,7 +45,8 @@ describe("caching", function() {
});
it("lets us set and get data without a callback", function(done) {
cache.set(key, value, ttl);
cache.set(key, value, {ttl: defaultTtl});
setTimeout(function() {
var result = cache.get(key);
assert.equal(result, value);
@ -49,8 +54,9 @@ describe("caching", function() {
}, 20);
});
it("lets us set and get data without a ttl or callback", function(done) {
it("lets us set and get data without options object or callback", function(done) {
cache.set(key, value);
setTimeout(function() {
var result = cache.get(key);
assert.equal(result, value);
@ -68,7 +74,7 @@ describe("caching", function() {
cache = caching({store: store});
key = support.random.string(20);
value = support.random.string();
cache.set(key, value, ttl, function(err) {
cache.set(key, value, {ttl: defaultTtl}, function(err) {
checkErr(err);
done();
});
@ -115,13 +121,13 @@ describe("caching", function() {
cache = caching({store: 'memory'});
key = support.random.string(20);
value = support.random.string();
cache.set(key, value, ttl, function(err) {
cache.set(key, value, function(err) {
checkErr(err);
key2 = support.random.string(20);
value2 = support.random.string();
cache.set(key2, value2, ttl, done);
cache.set(key2, value2, done);
});
});
@ -235,7 +241,7 @@ describe("caching", function() {
key = support.random.string(20);
savedKeys.push(key);
value = support.random.string();
cache.set(key, value, ttl, cb);
cache.set(key, value, cb);
}, done);
});
@ -256,14 +262,15 @@ describe("caching", function() {
describe("wrap()", function() {
describe("using memory (lru-cache) store", function() {
var memoryStoreStub;
var opts;
beforeEach(function() {
ttl = 0.1;
memoryStoreStub = memoryStore.create({ttl: ttl});
opts = {ttl: 0.1};
memoryStoreStub = memoryStore.create(opts);
sinon.stub(memoryStore, 'create').returns(memoryStoreStub);
cache = caching({store: 'memory', ttl: ttl, ignoreCacheErrors: false});
cache = caching({store: 'memory', ttl: opts.ttl, ignoreCacheErrors: false});
key = support.random.string(20);
name = support.random.string();
});
@ -284,10 +291,10 @@ describe("caching", function() {
it("when a ttl is passed in", function(done) {
cache.wrap(key, function(cb) {
methods.getWidget(name, cb);
}, ttl, function(err, widget) {
}, opts, function(err, widget) {
checkErr(err);
assert.deepEqual(widget, {name: name});
sinon.assert.calledWith(memoryStoreStub.set, key, {name: name}, ttl);
sinon.assert.calledWith(memoryStoreStub.set, key, {name: name}, opts);
done();
});
});
@ -298,7 +305,7 @@ describe("caching", function() {
}, function(err, widget) {
checkErr(err);
assert.deepEqual(widget, {name: name});
sinon.assert.calledWith(memoryStoreStub.set, key, {name: name}, undefined);
sinon.assert.calledWith(memoryStoreStub.set, key, {name: name}, {});
done();
});
});
@ -308,7 +315,7 @@ describe("caching", function() {
function getCachedWidget(name, cb) {
cache.wrap(key, function(cacheCb) {
methods.getWidget(name, cacheCb);
}, ttl, cb);
}, opts, cb);
}
beforeEach(function(done) {
@ -339,7 +346,7 @@ describe("caching", function() {
funcCalled = true;
cb(err, result);
});
}, ttl, function(err, widget) {
}, function(err, widget) {
checkErr(err);
assert.deepEqual(widget, {name: name});
assert.ok(memoryStoreStub.get.calledWith(key));
@ -349,6 +356,116 @@ describe("caching", function() {
});
});
var falseyValues = [false, null, 0];
falseyValues.forEach(function(falseyValue) {
context("when cached value is `" + falseyValue + "`", function() {
function getFalseyValue(cb) {
process.nextTick(function() {
cb(null, falseyValue);
});
}
function getCachedFalseyValue(cb) {
cache.wrap(key, function(cacheCb) {
getFalseyValue(cacheCb);
}, cb);
}
beforeEach(function(done) {
getCachedFalseyValue(function(err, result) {
checkErr(err);
assert.strictEqual(result, falseyValue);
memoryStoreStub.get(key, function(err, result) {
checkErr(err);
assert.strictEqual(result, falseyValue);
sinon.spy(memoryStoreStub, 'get');
done();
});
});
});
afterEach(function() {
memoryStoreStub.get.restore();
});
it("retrieves data from cache", function(done) {
getCachedFalseyValue(function(err, value) {
checkErr(err);
assert.strictEqual(value, falseyValue);
assert.ok(memoryStoreStub.get.calledWith(key));
done();
});
});
});
});
context("when we pass in an isCacheableValue function to the caching constructor", function() {
var testCallbacks = {
isCacheableValue: function(value) {
return value !== 'do_not_store_this' && value !== undefined;
}
};
function getValue(name, cb) {
process.nextTick(function() {
if (name === 'foo') {
cb(null, 'store_this');
} else {
cb(null, 'do_not_store_this');
}
});
}
function getCachedValue(name, cb) {
cache.wrap(key, function(cacheCb) {
getValue(name, function(err, result) {
cacheCb(err, result);
});
}, {ttl: defaultTtl}, cb);
}
beforeEach(function() {
sinon.spy(testCallbacks, 'isCacheableValue');
cache = caching({store: 'memory', isCacheableValue: testCallbacks.isCacheableValue});
sinon.spy(memoryStoreStub, 'set');
});
afterEach(function() {
memoryStoreStub.set.restore();
testCallbacks.isCacheableValue.restore();
});
it("stores allowed values", function(done) {
var name = 'foo';
getCachedValue(name, function(err) {
checkErr(err);
assert.ok(memoryStoreStub.set.called);
assert.ok(testCallbacks.isCacheableValue.called);
getCachedValue(name, function(err) {
checkErr(err);
done();
});
});
});
it("does not store non-allowed values", function(done) {
var name = 'bar';
getCachedValue(name, function(err) {
checkErr(err);
assert.ok(memoryStoreStub.set.notCalled);
assert.ok(testCallbacks.isCacheableValue.called);
done();
});
});
});
it("lets us make nested calls", function(done) {
function getCachedWidget(name, cb) {
cache.wrap(key, function(cacheCb) {
@ -374,9 +491,11 @@ describe("caching", function() {
});
it("expires cached result after ttl seconds", function(done) {
var ttl = 0.1;
cache.wrap(key, function(cb) {
methods.getWidget(name, cb);
}, ttl, function(err, widget) {
}, {ttl: ttl}, function(err, widget) {
checkErr(err);
assert.deepEqual(widget, {name: name});
@ -413,7 +532,7 @@ describe("caching", function() {
it("bubbles up that error", function(done) {
cache.wrap(key, function() {
throw fakeError;
}, ttl, function(err) {
}, function(err) {
assert.equal(err, fakeError);
done();
});
@ -431,7 +550,7 @@ describe("caching", function() {
cache.wrap(key, function(cb) {
methods.getWidget(name, cb);
}, ttl, function(err) {
}, function(err) {
assert.equal(err, fakeError);
memoryStoreStub.get.restore();
done();
@ -441,7 +560,7 @@ describe("caching", function() {
context("and ignoreCacheErrors is set to true", function() {
it("does not bubble up that error", function(done) {
cache = caching({store: 'memory', ttl: ttl, ignoreCacheErrors: true});
cache = caching({store: 'memory', ttl: defaultTtl, ignoreCacheErrors: true});
var fakeError = new Error(support.random.string());
@ -451,7 +570,7 @@ describe("caching", function() {
cache.wrap(key, function(cb) {
methods.getWidget(name, cb);
}, ttl, function(err) {
}, function(err) {
assert.equal(err, null);
memoryStoreStub.get.restore();
done();
@ -471,7 +590,7 @@ describe("caching", function() {
cache.wrap(key, function(cb) {
methods.getWidget(name, cb);
}, ttl, function(err) {
}, function(err) {
assert.equal(err, fakeError);
memoryStoreStub.set.restore();
done();
@ -481,16 +600,14 @@ describe("caching", function() {
context("and ignoreCacheErrors is set to true", function() {
it("does not bubbles up that error", function(done) {
cache = caching({store: 'memory', ttl: ttl, ignoreCacheErrors: true});
cache = caching({store: 'memory', ttl: defaultTtl, ignoreCacheErrors: true});
var fakeError = new Error(support.random.string());
sinon.stub(memoryStoreStub, 'set', function(key, val, ttl, cb) {
cb(fakeError);
});
sinon.stub(memoryStoreStub, 'set').yields(fakeError);
cache.wrap(key, function(cb) {
methods.getWidget(name, cb);
}, ttl, function(err) {
}, function(err) {
assert.equal(err, null);
memoryStoreStub.set.restore();
done();
@ -508,7 +625,7 @@ describe("caching", function() {
cache.wrap(key, function(cb) {
methods.getWidget(name, cb);
}, ttl, function(err, widget) {
}, function(err, widget) {
methods.getWidget.restore();
assert.equal(err, fakeError);
assert.ok(!widget);
@ -545,7 +662,7 @@ describe("caching", function() {
async.each(values, function(val, next) {
cache.wrap('key', function(cb) {
construct(val, cb);
}, ttl, function(err, result) {
}, function(err, result) {
assert.equal(result, 'value');
next(err);
});
@ -567,21 +684,9 @@ describe("caching", function() {
describe("instantiating with custom store", function() {
it("allows us to pass in our own store object", function(done) {
var store = memoryStore.create({ttl: ttl});
var store = memoryStore.create({ttl: defaultTtl});
cache = caching({store: store});
cache.set(key, value, ttl, function(err) {
checkErr(err);
cache.get(key, function(err, result) {
assert.equal(result, value);
done();
});
});
});
it("allows us to pass in a path to our own store", function(done) {
var storePath = '../lib/stores/memory';
cache = caching({store: storePath});
cache.set(key, value, ttl, function(err) {
cache.set(key, value, function(err) {
checkErr(err);
cache.get(key, function(err, result) {
assert.equal(result, value);
@ -593,7 +698,7 @@ describe("caching", function() {
it("allows us to pass in a module (uninstantiated)", function(done) {
var store = memoryStore;
cache = caching({store: store});
cache.set(key, value, ttl, function(err) {
cache.set(key, value, {ttl: defaultTtl}, function(err) {
checkErr(err);
cache.get(key, function(err, result) {
assert.equal(result, value);

236
test/multi_caching.unit.js

@ -9,7 +9,9 @@ var memoryStore = require('../lib/stores/memory');
var methods = {
getWidget: function(name, cb) {
process.nextTick(function() {
cb(null, {name: name});
});
}
};
@ -21,10 +23,11 @@ describe("multiCaching", function() {
var key;
var memoryTtl;
var name;
var ttl = 5;
var defaultTtl;
beforeEach(function() {
memoryTtl = 0.1;
defaultTtl = 5;
memoryCache = caching({store: 'memory', ttl: memoryTtl});
memoryCache2 = caching({store: 'memory', ttl: memoryTtl});
@ -45,7 +48,7 @@ describe("multiCaching", function() {
describe("set()", function() {
it("lets us set data in all caches", function(done) {
multiCache.set(key, value, ttl, function(err) {
multiCache.set(key, value, {ttl: defaultTtl}, function(err) {
checkErr(err);
memoryCache.get(key, function(err, result) {
@ -67,7 +70,7 @@ describe("multiCaching", function() {
});
it("lets us set data without a callback", function(done) {
multiCache.set(key, value, ttl);
multiCache.set(key, value, {ttl: defaultTtl});
setTimeout(function() {
multiCache.get(key, function(err, result) {
checkErr(err);
@ -92,7 +95,34 @@ describe("multiCaching", function() {
}, 20);
});
it("lets us set data without a ttl or callback", function(done) {
it("lets us set data without an options param", function(done) {
multiCache.set(key, value, function(err) {
checkErr(err);
multiCache.get(key, function(err, result) {
checkErr(err);
assert.equal(result, value);
memoryCache.get(key, function(err, result) {
checkErr(err);
assert.equal(result, value);
memoryCache2.get(key, function(err, result) {
checkErr(err);
assert.equal(result, value);
memoryCache3.get(key, function(err, result) {
checkErr(err);
assert.equal(result, value);
done();
});
});
});
});
});
});
it("lets us set data without options or callback", function(done) {
multiCache.set(key, value);
setTimeout(function() {
multiCache.get(key, function(err, result) {
@ -121,7 +151,7 @@ describe("multiCaching", function() {
describe("get()", function() {
it("gets data from first cache that has it", function(done) {
memoryCache3.set(key, value, ttl, function(err) {
memoryCache3.set(key, value, function(err) {
checkErr(err);
multiCache.get(key, function(err, result) {
@ -131,11 +161,31 @@ describe("multiCaching", function() {
});
});
});
it("passes any options to underlying caches", function(done) {
multiCache.set(key, value, function(err) {
checkErr(err);
sinon.spy(memoryCache.store, 'get');
var opts = {foo: 'bar'};
multiCache.get(key, opts, function(err, result) {
checkErr(err);
assert.equal(result, value);
assert.ok(memoryCache.store.get.calledWith(key, opts));
memoryCache.store.get.restore();
done();
});
});
});
});
describe("del()", function() {
it("lets us delete data in all caches", function(done) {
multiCache.set(key, value, ttl, function(err) {
multiCache.set(key, value, function(err) {
checkErr(err);
multiCache.del(key, function(err) {
@ -160,7 +210,7 @@ describe("multiCaching", function() {
});
it("lets us delete data without a callback", function(done) {
multiCache.set(key, value, ttl, function(err) {
multiCache.set(key, value, function(err) {
checkErr(err);
multiCache.del(key);
@ -199,7 +249,7 @@ describe("multiCaching", function() {
});
it("gets data from first cache that has it", function(done) {
memoryCache3.set(key, value, ttl, function(err) {
memoryCache3.set(key, value, function(err) {
checkErr(err);
multiCache.getAndPassUp(key, function(err, result) {
@ -255,7 +305,7 @@ describe("multiCaching", function() {
});
it("checks to see if higher levels have item", function(done) {
memoryCache3.set(key, value, ttl, function(err) {
memoryCache3.set(key, value, function(err) {
checkErr(err);
multiCache.getAndPassUp(key, function(err, result) {
@ -278,9 +328,9 @@ describe("multiCaching", function() {
var memoryStoreStub;
beforeEach(function() {
memoryStoreStub = memoryStore.create({ttl: ttl});
memoryStoreStub = memoryStore.create({ttl: defaultTtl});
sinon.stub(memoryStore, 'create').returns(memoryStoreStub);
memoryCache = caching({store: 'memory', ttl: ttl});
memoryCache = caching({store: 'memory', ttl: defaultTtl});
multiCache = multiCaching([memoryCache]);
fakeError = new Error(support.random.string());
sinon.stub(memoryStoreStub, 'get').yields(fakeError);
@ -316,13 +366,17 @@ describe("multiCaching", function() {
memoryCache3.store.set.restore();
});
/**
* Note: it's up to the underlying cache implementation to handle the ttl number.
* We're just testing that the ttl gets passed to the underlying store.
*/
it('when a ttl number is passed in', function(done) {
multiCache.wrap(key, function(cb) {
methods.getWidget(name, cb);
}, ttl, function(err, widget) {
}, defaultTtl, function(err, widget) {
checkErr(err);
assert.deepEqual(widget, {name: name});
sinon.assert.calledWith(memoryCache3.store.set, key, {name: name}, ttl);
sinon.assert.calledWith(memoryCache3.store.set, key, {name: name}, defaultTtl);
done();
});
});
@ -330,15 +384,15 @@ describe("multiCaching", function() {
it('when a ttl option is passed in', function(done) {
multiCache.wrap(key, function(cb) {
methods.getWidget(name, cb);
}, {ttl: ttl}, function(err, widget) {
}, {ttl: defaultTtl}, function(err, widget) {
checkErr(err);
assert.deepEqual(widget, {name: name});
sinon.assert.calledWith(memoryCache3.store.set, key, {name: name}, {ttl: ttl});
sinon.assert.calledWith(memoryCache3.store.set, key, {name: name}, {ttl: defaultTtl});
done();
});
});
it('when a ttl is not passed in', function(done) {
it('when no options are passed in', function(done) {
multiCache.wrap(key, function(cb) {
methods.getWidget(name, cb);
}, function(err, widget) {
@ -353,9 +407,7 @@ describe("multiCaching", function() {
context("when wrapped function calls back with an error", function() {
it("calls back with that error", function(done) {
var fakeError = new Error(support.random.string());
sinon.stub(methods, 'getWidget', function(name, cb) {
cb(fakeError, {name: name});
});
sinon.stub(methods, 'getWidget').yields(fakeError, {name: name});
multiCache.wrap(key, function(cb) {
methods.getWidget(name, cb);
@ -367,6 +419,121 @@ describe("multiCaching", function() {
});
});
});
var falseyValues = [false, null, 0];
falseyValues.forEach(function(falseyValue) {
context("when cached value is `" + falseyValue + "`", function() {
function getFalseyValue(cb) {
process.nextTick(function() {
cb(null, falseyValue);
});
}
function getCachedFalseyValue(cb) {
multiCache.wrap(key, function(cacheCb) {
getFalseyValue(cacheCb);
}, cb);
}
beforeEach(function(done) {
multiCache = multiCaching([memoryCache3]);
sinon.spy(memoryCache3.store, 'set');
getCachedFalseyValue(function(err, result) {
checkErr(err);
assert.strictEqual(result, falseyValue);
memoryCache3.get(key, function(err, result) {
checkErr(err);
assert.strictEqual(result, falseyValue);
sinon.spy(memoryCache3.store, 'get');
done();
});
});
});
afterEach(function() {
memoryCache3.store.set.restore();
memoryCache3.store.get.restore();
});
it("sets data in and retrieves data from cache", function(done) {
getCachedFalseyValue(function(err, value) {
checkErr(err);
assert.strictEqual(value, falseyValue);
assert.ok(memoryCache3.store.set.calledWith(key));
assert.ok(memoryCache3.store.get.calledWith(key));
done();
});
});
});
});
context("when we pass in an isCacheableValue function to the caching constructor", function() {
var testCallbacks = {
isCacheableValue: function(value) {
return value !== 'do_not_store_this' && value !== undefined;
}
};
function getValue(name, cb) {
process.nextTick(function() {
if (name === 'foo') {
cb(null, 'store_this');
} else {
cb(null, 'do_not_store_this');
}
});
}
function getCachedValue(name, cb) {
multiCache.wrap(key, function(cacheCb) {
getValue(name, function(err, result) {
cacheCb(err, result);
});
}, cb);
}
beforeEach(function() {
sinon.spy(testCallbacks, 'isCacheableValue');
multiCache = multiCaching([memoryCache3], {isCacheableValue: testCallbacks.isCacheableValue});
sinon.spy(memoryCache3.store, 'set');
});
afterEach(function() {
memoryCache3.store.set.restore();
testCallbacks.isCacheableValue.restore();
});
it("stores allowed values", function(done) {
var name = 'foo';
getCachedValue(name, function(err) {
checkErr(err);
assert.ok(memoryCache3.store.set.called);
assert.ok(testCallbacks.isCacheableValue.called);
getCachedValue(name, function(err) {
checkErr(err);
done();
});
});
});
it("does not store non-allowed values", function(done) {
var name = 'bar';
getCachedValue(name, function(err) {
checkErr(err);
assert.ok(memoryCache3.store.set.notCalled);
done();
});
});
});
});
describe("using two cache stores", function() {
@ -406,7 +573,7 @@ describe("multiCaching", function() {
context("when value exists in first store but not second", function() {
it("returns value from first store, does not set it in second", function(done) {
memoryCache.set(key, {name: name}, ttl, function(err) {
memoryCache.set(key, {name: name}, function(err) {
checkErr(err);
multiCache.wrap(key, function(cb) {
@ -427,7 +594,7 @@ describe("multiCaching", function() {
context("when value exists in second store but not first", function() {
it("returns value from second store, sets it in first store", function(done) {
memoryCache3.set(key, {name: name}, ttl, function(err) {
memoryCache3.set(key, {name: name}, function(err) {
checkErr(err);
multiCache.wrap(key, function(cb) {
@ -489,7 +656,7 @@ describe("multiCaching", function() {
context("when value exists in first store only", function() {
it("returns value from first store, does not set it in second or third", function(done) {
memoryCache.set(key, {name: name}, ttl, function(err) {
memoryCache.set(key, {name: name}, function(err) {
checkErr(err);
multiCache.wrap(key, function(cb) {
@ -515,7 +682,7 @@ describe("multiCaching", function() {
context("when value exists in second store only", function() {
it("returns value from second store, sets it in first store, does not set third store", function(done) {
memoryCache3.set(key, {name: name}, ttl, function(err) {
memoryCache3.set(key, {name: name}, function(err) {
checkErr(err);
multiCache.wrap(key, function(cb) {
@ -541,7 +708,7 @@ describe("multiCaching", function() {
context("when value exists in third store only", function() {
it("returns value from third store, sets it in first and second stores", function(done) {
memoryCache2.set(key, {name: name}, ttl, function(err) {
memoryCache2.set(key, {name: name}, function(err) {
checkErr(err);
multiCache.wrap(key, function(cb) {
@ -628,9 +795,7 @@ describe("multiCaching", function() {
it("bubbles up that error", function(done) {
var fakeError = new Error(support.random.string());
sinon.stub(memoryStoreStub, 'get', function(key, cb) {
cb(fakeError);
});
sinon.stub(memoryStoreStub, 'get').yields(fakeError);
multiCache.wrap(key, function(cb) {
methods.getWidget(name, cb);
@ -646,9 +811,7 @@ describe("multiCaching", function() {
it("bubbles up that error", function(done) {
var fakeError = new Error(support.random.string());
sinon.stub(memoryStoreStub, 'set', function(key, val, ttl, cb) {
cb(fakeError);
});
sinon.stub(memoryStoreStub, 'set').yields(fakeError);
multiCache.wrap(key, function(cb) {
methods.getWidget(name, cb);
@ -666,9 +829,20 @@ describe("multiCaching", function() {
beforeEach(function() {
multiCache = multiCaching([memoryCache, memoryCache3]);
var firstTimeout = 110;
var firstTimeoutUsed = false;
function getTimeout() {
if (firstTimeoutUsed) {
support.random.number(100);
} else {
firstTimeoutUsed = true;
return firstTimeout;
}
}
construct = sinon.spy(function(val, cb) {
var timeout = support.random.number(100);
var timeout = getTimeout();
setTimeout(function() {
cb(null, 'value');
}, timeout);

209
test/stores/options.unit.js

@ -1,209 +0,0 @@
var caching = require("../../index");
var assert = require("assert");
var support = require("../support");
var checkErr = support.checkErr;
var memoryFlag = "";
var key;
var value;
var testStore = function(args) {
args = args || {};
var self = {};
self.name = "options";
self.store = {};
self.get = function(key, options, cb) {
var optionsMapped = false;
if (typeof options === "function") {
cb = options;
options = false;
optionsMapped = true;
}
if (options && options.value) {
return cb(null, options.value + "ValueOption");
} else if (options && options.fn) {
options.fn("GetFunctionOption");
return cb(null, "GetFunctionOption");
} else if (options && options.runNormal) {
return cb(null, self.store[key]);
} else if (optionsMapped) {
return cb();
}
return cb("Error No Options");
};
self.set = function(key, value, options, cb) {
var optionsMapped = false;
if (typeof options === "function") {
cb = options;
options = false;
optionsMapped = true;
} else if (typeof options !== "object") {
options = {ttl: options, runNormal: true};
}
if (options && options.value) {
memoryFlag = options.value + "ValueOption";
return cb();
} else if (options && options.fn) {
options.fn("SetFunctionOption");
return cb();
} else if (options && options.runNormal) {
self.store[key] = value;
return cb(null, self.store[key]);
} else if (optionsMapped) {
return cb();
}
return cb("Error No Options");
};
self.del = function(key, options, cb) {
var optionsMapped = false;
if (typeof options === "function") {
cb = options;
options = false;
optionsMapped = true;
}
if (options && options.value) {
memoryFlag = options.value + "ValueOption";
return cb();
} else if (options && options.fn) {
options.fn("DeleteFunctionOption");
return cb();
} else if (options && options.runNormal) {
delete self.store[key];
return cb(null, "");
} else if (optionsMapped) {
return cb();
}
return cb("Error No Options");
};
return {
create: function() {
return self;
}
};
};
describe("Methods with options", function() {
var testInstance = caching.caching({store: testStore()});
var testCache;
before(function() {
key = support.random.string(20);
value = support.random.string(20);
testCache = caching.multiCaching([testInstance]);
});
describe("get with options", function() {
it("lets us pass options by value", function(done) {
var options = {value: value};
testCache.get(key, options, function(err, response) {
assert.equal(response, value + "ValueOption");
done();
});
});
it("lets us pass options by function", function(done) {
var options = {
fn: function(response) {
assert.equal(response, "GetFunctionOption");
done();
}
};
testCache.get(key, options, function(err, response) {
assert.equal(response, "GetFunctionOption");
});
});
});
describe("set with options", function() {
var ttl = 60;
it("lets us pass options by value", function(done) {
var options = {ttl: ttl, value: value};
testCache.set(key, value, options, function() {
assert.equal(memoryFlag, value + "ValueOption");
done();
});
});
it("lets us pass options by function", function(done) {
var options = {
ttl: ttl,
fn: function(response) {
assert.equal(response, "SetFunctionOption");
done();
}
};
testCache.set(key, value, options, function() {}, options);
});
});
describe("delete with options", function() {
it("lets us pass options by value", function(done) {
var options = {value: value};
testCache.del(key, options, function() {
assert.equal(memoryFlag,value + "ValueOption");
done();
});
});
it("lets us pass options by function", function(done) {
var options = {
fn: function(response) {
assert.equal(response, "DeleteFunctionOption");
done();
}
};
testCache.del(key, options, function() {}, options);
});
});
});
describe("Multiple stores with options", function() {
var testInstance = caching.caching({store: testStore()});
var memInstance = caching.caching({store: "memory"});
var testCache;
var options = {runNormal: true};
var ttl = 1;
before(function() {
key = support.random.string(20);
value = support.random.string(20);
testCache = caching.multiCaching([testInstance, memInstance]);
});
it("lets us pass options which only one store uses", function() {
testCache.set(key, value, options, function(err) {
checkErr(err);
testCache.get(key, options, function(err, response) {
checkErr(err);
assert.equal(response, value);
testCache.del(key, options, function(err) {
checkErr(err);
testCache.get(key, options, function(err, response) {
checkErr(err);
assert.equal(response, undefined);
});
});
});
});
});
it("lets us not pass options which only one store uses", function() {
testCache.set(key, value, ttl, function(err) {
checkErr(err);
testCache.get(key, function(err, response) {
checkErr(err);
assert.equal(response, value);
testCache.del(key, function(err) {
checkErr(err);
testCache.get(key, function(err, response) {
checkErr(err);
assert.equal(response, undefined);
});
});
});
});
});
});

3
test/support.js

@ -90,9 +90,8 @@ var support = {
testSetGetDel: function(cache, cb) {
var key = 'TEST' + support.random.string();
var val = support.random.string();
var ttl;
cache.set(key, val, ttl, function(err) {
cache.set(key, val, function(err) {
if (err) { return cb(err); }
cache.get(key, function(err, result) {

Loading…
Cancel
Save