var assert = require('assert'); var async = require('async'); var sinon = require('sinon'); var support = require('./support'); var checkErr = support.checkErr; var caching = require('../index').caching; var multiCaching = require('../index').multiCaching; var memoryStore = require('../lib/stores/memory'); var methods = { getWidget: function(name, cb) { cb(null, {name: name}); } }; describe("multiCaching", function() { var memoryCache; var memoryCache2; var memoryCache3; var multiCache; var key; var memoryTtl; var name; var ttl = 5; beforeEach(function() { memoryTtl = 0.1; memoryCache = caching({store: 'memory', ttl: memoryTtl}); memoryCache2 = caching({store: 'memory', ttl: memoryTtl}); memoryCache3 = caching({store: 'memory', ttl: memoryTtl}); key = support.random.string(20); name = support.random.string(); }); describe("get(), set(), del()", function() { var value; beforeEach(function() { multiCache = multiCaching([memoryCache, memoryCache2, memoryCache3]); key = support.random.string(20); value = support.random.string(); }); describe("set()", function() { it("lets us set data in all caches", function(done) { multiCache.set(key, value, ttl, function(err) { checkErr(err); memoryCache.get(key, function(err, result) { 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 a callback", function(done) { multiCache.set(key, value, ttl); setTimeout(function() { multiCache.get(key, function(err, result) { assert.equal(result, value); memoryCache.get(key, function(err, result) { 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(); }); }); }); }); }, 20); }); it("lets us set data without a ttl or callback", function(done) { multiCache.set(key, value); setTimeout(function() { multiCache.get(key, function(err, result) { assert.equal(result, value); memoryCache.get(key, function(err, result) { 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(); }); }); }); }); }, 20); }); }); describe("get()", function() { it("gets data from first cache that has it", function(done) { memoryCache3.set(key, value, ttl, function(err) { checkErr(err); multiCache.get(key, function(err, result) { checkErr(err); assert.equal(result, value); done(); }); }); }); }); describe("del()", function() { it("lets us delete data in all caches", function(done) { multiCache.set(key, value, ttl, function(err) { checkErr(err); multiCache.del(key, function(err) { checkErr(err); memoryCache.get(key, function(err, result) { assert.ok(!result); memoryCache2.get(key, function(err, result) { checkErr(err); assert.ok(!result); memoryCache3.get(key, function(err, result) { checkErr(err); assert.ok(!result); done(); }); }); }); }); }); }); it("lets us delete data without a callback", function(done) { multiCache.set(key, value, ttl, function(err) { checkErr(err); multiCache.del(key); setTimeout(function() { memoryCache.get(key, function(err, result) { assert.ok(!result); memoryCache2.get(key, function(err, result) { checkErr(err); assert.ok(!result); memoryCache3.get(key, function(err, result) { checkErr(err); assert.ok(!result); done(); }); }); }); }, 10); }); }); }); }); describe("getAndPassUp()", function() { var value; var key; describe("using a single cache store", function() { beforeEach(function() { multiCache = multiCaching([memoryCache3]); key = support.random.string(20); value = support.random.string(); }); it("gets data from first cache that has it", function(done) { memoryCache3.set(key, value, ttl, function(err) { checkErr(err); multiCache.getAndPassUp(key, function(err, result) { checkErr(err); assert.equal(result, value); done(); }); }); }); }); describe("when value is not found in any cache", function() { var response; beforeEach(function(done) { key = support.random.string(10); sinon.spy(memoryCache, 'set'); sinon.spy(memoryCache2, 'set'); sinon.spy(memoryCache3, 'set'); multiCache.getAndPassUp(key, function(err, result) { checkErr(err); response = result; done(); }); }); afterEach(function() { memoryCache.set.restore(); memoryCache2.set.restore(); memoryCache3.set.restore(); }); it("calls back with undefined", function() { assert.strictEqual(response, undefined); }); it("does not set anything in caches", function(done) { process.nextTick(function() { assert.ok(memoryCache.set.notCalled); assert.ok(memoryCache2.set.notCalled); assert.ok(memoryCache3.set.notCalled); done(); }); }); }); describe("using multi cache store", function() { beforeEach(function() { multiCache = multiCaching([memoryCache, memoryCache2, memoryCache3]); key = support.random.string(20); value = support.random.string(); }); it("checks to see if higher levels have item", function(done) { memoryCache3.set(key, value, ttl, function(err) { checkErr(err); multiCache.getAndPassUp(key, function(err, result) { checkErr(err); assert.equal(result, value); process.nextTick(function() { memoryCache.get(key, function(err, result) { assert.equal(result, value); checkErr(err); done(); }); }); }); }); }); }); }); describe("wrap()", function() { describe("using a single cache store", function() { beforeEach(function() { multiCache = multiCaching([memoryCache3]); }); context("calls back with the result of a function", function() { beforeEach(function() { sinon.spy(memoryCache3.store, 'set'); }); afterEach(function() { memoryCache3.store.set.restore(); }); it('when a ttl is passed in', function(done) { multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, ttl, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); sinon.assert.calledWith(memoryCache3.store.set, key, {name: name}, ttl); done(); }); }); it('when a ttl is not passed in', function(done) { multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); sinon.assert.calledWith(memoryCache3.store.set, key, {name: name}); done(); }); }); }); 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}); }); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { methods.getWidget.restore(); assert.equal(err, fakeError); assert.ok(!widget); done(); }); }); }); }); describe("using two cache stores", function() { beforeEach(function() { multiCache = multiCaching([memoryCache, memoryCache3]); }); it("calls back with the result of a function", function(done) { multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); done(); }); }); it("sets value in all caches", function(done) { multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); memoryCache.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); memoryCache3.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); done(); }); }); }); }); 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) { checkErr(err); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); memoryCache3.get(key, function(err, result) { checkErr(err); assert.equal(result, null); done(); }); }); }); }); }); 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) { checkErr(err); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); memoryCache.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); done(); }); }); }); }); }); }); describe("using three cache stores", function() { beforeEach(function() { multiCache = multiCaching([memoryCache, memoryCache3, memoryCache2]); }); it("calls back with the result of a function", function(done) { multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); done(); }); }); it("sets value in all caches", function(done) { multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); memoryCache.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); memoryCache2.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); memoryCache3.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); done(); }); }); }); }); }); 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) { checkErr(err); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); memoryCache2.get(key, function(err, result) { checkErr(err); assert.equal(result, null); memoryCache3.get(key, function(err, result) { checkErr(err); assert.equal(result, null); done(); }); }); }); }); }); }); 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) { checkErr(err); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); memoryCache.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); memoryCache2.get(key, function(err, result) { checkErr(err); assert.equal(result, null); done(); }); }); }); }); }); }); 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) { checkErr(err); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err, widget) { checkErr(err); assert.deepEqual(widget, {name: name}); memoryCache3.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); memoryCache.get(key, function(err, result) { checkErr(err); assert.deepEqual(result, {name: name}); done(); }); }); }); }); }); }); it("lets us make nested calls", function(done) { function getCachedWidget(name, cb) { multiCache.wrap(key, function(cacheCb) { methods.getWidget(name, cacheCb); }, cb); } getCachedWidget(name, function(err, widget) { checkErr(err); assert.equal(widget.name, name); getCachedWidget(name, function(err, widget) { checkErr(err); assert.equal(widget.name, name); getCachedWidget(name, function(err, widget) { checkErr(err); assert.equal(widget.name, name); done(); }); }); }); }); }); context("error handling", function() { var memoryStoreStub; var ttl; beforeEach(function() { ttl = 0.1; memoryStoreStub = memoryStore.create({ttl: ttl}); sinon.stub(memoryStore, 'create').returns(memoryStoreStub); memoryCache = caching({store: 'memory', ttl: ttl}); multiCache = multiCaching([memoryCache]); }); afterEach(function() { memoryStore.create.restore(); }); context("when an error is thrown in the work function", function() { var fakeError; beforeEach(function() { fakeError = new Error(support.random.string()); }); it("bubbles up that error", function(done) { multiCache.wrap(key, function() { throw fakeError; }, ttl, function(err) { assert.equal(err, fakeError); done(); }); }); }); context("when store.get() calls back with an error", function() { it("bubbles up that error", function(done) { var fakeError = new Error(support.random.string()); sinon.stub(memoryStoreStub, 'get', function(key, cb) { cb(fakeError); }); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err) { assert.equal(err, fakeError); memoryStoreStub.get.restore(); done(); }); }); }); context("when store.set() calls back with an error", 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); }); multiCache.wrap(key, function(cb) { methods.getWidget(name, cb); }, function(err) { assert.equal(err, fakeError); memoryStoreStub.set.restore(); done(); }); }); }); }); describe("when called multiple times in parallel with same key", function() { var construct; beforeEach(function() { multiCache = multiCaching([memoryCache, memoryCache3]); construct = sinon.spy(function(val, cb) { var timeout = support.random.number(100); setTimeout(function() { cb(null, 'value'); }, timeout); }); }); it("calls the wrapped function once", function(done) { var values = []; for (var i = 0; i < 5; i++) { values.push(i); } async.each(values, function(val, next) { multiCache.wrap('key', function(cb) { construct(val, cb); }, function(err, result) { assert.equal(result, 'value'); next(err); }); }, function(err) { checkErr(err); assert.equal(construct.callCount, 1); done(); }); }); }); }); context("when instantiated with a non-Array 'caches' arg", function() { it("throws an error", function() { assert.throws(function() { multiCaching({foo: 'bar'}); }, /multiCaching requires an array/); }); }); });