diff --git a/.jshintrc b/.jshintrc index d2de774..fb5dc81 100644 --- a/.jshintrc +++ b/.jshintrc @@ -20,6 +20,7 @@ "predef" : [ // Extra globals. "__dirname", + "Promise", "Buffer", "event", "exports", @@ -58,7 +59,7 @@ "expr" : false, // Tolerate `ExpressionStatement` as Programs. "forin" : false, // Tolerate `for in` loops without `hasOwnProperty`. "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` - "latedef" : true, // Prohibit variable use before definition. + "latedef" : "nofunc", // Prohibit variable use before definition. "loopfunc" : true, // Allow functions to be defined within loops. "maxparams" : 4, "maxdepth" : 5, diff --git a/README.md b/README.md index 611b3ec..92553a0 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ See the [Express.js cache-manager example app](https://github.com/BryanDonovan/n * [node-cache-manager-mongodb](https://github.com/v4l3r10/node-cache-manager-mongodb) +* [node-cache-manager-fs](https://github.com/hotelde/node-cache-manager-fs) + ## Overview First, it includes a `wrap` function that lets you wrap any function in cache. diff --git a/lib/caching.js b/lib/caching.js index f98bb3a..b162837 100644 --- a/lib/caching.js +++ b/lib/caching.js @@ -42,6 +42,24 @@ var caching = function(args) { }; } + function wrapPromise(key, promise, options) { + return new Promise(function(resolve, reject) { + self.wrap(key, function(cb) { + Promise.resolve() + .then(promise) + .then(function(result) { + cb(null, result); + }) + .catch(cb); + }, options, function(err, result) { + if (err) { + return reject(err); + } + resolve(result); + }); + }); + } + /** * 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 @@ -69,6 +87,10 @@ var caching = function(args) { options = {}; } + if (!cb) { + return wrapPromise(key, work, options); + } + var hasKey = callbackFiller.has(key); callbackFiller.add(key, {cb: cb, domain: process.domain}); if (hasKey) { return; } diff --git a/lib/multi_caching.js b/lib/multi_caching.js index 5b4a90b..15dcfe0 100644 --- a/lib/multi_caching.js +++ b/lib/multi_caching.js @@ -47,12 +47,27 @@ var multiCaching = function(caches, options) { } } + function getFromHighestPriorityCachePromise(key, options) { + return new Promise(function(resolve, reject) { + getFromHighestPriorityCache(key, options, function(err, result) { + if (err) { + return reject(err); + } + resolve(result); + }); + }); + } + function getFromHighestPriorityCache(key, options, cb) { if (typeof options === 'function') { cb = options; options = {}; } + if (!cb) { + return getFromHighestPriorityCachePromise(key, options); + } + var i = 0; async.eachSeries(caches, function(cache, next) { var callback = function(err, result) { @@ -72,11 +87,29 @@ var multiCaching = function(caches, options) { }; cache.store.get(key, options, callback); - }, cb); + }, function(err, result) { + return cb(err, result); + }); + } + + function setInMultipleCachesPromise(caches, opts) { + return new Promise(function(resolve, reject) { + setInMultipleCaches(caches, opts, function(err, result) { + if (err) { + return reject(err); + } + resolve(result); + }); + }); } function setInMultipleCaches(caches, opts, cb) { opts.options = opts.options || {}; + + if (!cb) { + return setInMultipleCachesPromise(caches, opts); + } + async.each(caches, function(cache, next) { var _isCacheableValue = getIsCacheableValueFunction(cache); @@ -85,7 +118,20 @@ var multiCaching = function(caches, options) { } else { next(); } - }, cb); + }, function(err, result) { + cb(err, result); + }); + } + + function getAndPassUpPromise(key) { + return new Promise(function(resolve, reject) { + self.getAndPassUp(key, function(err, result) { + if (err) { + return reject(err); + } + resolve(result); + }); + }); } /** @@ -96,13 +142,15 @@ var multiCaching = function(caches, options) { * @param {function} cb */ self.getAndPassUp = function(key, cb) { + if (!cb) { + return getAndPassUpPromise(key); + } + getFromHighestPriorityCache(key, function(err, result, index) { if (err) { return cb(err); } - cb(err, result); - if (index) { var cachesToUpdate = caches.slice(0, index); async.each(cachesToUpdate, function(cache, next) { @@ -113,9 +161,29 @@ var multiCaching = function(caches, options) { } }); } + + return cb(err, result); }); }; + function wrapPromise(key, promise, options) { + return new Promise(function(resolve, reject) { + self.wrap(key, function(cb) { + Promise.resolve() + .then(promise) + .then(function(result) { + cb(null, result); + }) + .catch(cb); + }, options, function(err, result) { + if (err) { + return reject(err); + } + resolve(result); + }); + }); + } + /** * Wraps a function in one or more caches. * Has same API as regular caching module. @@ -145,6 +213,10 @@ var multiCaching = function(caches, options) { }; } + if (!cb) { + return wrapPromise(key, work, options); + } + var hasKey = callbackFiller.has(key); callbackFiller.add(key, {cb: cb, domain: process.domain}); if (hasKey) { return; } @@ -165,8 +237,6 @@ var multiCaching = function(caches, options) { .on('error', function(err) { if (callbackFiller.has(key)) { callbackFiller.fill(key, err); - } else { - cb(err); } }) .bind(work)(function(err, data) { @@ -211,7 +281,7 @@ var multiCaching = function(caches, options) { options: options }; - setInMultipleCaches(caches, opts, cb); + return setInMultipleCaches(caches, opts, cb); }; /** @@ -230,7 +300,7 @@ var multiCaching = function(caches, options) { options = {}; } - getFromHighestPriorityCache(key, options, cb); + return getFromHighestPriorityCache(key, options, cb); }; /** diff --git a/lib/stores/memory.js b/lib/stores/memory.js index 0d6d4f9..7480382 100644 --- a/lib/stores/memory.js +++ b/lib/stores/memory.js @@ -27,6 +27,8 @@ var memoryStore = function(args) { lruCache.set(key, value, maxAge); if (cb) { process.nextTick(cb); + } else { + return Promise.resolve(value); } }; @@ -35,6 +37,7 @@ var memoryStore = function(args) { cb = options; } var value = lruCache.get(key); + if (cb) { process.nextTick(function() { cb(null, value); diff --git a/package.json b/package.json index 5e83012..8d08f92 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ }, "devDependencies": { "coveralls": "^2.3.0", + "es6-promise": "^3.0.2", "istanbul": "^0.2.11", "jscs": "^1.9.0", "jsdoc": "^3.3.0", diff --git a/test/caching.unit.js b/test/caching.unit.js index 227e4f9..c43ce7a 100644 --- a/test/caching.unit.js +++ b/test/caching.unit.js @@ -673,6 +673,62 @@ describe("caching", function() { }); }); }); + + describe("using native promises", function() { + beforeEach(function() { + cache = caching({ + store: 'memory', + max: 50, + ttl: 5 * 60 + }); + }); + + it("should be able to chain with simple promise", function(done) { + cache.wrap('key', function() { + return 'OK'; + }) + .then(function(res) { + assert.equal(res, 'OK'); + done(); + }); + }); + + it("should be able to chain with cache function as a promise", function(done) { + cache.wrap('key', function() { + return new Promise(function(resolve) { + resolve('OK'); + }); + }) + .then(function(res) { + assert.equal(res, 'OK'); + done(); + }); + }); + + it("should be able to catch errors in cache function as a promise", function(done) { + cache.wrap('key', function() { + return new Promise(function(resolve, reject) { + reject('NOK'); + }); + }) + .then(function() { + done(new Error('It should not call then since there is an error in the cache function!')); + }) + .catch(function() { + done(); + }); + }); + + it("should be able to chain with non-cacheable value", function(done) { + cache.wrap('key', function() { + return; + }) + .then(function(res) { + assert.equal(res, undefined); + done(); + }); + }); + }); }); describe("instantiating with no store passed in", function() { diff --git a/test/multi_caching.unit.js b/test/multi_caching.unit.js index 4b81b0c..97da932 100644 --- a/test/multi_caching.unit.js +++ b/test/multi_caching.unit.js @@ -181,6 +181,36 @@ describe("multiCaching", function() { }); }); }); + + describe('using promises', function() { + it('gets data from first cache that has it', function(done) { + memoryCache3.set(key, value) + .then(function() { + return multiCache.get(key); + }) + .then(function(result) { + assert.equal(result, value); + }) + .then(done); + }); + + it("passes any options to underlying caches", function(done) { + var opts = {foo: 'bar'}; + + multiCache.set(key, value) + .then(function() { + sinon.spy(memoryCache.store, 'get'); + return multiCache.get(key, opts); + }) + .then(function(result) { + assert.equal(result, value); + assert.ok(memoryCache.store.get.calledWith(key, opts)); + + memoryCache.store.get.restore(); + }) + .then(done); + }); + }); }); describe("del()", function() { @@ -259,6 +289,17 @@ describe("multiCaching", function() { }); }); }); + + it("gets data from first cache that has it using promises", function(done) { + memoryCache3.set(key, value) + .then(function() { + return multiCache.getAndPassUp(key); + }) + .then(function(result) { + assert.equal(result, value); + done(); + }); + }); }); describe("when value is not found in any cache", function() { @@ -323,6 +364,22 @@ describe("multiCaching", function() { }); }); + it("checks to see if higher levels have item using promises", function(done) { + memoryCache3.set(key, value) + .then(function() { + return multiCache.getAndPassUp(key); + }) + .then(function(result) { + assert.equal(result, value); + }) + .then(function() { + process.nextTick(function() { + assert.equal(memoryCache.get(key), value); + }); + }) + .then(done); + }); + context("when a cache store calls back with an error", function() { var fakeError; var memoryStoreStub; @@ -347,6 +404,15 @@ describe("multiCaching", function() { done(); }); }); + + it("bubbles up errors from caches and reject promise", function(done) { + multiCache.getAndPassUp(key) + .catch(function(err) { + assert.ok(memoryStoreStub.get.called); + assert.equal(err, fakeError); + done(); + }); + }); }); }); }); @@ -970,6 +1036,70 @@ describe("multiCaching", function() { }); }); }); + + describe("using native promises", function() { + beforeEach(function() { + multiCache = multiCaching([memoryCache, memoryCache3]); + }); + + it("should be able to chain with simple promise", function(done) { + multiCache.wrap('key', function() { + return 'OK'; + }) + .then(function(res) { + assert.equal(res, 'OK'); + done(); + }); + }); + + it("should be able to chain with cache function as a promise", function(done) { + multiCache.wrap('key', function() { + return new Promise(function(resolve) { + resolve('OK'); + }); + }) + .then(function(res) { + assert.equal(res, 'OK'); + done(); + }); + }); + + it("should be able to catch errors in cache function as a promise", function(done) { + multiCache.wrap('key', function() { + return new Promise(function(resolve, reject) { + reject('NOK'); + }); + }) + .then(function() { + done(new Error('It should not call then since there is an error in the cache function!')); + }) + .catch(function() { + done(); + }); + }); + + it("should be able to catch a throw in cache function as a promise", function(done) { + multiCache.wrap('key', function() { + throw 'NOK'; + }) + .then(function() { + done(new Error('It should not call then since there is an error in the cache function!')); + }) + .catch(function() { + done(); + }); + }); + + it("should be able to chain with non-cacheable value", function(done) { + multiCache.wrap('key', function() { + return; + }) + .then(function(res) { + assert.equal(res, undefined); + done(); + }); + }); + }); }); context("when instantiated with a non-Array 'caches' arg", function() { diff --git a/test/run.js b/test/run.js index 2bfa5c0..7aac560 100755 --- a/test/run.js +++ b/test/run.js @@ -6,6 +6,10 @@ var Mocha = require('mocha'); var optimist = require('optimist'); var walkDir = require('./support').walkDir; +if (typeof Promise === "undefined") { + global.Promise = require('es6-promise').Promise; +} + var argv = optimist .usage("Usage: $0 -t [types] --reporter [reporter] --timeout [timeout]")['default']( {types: 'unit,functional', reporter: 'spec', timeout: 6000})