@nrk/nodecache-as-promised
Advanced tools
Comparing version 1.1.0 to 1.2.1
@@ -1,23 +0,14 @@ | ||
'use strict'; | ||
"use strict"; | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /* eslint max-nested-callbacks: 0 */ | ||
var _ = _interopRequireWildcard(require("../")); | ||
var _expect = _interopRequireDefault(require("expect.js")); | ||
var _ = require('../'); | ||
var _sinon = _interopRequireDefault(require("sinon")); | ||
var _2 = _interopRequireDefault(_); | ||
var _logHelper = require("../utils/log-helper"); | ||
var _expect = require('expect.js'); | ||
var _expect2 = _interopRequireDefault(_expect); | ||
var _sinon = require('sinon'); | ||
var _sinon2 = _interopRequireDefault(_sinon); | ||
var _logHelper = require('../utils/log-helper'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } | ||
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } | ||
@@ -31,79 +22,84 @@ const dummyKey = 'hei/verden'; | ||
}; | ||
describe('CacheManager', () => { | ||
describe('instantation', () => { | ||
it('should create a new empty instance', () => { | ||
const cacheInstance = (0, _2.default)({}, {}); | ||
(0, _expect2.default)(cacheInstance).to.be.a(Object); | ||
const cacheInstance = (0, _.default)({}, {}); | ||
(0, _expect.default)(cacheInstance).to.be.a(Object); | ||
const info = cacheInstance.debug(); | ||
(0, _expect2.default)(info.itemCount).to.equal(0); | ||
(0, _expect.default)(info.itemCount).to.equal(0); | ||
}); | ||
it('should have exported plugins', () => { | ||
(0, _expect2.default)(_.distCache).to.be.a('function'); | ||
(0, _expect2.default)(_.persistentCache).to.be.a('function'); | ||
(0, _expect.default)(_.distCache).to.be.a('function'); | ||
(0, _expect.default)(_.persistentCache).to.be.a('function'); | ||
}); | ||
it('should create a new prefilled instance with a cloned copy', () => { | ||
const obj = { hei: 'verden' }; | ||
const cacheInstance = (0, _2.default)({ initial: obj }); | ||
const obj = { | ||
hei: 'verden' | ||
}; | ||
const cacheInstance = (0, _.default)({ | ||
initial: obj | ||
}); | ||
obj.hei = 'world'; | ||
(0, _expect2.default)(cacheInstance).to.be.a(Object); | ||
(0, _expect.default)(cacheInstance).to.be.a(Object); | ||
const info = cacheInstance.debug(); | ||
(0, _expect2.default)(info.itemCount).to.equal(1); | ||
(0, _expect2.default)(cacheInstance.get('hei').value).to.equal('verden'); | ||
(0, _expect2.default)(cacheInstance.get('hei').cache).to.equal('hit'); | ||
(0, _expect.default)(info.itemCount).to.equal(1); | ||
(0, _expect.default)(cacheInstance.get('hei').value).to.equal('verden'); | ||
(0, _expect.default)(cacheInstance.get('hei').cache).to.equal('hit'); | ||
}); | ||
}); | ||
describe('debug', () => { | ||
it('should print a debug of the cache with extra options', () => { | ||
// more thorough testing of debug in debug.spec.js | ||
const cacheInstance = (0, _2.default)({ initial: { hello: 'world' } }); | ||
const info = cacheInstance.debug({ extraData: 'values' }); | ||
(0, _expect2.default)(info.extraData).to.equal('values'); | ||
const cacheInstance = (0, _.default)({ | ||
initial: { | ||
hello: 'world' | ||
} | ||
}); | ||
const info = cacheInstance.debug({ | ||
extraData: 'values' | ||
}); | ||
(0, _expect.default)(info.extraData).to.equal('values'); | ||
}); | ||
}); | ||
describe('-> hot cache', () => { | ||
let cacheInstance; | ||
let spy; | ||
beforeEach(() => { | ||
cacheInstance = (0, _.default)({ | ||
initial: preCached | ||
}); | ||
beforeEach(() => { | ||
cacheInstance = (0, _2.default)({ initial: preCached }); | ||
const p = () => Promise.resolve(); | ||
spy = _sinon2.default.spy(p); | ||
spy = _sinon.default.spy(p); | ||
}); | ||
it('should return cached content if not stale', () => { | ||
return cacheInstance.get(dummyKey, { worker: spy }).then(obj => { | ||
(0, _expect2.default)(obj.value).to.eql(cacheValue); | ||
(0, _expect2.default)(obj.cache).to.equal('hit'); | ||
(0, _expect2.default)(spy.called).to.equal(false); | ||
return cacheInstance.get(dummyKey, { | ||
worker: spy | ||
}).then(obj => { | ||
(0, _expect.default)(obj.value).to.eql(cacheValue); | ||
(0, _expect.default)(obj.cache).to.equal('hit'); | ||
(0, _expect.default)(spy.called).to.equal(false); | ||
}); | ||
}); | ||
}); | ||
describe('-> has/del/clear', () => { | ||
let cacheInstance; | ||
beforeEach(() => { | ||
cacheInstance = (0, _2.default)({ initial: preCached }); | ||
cacheInstance = (0, _.default)({ | ||
initial: preCached | ||
}); | ||
}); | ||
it('should return true if key exists in cache', () => { | ||
cacheInstance.set('key', 'value'); | ||
(0, _expect2.default)(cacheInstance.has('key')).to.equal(true); | ||
(0, _expect.default)(cacheInstance.has('key')).to.equal(true); | ||
}); | ||
it('should return false if key is not in cache', () => { | ||
(0, _expect2.default)(cacheInstance.has('key')).to.equal(false); | ||
(0, _expect.default)(cacheInstance.has('key')).to.equal(false); | ||
}); | ||
it('should return false if key was deleted from cache', () => { | ||
cacheInstance.set('key', 'value'); | ||
cacheInstance.del('key'); | ||
(0, _expect2.default)(cacheInstance.has('key')).to.equal(false); | ||
(0, _expect.default)(cacheInstance.has('key')).to.equal(false); | ||
}); | ||
it('should return false if key was deleted from cache', () => { | ||
@@ -113,7 +109,6 @@ cacheInstance.set('key1', 'value'); | ||
cacheInstance.clear(); | ||
(0, _expect2.default)(cacheInstance.has('key1')).to.equal(false); | ||
(0, _expect2.default)(cacheInstance.has('key2')).to.equal(false); | ||
(0, _expect.default)(cacheInstance.has('key1')).to.equal(false); | ||
(0, _expect.default)(cacheInstance.has('key2')).to.equal(false); | ||
}); | ||
}); | ||
describe('-> cold/stale cache', () => { | ||
@@ -123,44 +118,49 @@ let cacheInstance; | ||
let now; | ||
beforeEach(() => { | ||
cacheInstance = (0, _2.default)({ initial: preCached }); | ||
cacheInstance = (0, _.default)({ | ||
initial: preCached | ||
}); | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000); | ||
now = Date.now(); | ||
spy = _sinon2.default.spy(() => new Promise(resolve => { | ||
spy = _sinon.default.spy(() => new Promise(resolve => { | ||
setTimeout(() => resolve(now), 10); | ||
})); | ||
}); | ||
it('should return promised content when key is not present', () => { | ||
return cacheInstance.get('N/A', { worker: spy }).then(obj => { | ||
(0, _expect2.default)(obj.value).to.eql(now); | ||
(0, _expect2.default)(obj.cache).to.equal('miss'); | ||
(0, _expect2.default)(spy.called).to.equal(true); | ||
return cacheInstance.get('N/A', { | ||
worker: spy | ||
}).then(obj => { | ||
(0, _expect.default)(obj.value).to.eql(now); | ||
(0, _expect.default)(obj.cache).to.equal('miss'); | ||
(0, _expect.default)(spy.called).to.equal(true); | ||
}); | ||
}); | ||
it('should return synchronous get when no worker is given', () => { | ||
// miss | ||
const obj = cacheInstance.get('N/A'); | ||
(0, _expect2.default)(obj).to.equal(null); | ||
// stale | ||
(0, _expect.default)(obj).to.equal(null); // stale | ||
const obj2 = cacheInstance.get(dummyKey); | ||
(0, _expect2.default)(obj2.value).to.eql(cacheValue); | ||
(0, _expect2.default)(obj2.cache).to.equal('stale'); | ||
// hot | ||
cacheInstance.set('hello', { yoman: 'world' }); | ||
(0, _expect.default)(obj2.value).to.eql(cacheValue); | ||
(0, _expect.default)(obj2.cache).to.equal('stale'); // hot | ||
cacheInstance.set('hello', { | ||
yoman: 'world' | ||
}); | ||
const obj3 = cacheInstance.get('hello'); | ||
(0, _expect2.default)(obj3.value).to.eql({ yoman: 'world' }); | ||
(0, _expect2.default)(obj3.cache).to.equal('hit'); | ||
(0, _expect.default)(obj3.value).to.eql({ | ||
yoman: 'world' | ||
}); | ||
(0, _expect.default)(obj3.cache).to.equal('hit'); | ||
}); | ||
it('should return promised content if cache is stale', () => { | ||
return cacheInstance.get(dummyKey, { worker: spy }).then(obj => { | ||
(0, _expect2.default)(obj.value).to.eql(now); | ||
(0, _expect2.default)(obj.cache).to.equal('miss'); | ||
(0, _expect2.default)(spy.called).to.equal(true); | ||
return cacheInstance.get(dummyKey, { | ||
worker: spy | ||
}).then(obj => { | ||
(0, _expect.default)(obj.value).to.eql(now); | ||
(0, _expect.default)(obj.cache).to.equal('miss'); | ||
(0, _expect.default)(spy.called).to.equal(true); | ||
}); | ||
}); | ||
}); | ||
describe('-> worker queue', () => { | ||
@@ -170,61 +170,75 @@ let cacheInstance; | ||
let now; | ||
beforeEach(() => { | ||
cacheInstance = (0, _2.default)({ initial: preCached }); | ||
cacheInstance = (0, _.default)({ | ||
initial: preCached | ||
}); | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000); | ||
now = Date.now(); | ||
spy = _sinon2.default.spy(() => new Promise(resolve => { | ||
spy = _sinon.default.spy(() => new Promise(resolve => { | ||
setTimeout(() => resolve(now), 10); | ||
})); | ||
}); | ||
it('should run only one promise, while two requests asks for data from cold cache concurrently', () => { | ||
return Promise.all([cacheInstance.get(dummyKey, { worker: spy }), cacheInstance.get(dummyKey, { worker: spy })]).then(([val1, val2]) => { | ||
(0, _expect2.default)(val1.value).to.eql(val2.value); | ||
(0, _expect2.default)(spy.callCount).to.equal(1); | ||
(0, _expect2.default)(val1.cache).to.equal('miss'); | ||
(0, _expect2.default)(val2.cache).to.equal('hit'); | ||
return Promise.all([cacheInstance.get(dummyKey, { | ||
worker: spy | ||
}), cacheInstance.get(dummyKey, { | ||
worker: spy | ||
})]).then(([val1, val2]) => { | ||
(0, _expect.default)(val1.value).to.eql(val2.value); | ||
(0, _expect.default)(spy.callCount).to.equal(1); | ||
(0, _expect.default)(val1.cache).to.equal('miss'); | ||
(0, _expect.default)(val2.cache).to.equal('hit'); | ||
}); | ||
}); | ||
}); | ||
describe('-> error handling (timeouts)', () => { | ||
let cacheInstance; | ||
beforeEach(() => { | ||
cacheInstance = (0, _2.default)({ initial: preCached, log: _logHelper.dummyLog }); | ||
cacheInstance = (0, _.default)({ | ||
initial: preCached, | ||
log: _logHelper.dummyLog | ||
}); | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000); | ||
}); | ||
it('should return stale cache and increase wait if promise reaches timeout', () => { | ||
const timeoutSpy = _sinon2.default.spy(() => new Promise(resolve => { | ||
const timeoutSpy = _sinon.default.spy(() => new Promise(resolve => { | ||
setTimeout(() => resolve('another object'), 1000); | ||
})); | ||
const info = cacheInstance.debug(); | ||
(0, _expect2.default)(info.waiting.get(dummyKey)).to.be.a('undefined'); | ||
return cacheInstance.get(dummyKey, { workerTimeout: 0, worker: timeoutSpy }).then(obj => { | ||
(0, _expect2.default)(timeoutSpy.called).to.equal(true); | ||
(0, _expect2.default)(info.waiting.get(dummyKey)).not.to.equal(0); | ||
(0, _expect2.default)(obj.value).to.eql(cacheValue); | ||
(0, _expect2.default)(obj.cache).to.equal('stale'); | ||
(0, _expect.default)(info.waiting.get(dummyKey)).to.be.a('undefined'); | ||
return cacheInstance.get(dummyKey, { | ||
workerTimeout: 0, | ||
worker: timeoutSpy | ||
}).then(obj => { | ||
(0, _expect.default)(timeoutSpy.called).to.equal(true); | ||
(0, _expect.default)(info.waiting.get(dummyKey)).not.to.equal(0); | ||
(0, _expect.default)(obj.value).to.eql(cacheValue); | ||
(0, _expect.default)(obj.cache).to.equal('stale'); | ||
}); | ||
}); | ||
it('should reject if cache is cold and a timeout occurs', done => { | ||
const timeoutSpy = _sinon2.default.spy(() => new Promise(resolve => { | ||
const timeoutSpy = _sinon.default.spy(() => new Promise(resolve => { | ||
setTimeout(() => resolve('another object'), 1000); | ||
})); | ||
cacheInstance = (0, _2.default)({ log: _logHelper.dummyLog }); | ||
cacheInstance.get(dummyKey, { workerTimeout: 0, worker: timeoutSpy }).catch(err => { | ||
(0, _expect2.default)(timeoutSpy.called).to.equal(true); | ||
(0, _expect2.default)(err).to.be.an(Error); | ||
cacheInstance = (0, _.default)({ | ||
log: _logHelper.dummyLog | ||
}); | ||
cacheInstance.get(dummyKey, { | ||
workerTimeout: 0, | ||
worker: timeoutSpy | ||
}).catch(err => { | ||
(0, _expect.default)(timeoutSpy.called).to.equal(true); | ||
(0, _expect.default)(err).to.be.an(Error); | ||
done(); | ||
}); | ||
}); | ||
it('should re-run promise after deltaWait time has passed', done => { | ||
const timeoutSpy = _sinon2.default.spy(() => new Promise(resolve => { | ||
const timeoutSpy = _sinon.default.spy(() => new Promise(resolve => { | ||
setTimeout(() => resolve('another object'), 1000); | ||
})); | ||
const resolveSpy = _sinon2.default.spy(() => Promise.resolve('hei verden')); | ||
const resolveSpy = _sinon.default.spy(() => Promise.resolve('hei verden')); | ||
const conf = { | ||
@@ -234,16 +248,22 @@ deltaWait: 10, | ||
}; | ||
cacheInstance.get(dummyKey, _extends({}, conf, { worker: timeoutSpy })).then(obj => { | ||
cacheInstance.get(dummyKey, { ...conf, | ||
worker: timeoutSpy | ||
}).then(obj => { | ||
// 1. should return stale cache when timeout occurs | ||
(0, _expect2.default)(obj.value).to.eql(cacheValue); | ||
(0, _expect.default)(obj.value).to.eql(cacheValue); | ||
const info = cacheInstance.debug(); | ||
(0, _expect2.default)(info.waiting.get(dummyKey).wait).to.equal(10); | ||
return cacheInstance.get(dummyKey, _extends({}, conf, { worker: resolveSpy })).then(obj => { | ||
(0, _expect.default)(info.waiting.get(dummyKey).wait).to.equal(10); | ||
return cacheInstance.get(dummyKey, { ...conf, | ||
worker: resolveSpy | ||
}).then(obj => { | ||
// 2. should return stale cache before wait period has finished | ||
(0, _expect2.default)(obj.cache).to.equal('stale'); | ||
(0, _expect2.default)(obj.value).to.eql(cacheValue); | ||
(0, _expect.default)(obj.cache).to.equal('stale'); | ||
(0, _expect.default)(obj.value).to.eql(cacheValue); | ||
setTimeout(() => { | ||
return cacheInstance.get(dummyKey, _extends({}, conf, { worker: resolveSpy })).then(obj => { | ||
return cacheInstance.get(dummyKey, { ...conf, | ||
worker: resolveSpy | ||
}).then(obj => { | ||
// 3. should return fresh data when wait period has finished | ||
(0, _expect2.default)(obj.value).to.eql('hei verden'); | ||
(0, _expect2.default)(obj.cache).to.equal('miss'); | ||
(0, _expect.default)(obj.value).to.eql('hei verden'); | ||
(0, _expect.default)(obj.cache).to.equal('miss'); | ||
done(); | ||
@@ -256,68 +276,94 @@ }); | ||
}); | ||
describe('-> error handling (rejections)', () => { | ||
let cacheInstance; | ||
beforeEach(() => { | ||
cacheInstance = (0, _2.default)({ initial: preCached, log: _logHelper.dummyLog }); | ||
cacheInstance = (0, _.default)({ | ||
initial: preCached, | ||
log: _logHelper.dummyLog | ||
}); | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000); | ||
}); | ||
it('should return stale cache and set wait if a promise rejection occurs', () => { | ||
const errorLogSpy = _sinon.default.spy(); | ||
it('should return stale cache and set wait if a promise rejection occurs', () => { | ||
const errorLogSpy = _sinon2.default.spy(); | ||
const rejectionSpy = _sinon2.default.spy(() => Promise.reject(new Error('an error occurred'))); | ||
cacheInstance = (0, _2.default)({ initial: preCached, log: _extends({}, _logHelper.dummyLog, { error: errorLogSpy }) }); | ||
const rejectionSpy = _sinon.default.spy(() => Promise.reject(new Error('an error occurred'))); | ||
cacheInstance = (0, _.default)({ | ||
initial: preCached, | ||
log: { ..._logHelper.dummyLog, | ||
error: errorLogSpy | ||
} | ||
}); | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000); | ||
const info = cacheInstance.debug(); | ||
(0, _expect2.default)(info.waiting.get(dummyKey)).to.be.a('undefined'); | ||
return cacheInstance.get(dummyKey, { worker: rejectionSpy }).then(obj => { | ||
(0, _expect2.default)(rejectionSpy.called).to.equal(true); | ||
(0, _expect2.default)(info.waiting.get(dummyKey)).not.to.equal(0); | ||
(0, _expect2.default)(obj.value).to.eql(cacheValue); | ||
(0, _expect2.default)(obj.cache).to.equal('stale'); | ||
(0, _expect2.default)(errorLogSpy.called).to.equal(true); | ||
(0, _expect.default)(info.waiting.get(dummyKey)).to.be.a('undefined'); | ||
return cacheInstance.get(dummyKey, { | ||
worker: rejectionSpy | ||
}).then(obj => { | ||
(0, _expect.default)(rejectionSpy.called).to.equal(true); | ||
(0, _expect.default)(info.waiting.get(dummyKey)).not.to.equal(0); | ||
(0, _expect.default)(obj.value).to.eql(cacheValue); | ||
(0, _expect.default)(obj.cache).to.equal('stale'); | ||
(0, _expect.default)(errorLogSpy.called).to.equal(true); | ||
}); | ||
}); | ||
it('should reject if cache is cold and a rejection occurs', done => { | ||
const errorLogSpy = _sinon.default.spy(); | ||
it('should reject if cache is cold and a rejection occurs', done => { | ||
const errorLogSpy = _sinon2.default.spy(); | ||
cacheInstance = (0, _2.default)({ log: _extends({}, _logHelper.dummyLog, { error: errorLogSpy }) }); | ||
const rejectionSpy = _sinon2.default.spy(() => Promise.reject(new Error('an error occurred'))); | ||
cacheInstance.get(dummyKey, { worker: rejectionSpy }).catch(err => { | ||
(0, _expect2.default)(rejectionSpy.called).to.equal(true); | ||
(0, _expect2.default)(errorLogSpy.called).to.equal(true); | ||
(0, _expect2.default)(err).to.be.an(Error); | ||
cacheInstance = (0, _.default)({ | ||
log: { ..._logHelper.dummyLog, | ||
error: errorLogSpy | ||
} | ||
}); | ||
const rejectionSpy = _sinon.default.spy(() => Promise.reject(new Error('an error occurred'))); | ||
cacheInstance.get(dummyKey, { | ||
worker: rejectionSpy | ||
}).catch(err => { | ||
(0, _expect.default)(rejectionSpy.called).to.equal(true); | ||
(0, _expect.default)(errorLogSpy.called).to.equal(true); | ||
(0, _expect.default)(err).to.be.an(Error); | ||
done(); | ||
}); | ||
}); | ||
it('should reject if an Error is thrown', done => { | ||
const rejectionSpy = _sinon2.default.spy(() => { | ||
const rejectionSpy = _sinon.default.spy(() => { | ||
throw new Error('an error occurred'); | ||
}); | ||
cacheInstance.get(dummyKey, { worker: rejectionSpy }).catch(err => { | ||
(0, _expect2.default)(rejectionSpy.called).to.equal(true); | ||
(0, _expect2.default)(err).to.be.an(Error); | ||
cacheInstance.get(dummyKey, { | ||
worker: rejectionSpy | ||
}).catch(err => { | ||
(0, _expect.default)(rejectionSpy.called).to.equal(true); | ||
(0, _expect.default)(err).to.be.an(Error); | ||
done(); | ||
}); | ||
}); | ||
it('should re-run promise after deltaWait time has passed (when failing caused by a rejection)', done => { | ||
const rejectionSpy = _sinon.default.spy(() => Promise.reject(new Error(''))); | ||
it('should re-run promise after deltaWait time has passed (when failing caused by a rejection)', done => { | ||
const rejectionSpy = _sinon2.default.spy(() => Promise.reject(new Error(''))); | ||
const resolveSpy = _sinon2.default.spy(() => Promise.resolve('hei verden')); | ||
const resolveSpy = _sinon.default.spy(() => Promise.resolve('hei verden')); | ||
const conf = { | ||
deltaWait: 10 | ||
}; | ||
cacheInstance.get(dummyKey, _extends({}, conf, { worker: rejectionSpy })).then(obj => { | ||
cacheInstance.get(dummyKey, { ...conf, | ||
worker: rejectionSpy | ||
}).then(obj => { | ||
// 1. should return stale cache when rejection occurs | ||
(0, _expect2.default)(obj.value).to.eql(cacheValue); | ||
return cacheInstance.get(dummyKey, _extends({}, conf, { worker: resolveSpy })).then(obj => { | ||
(0, _expect.default)(obj.value).to.eql(cacheValue); | ||
return cacheInstance.get(dummyKey, { ...conf, | ||
worker: resolveSpy | ||
}).then(obj => { | ||
// 2. should return stale cache before wait period has finished | ||
(0, _expect2.default)(obj.value).to.eql(cacheValue); | ||
(0, _expect2.default)(obj.cache).to.equal('stale'); | ||
(0, _expect.default)(obj.value).to.eql(cacheValue); | ||
(0, _expect.default)(obj.cache).to.equal('stale'); | ||
setTimeout(() => { | ||
return cacheInstance.get(dummyKey, _extends({}, conf, { worker: resolveSpy })).then(obj => { | ||
return cacheInstance.get(dummyKey, { ...conf, | ||
worker: resolveSpy | ||
}).then(obj => { | ||
// 3. should return fresh data when wait period has finished | ||
(0, _expect2.default)(obj.value).to.eql('hei verden'); | ||
(0, _expect2.default)(obj.cache).to.equal('miss'); | ||
(0, _expect.default)(obj.value).to.eql('hei verden'); | ||
(0, _expect.default)(obj.cache).to.equal('miss'); | ||
done(); | ||
@@ -329,14 +375,18 @@ }); | ||
}); | ||
it('should re-run promise after deltaWait time has passed (when failing caused by a rejection and cache is cold)', done => { | ||
const rejectionSpy = _sinon.default.spy(() => Promise.reject(new Error(''))); | ||
it('should re-run promise after deltaWait time has passed (when failing caused by a rejection and cache is cold)', done => { | ||
const rejectionSpy = _sinon2.default.spy(() => Promise.reject(new Error(''))); | ||
const conf = { | ||
deltaWait: 10 | ||
}; | ||
cacheInstance.get('N/A', _extends({}, conf, { worker: rejectionSpy })).catch(err => { | ||
(0, _expect2.default)(err).to.be.an(Error); | ||
(0, _expect2.default)(rejectionSpy.callCount).to.equal(1); | ||
cacheInstance.get('N/A', _extends({}, conf, { worker: rejectionSpy })).catch(err => { | ||
(0, _expect2.default)(err).to.be.an(Error); | ||
(0, _expect2.default)(rejectionSpy.callCount).to.equal(1); | ||
cacheInstance.get('N/A', { ...conf, | ||
worker: rejectionSpy | ||
}).catch(err => { | ||
(0, _expect.default)(err).to.be.an(Error); | ||
(0, _expect.default)(rejectionSpy.callCount).to.equal(1); | ||
cacheInstance.get('N/A', { ...conf, | ||
worker: rejectionSpy | ||
}).catch(err => { | ||
(0, _expect.default)(err).to.be.an(Error); | ||
(0, _expect.default)(rejectionSpy.callCount).to.equal(1); | ||
cacheInstance.set('N/A', 'hei verden'); | ||
@@ -346,6 +396,8 @@ const info = cacheInstance.debug(); | ||
setTimeout(() => { | ||
return cacheInstance.get('N/A', _extends({}, conf, { worker: rejectionSpy })).then(obj => { | ||
(0, _expect2.default)(rejectionSpy.callCount).to.equal(1); | ||
(0, _expect2.default)(obj.value).to.eql('hei verden'); | ||
(0, _expect2.default)(obj.cache).to.equal('hit'); | ||
return cacheInstance.get('N/A', { ...conf, | ||
worker: rejectionSpy | ||
}).then(obj => { | ||
(0, _expect.default)(rejectionSpy.callCount).to.equal(1); | ||
(0, _expect.default)(obj.value).to.eql('hei verden'); | ||
(0, _expect.default)(obj.cache).to.equal('hit'); | ||
done(); | ||
@@ -357,5 +409,5 @@ }); | ||
}); | ||
it('should increase deltaWait after several re-runs', done => { | ||
const rejectionSpy = _sinon.default.spy(() => Promise.reject(new Error(''))); | ||
it('should increase deltaWait after several re-runs', done => { | ||
const rejectionSpy = _sinon2.default.spy(() => Promise.reject(new Error(''))); | ||
const conf = { | ||
@@ -365,12 +417,18 @@ deltaWait: 10 | ||
const info = cacheInstance.debug(); | ||
(0, _expect2.default)(info.waiting.get('N/A')).to.be.a('undefined'); | ||
cacheInstance.get('N/A', _extends({}, conf, { worker: rejectionSpy })).catch(err => { | ||
(0, _expect2.default)(err).to.be.an(Error); | ||
(0, _expect2.default)(rejectionSpy.callCount).to.equal(1); | ||
(0, _expect2.default)(info.waiting.get('N/A').wait).to.equal(10); | ||
const { started } = info.waiting.get('N/A'); | ||
cacheInstance.get('N/A', _extends({}, conf, { worker: rejectionSpy })).catch(err => { | ||
(0, _expect2.default)(err).to.be.an(Error); | ||
(0, _expect2.default)(rejectionSpy.callCount).to.equal(1); | ||
(0, _expect2.default)(info.waiting.get('N/A')).to.eql({ | ||
(0, _expect.default)(info.waiting.get('N/A')).to.be.a('undefined'); | ||
cacheInstance.get('N/A', { ...conf, | ||
worker: rejectionSpy | ||
}).catch(err => { | ||
(0, _expect.default)(err).to.be.an(Error); | ||
(0, _expect.default)(rejectionSpy.callCount).to.equal(1); | ||
(0, _expect.default)(info.waiting.get('N/A').wait).to.equal(10); | ||
const { | ||
started | ||
} = info.waiting.get('N/A'); | ||
cacheInstance.get('N/A', { ...conf, | ||
worker: rejectionSpy | ||
}).catch(err => { | ||
(0, _expect.default)(err).to.be.an(Error); | ||
(0, _expect.default)(rejectionSpy.callCount).to.equal(1); | ||
(0, _expect.default)(info.waiting.get('N/A')).to.eql({ | ||
started, | ||
@@ -381,7 +439,9 @@ wait: 10, | ||
setTimeout(() => { | ||
return cacheInstance.get('N/A', _extends({}, conf, { worker: rejectionSpy })).catch(err => { | ||
(0, _expect2.default)(err).to.be.an(Error); | ||
(0, _expect2.default)(rejectionSpy.callCount).to.equal(2); | ||
(0, _expect2.default)(info.waiting.get('N/A').wait).to.equal(10); | ||
(0, _expect2.default)(info.waiting.get('N/A').started).not.to.equal(started); | ||
return cacheInstance.get('N/A', { ...conf, | ||
worker: rejectionSpy | ||
}).catch(err => { | ||
(0, _expect.default)(err).to.be.an(Error); | ||
(0, _expect.default)(rejectionSpy.callCount).to.equal(2); | ||
(0, _expect.default)(info.waiting.get('N/A').wait).to.equal(10); | ||
(0, _expect.default)(info.waiting.get('N/A').started).not.to.equal(started); | ||
done(); | ||
@@ -394,62 +454,98 @@ }); | ||
}); | ||
describe('-> keys/values/entries', () => { | ||
let cacheInstance; | ||
beforeEach(() => { | ||
cacheInstance = (0, _2.default)({ initial: { | ||
'house/1': { hei: 'verden1' }, | ||
'house/2': { hei: 'verden2' }, | ||
'guest/2': { hei: 'verden3' } | ||
} }); | ||
cacheInstance = (0, _.default)({ | ||
initial: { | ||
'house/1': { | ||
hei: 'verden1' | ||
}, | ||
'house/2': { | ||
hei: 'verden2' | ||
}, | ||
'guest/2': { | ||
hei: 'verden3' | ||
} | ||
} | ||
}); | ||
}); | ||
it('should return keys', () => { | ||
(0, _expect2.default)(cacheInstance.keys()).to.eql(['house/1', 'house/2', 'guest/2'].reverse()); | ||
(0, _expect.default)(cacheInstance.keys()).to.eql(['house/1', 'house/2', 'guest/2'].reverse()); | ||
}); | ||
it('should return values', () => { | ||
(0, _expect2.default)(cacheInstance.values().map(({ value }) => value)).to.eql([{ hei: 'verden3' }, { hei: 'verden2' }, { hei: 'verden1' }]); | ||
(0, _expect.default)(cacheInstance.values().map(({ | ||
value | ||
}) => value)).to.eql([{ | ||
hei: 'verden3' | ||
}, { | ||
hei: 'verden2' | ||
}, { | ||
hei: 'verden1' | ||
}]); | ||
}); | ||
it('should return entries', () => { | ||
(0, _expect2.default)(Array.from(cacheInstance.entries()).map(([key, { value }]) => { | ||
return { [key]: value }; | ||
})).to.eql([{ 'guest/2': { hei: 'verden3' } }, { 'house/2': { hei: 'verden2' } }, { 'house/1': { hei: 'verden1' } }]); | ||
(0, _expect.default)(Array.from(cacheInstance.entries()).map(([key, { | ||
value | ||
}]) => { | ||
return { | ||
[key]: value | ||
}; | ||
})).to.eql([{ | ||
'guest/2': { | ||
hei: 'verden3' | ||
} | ||
}, { | ||
'house/2': { | ||
hei: 'verden2' | ||
} | ||
}, { | ||
'house/1': { | ||
hei: 'verden1' | ||
} | ||
}]); | ||
}); | ||
}); | ||
describe('-> expire', () => { | ||
let cacheInstance; | ||
beforeEach(() => { | ||
cacheInstance = (0, _2.default)({ initial: { | ||
'house/1': { hei: 'verden' }, | ||
'house/2': { hei: 'verden' }, | ||
'guest/2': { hei: 'verden' } | ||
} }); | ||
cacheInstance = (0, _.default)({ | ||
initial: { | ||
'house/1': { | ||
hei: 'verden' | ||
}, | ||
'house/2': { | ||
hei: 'verden' | ||
}, | ||
'guest/2': { | ||
hei: 'verden' | ||
} | ||
} | ||
}); | ||
}); | ||
it('should expire all house keys', () => { | ||
cacheInstance.expire(['house/*']); | ||
(0, _expect2.default)(cacheInstance.get('house/1').TTL).to.equal(0); | ||
(0, _expect2.default)(cacheInstance.get('house/2').TTL).to.equal(0); | ||
(0, _expect2.default)(cacheInstance.get('guest/2').TTL).not.to.equal(0); | ||
(0, _expect.default)(cacheInstance.get('house/1').TTL).to.equal(0); | ||
(0, _expect.default)(cacheInstance.get('house/2').TTL).to.equal(0); | ||
(0, _expect.default)(cacheInstance.get('guest/2').TTL).not.to.equal(0); | ||
}); | ||
it('should expire given house keys', () => { | ||
cacheInstance.expire(['house/*', 'guest/2']); | ||
(0, _expect2.default)(cacheInstance.get('house/1').TTL).to.equal(0); | ||
(0, _expect2.default)(cacheInstance.get('house/2').TTL).to.equal(0); | ||
(0, _expect2.default)(cacheInstance.get('guest/2').TTL).to.equal(0); | ||
(0, _expect.default)(cacheInstance.get('house/1').TTL).to.equal(0); | ||
(0, _expect.default)(cacheInstance.get('house/2').TTL).to.equal(0); | ||
(0, _expect.default)(cacheInstance.get('guest/2').TTL).to.equal(0); | ||
}); | ||
}); | ||
describe('-> LRU capabilities', () => { | ||
it('should throw away first entered entry on inital state', () => { | ||
const cacheInstance = (0, _2.default)({ | ||
const cacheInstance = (0, _.default)({ | ||
initial: { | ||
'house/1': { hei: 'verden' }, | ||
'house/2': { hei: 'verden' }, | ||
'guest/3': { hei: 'verden' } | ||
'house/1': { | ||
hei: 'verden' | ||
}, | ||
'house/2': { | ||
hei: 'verden' | ||
}, | ||
'guest/3': { | ||
hei: 'verden' | ||
} | ||
}, | ||
@@ -459,49 +555,73 @@ maxLength: 2 | ||
const info = cacheInstance.debug(); | ||
(0, _expect2.default)(info.itemCount).to.equal(2); | ||
(0, _expect2.default)(cacheInstance.keys()).to.eql(['guest/3', 'house/2']); | ||
(0, _expect.default)(info.itemCount).to.equal(2); | ||
(0, _expect.default)(cacheInstance.keys()).to.eql(['guest/3', 'house/2']); | ||
}); | ||
it('should call dispose on set operations when LRU cache evicts object', () => { | ||
const cacheInstance = (0, _.default)({ | ||
maxLength: 2 | ||
}); | ||
it('should call dispose on set operations when LRU cache evicts object', () => { | ||
const cacheInstance = (0, _2.default)({ maxLength: 2 }); | ||
const spy = _sinon2.default.spy(); | ||
const spy = _sinon.default.spy(); | ||
cacheInstance.addDisposer(spy); | ||
cacheInstance.set('house/1', { hei: 'verden' }); | ||
cacheInstance.set('house/2', { hei: 'verden' }); | ||
cacheInstance.set('guest/3', { hei: 'verden' }); | ||
(0, _expect2.default)(spy.called).to.equal(true); | ||
cacheInstance.set('house/1', { | ||
hei: 'verden' | ||
}); | ||
cacheInstance.set('house/2', { | ||
hei: 'verden' | ||
}); | ||
cacheInstance.set('guest/3', { | ||
hei: 'verden' | ||
}); | ||
(0, _expect.default)(spy.called).to.equal(true); | ||
const key = spy.args[0][0]; | ||
const _spy$args$0$ = spy.args[0][1], | ||
{ created } = _spy$args$0$, | ||
callArgs = _objectWithoutProperties(_spy$args$0$, ['created']); | ||
(0, _expect2.default)(key).to.equal('house/1'); | ||
(0, _expect2.default)(callArgs).to.eql({ | ||
const { | ||
created, | ||
...callArgs | ||
} = spy.args[0][1]; | ||
(0, _expect.default)(key).to.equal('house/1'); | ||
(0, _expect.default)(callArgs).to.eql({ | ||
TTL: 86400000, | ||
value: { hei: 'verden' }, | ||
value: { | ||
hei: 'verden' | ||
}, | ||
cache: 'hit' | ||
}); | ||
cacheInstance.removeDisposer(spy); | ||
cacheInstance.set('guest/4', { hei: 'verden' }); | ||
(0, _expect2.default)(spy.callCount).to.equal(1); | ||
cacheInstance.set('guest/4', { | ||
hei: 'verden' | ||
}); | ||
(0, _expect.default)(spy.callCount).to.equal(1); | ||
const info = cacheInstance.debug(); | ||
(0, _expect2.default)(info.itemCount).to.equal(2); | ||
(0, _expect2.default)(cacheInstance.keys()).to.eql(['guest/4', 'guest/3']); | ||
(0, _expect.default)(info.itemCount).to.equal(2); | ||
(0, _expect.default)(cacheInstance.keys()).to.eql(['guest/4', 'guest/3']); | ||
}); | ||
it('should call dispose on del operations', () => { | ||
const cacheInstance = (0, _.default)({ | ||
maxLength: 2 | ||
}); | ||
it('should call dispose on del operations', () => { | ||
const cacheInstance = (0, _2.default)({ maxLength: 2 }); | ||
const spy = _sinon2.default.spy(); | ||
const spy = _sinon.default.spy(); | ||
cacheInstance.addDisposer(spy); | ||
cacheInstance.set('house/1', { hei: 'verden' }); | ||
cacheInstance.set('house/1', { | ||
hei: 'verden' | ||
}); | ||
cacheInstance.del('house/1'); | ||
(0, _expect2.default)(spy.called).to.equal(true); | ||
(0, _expect.default)(spy.called).to.equal(true); | ||
cacheInstance.removeDisposer(spy); | ||
}); | ||
it('should call dispose on clear operations', () => { | ||
const cacheInstance = (0, _.default)({ | ||
maxLength: 2 | ||
}); | ||
it('should call dispose on clear operations', () => { | ||
const cacheInstance = (0, _2.default)({ maxLength: 2 }); | ||
const spy = _sinon2.default.spy(); | ||
const spy = _sinon.default.spy(); | ||
cacheInstance.addDisposer(spy); | ||
cacheInstance.set('house/1', { hei: 'verden' }); | ||
cacheInstance.set('house/1', { | ||
hei: 'verden' | ||
}); | ||
cacheInstance.clear(); | ||
(0, _expect2.default)(spy.called).to.equal(true); | ||
(0, _expect.default)(spy.called).to.equal(true); | ||
cacheInstance.removeDisposer(spy); | ||
@@ -508,0 +628,0 @@ }); |
@@ -1,80 +0,94 @@ | ||
'use strict'; | ||
"use strict"; | ||
var _ = require('../'); | ||
var _ = _interopRequireDefault(require("../")); | ||
var _2 = _interopRequireDefault(_); | ||
var _2 = _interopRequireDefault(require("../..")); | ||
var _3 = require('../..'); | ||
var _sinon = _interopRequireDefault(require("sinon")); | ||
var _4 = _interopRequireDefault(_3); | ||
var _expect = _interopRequireDefault(require("expect.js")); | ||
var _sinon = require('sinon'); | ||
var _mockRedisFactory = require("../../utils/mock-redis-factory"); | ||
var _sinon2 = _interopRequireDefault(_sinon); | ||
var _logHelper = require("../../utils/log-helper"); | ||
var _expect = require('expect.js'); | ||
var _expect2 = _interopRequireDefault(_expect); | ||
var _mockRedisFactory = require('../../utils/mock-redis-factory'); | ||
var _logHelper = require('../../utils/log-helper'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
const namespace = 'desketoy8080'; | ||
describe('dist-expire', () => { | ||
describe('-> istantiation', () => { | ||
it('should be possible', () => { | ||
const cache = (0, _4.default)({ log: _logHelper.dummyLog }); | ||
cache.use((0, _2.default)((0, _mockRedisFactory.mockRedisFactory)(), namespace)); | ||
(0, _expect2.default)(cache).to.be.an(Object); | ||
const cache = (0, _2.default)({ | ||
log: _logHelper.dummyLog | ||
}); | ||
cache.use((0, _.default)((0, _mockRedisFactory.mockRedisFactory)(), namespace)); | ||
(0, _expect.default)(cache).to.be.an(Object); | ||
}); | ||
}); | ||
describe('debug', () => { | ||
it('should print a debug of the cache with extra options', () => { | ||
// more thorough testing of debug in debug.spec.js | ||
const cache = (0, _4.default)({ initial: { hello: 'world' } }); | ||
cache.use((0, _2.default)((0, _mockRedisFactory.mockRedisFactory)(), namespace)); | ||
const info = cache.debug({ extraData: 'values' }); | ||
(0, _expect2.default)(info.extraData).to.equal('values'); | ||
const cache = (0, _2.default)({ | ||
initial: { | ||
hello: 'world' | ||
} | ||
}); | ||
cache.use((0, _.default)((0, _mockRedisFactory.mockRedisFactory)(), namespace)); | ||
const info = cache.debug({ | ||
extraData: 'values' | ||
}); | ||
(0, _expect.default)(info.extraData).to.equal('values'); | ||
}); | ||
}); | ||
describe('-> inheritance', () => { | ||
it('should be able to use methods from extended class (using middleware)', () => { | ||
const cache = (0, _4.default)({ log: _logHelper.dummyLog }); | ||
cache.use((0, _2.default)((0, _mockRedisFactory.mockRedisFactory)(), namespace)); | ||
const cache = (0, _2.default)({ | ||
log: _logHelper.dummyLog | ||
}); | ||
cache.use((0, _.default)((0, _mockRedisFactory.mockRedisFactory)(), namespace)); | ||
const p = () => Promise.resolve(); | ||
const spy = _sinon2.default.spy(p); | ||
const spy = _sinon.default.spy(p); | ||
cache.set('hello', 'world'); | ||
return cache.get('hello', { worker: spy }).then(obj => { | ||
(0, _expect2.default)(obj.value).to.equal('world'); | ||
(0, _expect2.default)(spy.called).to.equal(false); | ||
return cache.get('hello', { | ||
worker: spy | ||
}).then(obj => { | ||
(0, _expect.default)(obj.value).to.equal('world'); | ||
(0, _expect.default)(spy.called).to.equal(false); | ||
}); | ||
}); | ||
}); | ||
describe('-> distributed expire', () => { | ||
it('should expire content on expire', () => { | ||
const spy = _sinon2.default.spy(() => Promise.resolve('world2')); | ||
const cache = (0, _4.default)({ initial: { hello: 'world' }, log: _logHelper.dummyLog }); | ||
cache.use((0, _2.default)((0, _mockRedisFactory.mockRedisFactory)(), namespace)); | ||
const spy = _sinon.default.spy(() => Promise.resolve('world2')); | ||
const cache = (0, _2.default)({ | ||
initial: { | ||
hello: 'world' | ||
}, | ||
log: _logHelper.dummyLog | ||
}); | ||
cache.use((0, _.default)((0, _mockRedisFactory.mockRedisFactory)(), namespace)); | ||
cache.expire(['hello']); | ||
(0, _expect2.default)(cache.get('hello').TTL).to.equal(0); | ||
return cache.get('hello', { worker: spy }).then(obj => { | ||
(0, _expect2.default)(obj.value).to.equal('world2'); | ||
(0, _expect2.default)(spy.called).to.equal(true); | ||
(0, _expect.default)(cache.get('hello').TTL).to.equal(0); | ||
return cache.get('hello', { | ||
worker: spy | ||
}).then(obj => { | ||
(0, _expect.default)(obj.value).to.equal('world2'); | ||
(0, _expect.default)(spy.called).to.equal(true); | ||
}); | ||
}); | ||
it('should handle errors if data is non-parsable', () => { | ||
const cbs = []; | ||
const on = (event, cb) => cbs.push(cb); | ||
const onSpy = _sinon2.default.spy(on); | ||
const onSpy = _sinon.default.spy(on); | ||
const pub = (ns, data) => { | ||
cbs.forEach(cb => cb(ns, data)); | ||
}; | ||
const sub = (ns, cb) => { | ||
@@ -84,9 +98,18 @@ if (ns === namespace) { | ||
} | ||
return cb(new Error('dummyerror'), null); | ||
}; | ||
const publishSpy = _sinon2.default.spy(pub); | ||
const subscribeSpy = _sinon2.default.spy(sub); | ||
const cache = (0, _4.default)({ initial: { hello: 'world' }, log: _logHelper.dummyLog }); | ||
const publishSpy = _sinon.default.spy(pub); | ||
const subscribeSpy = _sinon.default.spy(sub); | ||
const cache = (0, _2.default)({ | ||
initial: { | ||
hello: 'world' | ||
}, | ||
log: _logHelper.dummyLog | ||
}); | ||
const callCount = _logHelper.dummyLog.error.callCount; | ||
cache.use((0, _2.default)((0, _mockRedisFactory.mockRedisFactory)({ | ||
cache.use((0, _.default)((0, _mockRedisFactory.mockRedisFactory)({ | ||
on: onSpy, | ||
@@ -97,5 +120,10 @@ publish: publishSpy, | ||
pub('asdf', '{'); | ||
(0, _expect2.default)(_logHelper.dummyLog.error.callCount).to.equal(callCount + 1); | ||
const cache2 = (0, _4.default)({ initial: { hello: 'world' }, log: _logHelper.dummyLog }); | ||
cache2.use((0, _2.default)((0, _mockRedisFactory.mockRedisFactory)({ | ||
(0, _expect.default)(_logHelper.dummyLog.error.callCount).to.equal(callCount + 1); | ||
const cache2 = (0, _2.default)({ | ||
initial: { | ||
hello: 'world' | ||
}, | ||
log: _logHelper.dummyLog | ||
}); | ||
cache2.use((0, _.default)((0, _mockRedisFactory.mockRedisFactory)({ | ||
on: onSpy, | ||
@@ -105,5 +133,5 @@ publish: publishSpy, | ||
}), 'dummy')); | ||
(0, _expect2.default)(_logHelper.dummyLog.error.callCount).to.equal(callCount + 2); | ||
(0, _expect.default)(_logHelper.dummyLog.error.callCount).to.equal(callCount + 2); | ||
}); | ||
}); | ||
}); |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -6,16 +6,12 @@ Object.defineProperty(exports, "__esModule", { | ||
}); | ||
exports.default = void 0; | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /** | ||
* @module | ||
**/ | ||
var _os = _interopRequireDefault(require("os")); | ||
var _os = require('os'); | ||
var _os2 = _interopRequireDefault(_os); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
/** | ||
* @module | ||
**/ | ||
const EXPIRE_MESSAGE_TYPE = 'EXPIRE_MESSAGE_TYPE'; | ||
/** | ||
@@ -32,6 +28,5 @@ * @description Create new middlware instance to be used by inMemoryCache module | ||
exports.default = (redisFactory, namespace) => cacheInstance => { | ||
var _default = (redisFactory, namespace) => cacheInstance => { | ||
const redisPub = redisFactory(); | ||
const redisSubClient = redisFactory(); | ||
/** | ||
@@ -43,7 +38,12 @@ * @description callback for messages recieved from redis | ||
**/ | ||
const onMessage = (namespace, data) => { | ||
try { | ||
const { type, message } = JSON.parse(data); | ||
const { | ||
type, | ||
message | ||
} = JSON.parse(data); | ||
if (type === EXPIRE_MESSAGE_TYPE) { | ||
cacheInstance.log.info(`expire cache for keys ${message.keys} using namespace ${namespace} on host ${_os2.default.hostname()}`); | ||
cacheInstance.log.info(`expire cache for keys ${message.keys} using namespace ${namespace} on host ${_os.default.hostname()}`); | ||
cacheInstance.expire(message.keys); | ||
@@ -55,3 +55,2 @@ } | ||
}; | ||
/** | ||
@@ -63,2 +62,4 @@ * @description setup subscription to redis | ||
**/ | ||
const setupSubscriber = (redisClient, namespace) => { | ||
@@ -69,2 +70,3 @@ redisClient.subscribe(namespace, (err, cnt) => { | ||
} | ||
return cacheInstance.log.debug(`Subscribing for incoming messages from redis#${namespace}. Count: ${cnt}`); | ||
@@ -74,3 +76,2 @@ }); | ||
}; | ||
/** | ||
@@ -81,2 +82,4 @@ * @description distributed wrapper for expire calls | ||
**/ | ||
const expire = keys => { | ||
@@ -93,7 +96,9 @@ const message = { | ||
const debug = (extraData, next) => { | ||
return next(_extends({ namespace }, extraData)); | ||
return next({ | ||
namespace, | ||
...extraData | ||
}); | ||
}; | ||
setupSubscriber(redisSubClient, namespace); | ||
return { | ||
@@ -103,2 +108,4 @@ expire, | ||
}; | ||
}; | ||
}; | ||
exports.default = _default; |
168
lib/index.js
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -6,48 +6,35 @@ Object.defineProperty(exports, "__esModule", { | ||
}); | ||
exports.persistentCache = exports.distCache = undefined; | ||
exports.default = exports.persistentCache = exports.distCache = void 0; | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /** | ||
* @module | ||
* @description Main module for creating an in-memory cache. Extendable using redis-wrapper and redis-persistence-wrapper | ||
**/ | ||
var _cloneDeep = _interopRequireDefault(require("lodash/cloneDeep")); | ||
var _cacheHelpers = require("./utils/cache-helpers"); | ||
// export plugins for convenience | ||
var _debug = require("./utils/debug"); | ||
var _lruCache = _interopRequireDefault(require("lru-cache")); | ||
var _cloneDeep = require('lodash/cloneDeep'); | ||
var _distExpire = _interopRequireDefault(require("./dist-expire")); | ||
var _cloneDeep2 = _interopRequireDefault(_cloneDeep); | ||
var _persistence = _interopRequireDefault(require("./persistence")); | ||
var _cacheHelpers = require('./utils/cache-helpers'); | ||
var _debug = require('./utils/debug'); | ||
var _lruCache = require('lru-cache'); | ||
var _lruCache2 = _interopRequireDefault(_lruCache); | ||
var _distExpire = require('./dist-expire'); | ||
var _distExpire2 = _interopRequireDefault(_distExpire); | ||
var _persistence = require('./persistence'); | ||
var _persistence2 = _interopRequireDefault(_persistence); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
const distCache = exports.distCache = _distExpire2.default; | ||
const persistentCache = exports.persistentCache = _persistence2.default; | ||
/** | ||
* @module | ||
* @description Main module for creating an in-memory cache. Extendable using redis-wrapper and redis-persistence-wrapper | ||
**/ | ||
// export plugins for convenience | ||
const distCache = _distExpire.default; | ||
exports.distCache = distCache; | ||
const persistentCache = _persistence.default; | ||
exports.persistentCache = persistentCache; | ||
const DEFAULT_CACHE_EXPIRE = 24 * 60 * 60 * 1000; | ||
const DEFAULT_DELTA_WAIT = 10000; | ||
const DEFAULT_MAX_LENGTH = 1000; | ||
// max stale period | ||
const DEFAULT_MAX_LENGTH = 1000; // max stale period | ||
const DEFAULT_MAX_AGE = 2 * DEFAULT_CACHE_EXPIRE; | ||
const CACHE_HIT = 'hit'; | ||
const CACHE_MISS = 'miss'; | ||
const CACHE_STALE = 'stale'; | ||
/** | ||
@@ -73,5 +60,6 @@ * Wrapper around LRU-cache, | ||
exports.default = (options = {}) => { | ||
var _default = (options = {}) => { | ||
const { | ||
log = console, // eslint-disable-line | ||
log = console, | ||
// eslint-disable-line | ||
initial = {}, | ||
@@ -81,7 +69,6 @@ maxLength = DEFAULT_MAX_LENGTH, | ||
} = options; | ||
let disposers = []; | ||
const jobs = new Map(); | ||
const waiting = new Map(); | ||
const cache = (0, _lruCache2.default)({ | ||
const cache = (0, _lruCache.default)({ | ||
max: maxLength, | ||
@@ -93,3 +80,2 @@ maxAge, | ||
}); | ||
/** | ||
@@ -101,4 +87,4 @@ * @description add a callback to lruCache#dispose | ||
**/ | ||
const addDisposer = cb => disposers.push(cb); | ||
/** | ||
@@ -110,4 +96,5 @@ * @description remove a callback from lruCache#dispose | ||
**/ | ||
const removeDisposer = cb => disposers = disposers.filter(disposer => disposer && disposer !== cb); | ||
/** | ||
@@ -121,6 +108,9 @@ * @description set value in cache | ||
**/ | ||
const set = (key, value, ttl = DEFAULT_CACHE_EXPIRE) => { | ||
cache.set(key, _extends({}, (0, _cacheHelpers.createEntry)(value, ttl), { cache: CACHE_HIT }), maxAge); | ||
cache.set(key, { ...(0, _cacheHelpers.createEntry)(value, ttl), | ||
cache: CACHE_HIT | ||
}, maxAge); | ||
}; | ||
/** | ||
@@ -132,6 +122,7 @@ * @description check if key exists in cache | ||
**/ | ||
const has = key => { | ||
return cache.has(key); | ||
}; | ||
/** | ||
@@ -143,6 +134,7 @@ * @description delete key from cache | ||
**/ | ||
const del = key => { | ||
cache.del(key); | ||
}; | ||
/** | ||
@@ -153,6 +145,7 @@ * @description removes all cache entries | ||
**/ | ||
const clear = () => { | ||
cache.reset(); | ||
}; | ||
/** | ||
@@ -169,4 +162,13 @@ * @description Create a job that subscribes to a rxJs-worker | ||
**/ | ||
const _createJob = ({ key, worker, workerTimeout, ttl, deltaWait }) => { | ||
const _createJob = ({ | ||
key, | ||
worker, | ||
workerTimeout, | ||
ttl, | ||
deltaWait | ||
}) => { | ||
const observable = (0, _cacheHelpers.createObservable)(worker, workerTimeout, log); | ||
const onNext = value => { | ||
@@ -178,2 +180,3 @@ // update cache | ||
}; | ||
const onError = err => { | ||
@@ -185,9 +188,10 @@ // handle error | ||
}; | ||
const onComplete = () => { | ||
jobs.delete(key); | ||
}; | ||
observable.subscribe(onNext, onError, onComplete); | ||
return observable; | ||
}; | ||
/** | ||
@@ -204,10 +208,25 @@ * @description Read from cache, check if stale, run promise (in a dedicated worker) or wait for other worker to complete | ||
**/ | ||
const _requestFromCache = ({ worker, key, workerTimeout, ttl, deltaWait }) => { | ||
const _requestFromCache = ({ | ||
worker, | ||
key, | ||
workerTimeout, | ||
ttl, | ||
deltaWait | ||
}) => { | ||
const obj = cache.get(key); | ||
if (!worker) { | ||
return obj && (0, _cacheHelpers.isFresh)(obj) && obj || obj && _extends({}, obj, { cache: CACHE_STALE }) || null; | ||
}if (obj && (0, _cacheHelpers.isFresh)(obj) && !waiting.get(key)) { | ||
return obj && (0, _cacheHelpers.isFresh)(obj) && obj || obj && { ...obj, | ||
cache: CACHE_STALE | ||
} || null; | ||
} | ||
if (obj && (0, _cacheHelpers.isFresh)(obj) && !waiting.get(key)) { | ||
return Promise.resolve(obj); | ||
} else if (obj && (0, _cacheHelpers.isWaiting)(waiting.get(key))) { | ||
return Promise.resolve(_extends({}, obj, { cache: CACHE_STALE })); | ||
return Promise.resolve({ ...obj, | ||
cache: CACHE_STALE | ||
}); | ||
} else if ((0, _cacheHelpers.isWaiting)(waiting.get(key))) { | ||
@@ -220,2 +239,3 @@ return Promise.reject((0, _cacheHelpers.waitingForError)(key, waiting.get(key))); | ||
let cacheType = CACHE_HIT; | ||
if (!job) { | ||
@@ -232,11 +252,16 @@ cacheType = CACHE_MISS; | ||
} | ||
job.subscribe(value => { | ||
resolve({ value, cache: cacheType }); | ||
resolve({ | ||
value, | ||
cache: cacheType | ||
}); | ||
}, err => { | ||
// serve stale object if it exists | ||
obj ? resolve(_extends({}, obj, { cache: CACHE_STALE })) : reject(err); | ||
obj ? resolve({ ...obj, | ||
cache: CACHE_STALE | ||
}) : reject(err); | ||
}); | ||
}); | ||
}; | ||
/** | ||
@@ -253,2 +278,4 @@ * @description get key from cache (or run promise to fill) | ||
**/ | ||
const get = (key, config = {}) => { | ||
@@ -271,3 +298,2 @@ // TODO: support stale-while-revalidate | ||
}; | ||
/** | ||
@@ -278,4 +304,5 @@ * @description get keys from cache | ||
**/ | ||
const keys = () => cache.keys(); | ||
/** | ||
@@ -286,4 +313,5 @@ * @description get values from cache | ||
**/ | ||
const values = () => cache.values(); | ||
/** | ||
@@ -294,2 +322,4 @@ * @description get cache entries | ||
**/ | ||
const entries = () => { | ||
@@ -302,3 +332,2 @@ const vals = values(); | ||
}; | ||
/** | ||
@@ -310,2 +339,4 @@ * @description expire a cache key (ie. set TTL = 0) | ||
**/ | ||
const expire = keys => { | ||
@@ -317,2 +348,3 @@ keys.forEach(key => { | ||
set(key, obj.value, 0); // TTL = 0 | ||
waiting.delete(key); | ||
@@ -324,14 +356,15 @@ }); | ||
const debug = (extraData = {}) => { | ||
return (0, _debug.getCacheInfo)(_extends({ | ||
return (0, _debug.getCacheInfo)({ | ||
maxAge, | ||
maxLength | ||
}, options, extraData, { | ||
maxLength, | ||
...options, | ||
...extraData, | ||
cache, | ||
waiting, | ||
jobs | ||
})); | ||
}); | ||
}; | ||
Object.keys(initial).forEach(key => { | ||
set(key, (0, _cloneDeep2.default)(initial[key])); | ||
set(key, (0, _cloneDeep.default)(initial[key])); | ||
}); | ||
@@ -364,8 +397,7 @@ | ||
// Keep a reference to the original function pointer | ||
const prevFacade = facade[key]; | ||
// overwrite/mutate the original function | ||
const prevFacade = facade[key]; // overwrite/mutate the original function | ||
facade[key] = (...args) => { | ||
// call middlware function | ||
return m[key]( | ||
// add next parameter | ||
return m[key]( // add next parameter | ||
...args.concat((...middlewareArgs) => { | ||
@@ -380,2 +412,4 @@ // call original function with args from middleware | ||
return facade; | ||
}; | ||
}; | ||
exports.default = _default; |
@@ -1,35 +0,23 @@ | ||
'use strict'; | ||
"use strict"; | ||
var _ = require('../'); | ||
var _ = _interopRequireDefault(require("../")); | ||
var _2 = _interopRequireDefault(_); | ||
var _2 = _interopRequireDefault(require("../../")); | ||
var _3 = require('../../'); | ||
var _sinon = _interopRequireDefault(require("sinon")); | ||
var _4 = _interopRequireDefault(_3); | ||
var _expect = _interopRequireDefault(require("expect.js")); | ||
var _sinon = require('sinon'); | ||
var _mockRedisFactory = require("../../utils/mock-redis-factory"); | ||
var _sinon2 = _interopRequireDefault(_sinon); | ||
var utils = _interopRequireWildcard(require("../persistence-helpers")); | ||
var _expect = require('expect.js'); | ||
var _cacheHelpers = require("../../utils/cache-helpers"); | ||
var _expect2 = _interopRequireDefault(_expect); | ||
var _logHelper = require("../../utils/log-helper"); | ||
var _mockRedisFactory = require('../../utils/mock-redis-factory'); | ||
var _package = _interopRequireDefault(require("../../../package.json")); | ||
var _persistenceHelpers = require('../persistence-helpers'); | ||
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } | ||
var utils = _interopRequireWildcard(_persistenceHelpers); | ||
var _cacheHelpers = require('../../utils/cache-helpers'); | ||
var _logHelper = require('../../utils/log-helper'); | ||
var _package = require('../../../package.json'); | ||
var _package2 = _interopRequireDefault(_package); | ||
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -40,18 +28,28 @@ | ||
it('should be possible', () => { | ||
const cache = (0, _4.default)({ log: _logHelper.dummyLog }); | ||
cache.use((0, _2.default)((0, _mockRedisFactory.mockRedisFactory)(), { bootload: false })); | ||
(0, _expect2.default)(cache).to.be.an(Object); | ||
const cache = (0, _2.default)({ | ||
log: _logHelper.dummyLog | ||
}); | ||
cache.use((0, _.default)((0, _mockRedisFactory.mockRedisFactory)(), { | ||
bootload: false | ||
})); | ||
(0, _expect.default)(cache).to.be.an(Object); | ||
}); | ||
}); | ||
describe('debug', () => { | ||
it('should print a debug of the cache with extra options', () => { | ||
// more thorough testing of debug in debug.spec.js | ||
const cache = (0, _4.default)({ initial: { hello: 'world' } }); | ||
cache.use((0, _2.default)((0, _mockRedisFactory.mockRedisFactory)(), { bootload: false })); | ||
const info = cache.debug({ extraData: 'values' }); | ||
(0, _expect2.default)(info.extraData).to.equal('values'); | ||
const cache = (0, _2.default)({ | ||
initial: { | ||
hello: 'world' | ||
} | ||
}); | ||
cache.use((0, _.default)((0, _mockRedisFactory.mockRedisFactory)(), { | ||
bootload: false | ||
})); | ||
const info = cache.debug({ | ||
extraData: 'values' | ||
}); | ||
(0, _expect.default)(info.extraData).to.equal('values'); | ||
}); | ||
}); | ||
describe('-> bootload', () => { | ||
@@ -61,17 +59,29 @@ it('should set TTL based on elapsed time', () => { | ||
const diff = 10; | ||
const loadObjectsStub = _sinon2.default.stub(utils, 'loadObjects').resolves({ | ||
[`${_package2.default.name}-myCache-house/1`]: (0, _cacheHelpers.createEntry)({ hello: 'world' }, 1000, now) | ||
const loadObjectsStub = _sinon.default.stub(utils, 'loadObjects').resolves({ | ||
[`${_package.default.name}-myCache-house/1`]: (0, _cacheHelpers.createEntry)({ | ||
hello: 'world' | ||
}, 1000, now) | ||
}); | ||
const cache = (0, _4.default)({ log: _logHelper.dummyLog }); | ||
const cache = (0, _2.default)({ | ||
log: _logHelper.dummyLog | ||
}); | ||
let cacheInstance; | ||
cache.use(ci => { | ||
cacheInstance = ci; | ||
return (0, _2.default)((0, _mockRedisFactory.mockRedisFactory)(), { bootload: false })(ci); | ||
return (0, _.default)((0, _mockRedisFactory.mockRedisFactory)(), { | ||
bootload: false | ||
})(ci); | ||
}); | ||
const setStub = _sinon2.default.stub(cacheInstance, 'set').returns('ok'); | ||
const setStub = _sinon.default.stub(cacheInstance, 'set').returns('ok'); | ||
return cache.load(now + diff).then(() => { | ||
(0, _expect2.default)(setStub.called).to.equal(true); | ||
(0, _expect2.default)(setStub.args[0][0]).to.equal(`${_package2.default.name}-myCache-house/1`); | ||
(0, _expect2.default)(setStub.args[0][1]).to.eql({ hello: 'world' }); | ||
(0, _expect2.default)(setStub.args[0][2]).to.equal(1000 - diff); | ||
(0, _expect.default)(setStub.called).to.equal(true); | ||
(0, _expect.default)(setStub.args[0][0]).to.equal(`${_package.default.name}-myCache-house/1`); | ||
(0, _expect.default)(setStub.args[0][1]).to.eql({ | ||
hello: 'world' | ||
}); | ||
(0, _expect.default)(setStub.args[0][2]).to.equal(1000 - diff); | ||
setStub.restore(); | ||
@@ -82,3 +92,2 @@ loadObjectsStub.restore(); | ||
}); | ||
describe('-> get (write to redis on MISS)', () => { | ||
@@ -88,18 +97,24 @@ let cache; | ||
let setSpy; | ||
beforeEach(() => { | ||
setSpy = _sinon2.default.spy((key, json, ex, ttl, cb) => { | ||
setSpy = _sinon.default.spy((key, json, ex, ttl, cb) => { | ||
if (key.indexOf('setFail') > -1) { | ||
return cb(new Error('dummyerror'), null); | ||
} | ||
cb(null, 'ok'); | ||
}); | ||
_sinon2.default.stub(utils, 'loadObjects').resolves({ | ||
[`${_package2.default.name}-myCache-house/1`]: (0, _cacheHelpers.createEntry)({ hello: 'world' }, 1000) | ||
_sinon.default.stub(utils, 'loadObjects').resolves({ | ||
[`${_package.default.name}-myCache-house/1`]: (0, _cacheHelpers.createEntry)({ | ||
hello: 'world' | ||
}, 1000) | ||
}); | ||
mockFactory = (0, _mockRedisFactory.mockRedisFactory)({ | ||
set: setSpy | ||
}); | ||
cache = (0, _4.default)({ log: _logHelper.dummyLog }); | ||
cache.use((0, _2.default)(mockFactory, { | ||
cache = (0, _2.default)({ | ||
log: _logHelper.dummyLog | ||
}); | ||
cache.use((0, _.default)(mockFactory, { | ||
doNotPersist: /store/, | ||
@@ -110,3 +125,2 @@ keySpace: 'myCache', | ||
}); | ||
afterEach(() => { | ||
@@ -116,53 +130,60 @@ cache.destroy(); | ||
}); | ||
it('should write to redis when a cache miss occurs', () => { | ||
const spy = _sinon.default.spy(() => Promise.resolve('hei')); | ||
it('should write to redis when a cache miss occurs', () => { | ||
const spy = _sinon2.default.spy(() => Promise.resolve('hei')); | ||
const now = Date.now(); | ||
const key = `key${now}`; | ||
return cache.get(key, { ttl: 1000, worker: spy }).then(obj => { | ||
(0, _expect2.default)(spy.called).to.equal(true); | ||
(0, _expect2.default)(obj.value).to.equal('hei'); | ||
(0, _expect2.default)(obj.cache).to.equal('miss'); | ||
return cache.get(key, { | ||
ttl: 1000, | ||
worker: spy | ||
}).then(obj => { | ||
(0, _expect.default)(spy.called).to.equal(true); | ||
(0, _expect.default)(obj.value).to.equal('hei'); | ||
(0, _expect.default)(obj.cache).to.equal('miss'); | ||
const [[redisKey, json, ex, expire]] = setSpy.args; | ||
const parsed = JSON.parse(json); | ||
(0, _expect2.default)(redisKey).to.contain(key); | ||
(0, _expect2.default)(parsed).to.have.keys(['created', 'TTL', 'value', 'cache']); | ||
(0, _expect2.default)(ex).to.equal('ex'); | ||
(0, _expect2.default)(expire).to.equal(2); | ||
(0, _expect2.default)(parsed.value).to.equal('hei'); | ||
(0, _expect.default)(redisKey).to.contain(key); | ||
(0, _expect.default)(parsed).to.have.keys(['created', 'TTL', 'value', 'cache']); | ||
(0, _expect.default)(ex).to.equal('ex'); | ||
(0, _expect.default)(expire).to.equal(2); | ||
(0, _expect.default)(parsed.value).to.equal('hei'); | ||
}); | ||
}); | ||
it('should log a warning when write to redis fails (on cache miss)', () => { | ||
const spy = _sinon.default.spy(() => Promise.resolve('hei')); | ||
it('should log a warning when write to redis fails (on cache miss)', () => { | ||
const spy = _sinon2.default.spy(() => Promise.resolve('hei')); | ||
const key = 'setFail'; | ||
const callCount = _logHelper.dummyLog.warn.callCount; | ||
return cache.get(key, { ttl: 1000, worker: spy }).then(obj => { | ||
(0, _expect2.default)(spy.called).to.equal(true); | ||
(0, _expect2.default)(obj.value).to.equal('hei'); | ||
(0, _expect2.default)(obj.cache).to.equal('miss'); | ||
return cache.get(key, { | ||
ttl: 1000, | ||
worker: spy | ||
}).then(obj => { | ||
(0, _expect.default)(spy.called).to.equal(true); | ||
(0, _expect.default)(obj.value).to.equal('hei'); | ||
(0, _expect.default)(obj.cache).to.equal('miss'); | ||
const [[redisKey, json, ex, expire]] = setSpy.args; | ||
const parsed = JSON.parse(json); | ||
(0, _expect2.default)(redisKey).to.contain(key); | ||
(0, _expect2.default)(parsed).to.have.keys(['created', 'TTL', 'value', 'cache']); | ||
(0, _expect2.default)(ex).to.equal('ex'); | ||
(0, _expect2.default)(expire).to.equal(2); | ||
(0, _expect2.default)(parsed.value).to.equal('hei'); | ||
(0, _expect2.default)(_logHelper.dummyLog.warn.callCount).to.equal(callCount + 1); | ||
(0, _expect.default)(redisKey).to.contain(key); | ||
(0, _expect.default)(parsed).to.have.keys(['created', 'TTL', 'value', 'cache']); | ||
(0, _expect.default)(ex).to.equal('ex'); | ||
(0, _expect.default)(expire).to.equal(2); | ||
(0, _expect.default)(parsed.value).to.equal('hei'); | ||
(0, _expect.default)(_logHelper.dummyLog.warn.callCount).to.equal(callCount + 1); | ||
}); | ||
}); | ||
it('should not write to redis when a cache miss occurs and key matches ignored keys', () => { | ||
const spy = _sinon.default.spy(() => Promise.resolve('hei')); | ||
it('should not write to redis when a cache miss occurs and key matches ignored keys', () => { | ||
const spy = _sinon2.default.spy(() => Promise.resolve('hei')); | ||
const now = Date.now(); | ||
const key = `/store/${now}`; | ||
return cache.get(key, { worker: spy }).then(obj => { | ||
(0, _expect2.default)(spy.called).to.equal(true); | ||
(0, _expect2.default)(obj.value).to.equal('hei'); | ||
(0, _expect2.default)(obj.cache).to.equal('miss'); | ||
(0, _expect2.default)(setSpy.called).to.equal(false); | ||
return cache.get(key, { | ||
worker: spy | ||
}).then(obj => { | ||
(0, _expect.default)(spy.called).to.equal(true); | ||
(0, _expect.default)(obj.value).to.equal('hei'); | ||
(0, _expect.default)(obj.cache).to.equal('miss'); | ||
(0, _expect.default)(setSpy.called).to.equal(false); | ||
}); | ||
}); | ||
}); | ||
describe('onDispose', () => { | ||
@@ -173,11 +194,11 @@ let delSpy; | ||
let cache; | ||
beforeEach(() => { | ||
delSpy = _sinon2.default.spy((key, cb) => { | ||
delSpy = _sinon.default.spy((key, cb) => { | ||
if (key.indexOf('house/1') > -1) { | ||
return cb(null, 'ok'); | ||
} | ||
cb(new Error('dummyerror'), null); | ||
}); | ||
setSpy = _sinon2.default.spy(() => {}); | ||
setSpy = _sinon.default.spy(() => {}); | ||
mockFactory = (0, _mockRedisFactory.mockRedisFactory)({ | ||
@@ -187,4 +208,7 @@ del: delSpy, | ||
}); | ||
cache = (0, _4.default)({ log: _logHelper.dummyLog, maxLength: 2 }); | ||
cache.use((0, _2.default)(mockFactory, { | ||
cache = (0, _2.default)({ | ||
log: _logHelper.dummyLog, | ||
maxLength: 2 | ||
}); | ||
cache.use((0, _.default)(mockFactory, { | ||
doNotPersist: /store/, | ||
@@ -196,32 +220,41 @@ bootload: false, | ||
}); | ||
afterEach(() => { | ||
cache.destroy(); | ||
}); | ||
it('should evict key from redis when lru cache evicts key', () => { | ||
cache.set('house/1', { hei: 'verden' }); | ||
cache.set('house/2', { hei: 'verden' }); | ||
cache.set('guest/3', { hei: 'verden' }); | ||
(0, _expect2.default)(setSpy.callCount).to.equal(3); | ||
(0, _expect2.default)(setSpy.args[0][0]).to.equal(utils.getRedisKey(`${_package2.default.name}-myCache`, 'house/1')); | ||
(0, _expect2.default)(setSpy.args[1][0]).to.equal(utils.getRedisKey(`${_package2.default.name}-myCache`, 'house/2')); | ||
(0, _expect2.default)(setSpy.args[2][0]).to.equal(utils.getRedisKey(`${_package2.default.name}-myCache`, 'guest/3')); | ||
(0, _expect2.default)(delSpy.called).to.equal(true); | ||
(0, _expect2.default)(delSpy.args[0][0]).to.equal(utils.getRedisKey(`${_package2.default.name}-myCache`, 'house/1')); | ||
cache.set('house/1', { | ||
hei: 'verden' | ||
}); | ||
cache.set('house/2', { | ||
hei: 'verden' | ||
}); | ||
cache.set('guest/3', { | ||
hei: 'verden' | ||
}); | ||
(0, _expect.default)(setSpy.callCount).to.equal(3); | ||
(0, _expect.default)(setSpy.args[0][0]).to.equal(utils.getRedisKey(`${_package.default.name}-myCache`, 'house/1')); | ||
(0, _expect.default)(setSpy.args[1][0]).to.equal(utils.getRedisKey(`${_package.default.name}-myCache`, 'house/2')); | ||
(0, _expect.default)(setSpy.args[2][0]).to.equal(utils.getRedisKey(`${_package.default.name}-myCache`, 'guest/3')); | ||
(0, _expect.default)(delSpy.called).to.equal(true); | ||
(0, _expect.default)(delSpy.args[0][0]).to.equal(utils.getRedisKey(`${_package.default.name}-myCache`, 'house/1')); | ||
}); | ||
it('should catch error in redis.del when lru cache evicts key', done => { | ||
const count = _logHelper.dummyLog.error.callCount; | ||
cache.set('guest/3', { hei: 'verden' }); | ||
cache.set('house/1', { hei: 'verden' }); | ||
cache.set('house/2', { hei: 'verden' }); | ||
(0, _expect2.default)(setSpy.callCount).to.equal(3); | ||
(0, _expect2.default)(setSpy.args[0][0]).to.equal(utils.getRedisKey(`${_package2.default.name}-myCache`, 'guest/3')); | ||
(0, _expect2.default)(setSpy.args[1][0]).to.equal(utils.getRedisKey(`${_package2.default.name}-myCache`, 'house/1')); | ||
(0, _expect2.default)(setSpy.args[2][0]).to.equal(utils.getRedisKey(`${_package2.default.name}-myCache`, 'house/2')); | ||
(0, _expect2.default)(delSpy.called).to.equal(true); | ||
(0, _expect2.default)(delSpy.args[0][0]).to.equal(utils.getRedisKey(`${_package2.default.name}-myCache`, 'guest/3')); | ||
cache.set('guest/3', { | ||
hei: 'verden' | ||
}); | ||
cache.set('house/1', { | ||
hei: 'verden' | ||
}); | ||
cache.set('house/2', { | ||
hei: 'verden' | ||
}); | ||
(0, _expect.default)(setSpy.callCount).to.equal(3); | ||
(0, _expect.default)(setSpy.args[0][0]).to.equal(utils.getRedisKey(`${_package.default.name}-myCache`, 'guest/3')); | ||
(0, _expect.default)(setSpy.args[1][0]).to.equal(utils.getRedisKey(`${_package.default.name}-myCache`, 'house/1')); | ||
(0, _expect.default)(setSpy.args[2][0]).to.equal(utils.getRedisKey(`${_package.default.name}-myCache`, 'house/2')); | ||
(0, _expect.default)(delSpy.called).to.equal(true); | ||
(0, _expect.default)(delSpy.args[0][0]).to.equal(utils.getRedisKey(`${_package.default.name}-myCache`, 'guest/3')); | ||
setTimeout(() => { | ||
(0, _expect2.default)(_logHelper.dummyLog.error.callCount).to.equal(count + 1); | ||
(0, _expect.default)(_logHelper.dummyLog.error.callCount).to.equal(count + 1); | ||
done(); | ||
@@ -231,35 +264,38 @@ }); | ||
}); | ||
describe('-> del/clear', () => { | ||
let delSpy; | ||
let cache; | ||
beforeEach(() => { | ||
delSpy = _sinon2.default.spy((key, cb) => { | ||
delSpy = _sinon.default.spy((key, cb) => { | ||
if (key.indexOf('house/1') > -1) { | ||
return cb(null, 'ok'); | ||
} | ||
cb(new Error('dummyerror'), null); | ||
}); | ||
cache = (0, _4.default)({ log: _logHelper.dummyLog }); | ||
cache.use((0, _2.default)((0, _mockRedisFactory.mockRedisFactory)({ del: delSpy }), { bootload: false })); | ||
cache = (0, _2.default)({ | ||
log: _logHelper.dummyLog | ||
}); | ||
cache.use((0, _.default)((0, _mockRedisFactory.mockRedisFactory)({ | ||
del: delSpy | ||
}), { | ||
bootload: false | ||
})); | ||
}); | ||
it('should delete key from redis when a key is deleted from lru-cache', () => { | ||
return cache.del('house/1').then(() => { | ||
(0, _expect2.default)(delSpy.called).to.equal(true); | ||
(0, _expect.default)(delSpy.called).to.equal(true); | ||
}); | ||
}); | ||
it('should throw an error key from redis when a key is deleted from lru-cache', () => { | ||
return cache.del('key').catch(() => { | ||
(0, _expect2.default)(delSpy.called).to.equal(true); | ||
(0, _expect.default)(delSpy.called).to.equal(true); | ||
}); | ||
}); | ||
it('should delete all keys in redis with prefix', () => { | ||
_sinon.default.stub(utils, 'deleteKeys').resolves(); | ||
it('should delete all keys in redis with prefix', () => { | ||
_sinon2.default.stub(utils, 'deleteKeys').resolves(); | ||
return cache.clear().then(() => { | ||
(0, _expect2.default)(utils.deleteKeys.called).to.equal(true); | ||
(0, _expect2.default)(utils.deleteKeys.args[0][0]).to.equal(`${_package2.default.name}-`); | ||
(0, _expect.default)(utils.deleteKeys.called).to.equal(true); | ||
(0, _expect.default)(utils.deleteKeys.args[0][0]).to.equal(`${_package.default.name}-`); | ||
utils.deleteKeys.restore(); | ||
@@ -266,0 +302,0 @@ }); |
@@ -1,25 +0,26 @@ | ||
'use strict'; | ||
"use strict"; | ||
var _persistenceHelpers = require('../persistence-helpers'); | ||
var _persistenceHelpers = require("../persistence-helpers"); | ||
var _mockRedisFactory = require('../../utils/mock-redis-factory'); | ||
var _mockRedisFactory = require("../../utils/mock-redis-factory"); | ||
var _logHelper = require('../../utils/log-helper'); | ||
var _logHelper = require("../../utils/log-helper"); | ||
var _sinon = require('sinon'); | ||
var _sinon = _interopRequireDefault(require("sinon")); | ||
var _sinon2 = _interopRequireDefault(_sinon); | ||
var _expect = _interopRequireDefault(require("expect.js")); | ||
var _expect = require('expect.js'); | ||
var _expect2 = _interopRequireDefault(_expect); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
const parsedCache = { | ||
'asdf-123': { hello: 'world1' }, | ||
'asdf-345': { hello: 'world2' }, | ||
'asdf-100': { hello: 'world3' } | ||
'asdf-123': { | ||
hello: 'world1' | ||
}, | ||
'asdf-345': { | ||
hello: 'world2' | ||
}, | ||
'asdf-100': { | ||
hello: 'world3' | ||
} | ||
}; | ||
const redisCache = Object.keys(parsedCache).reduce((acc, key) => { | ||
@@ -29,3 +30,2 @@ acc[key] = JSON.stringify(parsedCache[key]); | ||
}, {}); | ||
describe('persistence-helpers', () => { | ||
@@ -35,17 +35,14 @@ describe('-> getRedisKey', () => { | ||
const key = (0, _persistenceHelpers.getRedisKey)('prefix', 'key'); | ||
(0, _expect2.default)(key).to.equal('prefix-key'); | ||
(0, _expect.default)(key).to.equal('prefix-key'); | ||
}); | ||
}); | ||
describe('-> extractKeyFromRedis', () => { | ||
it('should match keys', () => { | ||
const key = (0, _persistenceHelpers.extractKeyFromRedis)('prefix-http://localhost:8080', 'prefix-http://localhost:8080-myKey'); | ||
(0, _expect2.default)(key).to.equal('myKey'); | ||
(0, _expect.default)(key).to.equal('myKey'); | ||
}); | ||
}); | ||
describe('-> loadObjects', () => { | ||
let redisClient; | ||
let mgetSpy; | ||
it('should load keys', () => { | ||
@@ -57,8 +54,14 @@ const events = {}; | ||
}, 20); | ||
mgetSpy = _sinon2.default.spy((keysToRead, cb) => { | ||
cb(null, [JSON.stringify({ hei: 'verden' })]); | ||
mgetSpy = _sinon.default.spy((keysToRead, cb) => { | ||
cb(null, [JSON.stringify({ | ||
hei: 'verden' | ||
})]); | ||
}); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({ mget: mgetSpy }, { events })(); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({ | ||
mget: mgetSpy | ||
}, { | ||
events | ||
})(); | ||
return (0, _persistenceHelpers.loadObjects)('test-localhost8080', redisClient, _logHelper.dummyLog).then(results => { | ||
(0, _expect2.default)(results).to.eql({ | ||
(0, _expect.default)(results).to.eql({ | ||
'test-localhost8080-myKey': { | ||
@@ -70,3 +73,2 @@ hei: 'verden' | ||
}); | ||
it('should handle errors when loading keys', () => { | ||
@@ -77,37 +79,42 @@ const events = {}; | ||
}, 100); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({}, { events })(); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({}, { | ||
events | ||
})(); | ||
return (0, _persistenceHelpers.loadObjects)('test-localhost8080', redisClient, _logHelper.dummyLog).catch(err => { | ||
(0, _expect2.default)(err.message).to.equal('dummyerror'); | ||
(0, _expect.default)(err.message).to.equal('dummyerror'); | ||
}); | ||
}); | ||
}); | ||
describe('-> deleteKey', () => { | ||
let redisClient; | ||
let delSpy; | ||
it('should delete keys', () => { | ||
const p = (key, cb) => cb(null, 'ok'); | ||
delSpy = _sinon2.default.spy(p); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({ del: delSpy })(); | ||
delSpy = _sinon.default.spy(p); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({ | ||
del: delSpy | ||
})(); | ||
return (0, _persistenceHelpers.deleteKey)('testkey', redisClient, _logHelper.dummyLog).then(result => { | ||
(0, _expect2.default)(delSpy.called).to.equal(true); | ||
(0, _expect2.default)(result).to.equal('ok'); | ||
(0, _expect.default)(delSpy.called).to.equal(true); | ||
(0, _expect.default)(result).to.equal('ok'); | ||
}); | ||
}); | ||
it('should reject if an error occurrs keys', () => { | ||
const p = (key, cb) => cb(new Error('not ok'), null); | ||
delSpy = _sinon2.default.spy(p); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({ del: delSpy })(); | ||
delSpy = _sinon.default.spy(p); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({ | ||
del: delSpy | ||
})(); | ||
return (0, _persistenceHelpers.deleteKey)('testkey', redisClient, _logHelper.dummyLog).catch(err => { | ||
(0, _expect2.default)(delSpy.called).to.equal(true); | ||
(0, _expect2.default)(err).to.be.an(Error); | ||
(0, _expect.default)(delSpy.called).to.equal(true); | ||
(0, _expect.default)(err).to.be.an(Error); | ||
}); | ||
}); | ||
}); | ||
describe('-> deleteKeys', () => { | ||
it('should delete all keys with prefix from redis', () => { | ||
const delSpy = _sinon2.default.spy((key, cb) => cb(null, 'ok')); | ||
const delSpy = _sinon.default.spy((key, cb) => cb(null, 'ok')); | ||
const events = {}; | ||
@@ -118,57 +125,75 @@ setTimeout(() => { | ||
}, 100); | ||
const mgetSpy = _sinon2.default.spy((keysToRead, cb) => { | ||
cb(null, [JSON.stringify({ hei: 'verden' })]); | ||
const mgetSpy = _sinon.default.spy((keysToRead, cb) => { | ||
cb(null, [JSON.stringify({ | ||
hei: 'verden' | ||
})]); | ||
}); | ||
const redisClient = (0, _mockRedisFactory.mockRedisFactory)({ del: delSpy, mget: mgetSpy }, { events })(); | ||
return (0, _persistenceHelpers.deleteKeys)('asdf', redisClient).then((...args) => { | ||
(0, _expect2.default)(delSpy.called).to.equal(true); | ||
const redisClient = (0, _mockRedisFactory.mockRedisFactory)({ | ||
del: delSpy, | ||
mget: mgetSpy | ||
}, { | ||
events | ||
})(); | ||
return (0, _persistenceHelpers.deleteKeys)('asdf', redisClient).then(() => { | ||
(0, _expect.default)(delSpy.called).to.equal(true); | ||
}); | ||
}); | ||
}); | ||
describe('-> readKeys', () => { | ||
let redisClient; | ||
let mgetSpy; | ||
it('should read multiple keys', () => { | ||
const values = Object.keys(redisCache).map(key => redisCache[key]); | ||
mgetSpy = _sinon2.default.spy((keys, cb) => cb(null, values)); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({ mget: mgetSpy })(); | ||
mgetSpy = _sinon.default.spy((keys, cb) => cb(null, values)); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({ | ||
mget: mgetSpy | ||
})(); | ||
return (0, _persistenceHelpers.readKeys)(Object.keys(redisCache), redisClient, _logHelper.dummyLog).then(result => { | ||
(0, _expect2.default)(mgetSpy.called).to.equal(true); | ||
(0, _expect2.default)(result).to.eql(parsedCache); | ||
(0, _expect.default)(mgetSpy.called).to.equal(true); | ||
(0, _expect.default)(result).to.eql(parsedCache); | ||
}); | ||
}); | ||
it('should resolve empty when no keys match', () => { | ||
const p = (keys, cb) => cb(null, []); | ||
mgetSpy = _sinon2.default.spy(p); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({ mget: mgetSpy })(); | ||
mgetSpy = _sinon.default.spy(p); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({ | ||
mget: mgetSpy | ||
})(); | ||
return (0, _persistenceHelpers.readKeys)(Object.keys([]), redisClient, _logHelper.dummyLog).then(result => { | ||
(0, _expect2.default)(mgetSpy.called).to.equal(false); | ||
(0, _expect2.default)(result).to.eql({}); | ||
(0, _expect.default)(mgetSpy.called).to.equal(false); | ||
(0, _expect.default)(result).to.eql({}); | ||
}); | ||
}); | ||
it('should skip keys with invalid json', () => { | ||
const p = (keys, cb) => cb(null, ['{1}', '{"hello": "world"}']); | ||
mgetSpy = _sinon2.default.spy(p); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({ mget: mgetSpy })(); | ||
mgetSpy = _sinon.default.spy(p); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({ | ||
mget: mgetSpy | ||
})(); | ||
return (0, _persistenceHelpers.readKeys)(['key1', 'key2'], redisClient, _logHelper.dummyLog).then(result => { | ||
(0, _expect2.default)(mgetSpy.called).to.equal(true); | ||
(0, _expect2.default)(result).to.eql({ key2: { hello: 'world' } }); | ||
(0, _expect.default)(mgetSpy.called).to.equal(true); | ||
(0, _expect.default)(result).to.eql({ | ||
key2: { | ||
hello: 'world' | ||
} | ||
}); | ||
}); | ||
}); | ||
it('should reject when an error occurrs', () => { | ||
const p = (keys, cb) => cb(new Error('not ok'), null); | ||
mgetSpy = _sinon2.default.spy(p); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({ mget: mgetSpy })(); | ||
mgetSpy = _sinon.default.spy(p); | ||
redisClient = (0, _mockRedisFactory.mockRedisFactory)({ | ||
mget: mgetSpy | ||
})(); | ||
return (0, _persistenceHelpers.readKeys)(Object.keys(redisCache), redisClient, _logHelper.dummyLog).catch(err => { | ||
(0, _expect2.default)(mgetSpy.called).to.equal(true); | ||
(0, _expect2.default)(err).to.be.an(Error); | ||
(0, _expect.default)(mgetSpy.called).to.equal(true); | ||
(0, _expect.default)(err).to.be.an(Error); | ||
}); | ||
}); | ||
}); | ||
describe('isSerializable', () => { | ||
@@ -184,5 +209,4 @@ it('should return true for plain objects', () => { | ||
}; | ||
(0, _expect2.default)((0, _persistenceHelpers.isSerializable)(obj)).to.equal(true); | ||
(0, _expect.default)((0, _persistenceHelpers.isSerializable)(obj)).to.equal(true); | ||
}); | ||
it('should return false for objects with functions', () => { | ||
@@ -192,5 +216,4 @@ const obj = { | ||
}; | ||
(0, _expect2.default)((0, _persistenceHelpers.isSerializable)(obj)).to.equal(false); | ||
(0, _expect.default)((0, _persistenceHelpers.isSerializable)(obj)).to.equal(false); | ||
}); | ||
it('should return false for objects with built in native objects', () => { | ||
@@ -201,5 +224,5 @@ const obj = { | ||
}; | ||
(0, _expect2.default)((0, _persistenceHelpers.isSerializable)(obj)).to.equal(false); | ||
(0, _expect.default)((0, _persistenceHelpers.isSerializable)(obj)).to.equal(false); | ||
}); | ||
}); | ||
}); |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -6,18 +6,14 @@ Object.defineProperty(exports, "__esModule", { | ||
}); | ||
exports.default = void 0; | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /** | ||
* @module | ||
**/ | ||
var _persistenceHelpers = require("./persistence-helpers.js"); | ||
var _package = _interopRequireDefault(require("../../package.json")); | ||
var _persistenceHelpers = require('./persistence-helpers.js'); | ||
var _package = require('../../package.json'); | ||
var _package2 = _interopRequireDefault(_package); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
/** | ||
* @module | ||
**/ | ||
const DEFAULT_GRACE = 60 * 60 * 24 * 1000; | ||
/** | ||
@@ -32,3 +28,3 @@ * @description Create new persistentCache middleware instance to be used by inMemoryCache | ||
exports.default = (redisFactory, { | ||
var _default = (redisFactory, { | ||
doNotPersist = null, | ||
@@ -40,3 +36,3 @@ keySpace = '', | ||
const redisClient = redisFactory(); | ||
const cacheKeyPrefix = `${_package2.default.name}-${keySpace}`; | ||
const cacheKeyPrefix = `${_package.default.name}-${keySpace}`; | ||
const persisting = {}; | ||
@@ -54,2 +50,3 @@ | ||
} | ||
delete persisting[key]; | ||
@@ -66,5 +63,5 @@ }); | ||
const [key] = args; | ||
if (val.cache === 'miss') { | ||
persist(key, val); | ||
// return valj | ||
persist(key, val); // return valj | ||
// if ((!doNotPersist || !doNotPersist.test(key)) && isSerializable(obj) && !persisting[key]) { | ||
@@ -85,2 +82,3 @@ // persisting[key] = true | ||
} | ||
return val; | ||
@@ -121,3 +119,6 @@ }); | ||
const debug = (extraData, next) => { | ||
return next(_extends({ cacheKeyPrefix }, extraData)); | ||
return next({ | ||
cacheKeyPrefix, | ||
...extraData | ||
}); | ||
}; | ||
@@ -152,2 +153,4 @@ | ||
}; | ||
}; | ||
}; | ||
exports.default = _default; |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -6,50 +6,44 @@ Object.defineProperty(exports, "__esModule", { | ||
}); | ||
exports.isSerializable = exports.loadObjects = exports.deleteKeys = exports.deleteKey = exports.scanKeys = exports.readKeys = exports.getRedisKey = exports.extractKeyFromRedis = undefined; | ||
exports.isSerializable = exports.loadObjects = exports.deleteKeys = exports.deleteKey = exports.scanKeys = exports.readKeys = exports.getRedisKey = exports.extractKeyFromRedis = void 0; | ||
var _isUndefined = require('lodash/isUndefined'); | ||
var _isUndefined = _interopRequireDefault(require("lodash/isUndefined")); | ||
var _isUndefined2 = _interopRequireDefault(_isUndefined); | ||
var _isNull = _interopRequireDefault(require("lodash/isNull")); | ||
var _isNull = require('lodash/isNull'); | ||
var _isBoolean = _interopRequireDefault(require("lodash/isBoolean")); | ||
var _isNull2 = _interopRequireDefault(_isNull); | ||
var _isNumber = _interopRequireDefault(require("lodash/isNumber")); | ||
var _isBoolean = require('lodash/isBoolean'); | ||
var _isString = _interopRequireDefault(require("lodash/isString")); | ||
var _isBoolean2 = _interopRequireDefault(_isBoolean); | ||
var _isArray = _interopRequireDefault(require("lodash/isArray")); | ||
var _isNumber = require('lodash/isNumber'); | ||
var _isPlainObject = _interopRequireDefault(require("lodash/isPlainObject")); | ||
var _isNumber2 = _interopRequireDefault(_isNumber); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
var _isString = require('lodash/isString'); | ||
/** | ||
* @module | ||
**/ | ||
const MAX_PAGE_SIZE = 100; | ||
var _isString2 = _interopRequireDefault(_isString); | ||
var _isArray = require('lodash/isArray'); | ||
var _isArray2 = _interopRequireDefault(_isArray); | ||
var _isPlainObject = require('lodash/isPlainObject'); | ||
var _isPlainObject2 = _interopRequireDefault(_isPlainObject); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
const MAX_PAGE_SIZE = 100; /** | ||
* @module | ||
**/ | ||
const extractKeyFromRedis = exports.extractKeyFromRedis = (prefix, key) => { | ||
const extractKeyFromRedis = (prefix, key) => { | ||
return key.replace(new RegExp(`${prefix}-`), ''); | ||
}; | ||
const getRedisKey = exports.getRedisKey = (prefix, key = '') => { | ||
exports.extractKeyFromRedis = extractKeyFromRedis; | ||
const getRedisKey = (prefix, key = '') => { | ||
return `${[prefix, key].join('-')}`; | ||
}; | ||
const readKeys = exports.readKeys = (keys, redisClient, log) => { | ||
exports.getRedisKey = getRedisKey; | ||
const readKeys = (keys, redisClient, log) => { | ||
if (keys.length === 0) { | ||
return Promise.resolve({}); | ||
} | ||
const p = []; | ||
for (let i = 0; i < keys.length; i = i + MAX_PAGE_SIZE) { | ||
@@ -64,2 +58,3 @@ const keysToRead = keys.slice(i, i + MAX_PAGE_SIZE); | ||
} | ||
resolve(keysToRead.reduce((acc, key, i) => { | ||
@@ -71,2 +66,3 @@ try { | ||
} | ||
return acc; | ||
@@ -77,2 +73,3 @@ }, {})); | ||
} | ||
return Promise.all(p).then(results => { | ||
@@ -86,3 +83,5 @@ return results.reduce((acc, next) => { | ||
const scanKeys = exports.scanKeys = (cacheKeyPrefix, redisClient) => { | ||
exports.readKeys = readKeys; | ||
const scanKeys = (cacheKeyPrefix, redisClient) => { | ||
const keys = []; | ||
@@ -106,3 +105,5 @@ return new Promise((resolve, reject) => { | ||
const deleteKey = exports.deleteKey = (key, redisClient) => { | ||
exports.scanKeys = scanKeys; | ||
const deleteKey = (key, redisClient) => { | ||
return new Promise((resolve, reject) => { | ||
@@ -114,2 +115,3 @@ redisClient.del(key, (err, res) => { | ||
} | ||
resolve(res); | ||
@@ -120,3 +122,5 @@ }); | ||
const deleteKeys = exports.deleteKeys = (cacheKeyPrefix, redisClient) => { | ||
exports.deleteKey = deleteKey; | ||
const deleteKeys = (cacheKeyPrefix, redisClient) => { | ||
return scanKeys(cacheKeyPrefix, redisClient).then(keys => { | ||
@@ -127,16 +131,20 @@ return Promise.all(keys.map(key => deleteKey(key, redisClient))); | ||
const loadObjects = exports.loadObjects = (cacheKeyPrefix, redisClient, log) => { | ||
exports.deleteKeys = deleteKeys; | ||
const loadObjects = (cacheKeyPrefix, redisClient, log) => { | ||
return scanKeys(cacheKeyPrefix, redisClient).then(keys => { | ||
return readKeys(keys, redisClient, log); | ||
}); | ||
}; | ||
}; // credits to https://stackoverflow.com/users/128816/treznik | ||
// https://stackoverflow.com/questions/30579940/reliable-way-to-check-if-objects-is-serializable-in-javascript | ||
// credits to https://stackoverflow.com/users/128816/treznik | ||
// https://stackoverflow.com/questions/30579940/reliable-way-to-check-if-objects-is-serializable-in-javascript | ||
const isSerializable = exports.isSerializable = obj => { | ||
if ((0, _isUndefined2.default)(obj) || (0, _isNull2.default)(obj) || (0, _isBoolean2.default)(obj) || (0, _isNumber2.default)(obj) || (0, _isString2.default)(obj)) { | ||
exports.loadObjects = loadObjects; | ||
const isSerializable = obj => { | ||
if ((0, _isUndefined.default)(obj) || (0, _isNull.default)(obj) || (0, _isBoolean.default)(obj) || (0, _isNumber.default)(obj) || (0, _isString.default)(obj)) { | ||
return true; | ||
} | ||
if (!(0, _isPlainObject2.default)(obj) && !(0, _isArray2.default)(obj)) { | ||
if (!(0, _isPlainObject.default)(obj) && !(0, _isArray.default)(obj)) { | ||
return false; | ||
@@ -152,2 +160,4 @@ } | ||
return true; | ||
}; | ||
}; | ||
exports.isSerializable = isSerializable; |
@@ -1,13 +0,9 @@ | ||
'use strict'; | ||
"use strict"; | ||
var _cacheHelpers = require('../cache-helpers'); | ||
var _cacheHelpers = require("../cache-helpers"); | ||
var _sinon = require('sinon'); | ||
var _sinon = _interopRequireDefault(require("sinon")); | ||
var _sinon2 = _interopRequireDefault(_sinon); | ||
var _expect = _interopRequireDefault(require("expect.js")); | ||
var _expect = require('expect.js'); | ||
var _expect2 = _interopRequireDefault(_expect); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -19,3 +15,3 @@ | ||
const waiting = (0, _cacheHelpers.createWait)(100, 200); | ||
(0, _expect2.default)(waiting).to.eql({ | ||
(0, _expect.default)(waiting).to.eql({ | ||
started: 200, | ||
@@ -27,50 +23,48 @@ wait: 100, | ||
}); | ||
describe('-> isWaiting', () => { | ||
it('should return true if waiting', () => { | ||
(0, _expect2.default)((0, _cacheHelpers.isWaiting)((0, _cacheHelpers.createWait)(1000))).to.equal(true); | ||
(0, _expect.default)((0, _cacheHelpers.isWaiting)((0, _cacheHelpers.createWait)(1000))).to.equal(true); | ||
}); | ||
it('should return false if waiting is finished', () => { | ||
(0, _expect2.default)((0, _cacheHelpers.isWaiting)((0, _cacheHelpers.createWait)(-1000))).to.equal(false); | ||
(0, _expect.default)((0, _cacheHelpers.isWaiting)((0, _cacheHelpers.createWait)(-1000))).to.equal(false); | ||
}); | ||
it('should return false if not waiting', () => { | ||
(0, _expect2.default)((0, _cacheHelpers.isWaiting)()).to.equal(false); | ||
(0, _expect.default)((0, _cacheHelpers.isWaiting)()).to.equal(false); | ||
}); | ||
}); | ||
describe('-> isFresh', () => { | ||
it('should return true if TTL is not reached', () => { | ||
(0, _expect2.default)((0, _cacheHelpers.isFresh)((0, _cacheHelpers.createEntry)('yo', 1000))).to.equal(true); | ||
(0, _expect.default)((0, _cacheHelpers.isFresh)((0, _cacheHelpers.createEntry)('yo', 1000))).to.equal(true); | ||
}); | ||
it('should return false if TTL is reached', () => { | ||
(0, _expect2.default)((0, _cacheHelpers.isFresh)((0, _cacheHelpers.createEntry)('yo', -1000))).to.equal(false); | ||
(0, _expect.default)((0, _cacheHelpers.isFresh)((0, _cacheHelpers.createEntry)('yo', -1000))).to.equal(false); | ||
}); | ||
}); | ||
describe('-> createEntry', () => { | ||
it('should create a wrapper object with metadata for cached', () => { | ||
const obj = { hello: 'world' }; | ||
const obj = { | ||
hello: 'world' | ||
}; | ||
const entry = (0, _cacheHelpers.createEntry)(obj, 10); | ||
(0, _expect2.default)(entry).to.only.have.keys(['created', 'TTL', 'value']); | ||
(0, _expect2.default)(entry.value).to.eql(obj); | ||
(0, _expect.default)(entry).to.only.have.keys(['created', 'TTL', 'value']); | ||
(0, _expect.default)(entry.value).to.eql(obj); | ||
}); | ||
}); | ||
describe('-> createObservable', () => { | ||
it('should create an Observable with subscription capabilities from promise', done => { | ||
const p = () => Promise.resolve(); | ||
const spy = _sinon2.default.spy(p); | ||
const spy = _sinon.default.spy(p); | ||
const obs = (0, _cacheHelpers.createObservable)(spy, 0); | ||
obs.subscribe(() => done()); | ||
}); | ||
it('should create an Observable from promise with timeout support', done => { | ||
const p = () => new Promise(resolve => setTimeout(() => resolve(), 10)); | ||
const spy = _sinon2.default.spy(p); | ||
const spy = _sinon.default.spy(p); | ||
const obs = (0, _cacheHelpers.createObservable)(spy, 0); | ||
obs.subscribe(Function.prototype, err => { | ||
(0, _expect2.default)(err).to.be.an(Error); | ||
(0, _expect.default)(err).to.be.an(Error); | ||
done(); | ||
@@ -80,49 +74,40 @@ }); | ||
}); | ||
describe('-> createRegExp', () => { | ||
it('should create a regexp from a string', () => { | ||
const re = (0, _cacheHelpers.createRegExp)('/houses/2'); | ||
(0, _expect2.default)(re).to.eql(/\/houses\/2/); | ||
(0, _expect.default)(re).to.eql(/\/houses\/2/); | ||
}); | ||
it('should support * to .* rewrite', () => { | ||
const re = (0, _cacheHelpers.createRegExp)('/houses/2*'); | ||
(0, _expect2.default)(re).to.eql(/\/houses\/2.*/); | ||
(0, _expect.default)(re).to.eql(/\/houses\/2.*/); | ||
}); | ||
it('should support ? to \\? rewrite', () => { | ||
const re = (0, _cacheHelpers.createRegExp)('/houses/2?hallo'); | ||
(0, _expect2.default)(re).to.eql(/\/houses\/2\?hallo/); | ||
(0, _expect.default)(re).to.eql(/\/houses\/2\?hallo/); | ||
}); | ||
it('should support . to \\. rewrite', () => { | ||
const re = (0, _cacheHelpers.createRegExp)('/houses/2.hallo'); | ||
(0, _expect2.default)(re).to.eql(/\/houses\/2\.hallo/); | ||
(0, _expect.default)(re).to.eql(/\/houses\/2\.hallo/); | ||
}); | ||
it('should support [ to \\[ rewrite', () => { | ||
const re = (0, _cacheHelpers.createRegExp)('/houses/2hallo['); | ||
(0, _expect2.default)(re).to.eql(/\/houses\/2hallo\[/); | ||
(0, _expect.default)(re).to.eql(/\/houses\/2hallo\[/); | ||
}); | ||
it('should support ] to \\] rewrite', () => { | ||
const re = (0, _cacheHelpers.createRegExp)('/houses/2hallo]'); | ||
(0, _expect2.default)(re).to.eql(/\/houses\/2hallo\]/); | ||
(0, _expect.default)(re).to.eql(/\/houses\/2hallo\]/); | ||
}); | ||
it('should support ^ to \\^ rewrite', () => { | ||
const re = (0, _cacheHelpers.createRegExp)('/houses/2hallo^'); | ||
(0, _expect2.default)(re).to.eql(/\/houses\/2hallo\^/); | ||
(0, _expect.default)(re).to.eql(/\/houses\/2hallo\^/); | ||
}); | ||
it('should support $ to \\$ rewrite', () => { | ||
const re = (0, _cacheHelpers.createRegExp)('/houses/2hallo$'); | ||
(0, _expect2.default)(re).to.eql(/\/houses\/2hallo\$/); | ||
(0, _expect.default)(re).to.eql(/\/houses\/2hallo\$/); | ||
}); | ||
it('should rewrite urls', () => { | ||
const re = (0, _cacheHelpers.createRegExp)('http://localhost.no/?param1=yo¶m2=yo'); | ||
(0, _expect2.default)(re).to.eql(/http:\/\/localhost\.no\/\?param1=yo¶m2=yo/); | ||
(0, _expect.default)(re).to.eql(/http:\/\/localhost\.no\/\?param1=yo¶m2=yo/); | ||
}); | ||
}); | ||
}); |
@@ -1,21 +0,13 @@ | ||
'use strict'; | ||
"use strict"; | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; | ||
var _debug = require("../debug"); | ||
var _debug = require('../debug'); | ||
var _expect = _interopRequireDefault(require("expect.js")); | ||
var _expect = require('expect.js'); | ||
var _lruCache = _interopRequireDefault(require("lru-cache")); | ||
var _expect2 = _interopRequireDefault(_expect); | ||
var _cacheHelpers = require("../cache-helpers"); | ||
var _lruCache = require('lru-cache'); | ||
var _lruCache2 = _interopRequireDefault(_lruCache); | ||
var _cacheHelpers = require('../cache-helpers'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } | ||
describe('debug', () => { | ||
@@ -33,4 +25,9 @@ describe('-> formatWait', () => { | ||
const full = false; | ||
const debugKey = (0, _debug.buildKey)({ key, value, waiting, full }); | ||
(0, _expect2.default)(debugKey).to.eql({ | ||
const debugKey = (0, _debug.buildKey)({ | ||
key, | ||
value, | ||
waiting, | ||
full | ||
}); | ||
(0, _expect.default)(debugKey).to.eql({ | ||
key: 'hei', | ||
@@ -46,3 +43,2 @@ created: new Date('2017-09-04T08:00:00.000Z'), | ||
}); | ||
it('should not print waiting if wait is not set', () => { | ||
@@ -56,4 +52,8 @@ const key = 'hei'; | ||
const full = false; | ||
const debugKey = (0, _debug.buildKey)({ key, value, full }); | ||
(0, _expect2.default)(debugKey).to.eql({ | ||
const debugKey = (0, _debug.buildKey)({ | ||
key, | ||
value, | ||
full | ||
}); | ||
(0, _expect.default)(debugKey).to.eql({ | ||
key: 'hei', | ||
@@ -65,19 +65,33 @@ created: new Date('2017-09-04T08:00:00.000Z'), | ||
}); | ||
describe('getCacheInfo', () => { | ||
it('should print debug info from cache', () => { | ||
const maxAge = 10000; | ||
const cache = (0, _lruCache2.default)({ | ||
const cache = (0, _lruCache.default)({ | ||
max: 100, | ||
maxAge | ||
}); | ||
const entry = _extends({}, (0, _cacheHelpers.createEntry)({ hello: 'world' }, 12345), { cache: 'hit' }); | ||
const entry2 = _extends({}, (0, _cacheHelpers.createEntry)({ foo: 'bar' }, 12345), { cache: 'hit' }); | ||
const entry3 = _extends({}, (0, _cacheHelpers.createEntry)({ hello: 'world' }, -1), { cache: 'hit' }); | ||
const entry = { ...(0, _cacheHelpers.createEntry)({ | ||
hello: 'world' | ||
}, 12345), | ||
cache: 'hit' | ||
}; | ||
const entry2 = { ...(0, _cacheHelpers.createEntry)({ | ||
foo: 'bar' | ||
}, 12345), | ||
cache: 'hit' | ||
}; | ||
const entry3 = { ...(0, _cacheHelpers.createEntry)({ | ||
hello: 'world' | ||
}, -1), | ||
cache: 'hit' | ||
}; | ||
cache.set('yo', entry); | ||
cache.set('notinresults', entry2); | ||
cache.set('yo2', entry3); | ||
// omit now and waiting field | ||
const _getCacheInfo = (0, _debug.getCacheInfo)({ | ||
cache.set('yo2', entry3); // omit now and waiting field | ||
const { | ||
now, | ||
waiting, | ||
...info | ||
} = (0, _debug.getCacheInfo)({ | ||
full: true, | ||
@@ -88,6 +102,4 @@ search: 'yo*', | ||
waiting: new Map() | ||
}), | ||
{ now, waiting } = _getCacheInfo, | ||
info = _objectWithoutProperties(_getCacheInfo, ['now', 'waiting']); | ||
(0, _expect2.default)(info).to.eql({ | ||
}); | ||
(0, _expect.default)(info).to.eql({ | ||
full: true, | ||
@@ -94,0 +106,0 @@ itemCount: 3, |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -6,16 +6,20 @@ Object.defineProperty(exports, "__esModule", { | ||
}); | ||
exports.createObservable = exports.createWait = exports.createEntry = exports.waitingForError = exports.isWaiting = exports.isFresh = exports.createRegExp = undefined; | ||
exports.createObservable = exports.createWait = exports.createEntry = exports.waitingForError = exports.isWaiting = exports.isFresh = exports.createRegExp = void 0; | ||
var _Observable = require('rxjs/Observable'); | ||
var _Observable = require("rxjs/Observable"); | ||
require('rxjs/add/observable/fromPromise'); | ||
require("rxjs/add/observable/fromPromise"); | ||
require('rxjs/add/operator/timeout'); | ||
require("rxjs/add/operator/timeout"); | ||
const createRegExp = exports.createRegExp = search => { | ||
/** | ||
* @module | ||
**/ | ||
const createRegExp = search => { | ||
return new RegExp(search.replace(/\./g, '\\.').replace(/\[/g, '\\[').replace(/\]/g, '\\]').replace(/\^/g, '\\^').replace(/\$/g, '\\$').replace(/\?/g, '\\?').replace(/\*/g, '.*')); | ||
}; /** | ||
* @module | ||
**/ | ||
const isFresh = exports.isFresh = (entry, nowDefault) => { | ||
}; | ||
exports.createRegExp = createRegExp; | ||
const isFresh = (entry, nowDefault) => { | ||
const now = nowDefault || Date.now(); | ||
@@ -25,15 +29,23 @@ return entry.created + entry.TTL > now; | ||
const isWaiting = exports.isWaiting = (waiting, nowDefault) => { | ||
exports.isFresh = isFresh; | ||
const isWaiting = (waiting, nowDefault) => { | ||
const now = nowDefault || Date.now(); | ||
if (waiting) { | ||
return waiting.waitUntil > now; | ||
} | ||
return false; | ||
}; | ||
const waitingForError = exports.waitingForError = (key, wait = {}) => { | ||
exports.isWaiting = isWaiting; | ||
const waitingForError = (key, wait = {}) => { | ||
return new Error(`Waiting for next run for ${key}, wait: ${JSON.stringify(wait, null, 2)}`); | ||
}; | ||
const createEntry = exports.createEntry = (value, TTL, date) => { | ||
exports.waitingForError = waitingForError; | ||
const createEntry = (value, TTL, date) => { | ||
const created = date || Date.now(); | ||
@@ -47,3 +59,5 @@ return { | ||
const createWait = exports.createWait = (wait, now) => { | ||
exports.createEntry = createEntry; | ||
const createWait = (wait, now) => { | ||
const started = now || Date.now(); | ||
@@ -57,3 +71,5 @@ return { | ||
const createObservable = exports.createObservable = (promiseCreator, timeout, logger) => { | ||
exports.createWait = createWait; | ||
const createObservable = (promiseCreator, timeout, logger) => { | ||
const promise = promiseCreator().catch(err => { | ||
@@ -63,4 +79,5 @@ logger.error('An error occured while executing worker promise', err); | ||
}); | ||
return _Observable.Observable.fromPromise(promise).timeout(timeout); | ||
}; | ||
return _Observable.Observable.fromPromise(promise).timeout(timeout); | ||
}; | ||
exports.createObservable = createObservable; |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -6,12 +6,15 @@ Object.defineProperty(exports, "__esModule", { | ||
}); | ||
exports.getCacheInfo = exports.buildKey = undefined; | ||
exports.getCacheInfo = exports.buildKey = void 0; | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /** | ||
* @module | ||
**/ | ||
var _cacheHelpers = require("./cache-helpers"); | ||
var _cacheHelpers = require('./cache-helpers'); | ||
const buildKey = exports.buildKey = ({ key, value, waiting, full }) => { | ||
/** | ||
* @module | ||
**/ | ||
const buildKey = ({ | ||
key, | ||
value, | ||
waiting, | ||
full | ||
}) => { | ||
const expire = value.created + value.TTL; | ||
@@ -29,5 +32,9 @@ const expireKey = expire < Date.now() ? 'expired' : 'expires'; | ||
} | ||
} : {}, full ? { value: value.value } : {}); | ||
} : {}, full ? { | ||
value: value.value | ||
} : {}); | ||
}; | ||
exports.buildKey = buildKey; | ||
const extractProps = obj => { | ||
@@ -41,3 +48,3 @@ const ret = {}; | ||
const getCacheInfo = exports.getCacheInfo = info => { | ||
const getCacheInfo = info => { | ||
const { | ||
@@ -59,3 +66,10 @@ full, | ||
} | ||
const keyInfo = buildKey({ key, value, waiting: waiting.get(key), full }); | ||
const keyInfo = buildKey({ | ||
key, | ||
value, | ||
waiting: waiting.get(key), | ||
full | ||
}); | ||
if ((0, _cacheHelpers.isFresh)(value)) { | ||
@@ -67,9 +81,11 @@ keys.hot.push(keyInfo); | ||
}); | ||
return _extends({ | ||
now: new Date() | ||
}, extractProps(info), { | ||
return { | ||
now: new Date(), | ||
...extractProps(info), | ||
maxAge: `${maxAge / (1000 * 60 * 60)}h`, | ||
itemCount: cache.itemCount, | ||
keys | ||
}); | ||
}; | ||
}; | ||
}; | ||
exports.getCacheInfo = getCacheInfo; |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -6,15 +6,14 @@ Object.defineProperty(exports, "__esModule", { | ||
}); | ||
exports.dummyLog = undefined; | ||
exports.dummyLog = void 0; | ||
var _sinon = require('sinon'); | ||
var _sinon = _interopRequireDefault(require("sinon")); | ||
var _sinon2 = _interopRequireDefault(_sinon); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
const dummyLog = exports.dummyLog = { | ||
info: _sinon2.default.spy(Function.prototype), | ||
error: _sinon2.default.spy(Function.prototype), | ||
debug: _sinon2.default.spy(Function.prototype), | ||
warn: _sinon2.default.spy(Function.prototype) | ||
}; | ||
const dummyLog = { | ||
info: _sinon.default.spy(Function.prototype), | ||
error: _sinon.default.spy(Function.prototype), | ||
debug: _sinon.default.spy(Function.prototype), | ||
warn: _sinon.default.spy(Function.prototype) | ||
}; | ||
exports.dummyLog = dummyLog; |
@@ -6,3 +6,7 @@ "use strict"; | ||
}); | ||
const mockRedisFactory = exports.mockRedisFactory = (overrideMethods = {}, { events = {} } = {}) => { | ||
exports.mockRedisFactory = void 0; | ||
const mockRedisFactory = (overrideMethods = {}, { | ||
events = {} | ||
} = {}) => { | ||
let instance; | ||
@@ -12,5 +16,7 @@ return () => { | ||
const namespaces = []; | ||
if (instance) { | ||
return instance; | ||
} | ||
instance = Object.assign({ | ||
@@ -28,6 +34,7 @@ subscribe: ns => { | ||
} | ||
cbs[ns].push(cb); | ||
}); | ||
}, | ||
scanStream: ({ match, cound }) => { | ||
scanStream: () => { | ||
return { | ||
@@ -38,2 +45,3 @@ on: (event, cb) => { | ||
} | ||
events[event].push(cb); | ||
@@ -43,6 +51,7 @@ } | ||
} | ||
}, overrideMethods); | ||
return instance; | ||
}; | ||
}; | ||
}; | ||
exports.mockRedisFactory = mockRedisFactory; |
@@ -9,19 +9,11 @@ { | ||
"homepage": "https://github.com/nrkno/nodecache-as-promised#readme", | ||
"version": "1.1.0", | ||
"version": "1.2.1", | ||
"description": "NodeJs in-memory cache with Promise support. Extendable with middlewares. Middlewares provided: Distributed invalidation and persistence of cache misses", | ||
"main": "lib/index.js", | ||
"scripts": { | ||
"preversion": "echo prepublish clean, test and build && rm -rf lib node_modules && npm i && npm run test && npm run build", | ||
"bump:patch": "npm version patch -m 'Release patch %s' && npm run push", | ||
"bump:minor": "npm version minor -m 'Release minor %s' && npm run push", | ||
"bump:major": "npm version major -m 'Release major %s' && npm run push", | ||
"bump:prerelease": "npm version prerelease -m 'Release prerelease %s' && npm run push", | ||
"push": "git push && npm publish", | ||
"build": "babel src --out-dir lib ", | ||
"build:watch": "babel src --watch --source-maps --out-dir lib", | ||
"lint": "npm run lint:js", | ||
"lint:js": "standard", | ||
"test:unit": "mocha --recursive ./src", | ||
"test:watch": "mocha -w --recursive ./src", | ||
"test": "npm run lint && npm run test:coverage", | ||
"test": "npm run test:coverage", | ||
"test:coverage": "nyc --reporter=lcov npm run test:unit", | ||
@@ -32,3 +24,6 @@ "test:perf": "npm run build && npm run perf:nocache ; npm run perf:nocache-cache-file ; npm run perf:cache ; npm run perf:cache-cluster", | ||
"perf:cache": "node test/cache.js", | ||
"perf:cache-cluster": "node test/cache-cluster.js" | ||
"perf:cache-cluster": "node test/cache-cluster.js", | ||
"format": "prettier --write './{src,test}/**/*.{js,json}'", | ||
"lint": "eslint './{src,test}/**/*.js'", | ||
"precommit": "lint-staged && npm run test" | ||
}, | ||
@@ -40,33 +35,25 @@ "repository": { | ||
"dependencies": { | ||
"lodash": "4.17.4", | ||
"lru-cache": "4.1.1", | ||
"lodash": "4.17.11", | ||
"lru-cache": "4.1.3", | ||
"rxjs": "5.4.3" | ||
}, | ||
"devDependencies": { | ||
"babel-cli": "6.26.0", | ||
"babel-core": "6.26.0", | ||
"babel-eslint": "7.2.3", | ||
"babel-preset-env": "1.6.1", | ||
"babel-preset-stage-3": "6.24.1", | ||
"chalk": "2.1.0", | ||
"@babel/cli": "7.0.0", | ||
"@babel/core": "7.0.1", | ||
"@babel/preset-env": "7.0.0", | ||
"@babel/register": "7.0.0", | ||
"babel-eslint": "9.0.0", | ||
"chalk": "2.4.1", | ||
"eslint": "5.5.0", | ||
"eslint-config-prettier": "3.0.1", | ||
"eslint-plugin-prettier": "2.6.2", | ||
"expect.js": "0.3.1", | ||
"mocha": "4.0.1", | ||
"nyc": "11.4.1", | ||
"sinon": "3.2.1", | ||
"standard": "10.0.3", | ||
"yargs": "10.0.3" | ||
"husky": "0.14.3", | ||
"lint-staged": "7.2.2", | ||
"mocha": "5.2.0", | ||
"nyc": "13.0.1", | ||
"prettier": "1.14.2", | ||
"sinon": "6.3.1", | ||
"yargs": "12.0.2" | ||
}, | ||
"standard": { | ||
"globals": [ | ||
"describe", | ||
"it", | ||
"before", | ||
"after", | ||
"beforeEach", | ||
"afterEach" | ||
], | ||
"ignore": [ | ||
"lib/" | ||
] | ||
}, | ||
"nyc": { | ||
@@ -73,0 +60,0 @@ "exclude": [ |
@@ -171,3 +171,3 @@ # @nrk/nodecache-as-promised | ||
// imiplicit set cache on miss, or use cached value | ||
// implicit set cache on miss, or use cached value | ||
cache.get('key', { worker: () => Promise.resolve({hello: 'world'}) }) | ||
@@ -174,0 +174,0 @@ .then((data) => { |
@@ -1,14 +0,13 @@ | ||
/* eslint max-nested-callbacks: 0 */ | ||
import inMemoryCache, {distCache, persistentCache} from '../' | ||
import expect from 'expect.js' | ||
import sinon from 'sinon' | ||
import {dummyLog} from '../utils/log-helper' | ||
import inMemoryCache, { distCache, persistentCache } from '../'; | ||
import expect from 'expect.js'; | ||
import sinon from 'sinon'; | ||
import { dummyLog } from '../utils/log-helper'; | ||
const dummyKey = 'hei/verden' | ||
const dummyKey = 'hei/verden'; | ||
const cacheValue = { | ||
keyNamespace: 'valueAsString' | ||
} | ||
}; | ||
const preCached = { | ||
[dummyKey]: cacheValue | ||
} | ||
}; | ||
@@ -18,24 +17,24 @@ describe('CacheManager', () => { | ||
it('should create a new empty instance', () => { | ||
const cacheInstance = inMemoryCache({}, {}) | ||
expect(cacheInstance).to.be.a(Object) | ||
const info = cacheInstance.debug() | ||
expect(info.itemCount).to.equal(0) | ||
}) | ||
const cacheInstance = inMemoryCache({}, {}); | ||
expect(cacheInstance).to.be.a(Object); | ||
const info = cacheInstance.debug(); | ||
expect(info.itemCount).to.equal(0); | ||
}); | ||
it('should have exported plugins', () => { | ||
expect(distCache).to.be.a('function') | ||
expect(persistentCache).to.be.a('function') | ||
}) | ||
expect(distCache).to.be.a('function'); | ||
expect(persistentCache).to.be.a('function'); | ||
}); | ||
it('should create a new prefilled instance with a cloned copy', () => { | ||
const obj = {hei: 'verden'} | ||
const cacheInstance = inMemoryCache({initial: obj}) | ||
obj.hei = 'world' | ||
expect(cacheInstance).to.be.a(Object) | ||
const info = cacheInstance.debug() | ||
expect(info.itemCount).to.equal(1) | ||
expect(cacheInstance.get('hei').value).to.equal('verden') | ||
expect(cacheInstance.get('hei').cache).to.equal('hit') | ||
}) | ||
}) | ||
const obj = { hei: 'verden' }; | ||
const cacheInstance = inMemoryCache({ initial: obj }); | ||
obj.hei = 'world'; | ||
expect(cacheInstance).to.be.a(Object); | ||
const info = cacheInstance.debug(); | ||
expect(info.itemCount).to.equal(1); | ||
expect(cacheInstance.get('hei').value).to.equal('verden'); | ||
expect(cacheInstance.get('hei').cache).to.equal('hit'); | ||
}); | ||
}); | ||
@@ -45,388 +44,423 @@ describe('debug', () => { | ||
// more thorough testing of debug in debug.spec.js | ||
const cacheInstance = inMemoryCache({initial: {hello: 'world'}}) | ||
const info = cacheInstance.debug({extraData: 'values'}) | ||
expect(info.extraData).to.equal('values') | ||
}) | ||
}) | ||
const cacheInstance = inMemoryCache({ initial: { hello: 'world' } }); | ||
const info = cacheInstance.debug({ extraData: 'values' }); | ||
expect(info.extraData).to.equal('values'); | ||
}); | ||
}); | ||
describe('-> hot cache', () => { | ||
let cacheInstance | ||
let spy | ||
let cacheInstance; | ||
let spy; | ||
beforeEach(() => { | ||
cacheInstance = inMemoryCache({initial: preCached}) | ||
const p = () => Promise.resolve() | ||
spy = sinon.spy(p) | ||
}) | ||
cacheInstance = inMemoryCache({ initial: preCached }); | ||
const p = () => Promise.resolve(); | ||
spy = sinon.spy(p); | ||
}); | ||
it('should return cached content if not stale', () => { | ||
return cacheInstance.get(dummyKey, {worker: spy}).then((obj) => { | ||
expect(obj.value).to.eql(cacheValue) | ||
expect(obj.cache).to.equal('hit') | ||
expect(spy.called).to.equal(false) | ||
}) | ||
}) | ||
}) | ||
return cacheInstance.get(dummyKey, { worker: spy }).then((obj) => { | ||
expect(obj.value).to.eql(cacheValue); | ||
expect(obj.cache).to.equal('hit'); | ||
expect(spy.called).to.equal(false); | ||
}); | ||
}); | ||
}); | ||
describe('-> has/del/clear', () => { | ||
let cacheInstance | ||
let cacheInstance; | ||
beforeEach(() => { | ||
cacheInstance = inMemoryCache({initial: preCached}) | ||
}) | ||
cacheInstance = inMemoryCache({ initial: preCached }); | ||
}); | ||
it('should return true if key exists in cache', () => { | ||
cacheInstance.set('key', 'value') | ||
expect(cacheInstance.has('key')).to.equal(true) | ||
}) | ||
cacheInstance.set('key', 'value'); | ||
expect(cacheInstance.has('key')).to.equal(true); | ||
}); | ||
it('should return false if key is not in cache', () => { | ||
expect(cacheInstance.has('key')).to.equal(false) | ||
}) | ||
expect(cacheInstance.has('key')).to.equal(false); | ||
}); | ||
it('should return false if key was deleted from cache', () => { | ||
cacheInstance.set('key', 'value') | ||
cacheInstance.del('key') | ||
expect(cacheInstance.has('key')).to.equal(false) | ||
}) | ||
cacheInstance.set('key', 'value'); | ||
cacheInstance.del('key'); | ||
expect(cacheInstance.has('key')).to.equal(false); | ||
}); | ||
it('should return false if key was deleted from cache', () => { | ||
cacheInstance.set('key1', 'value') | ||
cacheInstance.set('key2', 'value') | ||
cacheInstance.clear() | ||
expect(cacheInstance.has('key1')).to.equal(false) | ||
expect(cacheInstance.has('key2')).to.equal(false) | ||
}) | ||
}) | ||
cacheInstance.set('key1', 'value'); | ||
cacheInstance.set('key2', 'value'); | ||
cacheInstance.clear(); | ||
expect(cacheInstance.has('key1')).to.equal(false); | ||
expect(cacheInstance.has('key2')).to.equal(false); | ||
}); | ||
}); | ||
describe('-> cold/stale cache', () => { | ||
let cacheInstance | ||
let spy | ||
let now | ||
let cacheInstance; | ||
let spy; | ||
let now; | ||
beforeEach(() => { | ||
cacheInstance = inMemoryCache({initial: preCached}) | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000) | ||
now = Date.now() | ||
spy = sinon.spy(() => new Promise((resolve) => { | ||
setTimeout(() => resolve(now), 10) | ||
})) | ||
}) | ||
cacheInstance = inMemoryCache({ initial: preCached }); | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000); | ||
now = Date.now(); | ||
spy = sinon.spy( | ||
() => | ||
new Promise((resolve) => { | ||
setTimeout(() => resolve(now), 10); | ||
}) | ||
); | ||
}); | ||
it('should return promised content when key is not present', () => { | ||
return cacheInstance.get('N/A', {worker: spy}).then((obj) => { | ||
expect(obj.value).to.eql(now) | ||
expect(obj.cache).to.equal('miss') | ||
expect(spy.called).to.equal(true) | ||
}) | ||
}) | ||
return cacheInstance.get('N/A', { worker: spy }).then((obj) => { | ||
expect(obj.value).to.eql(now); | ||
expect(obj.cache).to.equal('miss'); | ||
expect(spy.called).to.equal(true); | ||
}); | ||
}); | ||
it('should return synchronous get when no worker is given', () => { | ||
// miss | ||
const obj = cacheInstance.get('N/A') | ||
expect(obj).to.equal(null) | ||
const obj = cacheInstance.get('N/A'); | ||
expect(obj).to.equal(null); | ||
// stale | ||
const obj2 = cacheInstance.get(dummyKey) | ||
expect(obj2.value).to.eql(cacheValue) | ||
expect(obj2.cache).to.equal('stale') | ||
const obj2 = cacheInstance.get(dummyKey); | ||
expect(obj2.value).to.eql(cacheValue); | ||
expect(obj2.cache).to.equal('stale'); | ||
// hot | ||
cacheInstance.set('hello', {yoman: 'world'}) | ||
const obj3 = cacheInstance.get('hello') | ||
expect(obj3.value).to.eql({yoman: 'world'}) | ||
expect(obj3.cache).to.equal('hit') | ||
}) | ||
cacheInstance.set('hello', { yoman: 'world' }); | ||
const obj3 = cacheInstance.get('hello'); | ||
expect(obj3.value).to.eql({ yoman: 'world' }); | ||
expect(obj3.cache).to.equal('hit'); | ||
}); | ||
it('should return promised content if cache is stale', () => { | ||
return cacheInstance.get(dummyKey, {worker: spy}).then((obj) => { | ||
expect(obj.value).to.eql(now) | ||
expect(obj.cache).to.equal('miss') | ||
expect(spy.called).to.equal(true) | ||
}) | ||
}) | ||
}) | ||
return cacheInstance.get(dummyKey, { worker: spy }).then((obj) => { | ||
expect(obj.value).to.eql(now); | ||
expect(obj.cache).to.equal('miss'); | ||
expect(spy.called).to.equal(true); | ||
}); | ||
}); | ||
}); | ||
describe('-> worker queue', () => { | ||
let cacheInstance | ||
let spy | ||
let now | ||
let cacheInstance; | ||
let spy; | ||
let now; | ||
beforeEach(() => { | ||
cacheInstance = inMemoryCache({initial: preCached}) | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000) | ||
now = Date.now() | ||
spy = sinon.spy(() => new Promise((resolve) => { | ||
setTimeout(() => resolve(now), 10) | ||
})) | ||
}) | ||
cacheInstance = inMemoryCache({ initial: preCached }); | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000); | ||
now = Date.now(); | ||
spy = sinon.spy( | ||
() => | ||
new Promise((resolve) => { | ||
setTimeout(() => resolve(now), 10); | ||
}) | ||
); | ||
}); | ||
it('should run only one promise, while two requests asks for data from cold cache concurrently', () => { | ||
return Promise.all([ | ||
cacheInstance.get(dummyKey, {worker: spy}), | ||
cacheInstance.get(dummyKey, {worker: spy}) | ||
cacheInstance.get(dummyKey, { worker: spy }), | ||
cacheInstance.get(dummyKey, { worker: spy }) | ||
]).then(([val1, val2]) => { | ||
expect(val1.value).to.eql(val2.value) | ||
expect(spy.callCount).to.equal(1) | ||
expect(val1.cache).to.equal('miss') | ||
expect(val2.cache).to.equal('hit') | ||
}) | ||
}) | ||
}) | ||
expect(val1.value).to.eql(val2.value); | ||
expect(spy.callCount).to.equal(1); | ||
expect(val1.cache).to.equal('miss'); | ||
expect(val2.cache).to.equal('hit'); | ||
}); | ||
}); | ||
}); | ||
describe('-> error handling (timeouts)', () => { | ||
let cacheInstance | ||
let cacheInstance; | ||
beforeEach(() => { | ||
cacheInstance = inMemoryCache({initial: preCached, log: dummyLog}) | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000) | ||
}) | ||
cacheInstance = inMemoryCache({ initial: preCached, log: dummyLog }); | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000); | ||
}); | ||
it('should return stale cache and increase wait if promise reaches timeout', () => { | ||
const timeoutSpy = sinon.spy(() => new Promise((resolve) => { | ||
setTimeout(() => resolve('another object'), 1000) | ||
})) | ||
const info = cacheInstance.debug() | ||
expect(info.waiting.get(dummyKey)).to.be.a('undefined') | ||
const timeoutSpy = sinon.spy( | ||
() => | ||
new Promise((resolve) => { | ||
setTimeout(() => resolve('another object'), 1000); | ||
}) | ||
); | ||
const info = cacheInstance.debug(); | ||
expect(info.waiting.get(dummyKey)).to.be.a('undefined'); | ||
return cacheInstance.get(dummyKey, { workerTimeout: 0, worker: timeoutSpy }).then((obj) => { | ||
expect(timeoutSpy.called).to.equal(true) | ||
expect(info.waiting.get(dummyKey)).not.to.equal(0) | ||
expect(obj.value).to.eql(cacheValue) | ||
expect(obj.cache).to.equal('stale') | ||
}) | ||
}) | ||
expect(timeoutSpy.called).to.equal(true); | ||
expect(info.waiting.get(dummyKey)).not.to.equal(0); | ||
expect(obj.value).to.eql(cacheValue); | ||
expect(obj.cache).to.equal('stale'); | ||
}); | ||
}); | ||
it('should reject if cache is cold and a timeout occurs', (done) => { | ||
const timeoutSpy = sinon.spy(() => new Promise((resolve) => { | ||
setTimeout(() => resolve('another object'), 1000) | ||
})) | ||
cacheInstance = inMemoryCache({log: dummyLog}) | ||
cacheInstance.get(dummyKey, {workerTimeout: 0, worker: timeoutSpy}) | ||
.catch((err) => { | ||
expect(timeoutSpy.called).to.equal(true) | ||
expect(err).to.be.an(Error) | ||
done() | ||
}) | ||
}) | ||
const timeoutSpy = sinon.spy( | ||
() => | ||
new Promise((resolve) => { | ||
setTimeout(() => resolve('another object'), 1000); | ||
}) | ||
); | ||
cacheInstance = inMemoryCache({ log: dummyLog }); | ||
cacheInstance.get(dummyKey, { workerTimeout: 0, worker: timeoutSpy }).catch((err) => { | ||
expect(timeoutSpy.called).to.equal(true); | ||
expect(err).to.be.an(Error); | ||
done(); | ||
}); | ||
}); | ||
it('should re-run promise after deltaWait time has passed', (done) => { | ||
const timeoutSpy = sinon.spy(() => new Promise((resolve) => { | ||
setTimeout(() => resolve('another object'), 1000) | ||
})) | ||
const resolveSpy = sinon.spy(() => Promise.resolve('hei verden')) | ||
const timeoutSpy = sinon.spy( | ||
() => | ||
new Promise((resolve) => { | ||
setTimeout(() => resolve('another object'), 1000); | ||
}) | ||
); | ||
const resolveSpy = sinon.spy(() => Promise.resolve('hei verden')); | ||
const conf = { | ||
deltaWait: 10, | ||
workerTimeout: 10 | ||
} | ||
cacheInstance.get(dummyKey, {...conf, worker: timeoutSpy}).then((obj) => { | ||
// 1. should return stale cache when timeout occurs | ||
expect(obj.value).to.eql(cacheValue) | ||
const info = cacheInstance.debug() | ||
expect(info.waiting.get(dummyKey).wait).to.equal(10) | ||
return cacheInstance.get(dummyKey, {...conf, worker: resolveSpy}).then((obj) => { | ||
// 2. should return stale cache before wait period has finished | ||
expect(obj.cache).to.equal('stale') | ||
expect(obj.value).to.eql(cacheValue) | ||
setTimeout(() => { | ||
return cacheInstance.get(dummyKey, {...conf, worker: resolveSpy}).then((obj) => { | ||
// 3. should return fresh data when wait period has finished | ||
expect(obj.value).to.eql('hei verden') | ||
expect(obj.cache).to.equal('miss') | ||
done() | ||
}) | ||
}, 10) | ||
}; | ||
cacheInstance | ||
.get(dummyKey, { ...conf, worker: timeoutSpy }) | ||
.then((obj) => { | ||
// 1. should return stale cache when timeout occurs | ||
expect(obj.value).to.eql(cacheValue); | ||
const info = cacheInstance.debug(); | ||
expect(info.waiting.get(dummyKey).wait).to.equal(10); | ||
return cacheInstance.get(dummyKey, { ...conf, worker: resolveSpy }).then((obj) => { | ||
// 2. should return stale cache before wait period has finished | ||
expect(obj.cache).to.equal('stale'); | ||
expect(obj.value).to.eql(cacheValue); | ||
setTimeout(() => { | ||
return cacheInstance.get(dummyKey, { ...conf, worker: resolveSpy }).then((obj) => { | ||
// 3. should return fresh data when wait period has finished | ||
expect(obj.value).to.eql('hei verden'); | ||
expect(obj.cache).to.equal('miss'); | ||
done(); | ||
}); | ||
}, 10); | ||
}); | ||
}) | ||
}).catch(done) | ||
}) | ||
}) | ||
.catch(done); | ||
}); | ||
}); | ||
describe('-> error handling (rejections)', () => { | ||
let cacheInstance | ||
let cacheInstance; | ||
beforeEach(() => { | ||
cacheInstance = inMemoryCache({initial: preCached, log: dummyLog}) | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000) | ||
}) | ||
cacheInstance = inMemoryCache({ initial: preCached, log: dummyLog }); | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000); | ||
}); | ||
it('should return stale cache and set wait if a promise rejection occurs', () => { | ||
const errorLogSpy = sinon.spy() | ||
const rejectionSpy = sinon.spy(() => Promise.reject(new Error('an error occurred'))) | ||
cacheInstance = inMemoryCache({initial: preCached, log: {...dummyLog, error: errorLogSpy}}) | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000) | ||
const info = cacheInstance.debug() | ||
expect(info.waiting.get(dummyKey)).to.be.a('undefined') | ||
return cacheInstance.get(dummyKey, {worker: rejectionSpy}).then((obj) => { | ||
expect(rejectionSpy.called).to.equal(true) | ||
expect(info.waiting.get(dummyKey)).not.to.equal(0) | ||
expect(obj.value).to.eql(cacheValue) | ||
expect(obj.cache).to.equal('stale') | ||
expect(errorLogSpy.called).to.equal(true) | ||
}) | ||
}) | ||
const errorLogSpy = sinon.spy(); | ||
const rejectionSpy = sinon.spy(() => Promise.reject(new Error('an error occurred'))); | ||
cacheInstance = inMemoryCache({ | ||
initial: preCached, | ||
log: { ...dummyLog, error: errorLogSpy } | ||
}); | ||
cacheInstance.set(dummyKey, cacheInstance.get(dummyKey).value, -1000); | ||
const info = cacheInstance.debug(); | ||
expect(info.waiting.get(dummyKey)).to.be.a('undefined'); | ||
return cacheInstance.get(dummyKey, { worker: rejectionSpy }).then((obj) => { | ||
expect(rejectionSpy.called).to.equal(true); | ||
expect(info.waiting.get(dummyKey)).not.to.equal(0); | ||
expect(obj.value).to.eql(cacheValue); | ||
expect(obj.cache).to.equal('stale'); | ||
expect(errorLogSpy.called).to.equal(true); | ||
}); | ||
}); | ||
it('should reject if cache is cold and a rejection occurs', (done) => { | ||
const errorLogSpy = sinon.spy() | ||
cacheInstance = inMemoryCache({log: {...dummyLog, error: errorLogSpy}}) | ||
const rejectionSpy = sinon.spy(() => Promise.reject(new Error('an error occurred'))) | ||
cacheInstance.get(dummyKey, {worker: rejectionSpy}).catch((err) => { | ||
expect(rejectionSpy.called).to.equal(true) | ||
expect(errorLogSpy.called).to.equal(true) | ||
expect(err).to.be.an(Error) | ||
done() | ||
}) | ||
}) | ||
const errorLogSpy = sinon.spy(); | ||
cacheInstance = inMemoryCache({ log: { ...dummyLog, error: errorLogSpy } }); | ||
const rejectionSpy = sinon.spy(() => Promise.reject(new Error('an error occurred'))); | ||
cacheInstance.get(dummyKey, { worker: rejectionSpy }).catch((err) => { | ||
expect(rejectionSpy.called).to.equal(true); | ||
expect(errorLogSpy.called).to.equal(true); | ||
expect(err).to.be.an(Error); | ||
done(); | ||
}); | ||
}); | ||
it('should reject if an Error is thrown', (done) => { | ||
const rejectionSpy = sinon.spy(() => { | ||
throw new Error('an error occurred') | ||
}) | ||
cacheInstance.get(dummyKey, {worker: rejectionSpy}).catch((err) => { | ||
expect(rejectionSpy.called).to.equal(true) | ||
expect(err).to.be.an(Error) | ||
done() | ||
}) | ||
}) | ||
throw new Error('an error occurred'); | ||
}); | ||
cacheInstance.get(dummyKey, { worker: rejectionSpy }).catch((err) => { | ||
expect(rejectionSpy.called).to.equal(true); | ||
expect(err).to.be.an(Error); | ||
done(); | ||
}); | ||
}); | ||
it('should re-run promise after deltaWait time has passed (when failing caused by a rejection)', (done) => { | ||
const rejectionSpy = sinon.spy(() => Promise.reject(new Error(''))) | ||
const resolveSpy = sinon.spy(() => Promise.resolve('hei verden')) | ||
const rejectionSpy = sinon.spy(() => Promise.reject(new Error(''))); | ||
const resolveSpy = sinon.spy(() => Promise.resolve('hei verden')); | ||
const conf = { | ||
deltaWait: 10 | ||
} | ||
cacheInstance.get(dummyKey, {...conf, worker: rejectionSpy}).then((obj) => { | ||
// 1. should return stale cache when rejection occurs | ||
expect(obj.value).to.eql(cacheValue) | ||
return cacheInstance.get(dummyKey, {...conf, worker: resolveSpy}).then((obj) => { | ||
// 2. should return stale cache before wait period has finished | ||
expect(obj.value).to.eql(cacheValue) | ||
expect(obj.cache).to.equal('stale') | ||
setTimeout(() => { | ||
return cacheInstance.get(dummyKey, {...conf, worker: resolveSpy}).then((obj) => { | ||
// 3. should return fresh data when wait period has finished | ||
expect(obj.value).to.eql('hei verden') | ||
expect(obj.cache).to.equal('miss') | ||
done() | ||
}) | ||
}, 10) | ||
}; | ||
cacheInstance | ||
.get(dummyKey, { ...conf, worker: rejectionSpy }) | ||
.then((obj) => { | ||
// 1. should return stale cache when rejection occurs | ||
expect(obj.value).to.eql(cacheValue); | ||
return cacheInstance.get(dummyKey, { ...conf, worker: resolveSpy }).then((obj) => { | ||
// 2. should return stale cache before wait period has finished | ||
expect(obj.value).to.eql(cacheValue); | ||
expect(obj.cache).to.equal('stale'); | ||
setTimeout(() => { | ||
return cacheInstance.get(dummyKey, { ...conf, worker: resolveSpy }).then((obj) => { | ||
// 3. should return fresh data when wait period has finished | ||
expect(obj.value).to.eql('hei verden'); | ||
expect(obj.cache).to.equal('miss'); | ||
done(); | ||
}); | ||
}, 10); | ||
}); | ||
}) | ||
}).catch(done) | ||
}) | ||
.catch(done); | ||
}); | ||
it('should re-run promise after deltaWait time has passed (when failing caused by a rejection and cache is cold)', (done) => { | ||
const rejectionSpy = sinon.spy(() => Promise.reject(new Error(''))) | ||
const rejectionSpy = sinon.spy(() => Promise.reject(new Error(''))); | ||
const conf = { | ||
deltaWait: 10 | ||
} | ||
cacheInstance.get('N/A', {...conf, worker: rejectionSpy}).catch((err) => { | ||
expect(err).to.be.an(Error) | ||
expect(rejectionSpy.callCount).to.equal(1) | ||
cacheInstance.get('N/A', {...conf, worker: rejectionSpy}).catch((err) => { | ||
expect(err).to.be.an(Error) | ||
expect(rejectionSpy.callCount).to.equal(1) | ||
cacheInstance.set('N/A', 'hei verden') | ||
const info = cacheInstance.debug() | ||
info.waiting.delete('N/A') | ||
setTimeout(() => { | ||
return cacheInstance.get('N/A', {...conf, worker: rejectionSpy}).then((obj) => { | ||
expect(rejectionSpy.callCount).to.equal(1) | ||
expect(obj.value).to.eql('hei verden') | ||
expect(obj.cache).to.equal('hit') | ||
done() | ||
}) | ||
}, 10) | ||
}; | ||
cacheInstance | ||
.get('N/A', { ...conf, worker: rejectionSpy }) | ||
.catch((err) => { | ||
expect(err).to.be.an(Error); | ||
expect(rejectionSpy.callCount).to.equal(1); | ||
cacheInstance.get('N/A', { ...conf, worker: rejectionSpy }).catch((err) => { | ||
expect(err).to.be.an(Error); | ||
expect(rejectionSpy.callCount).to.equal(1); | ||
cacheInstance.set('N/A', 'hei verden'); | ||
const info = cacheInstance.debug(); | ||
info.waiting.delete('N/A'); | ||
setTimeout(() => { | ||
return cacheInstance.get('N/A', { ...conf, worker: rejectionSpy }).then((obj) => { | ||
expect(rejectionSpy.callCount).to.equal(1); | ||
expect(obj.value).to.eql('hei verden'); | ||
expect(obj.cache).to.equal('hit'); | ||
done(); | ||
}); | ||
}, 10); | ||
}); | ||
}) | ||
}).catch(done) | ||
}) | ||
.catch(done); | ||
}); | ||
it('should increase deltaWait after several re-runs', (done) => { | ||
const rejectionSpy = sinon.spy(() => Promise.reject(new Error(''))) | ||
const rejectionSpy = sinon.spy(() => Promise.reject(new Error(''))); | ||
const conf = { | ||
deltaWait: 10 | ||
} | ||
const info = cacheInstance.debug() | ||
expect(info.waiting.get('N/A')).to.be.a('undefined') | ||
cacheInstance.get('N/A', {...conf, worker: rejectionSpy}).catch((err) => { | ||
expect(err).to.be.an(Error) | ||
expect(rejectionSpy.callCount).to.equal(1) | ||
expect(info.waiting.get('N/A').wait).to.equal(10) | ||
const {started} = info.waiting.get('N/A') | ||
cacheInstance.get('N/A', {...conf, worker: rejectionSpy}).catch((err) => { | ||
expect(err).to.be.an(Error) | ||
expect(rejectionSpy.callCount).to.equal(1) | ||
expect(info.waiting.get('N/A')).to.eql({ | ||
started, | ||
wait: 10, | ||
waitUntil: started + 10 | ||
}) | ||
setTimeout(() => { | ||
return cacheInstance.get('N/A', {...conf, worker: rejectionSpy}).catch((err) => { | ||
expect(err).to.be.an(Error) | ||
expect(rejectionSpy.callCount).to.equal(2) | ||
expect(info.waiting.get('N/A').wait).to.equal(10) | ||
expect(info.waiting.get('N/A').started).not.to.equal(started) | ||
done() | ||
}) | ||
}, 10) | ||
}; | ||
const info = cacheInstance.debug(); | ||
expect(info.waiting.get('N/A')).to.be.a('undefined'); | ||
cacheInstance | ||
.get('N/A', { ...conf, worker: rejectionSpy }) | ||
.catch((err) => { | ||
expect(err).to.be.an(Error); | ||
expect(rejectionSpy.callCount).to.equal(1); | ||
expect(info.waiting.get('N/A').wait).to.equal(10); | ||
const { started } = info.waiting.get('N/A'); | ||
cacheInstance.get('N/A', { ...conf, worker: rejectionSpy }).catch((err) => { | ||
expect(err).to.be.an(Error); | ||
expect(rejectionSpy.callCount).to.equal(1); | ||
expect(info.waiting.get('N/A')).to.eql({ | ||
started, | ||
wait: 10, | ||
waitUntil: started + 10 | ||
}); | ||
setTimeout(() => { | ||
return cacheInstance.get('N/A', { ...conf, worker: rejectionSpy }).catch((err) => { | ||
expect(err).to.be.an(Error); | ||
expect(rejectionSpy.callCount).to.equal(2); | ||
expect(info.waiting.get('N/A').wait).to.equal(10); | ||
expect(info.waiting.get('N/A').started).not.to.equal(started); | ||
done(); | ||
}); | ||
}, 10); | ||
}); | ||
}) | ||
}).catch(done) | ||
}) | ||
}) | ||
.catch(done); | ||
}); | ||
}); | ||
describe('-> keys/values/entries', () => { | ||
let cacheInstance | ||
let cacheInstance; | ||
beforeEach(() => { | ||
cacheInstance = inMemoryCache({initial: { | ||
'house/1': {hei: 'verden1'}, | ||
'house/2': {hei: 'verden2'}, | ||
'guest/2': {hei: 'verden3'} | ||
}}) | ||
}) | ||
cacheInstance = inMemoryCache({ | ||
initial: { | ||
'house/1': { hei: 'verden1' }, | ||
'house/2': { hei: 'verden2' }, | ||
'guest/2': { hei: 'verden3' } | ||
} | ||
}); | ||
}); | ||
it('should return keys', () => { | ||
expect(cacheInstance.keys()).to.eql(['house/1', 'house/2', 'guest/2'].reverse()) | ||
}) | ||
expect(cacheInstance.keys()).to.eql(['house/1', 'house/2', 'guest/2'].reverse()); | ||
}); | ||
it('should return values', () => { | ||
expect(cacheInstance | ||
.values() | ||
.map(({value}) => value)) | ||
.to.eql([{hei: 'verden3'}, {hei: 'verden2'}, {hei: 'verden1'}]) | ||
}) | ||
expect(cacheInstance.values().map(({ value }) => value)).to.eql([ | ||
{ hei: 'verden3' }, | ||
{ hei: 'verden2' }, | ||
{ hei: 'verden1' } | ||
]); | ||
}); | ||
it('should return entries', () => { | ||
expect(Array.from(cacheInstance.entries()) | ||
.map(([key, {value}]) => { | ||
return {[key]: value} | ||
})).to.eql([ | ||
{'guest/2': {hei: 'verden3'}}, | ||
{'house/2': {hei: 'verden2'}}, | ||
{'house/1': {hei: 'verden1'}} | ||
]) | ||
}) | ||
}) | ||
expect( | ||
Array.from(cacheInstance.entries()).map(([key, { value }]) => { | ||
return { [key]: value }; | ||
}) | ||
).to.eql([ | ||
{ 'guest/2': { hei: 'verden3' } }, | ||
{ 'house/2': { hei: 'verden2' } }, | ||
{ 'house/1': { hei: 'verden1' } } | ||
]); | ||
}); | ||
}); | ||
describe('-> expire', () => { | ||
let cacheInstance | ||
let cacheInstance; | ||
beforeEach(() => { | ||
cacheInstance = inMemoryCache({initial: { | ||
'house/1': {hei: 'verden'}, | ||
'house/2': {hei: 'verden'}, | ||
'guest/2': {hei: 'verden'} | ||
}}) | ||
}) | ||
cacheInstance = inMemoryCache({ | ||
initial: { | ||
'house/1': { hei: 'verden' }, | ||
'house/2': { hei: 'verden' }, | ||
'guest/2': { hei: 'verden' } | ||
} | ||
}); | ||
}); | ||
it('should expire all house keys', () => { | ||
cacheInstance.expire(['house/*']) | ||
expect(cacheInstance.get('house/1').TTL).to.equal(0) | ||
expect(cacheInstance.get('house/2').TTL).to.equal(0) | ||
expect(cacheInstance.get('guest/2').TTL).not.to.equal(0) | ||
}) | ||
cacheInstance.expire(['house/*']); | ||
expect(cacheInstance.get('house/1').TTL).to.equal(0); | ||
expect(cacheInstance.get('house/2').TTL).to.equal(0); | ||
expect(cacheInstance.get('guest/2').TTL).not.to.equal(0); | ||
}); | ||
it('should expire given house keys', () => { | ||
cacheInstance.expire(['house/*', 'guest/2']) | ||
expect(cacheInstance.get('house/1').TTL).to.equal(0) | ||
expect(cacheInstance.get('house/2').TTL).to.equal(0) | ||
expect(cacheInstance.get('guest/2').TTL).to.equal(0) | ||
}) | ||
}) | ||
cacheInstance.expire(['house/*', 'guest/2']); | ||
expect(cacheInstance.get('house/1').TTL).to.equal(0); | ||
expect(cacheInstance.get('house/2').TTL).to.equal(0); | ||
expect(cacheInstance.get('guest/2').TTL).to.equal(0); | ||
}); | ||
}); | ||
@@ -437,24 +471,24 @@ describe('-> LRU capabilities', () => { | ||
initial: { | ||
'house/1': {hei: 'verden'}, | ||
'house/2': {hei: 'verden'}, | ||
'guest/3': {hei: 'verden'} | ||
'house/1': { hei: 'verden' }, | ||
'house/2': { hei: 'verden' }, | ||
'guest/3': { hei: 'verden' } | ||
}, | ||
maxLength: 2 | ||
}) | ||
const info = cacheInstance.debug() | ||
expect(info.itemCount).to.equal(2) | ||
expect(cacheInstance.keys()).to.eql(['guest/3', 'house/2']) | ||
}) | ||
}); | ||
const info = cacheInstance.debug(); | ||
expect(info.itemCount).to.equal(2); | ||
expect(cacheInstance.keys()).to.eql(['guest/3', 'house/2']); | ||
}); | ||
it('should call dispose on set operations when LRU cache evicts object', () => { | ||
const cacheInstance = inMemoryCache({maxLength: 2}) | ||
const spy = sinon.spy() | ||
cacheInstance.addDisposer(spy) | ||
cacheInstance.set('house/1', {hei: 'verden'}) | ||
cacheInstance.set('house/2', {hei: 'verden'}) | ||
cacheInstance.set('guest/3', {hei: 'verden'}) | ||
expect(spy.called).to.equal(true) | ||
const key = spy.args[0][0] | ||
const {created, ...callArgs} = spy.args[0][1] | ||
expect(key).to.equal('house/1') | ||
const cacheInstance = inMemoryCache({ maxLength: 2 }); | ||
const spy = sinon.spy(); | ||
cacheInstance.addDisposer(spy); | ||
cacheInstance.set('house/1', { hei: 'verden' }); | ||
cacheInstance.set('house/2', { hei: 'verden' }); | ||
cacheInstance.set('guest/3', { hei: 'verden' }); | ||
expect(spy.called).to.equal(true); | ||
const key = spy.args[0][0]; | ||
const { created, ...callArgs } = spy.args[0][1]; | ||
expect(key).to.equal('house/1'); | ||
expect(callArgs).to.eql({ | ||
@@ -464,31 +498,31 @@ TTL: 86400000, | ||
cache: 'hit' | ||
}) | ||
cacheInstance.removeDisposer(spy) | ||
cacheInstance.set('guest/4', {hei: 'verden'}) | ||
expect(spy.callCount).to.equal(1) | ||
const info = cacheInstance.debug() | ||
expect(info.itemCount).to.equal(2) | ||
expect(cacheInstance.keys()).to.eql(['guest/4', 'guest/3']) | ||
}) | ||
}); | ||
cacheInstance.removeDisposer(spy); | ||
cacheInstance.set('guest/4', { hei: 'verden' }); | ||
expect(spy.callCount).to.equal(1); | ||
const info = cacheInstance.debug(); | ||
expect(info.itemCount).to.equal(2); | ||
expect(cacheInstance.keys()).to.eql(['guest/4', 'guest/3']); | ||
}); | ||
it('should call dispose on del operations', () => { | ||
const cacheInstance = inMemoryCache({maxLength: 2}) | ||
const spy = sinon.spy() | ||
cacheInstance.addDisposer(spy) | ||
cacheInstance.set('house/1', {hei: 'verden'}) | ||
cacheInstance.del('house/1') | ||
expect(spy.called).to.equal(true) | ||
cacheInstance.removeDisposer(spy) | ||
}) | ||
const cacheInstance = inMemoryCache({ maxLength: 2 }); | ||
const spy = sinon.spy(); | ||
cacheInstance.addDisposer(spy); | ||
cacheInstance.set('house/1', { hei: 'verden' }); | ||
cacheInstance.del('house/1'); | ||
expect(spy.called).to.equal(true); | ||
cacheInstance.removeDisposer(spy); | ||
}); | ||
it('should call dispose on clear operations', () => { | ||
const cacheInstance = inMemoryCache({maxLength: 2}) | ||
const spy = sinon.spy() | ||
cacheInstance.addDisposer(spy) | ||
cacheInstance.set('house/1', {hei: 'verden'}) | ||
cacheInstance.clear() | ||
expect(spy.called).to.equal(true) | ||
cacheInstance.removeDisposer(spy) | ||
}) | ||
}) | ||
}) | ||
const cacheInstance = inMemoryCache({ maxLength: 2 }); | ||
const spy = sinon.spy(); | ||
cacheInstance.addDisposer(spy); | ||
cacheInstance.set('house/1', { hei: 'verden' }); | ||
cacheInstance.clear(); | ||
expect(spy.called).to.equal(true); | ||
cacheInstance.removeDisposer(spy); | ||
}); | ||
}); | ||
}); |
@@ -1,9 +0,9 @@ | ||
import distCache from '../' | ||
import inMemoryCache from '../..' | ||
import sinon from 'sinon' | ||
import expect from 'expect.js' | ||
import {mockRedisFactory} from '../../utils/mock-redis-factory' | ||
import {dummyLog} from '../../utils/log-helper' | ||
import distCache from '../'; | ||
import inMemoryCache from '../..'; | ||
import sinon from 'sinon'; | ||
import expect from 'expect.js'; | ||
import { mockRedisFactory } from '../../utils/mock-redis-factory'; | ||
import { dummyLog } from '../../utils/log-helper'; | ||
const namespace = 'desketoy8080' | ||
const namespace = 'desketoy8080'; | ||
@@ -13,7 +13,7 @@ describe('dist-expire', () => { | ||
it('should be possible', () => { | ||
const cache = inMemoryCache({log: dummyLog}) | ||
cache.use(distCache(mockRedisFactory(), namespace)) | ||
expect(cache).to.be.an(Object) | ||
}) | ||
}) | ||
const cache = inMemoryCache({ log: dummyLog }); | ||
cache.use(distCache(mockRedisFactory(), namespace)); | ||
expect(cache).to.be.an(Object); | ||
}); | ||
}); | ||
@@ -23,69 +23,79 @@ describe('debug', () => { | ||
// more thorough testing of debug in debug.spec.js | ||
const cache = inMemoryCache({initial: {hello: 'world'}}) | ||
cache.use(distCache(mockRedisFactory(), namespace)) | ||
const info = cache.debug({extraData: 'values'}) | ||
expect(info.extraData).to.equal('values') | ||
}) | ||
}) | ||
const cache = inMemoryCache({ initial: { hello: 'world' } }); | ||
cache.use(distCache(mockRedisFactory(), namespace)); | ||
const info = cache.debug({ extraData: 'values' }); | ||
expect(info.extraData).to.equal('values'); | ||
}); | ||
}); | ||
describe('-> inheritance', () => { | ||
it('should be able to use methods from extended class (using middleware)', () => { | ||
const cache = inMemoryCache({log: dummyLog}) | ||
cache.use(distCache(mockRedisFactory(), namespace)) | ||
const p = () => Promise.resolve() | ||
const spy = sinon.spy(p) | ||
cache.set('hello', 'world') | ||
return cache.get('hello', {worker: spy}).then((obj) => { | ||
expect(obj.value).to.equal('world') | ||
expect(spy.called).to.equal(false) | ||
}) | ||
}) | ||
}) | ||
const cache = inMemoryCache({ log: dummyLog }); | ||
cache.use(distCache(mockRedisFactory(), namespace)); | ||
const p = () => Promise.resolve(); | ||
const spy = sinon.spy(p); | ||
cache.set('hello', 'world'); | ||
return cache.get('hello', { worker: spy }).then((obj) => { | ||
expect(obj.value).to.equal('world'); | ||
expect(spy.called).to.equal(false); | ||
}); | ||
}); | ||
}); | ||
describe('-> distributed expire', () => { | ||
it('should expire content on expire', () => { | ||
const spy = sinon.spy(() => Promise.resolve('world2')) | ||
const cache = inMemoryCache({initial: {hello: 'world'}, log: dummyLog}) | ||
cache.use(distCache(mockRedisFactory(), namespace)) | ||
cache.expire(['hello']) | ||
expect(cache.get('hello').TTL).to.equal(0) | ||
return cache.get('hello', {worker: spy}).then((obj) => { | ||
expect(obj.value).to.equal('world2') | ||
expect(spy.called).to.equal(true) | ||
}) | ||
}) | ||
const spy = sinon.spy(() => Promise.resolve('world2')); | ||
const cache = inMemoryCache({ initial: { hello: 'world' }, log: dummyLog }); | ||
cache.use(distCache(mockRedisFactory(), namespace)); | ||
cache.expire(['hello']); | ||
expect(cache.get('hello').TTL).to.equal(0); | ||
return cache.get('hello', { worker: spy }).then((obj) => { | ||
expect(obj.value).to.equal('world2'); | ||
expect(spy.called).to.equal(true); | ||
}); | ||
}); | ||
it('should handle errors if data is non-parsable', () => { | ||
const cbs = [] | ||
const on = (event, cb) => cbs.push(cb) | ||
const onSpy = sinon.spy(on) | ||
const cbs = []; | ||
const on = (event, cb) => cbs.push(cb); | ||
const onSpy = sinon.spy(on); | ||
const pub = (ns, data) => { | ||
cbs.forEach((cb) => cb(ns, data)) | ||
} | ||
cbs.forEach((cb) => cb(ns, data)); | ||
}; | ||
const sub = (ns, cb) => { | ||
if (ns === namespace) { | ||
return cb(null, 'ok') | ||
return cb(null, 'ok'); | ||
} | ||
return cb(new Error('dummyerror'), null) | ||
} | ||
const publishSpy = sinon.spy(pub) | ||
const subscribeSpy = sinon.spy(sub) | ||
const cache = inMemoryCache({initial: {hello: 'world'}, log: dummyLog}) | ||
const callCount = dummyLog.error.callCount | ||
cache.use(distCache(mockRedisFactory({ | ||
on: onSpy, | ||
publish: publishSpy, | ||
subscribe: subscribeSpy | ||
}), namespace)) | ||
pub('asdf', '{') | ||
expect(dummyLog.error.callCount).to.equal(callCount + 1) | ||
const cache2 = inMemoryCache({initial: {hello: 'world'}, log: dummyLog}) | ||
cache2.use(distCache(mockRedisFactory({ | ||
on: onSpy, | ||
publish: publishSpy, | ||
subscribe: subscribeSpy | ||
}), 'dummy')) | ||
expect(dummyLog.error.callCount).to.equal(callCount + 2) | ||
}) | ||
}) | ||
}) | ||
return cb(new Error('dummyerror'), null); | ||
}; | ||
const publishSpy = sinon.spy(pub); | ||
const subscribeSpy = sinon.spy(sub); | ||
const cache = inMemoryCache({ initial: { hello: 'world' }, log: dummyLog }); | ||
const callCount = dummyLog.error.callCount; | ||
cache.use( | ||
distCache( | ||
mockRedisFactory({ | ||
on: onSpy, | ||
publish: publishSpy, | ||
subscribe: subscribeSpy | ||
}), | ||
namespace | ||
) | ||
); | ||
pub('asdf', '{'); | ||
expect(dummyLog.error.callCount).to.equal(callCount + 1); | ||
const cache2 = inMemoryCache({ initial: { hello: 'world' }, log: dummyLog }); | ||
cache2.use( | ||
distCache( | ||
mockRedisFactory({ | ||
on: onSpy, | ||
publish: publishSpy, | ||
subscribe: subscribeSpy | ||
}), | ||
'dummy' | ||
) | ||
); | ||
expect(dummyLog.error.callCount).to.equal(callCount + 2); | ||
}); | ||
}); | ||
}); |
/** | ||
* @module | ||
**/ | ||
import os from 'os' | ||
import os from 'os'; | ||
const EXPIRE_MESSAGE_TYPE = 'EXPIRE_MESSAGE_TYPE' | ||
const EXPIRE_MESSAGE_TYPE = 'EXPIRE_MESSAGE_TYPE'; | ||
@@ -19,4 +19,4 @@ /** | ||
export default (redisFactory, namespace) => (cacheInstance) => { | ||
const redisPub = redisFactory() | ||
const redisSubClient = redisFactory() | ||
const redisPub = redisFactory(); | ||
const redisSubClient = redisFactory(); | ||
@@ -31,11 +31,15 @@ /** | ||
try { | ||
const {type, message} = JSON.parse(data) | ||
const { type, message } = JSON.parse(data); | ||
if (type === EXPIRE_MESSAGE_TYPE) { | ||
cacheInstance.log.info(`expire cache for keys ${message.keys} using namespace ${namespace} on host ${os.hostname()}`) | ||
cacheInstance.expire(message.keys) | ||
cacheInstance.log.info( | ||
`expire cache for keys ${ | ||
message.keys | ||
} using namespace ${namespace} on host ${os.hostname()}` | ||
); | ||
cacheInstance.expire(message.keys); | ||
} | ||
} catch (e) { | ||
cacheInstance.log.error(`failed to parse message on ${namespace} - ${data}. Reason: ${e}`) | ||
cacheInstance.log.error(`failed to parse message on ${namespace} - ${data}. Reason: ${e}`); | ||
} | ||
} | ||
}; | ||
@@ -51,8 +55,10 @@ /** | ||
if (err) { | ||
return cacheInstance.log.error(`oh oh. Subscribing for redis#${namespace} failed`) | ||
return cacheInstance.log.error(`oh oh. Subscribing for redis#${namespace} failed`); | ||
} | ||
return cacheInstance.log.debug(`Subscribing for incoming messages from redis#${namespace}. Count: ${cnt}`) | ||
}) | ||
redisClient.on('message', onMessage) | ||
} | ||
return cacheInstance.log.debug( | ||
`Subscribing for incoming messages from redis#${namespace}. Count: ${cnt}` | ||
); | ||
}); | ||
redisClient.on('message', onMessage); | ||
}; | ||
@@ -70,11 +76,11 @@ /** | ||
} | ||
} | ||
redisPub.publish(namespace, JSON.stringify(message)) | ||
} | ||
}; | ||
redisPub.publish(namespace, JSON.stringify(message)); | ||
}; | ||
const debug = (extraData, next) => { | ||
return next({namespace, ...extraData}) | ||
} | ||
return next({ namespace, ...extraData }); | ||
}; | ||
setupSubscriber(redisSubClient, namespace) | ||
setupSubscriber(redisSubClient, namespace); | ||
@@ -84,3 +90,3 @@ return { | ||
debug | ||
} | ||
} | ||
}; | ||
}; |
241
src/index.js
@@ -5,3 +5,3 @@ /** | ||
**/ | ||
import cloneDeep from 'lodash/cloneDeep' | ||
import cloneDeep from 'lodash/cloneDeep'; | ||
import { | ||
@@ -16,24 +16,22 @@ // existsAndNotStale, | ||
isWaiting | ||
} from './utils/cache-helpers' | ||
import { | ||
getCacheInfo | ||
} from './utils/debug' | ||
import lruCache from 'lru-cache' | ||
} from './utils/cache-helpers'; | ||
import { getCacheInfo } from './utils/debug'; | ||
import lruCache from 'lru-cache'; | ||
// export plugins for convenience | ||
import dc from './dist-expire' | ||
import pc from './persistence' | ||
import dc from './dist-expire'; | ||
import pc from './persistence'; | ||
export const distCache = dc | ||
export const persistentCache = pc | ||
export const distCache = dc; | ||
export const persistentCache = pc; | ||
const DEFAULT_CACHE_EXPIRE = 24 * 60 * 60 * 1000 | ||
const DEFAULT_DELTA_WAIT = 10000 | ||
const DEFAULT_MAX_LENGTH = 1000 | ||
const DEFAULT_CACHE_EXPIRE = 24 * 60 * 60 * 1000; | ||
const DEFAULT_DELTA_WAIT = 10000; | ||
const DEFAULT_MAX_LENGTH = 1000; | ||
// max stale period | ||
const DEFAULT_MAX_AGE = 2 * DEFAULT_CACHE_EXPIRE | ||
const DEFAULT_MAX_AGE = 2 * DEFAULT_CACHE_EXPIRE; | ||
const CACHE_HIT = 'hit' | ||
const CACHE_MISS = 'miss' | ||
const CACHE_STALE = 'stale' | ||
const CACHE_HIT = 'hit'; | ||
const CACHE_MISS = 'miss'; | ||
const CACHE_STALE = 'stale'; | ||
@@ -46,26 +44,26 @@ /** | ||
/** | ||
* @description Creates a new in-memory cache. | ||
* @param {Object} options - an options object. | ||
* @param {function} options.log - logger interface. Expects standard methods: info, warn, error, debug, trace | ||
* @param {Object} options.initial - initial state, key/value based. | ||
* @param {number} options.maxLength - max LRU-size (object count) | ||
* @param {number} options.maxAge - max LRU-age (UX timestamp) | ||
* @returns {Object} facade | ||
* @returns {function} object.get method | ||
* @returns {function} object.set method | ||
* @returns {function} object.expire method | ||
* @returns {function} object.debug method | ||
**/ | ||
/** | ||
* @description Creates a new in-memory cache. | ||
* @param {Object} options - an options object. | ||
* @param {function} options.log - logger interface. Expects standard methods: info, warn, error, debug, trace | ||
* @param {Object} options.initial - initial state, key/value based. | ||
* @param {number} options.maxLength - max LRU-size (object count) | ||
* @param {number} options.maxAge - max LRU-age (UX timestamp) | ||
* @returns {Object} facade | ||
* @returns {function} object.get method | ||
* @returns {function} object.set method | ||
* @returns {function} object.expire method | ||
* @returns {function} object.debug method | ||
**/ | ||
export default (options = {}) => { | ||
const { | ||
log = console, // eslint-disable-line | ||
log = console, // eslint-disable-line | ||
initial = {}, | ||
maxLength = DEFAULT_MAX_LENGTH, | ||
maxAge = DEFAULT_MAX_AGE | ||
} = options | ||
} = options; | ||
let disposers = [] | ||
const jobs = new Map() | ||
const waiting = new Map() | ||
let disposers = []; | ||
const jobs = new Map(); | ||
const waiting = new Map(); | ||
const cache = lruCache({ | ||
@@ -75,5 +73,5 @@ max: maxLength, | ||
dispose: (key, value) => { | ||
disposers.forEach((disposer) => disposer(key, value)) | ||
disposers.forEach((disposer) => disposer(key, value)); | ||
} | ||
}) | ||
}); | ||
@@ -86,3 +84,3 @@ /** | ||
**/ | ||
const addDisposer = (cb) => disposers.push(cb) | ||
const addDisposer = (cb) => disposers.push(cb); | ||
@@ -95,3 +93,4 @@ /** | ||
**/ | ||
const removeDisposer = (cb) => (disposers = disposers.filter((disposer) => disposer && disposer !== cb)) | ||
const removeDisposer = (cb) => | ||
(disposers = disposers.filter((disposer) => disposer && disposer !== cb)); | ||
@@ -107,4 +106,4 @@ /** | ||
const set = (key, value, ttl = DEFAULT_CACHE_EXPIRE) => { | ||
cache.set(key, {...createEntry(value, ttl), cache: CACHE_HIT}, maxAge) | ||
} | ||
cache.set(key, { ...createEntry(value, ttl), cache: CACHE_HIT }, maxAge); | ||
}; | ||
@@ -118,4 +117,4 @@ /** | ||
const has = (key) => { | ||
return cache.has(key) | ||
} | ||
return cache.has(key); | ||
}; | ||
@@ -129,4 +128,4 @@ /** | ||
const del = (key) => { | ||
cache.del(key) | ||
} | ||
cache.del(key); | ||
}; | ||
@@ -139,4 +138,4 @@ /** | ||
const clear = () => { | ||
cache.reset() | ||
} | ||
cache.reset(); | ||
}; | ||
@@ -154,22 +153,22 @@ /** | ||
**/ | ||
const _createJob = ({key, worker, workerTimeout, ttl, deltaWait}) => { | ||
const observable = createObservable(worker, workerTimeout, log) | ||
const _createJob = ({ key, worker, workerTimeout, ttl, deltaWait }) => { | ||
const observable = createObservable(worker, workerTimeout, log); | ||
const onNext = (value) => { | ||
// update cache | ||
set(key, value, ttl) | ||
waiting.delete(key) | ||
jobs.delete(key) | ||
} | ||
set(key, value, ttl); | ||
waiting.delete(key); | ||
jobs.delete(key); | ||
}; | ||
const onError = (err) => { | ||
// handle error | ||
log.error(err) | ||
waiting.set(key, createWait(deltaWait)) | ||
jobs.delete(key) | ||
} | ||
log.error(err); | ||
waiting.set(key, createWait(deltaWait)); | ||
jobs.delete(key); | ||
}; | ||
const onComplete = () => { | ||
jobs.delete(key) | ||
} | ||
observable.subscribe(onNext, onError, onComplete) | ||
return observable | ||
} | ||
jobs.delete(key); | ||
}; | ||
observable.subscribe(onNext, onError, onComplete); | ||
return observable; | ||
}; | ||
@@ -187,21 +186,20 @@ /** | ||
**/ | ||
const _requestFromCache = ({worker, key, workerTimeout, ttl, deltaWait}) => { | ||
const obj = cache.get(key) | ||
const _requestFromCache = ({ worker, key, workerTimeout, ttl, deltaWait }) => { | ||
const obj = cache.get(key); | ||
if (!worker) { | ||
return (obj && isFresh(obj) && obj) || | ||
(obj && {...obj, cache: CACHE_STALE}) || | ||
null | ||
} if (obj && isFresh(obj) && !waiting.get(key)) { | ||
return Promise.resolve(obj) | ||
return (obj && isFresh(obj) && obj) || (obj && { ...obj, cache: CACHE_STALE }) || null; | ||
} | ||
if (obj && isFresh(obj) && !waiting.get(key)) { | ||
return Promise.resolve(obj); | ||
} else if (obj && isWaiting(waiting.get(key))) { | ||
return Promise.resolve({...obj, cache: CACHE_STALE}) | ||
return Promise.resolve({ ...obj, cache: CACHE_STALE }); | ||
} else if (isWaiting(waiting.get(key))) { | ||
return Promise.reject(waitingForError(key, waiting.get(key))) | ||
return Promise.reject(waitingForError(key, waiting.get(key))); | ||
} | ||
return new Promise((resolve, reject) => { | ||
let job = jobs.get(key) | ||
let cacheType = CACHE_HIT | ||
let job = jobs.get(key); | ||
let cacheType = CACHE_HIT; | ||
if (!job) { | ||
cacheType = CACHE_MISS | ||
cacheType = CACHE_MISS; | ||
job = _createJob({ | ||
@@ -213,13 +211,16 @@ key, | ||
deltaWait | ||
}) | ||
jobs.set(key, job) | ||
}); | ||
jobs.set(key, job); | ||
} | ||
job.subscribe((value) => { | ||
resolve({value, cache: cacheType}) | ||
}, (err) => { | ||
// serve stale object if it exists | ||
obj ? resolve({...obj, cache: CACHE_STALE}) : reject(err) | ||
}) | ||
}) | ||
} | ||
job.subscribe( | ||
(value) => { | ||
resolve({ value, cache: cacheType }); | ||
}, | ||
(err) => { | ||
// serve stale object if it exists | ||
obj ? resolve({ ...obj, cache: CACHE_STALE }) : reject(err); | ||
} | ||
); | ||
}); | ||
}; | ||
@@ -245,3 +246,3 @@ /** | ||
worker | ||
} = config | ||
} = config; | ||
return _requestFromCache({ | ||
@@ -253,4 +254,4 @@ worker, | ||
deltaWait | ||
}) | ||
} | ||
}); | ||
}; | ||
@@ -262,3 +263,3 @@ /** | ||
**/ | ||
const keys = () => cache.keys() | ||
const keys = () => cache.keys(); | ||
@@ -270,3 +271,3 @@ /** | ||
**/ | ||
const values = () => cache.values() | ||
const values = () => cache.values(); | ||
@@ -279,8 +280,10 @@ /** | ||
const entries = () => { | ||
const vals = values() | ||
return new Map(keys().reduce((acc, key, i) => { | ||
acc.push([key, vals[i]]) | ||
return acc | ||
}, [])) | ||
} | ||
const vals = values(); | ||
return new Map( | ||
keys().reduce((acc, key, i) => { | ||
acc.push([key, vals[i]]); | ||
return acc; | ||
}, []) | ||
); | ||
}; | ||
@@ -296,11 +299,9 @@ /** | ||
const search = createRegExp(key); | ||
[...cache.keys()] | ||
.filter((key) => search.test(key)) | ||
.forEach((key) => { | ||
const obj = cache.get(key) | ||
set(key, obj.value, 0) // TTL = 0 | ||
waiting.delete(key) | ||
}) | ||
}) | ||
} | ||
[...cache.keys()].filter((key) => search.test(key)).forEach((key) => { | ||
const obj = cache.get(key); | ||
set(key, obj.value, 0); // TTL = 0 | ||
waiting.delete(key); | ||
}); | ||
}); | ||
}; | ||
@@ -316,8 +317,8 @@ const debug = (extraData = {}) => { | ||
jobs | ||
}) | ||
} | ||
}); | ||
}; | ||
Object.keys(initial).forEach((key) => { | ||
set(key, cloneDeep(initial[key])) | ||
}) | ||
set(key, cloneDeep(initial[key])); | ||
}); | ||
@@ -340,12 +341,12 @@ const buildFacade = () => { | ||
log | ||
} | ||
} | ||
}; | ||
}; | ||
const facade = buildFacade() | ||
const facade = buildFacade(); | ||
facade.use = (middleware) => { | ||
const m = middleware(buildFacade()) | ||
const m = middleware(buildFacade()); | ||
Object.keys(m).forEach((key) => { | ||
// Keep a reference to the original function pointer | ||
const prevFacade = facade[key] | ||
const prevFacade = facade[key]; | ||
// overwrite/mutate the original function | ||
@@ -358,10 +359,10 @@ facade[key] = (...args) => { | ||
// call original function with args from middleware | ||
return prevFacade(...middlewareArgs) | ||
return prevFacade(...middlewareArgs); | ||
}) | ||
) | ||
} | ||
}) | ||
} | ||
); | ||
}; | ||
}); | ||
}; | ||
return facade | ||
} | ||
return facade; | ||
}; |
@@ -1,10 +0,10 @@ | ||
import persistentCache from '../' | ||
import inMemoryCache from '../../' | ||
import sinon from 'sinon' | ||
import expect from 'expect.js' | ||
import {mockRedisFactory} from '../../utils/mock-redis-factory' | ||
import * as utils from '../persistence-helpers' | ||
import {createEntry} from '../../utils/cache-helpers' | ||
import {dummyLog} from '../../utils/log-helper' | ||
import pkg from '../../../package.json' | ||
import persistentCache from '../'; | ||
import inMemoryCache from '../../'; | ||
import sinon from 'sinon'; | ||
import expect from 'expect.js'; | ||
import { mockRedisFactory } from '../../utils/mock-redis-factory'; | ||
import * as utils from '../persistence-helpers'; | ||
import { createEntry } from '../../utils/cache-helpers'; | ||
import { dummyLog } from '../../utils/log-helper'; | ||
import pkg from '../../../package.json'; | ||
@@ -14,7 +14,7 @@ describe('persistence', () => { | ||
it('should be possible', () => { | ||
const cache = inMemoryCache({log: dummyLog}) | ||
cache.use(persistentCache(mockRedisFactory(), {bootload: false})) | ||
expect(cache).to.be.an(Object) | ||
}) | ||
}) | ||
const cache = inMemoryCache({ log: dummyLog }); | ||
cache.use(persistentCache(mockRedisFactory(), { bootload: false })); | ||
expect(cache).to.be.an(Object); | ||
}); | ||
}); | ||
@@ -24,38 +24,38 @@ describe('debug', () => { | ||
// more thorough testing of debug in debug.spec.js | ||
const cache = inMemoryCache({initial: {hello: 'world'}}) | ||
cache.use(persistentCache(mockRedisFactory(), {bootload: false})) | ||
const info = cache.debug({extraData: 'values'}) | ||
expect(info.extraData).to.equal('values') | ||
}) | ||
}) | ||
const cache = inMemoryCache({ initial: { hello: 'world' } }); | ||
cache.use(persistentCache(mockRedisFactory(), { bootload: false })); | ||
const info = cache.debug({ extraData: 'values' }); | ||
expect(info.extraData).to.equal('values'); | ||
}); | ||
}); | ||
describe('-> bootload', () => { | ||
it('should set TTL based on elapsed time', () => { | ||
const now = Date.now() | ||
const diff = 10 | ||
const now = Date.now(); | ||
const diff = 10; | ||
const loadObjectsStub = sinon.stub(utils, 'loadObjects').resolves({ | ||
[`${pkg.name}-myCache-house/1`]: createEntry({hello: 'world'}, 1000, now) | ||
}) | ||
const cache = inMemoryCache({log: dummyLog}) | ||
let cacheInstance | ||
[`${pkg.name}-myCache-house/1`]: createEntry({ hello: 'world' }, 1000, now) | ||
}); | ||
const cache = inMemoryCache({ log: dummyLog }); | ||
let cacheInstance; | ||
cache.use((ci) => { | ||
cacheInstance = ci | ||
return persistentCache(mockRedisFactory(), {bootload: false})(ci) | ||
}) | ||
const setStub = sinon.stub(cacheInstance, 'set').returns('ok') | ||
cacheInstance = ci; | ||
return persistentCache(mockRedisFactory(), { bootload: false })(ci); | ||
}); | ||
const setStub = sinon.stub(cacheInstance, 'set').returns('ok'); | ||
return cache.load(now + diff).then(() => { | ||
expect(setStub.called).to.equal(true) | ||
expect(setStub.args[0][0]).to.equal(`${pkg.name}-myCache-house/1`) | ||
expect(setStub.args[0][1]).to.eql({hello: 'world'}) | ||
expect(setStub.args[0][2]).to.equal(1000 - diff) | ||
setStub.restore() | ||
loadObjectsStub.restore() | ||
}) | ||
}) | ||
}) | ||
expect(setStub.called).to.equal(true); | ||
expect(setStub.args[0][0]).to.equal(`${pkg.name}-myCache-house/1`); | ||
expect(setStub.args[0][1]).to.eql({ hello: 'world' }); | ||
expect(setStub.args[0][2]).to.equal(1000 - diff); | ||
setStub.restore(); | ||
loadObjectsStub.restore(); | ||
}); | ||
}); | ||
}); | ||
describe('-> get (write to redis on MISS)', () => { | ||
let cache | ||
let mockFactory | ||
let setSpy | ||
let cache; | ||
let mockFactory; | ||
let setSpy; | ||
@@ -65,83 +65,82 @@ beforeEach(() => { | ||
if (key.indexOf('setFail') > -1) { | ||
return cb(new Error('dummyerror'), null) | ||
return cb(new Error('dummyerror'), null); | ||
} | ||
cb(null, 'ok') | ||
}) | ||
cb(null, 'ok'); | ||
}); | ||
sinon.stub(utils, 'loadObjects').resolves({ | ||
[`${pkg.name}-myCache-house/1`]: createEntry({hello: 'world'}, 1000) | ||
}) | ||
[`${pkg.name}-myCache-house/1`]: createEntry({ hello: 'world' }, 1000) | ||
}); | ||
mockFactory = mockRedisFactory({ | ||
set: setSpy | ||
}) | ||
cache = inMemoryCache({log: dummyLog}) | ||
cache.use(persistentCache( | ||
mockFactory, | ||
{ | ||
}); | ||
cache = inMemoryCache({ log: dummyLog }); | ||
cache.use( | ||
persistentCache(mockFactory, { | ||
doNotPersist: /store/, | ||
keySpace: 'myCache', | ||
grace: 1000 | ||
} | ||
)) | ||
}) | ||
}) | ||
); | ||
}); | ||
afterEach(() => { | ||
cache.destroy() | ||
utils.loadObjects.restore() | ||
}) | ||
cache.destroy(); | ||
utils.loadObjects.restore(); | ||
}); | ||
it('should write to redis when a cache miss occurs', () => { | ||
const spy = sinon.spy(() => Promise.resolve('hei')) | ||
const now = Date.now() | ||
const key = `key${now}` | ||
return cache.get(key, {ttl: 1000, worker: spy}).then((obj) => { | ||
expect(spy.called).to.equal(true) | ||
expect(obj.value).to.equal('hei') | ||
expect(obj.cache).to.equal('miss') | ||
const [[redisKey, json, ex, expire]] = setSpy.args | ||
const parsed = JSON.parse(json) | ||
expect(redisKey).to.contain(key) | ||
expect(parsed).to.have.keys(['created', 'TTL', 'value', 'cache']) | ||
expect(ex).to.equal('ex') | ||
expect(expire).to.equal(2) | ||
expect(parsed.value).to.equal('hei') | ||
}) | ||
}) | ||
const spy = sinon.spy(() => Promise.resolve('hei')); | ||
const now = Date.now(); | ||
const key = `key${now}`; | ||
return cache.get(key, { ttl: 1000, worker: spy }).then((obj) => { | ||
expect(spy.called).to.equal(true); | ||
expect(obj.value).to.equal('hei'); | ||
expect(obj.cache).to.equal('miss'); | ||
const [[redisKey, json, ex, expire]] = setSpy.args; | ||
const parsed = JSON.parse(json); | ||
expect(redisKey).to.contain(key); | ||
expect(parsed).to.have.keys(['created', 'TTL', 'value', 'cache']); | ||
expect(ex).to.equal('ex'); | ||
expect(expire).to.equal(2); | ||
expect(parsed.value).to.equal('hei'); | ||
}); | ||
}); | ||
it('should log a warning when write to redis fails (on cache miss)', () => { | ||
const spy = sinon.spy(() => Promise.resolve('hei')) | ||
const key = 'setFail' | ||
const callCount = dummyLog.warn.callCount | ||
return cache.get(key, {ttl: 1000, worker: spy}).then((obj) => { | ||
expect(spy.called).to.equal(true) | ||
expect(obj.value).to.equal('hei') | ||
expect(obj.cache).to.equal('miss') | ||
const [[redisKey, json, ex, expire]] = setSpy.args | ||
const parsed = JSON.parse(json) | ||
expect(redisKey).to.contain(key) | ||
expect(parsed).to.have.keys(['created', 'TTL', 'value', 'cache']) | ||
expect(ex).to.equal('ex') | ||
expect(expire).to.equal(2) | ||
expect(parsed.value).to.equal('hei') | ||
expect(dummyLog.warn.callCount).to.equal(callCount + 1) | ||
}) | ||
}) | ||
const spy = sinon.spy(() => Promise.resolve('hei')); | ||
const key = 'setFail'; | ||
const callCount = dummyLog.warn.callCount; | ||
return cache.get(key, { ttl: 1000, worker: spy }).then((obj) => { | ||
expect(spy.called).to.equal(true); | ||
expect(obj.value).to.equal('hei'); | ||
expect(obj.cache).to.equal('miss'); | ||
const [[redisKey, json, ex, expire]] = setSpy.args; | ||
const parsed = JSON.parse(json); | ||
expect(redisKey).to.contain(key); | ||
expect(parsed).to.have.keys(['created', 'TTL', 'value', 'cache']); | ||
expect(ex).to.equal('ex'); | ||
expect(expire).to.equal(2); | ||
expect(parsed.value).to.equal('hei'); | ||
expect(dummyLog.warn.callCount).to.equal(callCount + 1); | ||
}); | ||
}); | ||
it('should not write to redis when a cache miss occurs and key matches ignored keys', () => { | ||
const spy = sinon.spy(() => Promise.resolve('hei')) | ||
const now = Date.now() | ||
const key = `/store/${now}` | ||
return cache.get(key, {worker: spy}).then((obj) => { | ||
expect(spy.called).to.equal(true) | ||
expect(obj.value).to.equal('hei') | ||
expect(obj.cache).to.equal('miss') | ||
expect(setSpy.called).to.equal(false) | ||
}) | ||
}) | ||
}) | ||
const spy = sinon.spy(() => Promise.resolve('hei')); | ||
const now = Date.now(); | ||
const key = `/store/${now}`; | ||
return cache.get(key, { worker: spy }).then((obj) => { | ||
expect(spy.called).to.equal(true); | ||
expect(obj.value).to.equal('hei'); | ||
expect(obj.cache).to.equal('miss'); | ||
expect(setSpy.called).to.equal(false); | ||
}); | ||
}); | ||
}); | ||
describe('onDispose', () => { | ||
let delSpy | ||
let setSpy | ||
let mockFactory | ||
let cache | ||
let delSpy; | ||
let setSpy; | ||
let mockFactory; | ||
let cache; | ||
@@ -151,15 +150,14 @@ beforeEach(() => { | ||
if (key.indexOf('house/1') > -1) { | ||
return cb(null, 'ok') | ||
return cb(null, 'ok'); | ||
} | ||
cb(new Error('dummyerror'), null) | ||
}) | ||
setSpy = sinon.spy(() => {}) | ||
cb(new Error('dummyerror'), null); | ||
}); | ||
setSpy = sinon.spy(() => {}); | ||
mockFactory = mockRedisFactory({ | ||
del: delSpy, | ||
set: setSpy | ||
}) | ||
cache = inMemoryCache({log: dummyLog, maxLength: 2}) | ||
cache.use(persistentCache( | ||
mockFactory, | ||
{ | ||
}); | ||
cache = inMemoryCache({ log: dummyLog, maxLength: 2 }); | ||
cache.use( | ||
persistentCache(mockFactory, { | ||
doNotPersist: /store/, | ||
@@ -169,43 +167,43 @@ bootload: false, | ||
grace: 1000 | ||
} | ||
)) | ||
}) | ||
}) | ||
); | ||
}); | ||
afterEach(() => { | ||
cache.destroy() | ||
}) | ||
cache.destroy(); | ||
}); | ||
it('should evict key from redis when lru cache evicts key', () => { | ||
cache.set('house/1', {hei: 'verden'}) | ||
cache.set('house/2', {hei: 'verden'}) | ||
cache.set('guest/3', {hei: 'verden'}) | ||
expect(setSpy.callCount).to.equal(3) | ||
expect(setSpy.args[0][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'house/1')) | ||
expect(setSpy.args[1][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'house/2')) | ||
expect(setSpy.args[2][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'guest/3')) | ||
expect(delSpy.called).to.equal(true) | ||
expect(delSpy.args[0][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'house/1')) | ||
}) | ||
cache.set('house/1', { hei: 'verden' }); | ||
cache.set('house/2', { hei: 'verden' }); | ||
cache.set('guest/3', { hei: 'verden' }); | ||
expect(setSpy.callCount).to.equal(3); | ||
expect(setSpy.args[0][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'house/1')); | ||
expect(setSpy.args[1][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'house/2')); | ||
expect(setSpy.args[2][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'guest/3')); | ||
expect(delSpy.called).to.equal(true); | ||
expect(delSpy.args[0][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'house/1')); | ||
}); | ||
it('should catch error in redis.del when lru cache evicts key', (done) => { | ||
const count = dummyLog.error.callCount | ||
cache.set('guest/3', {hei: 'verden'}) | ||
cache.set('house/1', {hei: 'verden'}) | ||
cache.set('house/2', {hei: 'verden'}) | ||
expect(setSpy.callCount).to.equal(3) | ||
expect(setSpy.args[0][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'guest/3')) | ||
expect(setSpy.args[1][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'house/1')) | ||
expect(setSpy.args[2][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'house/2')) | ||
expect(delSpy.called).to.equal(true) | ||
expect(delSpy.args[0][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'guest/3')) | ||
const count = dummyLog.error.callCount; | ||
cache.set('guest/3', { hei: 'verden' }); | ||
cache.set('house/1', { hei: 'verden' }); | ||
cache.set('house/2', { hei: 'verden' }); | ||
expect(setSpy.callCount).to.equal(3); | ||
expect(setSpy.args[0][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'guest/3')); | ||
expect(setSpy.args[1][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'house/1')); | ||
expect(setSpy.args[2][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'house/2')); | ||
expect(delSpy.called).to.equal(true); | ||
expect(delSpy.args[0][0]).to.equal(utils.getRedisKey(`${pkg.name}-myCache`, 'guest/3')); | ||
setTimeout(() => { | ||
expect(dummyLog.error.callCount).to.equal(count + 1) | ||
done() | ||
}) | ||
}) | ||
}) | ||
expect(dummyLog.error.callCount).to.equal(count + 1); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
describe('-> del/clear', () => { | ||
let delSpy | ||
let cache | ||
let delSpy; | ||
let cache; | ||
@@ -215,31 +213,31 @@ beforeEach(() => { | ||
if (key.indexOf('house/1') > -1) { | ||
return cb(null, 'ok') | ||
return cb(null, 'ok'); | ||
} | ||
cb(new Error('dummyerror'), null) | ||
}) | ||
cache = inMemoryCache({log: dummyLog}) | ||
cache.use(persistentCache(mockRedisFactory({del: delSpy}), {bootload: false})) | ||
}) | ||
cb(new Error('dummyerror'), null); | ||
}); | ||
cache = inMemoryCache({ log: dummyLog }); | ||
cache.use(persistentCache(mockRedisFactory({ del: delSpy }), { bootload: false })); | ||
}); | ||
it('should delete key from redis when a key is deleted from lru-cache', () => { | ||
return cache.del('house/1').then(() => { | ||
expect(delSpy.called).to.equal(true) | ||
}) | ||
}) | ||
expect(delSpy.called).to.equal(true); | ||
}); | ||
}); | ||
it('should throw an error key from redis when a key is deleted from lru-cache', () => { | ||
return cache.del('key').catch(() => { | ||
expect(delSpy.called).to.equal(true) | ||
}) | ||
}) | ||
expect(delSpy.called).to.equal(true); | ||
}); | ||
}); | ||
it('should delete all keys in redis with prefix', () => { | ||
sinon.stub(utils, 'deleteKeys').resolves() | ||
sinon.stub(utils, 'deleteKeys').resolves(); | ||
return cache.clear().then(() => { | ||
expect(utils.deleteKeys.called).to.equal(true) | ||
expect(utils.deleteKeys.args[0][0]).to.equal(`${pkg.name}-`) | ||
utils.deleteKeys.restore() | ||
}) | ||
}) | ||
}) | ||
}) | ||
expect(utils.deleteKeys.called).to.equal(true); | ||
expect(utils.deleteKeys.args[0][0]).to.equal(`${pkg.name}-`); | ||
utils.deleteKeys.restore(); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -9,20 +9,18 @@ import { | ||
loadObjects | ||
} from '../persistence-helpers' | ||
import { | ||
mockRedisFactory | ||
} from '../../utils/mock-redis-factory' | ||
import {dummyLog} from '../../utils/log-helper' | ||
import sinon from 'sinon' | ||
import expect from 'expect.js' | ||
} from '../persistence-helpers'; | ||
import { mockRedisFactory } from '../../utils/mock-redis-factory'; | ||
import { dummyLog } from '../../utils/log-helper'; | ||
import sinon from 'sinon'; | ||
import expect from 'expect.js'; | ||
const parsedCache = { | ||
'asdf-123': {hello: 'world1'}, | ||
'asdf-345': {hello: 'world2'}, | ||
'asdf-100': {hello: 'world3'} | ||
} | ||
'asdf-123': { hello: 'world1' }, | ||
'asdf-345': { hello: 'world2' }, | ||
'asdf-100': { hello: 'world3' } | ||
}; | ||
const redisCache = Object.keys(parsedCache).reduce((acc, key) => { | ||
acc[key] = JSON.stringify(parsedCache[key]) | ||
return acc | ||
}, {}) | ||
acc[key] = JSON.stringify(parsedCache[key]); | ||
return acc; | ||
}, {}); | ||
@@ -32,28 +30,31 @@ describe('persistence-helpers', () => { | ||
it('generate key', () => { | ||
const key = getRedisKey('prefix', 'key') | ||
expect(key).to.equal('prefix-key') | ||
}) | ||
}) | ||
const key = getRedisKey('prefix', 'key'); | ||
expect(key).to.equal('prefix-key'); | ||
}); | ||
}); | ||
describe('-> extractKeyFromRedis', () => { | ||
it('should match keys', () => { | ||
const key = extractKeyFromRedis('prefix-http://localhost:8080', 'prefix-http://localhost:8080-myKey') | ||
expect(key).to.equal('myKey') | ||
}) | ||
}) | ||
const key = extractKeyFromRedis( | ||
'prefix-http://localhost:8080', | ||
'prefix-http://localhost:8080-myKey' | ||
); | ||
expect(key).to.equal('myKey'); | ||
}); | ||
}); | ||
describe('-> loadObjects', () => { | ||
let redisClient | ||
let mgetSpy | ||
let redisClient; | ||
let mgetSpy; | ||
it('should load keys', () => { | ||
const events = {} | ||
const events = {}; | ||
setTimeout(() => { | ||
events.data[0](['test-localhost8080-myKey']) | ||
events.end[0]() | ||
}, 20) | ||
events.data[0](['test-localhost8080-myKey']); | ||
events.end[0](); | ||
}, 20); | ||
mgetSpy = sinon.spy((keysToRead, cb) => { | ||
cb(null, [JSON.stringify({hei: 'verden'})]) | ||
}) | ||
redisClient = mockRedisFactory({mget: mgetSpy}, {events})() | ||
cb(null, [JSON.stringify({ hei: 'verden' })]); | ||
}); | ||
redisClient = mockRedisFactory({ mget: mgetSpy }, { events })(); | ||
return loadObjects('test-localhost8080', redisClient, dummyLog).then((results) => { | ||
@@ -64,105 +65,105 @@ expect(results).to.eql({ | ||
} | ||
}) | ||
}) | ||
}) | ||
}); | ||
}); | ||
}); | ||
it('should handle errors when loading keys', () => { | ||
const events = {} | ||
const events = {}; | ||
setTimeout(() => { | ||
events.error[0](new Error('dummyerror')) | ||
}, 100) | ||
redisClient = mockRedisFactory({}, {events})() | ||
events.error[0](new Error('dummyerror')); | ||
}, 100); | ||
redisClient = mockRedisFactory({}, { events })(); | ||
return loadObjects('test-localhost8080', redisClient, dummyLog).catch((err) => { | ||
expect(err.message).to.equal('dummyerror') | ||
}) | ||
}) | ||
}) | ||
expect(err.message).to.equal('dummyerror'); | ||
}); | ||
}); | ||
}); | ||
describe('-> deleteKey', () => { | ||
let redisClient | ||
let delSpy | ||
let redisClient; | ||
let delSpy; | ||
it('should delete keys', () => { | ||
const p = (key, cb) => cb(null, 'ok') | ||
delSpy = sinon.spy(p) | ||
redisClient = mockRedisFactory({del: delSpy})() | ||
const p = (key, cb) => cb(null, 'ok'); | ||
delSpy = sinon.spy(p); | ||
redisClient = mockRedisFactory({ del: delSpy })(); | ||
return deleteKey('testkey', redisClient, dummyLog).then((result) => { | ||
expect(delSpy.called).to.equal(true) | ||
expect(result).to.equal('ok') | ||
}) | ||
}) | ||
expect(delSpy.called).to.equal(true); | ||
expect(result).to.equal('ok'); | ||
}); | ||
}); | ||
it('should reject if an error occurrs keys', () => { | ||
const p = (key, cb) => cb(new Error('not ok'), null) | ||
delSpy = sinon.spy(p) | ||
redisClient = mockRedisFactory({del: delSpy})() | ||
const p = (key, cb) => cb(new Error('not ok'), null); | ||
delSpy = sinon.spy(p); | ||
redisClient = mockRedisFactory({ del: delSpy })(); | ||
return deleteKey('testkey', redisClient, dummyLog).catch((err) => { | ||
expect(delSpy.called).to.equal(true) | ||
expect(err).to.be.an(Error) | ||
}) | ||
}) | ||
}) | ||
expect(delSpy.called).to.equal(true); | ||
expect(err).to.be.an(Error); | ||
}); | ||
}); | ||
}); | ||
describe('-> deleteKeys', () => { | ||
it('should delete all keys with prefix from redis', () => { | ||
const delSpy = sinon.spy((key, cb) => cb(null, 'ok')) | ||
const events = {} | ||
const delSpy = sinon.spy((key, cb) => cb(null, 'ok')); | ||
const events = {}; | ||
setTimeout(() => { | ||
events.data[0](['test-localhost8080-myKey']) | ||
events.end[0]() | ||
}, 100) | ||
events.data[0](['test-localhost8080-myKey']); | ||
events.end[0](); | ||
}, 100); | ||
const mgetSpy = sinon.spy((keysToRead, cb) => { | ||
cb(null, [JSON.stringify({hei: 'verden'})]) | ||
}) | ||
const redisClient = mockRedisFactory({del: delSpy, mget: mgetSpy}, {events})() | ||
return deleteKeys('asdf', redisClient).then((...args) => { | ||
expect(delSpy.called).to.equal(true) | ||
}) | ||
}) | ||
}) | ||
cb(null, [JSON.stringify({ hei: 'verden' })]); | ||
}); | ||
const redisClient = mockRedisFactory({ del: delSpy, mget: mgetSpy }, { events })(); | ||
return deleteKeys('asdf', redisClient).then(() => { | ||
expect(delSpy.called).to.equal(true); | ||
}); | ||
}); | ||
}); | ||
describe('-> readKeys', () => { | ||
let redisClient | ||
let mgetSpy | ||
let redisClient; | ||
let mgetSpy; | ||
it('should read multiple keys', () => { | ||
const values = Object.keys(redisCache).map((key) => redisCache[key]) | ||
mgetSpy = sinon.spy((keys, cb) => cb(null, values)) | ||
redisClient = mockRedisFactory({mget: mgetSpy})() | ||
const values = Object.keys(redisCache).map((key) => redisCache[key]); | ||
mgetSpy = sinon.spy((keys, cb) => cb(null, values)); | ||
redisClient = mockRedisFactory({ mget: mgetSpy })(); | ||
return readKeys(Object.keys(redisCache), redisClient, dummyLog).then((result) => { | ||
expect(mgetSpy.called).to.equal(true) | ||
expect(result).to.eql(parsedCache) | ||
}) | ||
}) | ||
expect(mgetSpy.called).to.equal(true); | ||
expect(result).to.eql(parsedCache); | ||
}); | ||
}); | ||
it('should resolve empty when no keys match', () => { | ||
const p = (keys, cb) => cb(null, []) | ||
mgetSpy = sinon.spy(p) | ||
redisClient = mockRedisFactory({mget: mgetSpy})() | ||
const p = (keys, cb) => cb(null, []); | ||
mgetSpy = sinon.spy(p); | ||
redisClient = mockRedisFactory({ mget: mgetSpy })(); | ||
return readKeys(Object.keys([]), redisClient, dummyLog).then((result) => { | ||
expect(mgetSpy.called).to.equal(false) | ||
expect(result).to.eql({}) | ||
}) | ||
}) | ||
expect(mgetSpy.called).to.equal(false); | ||
expect(result).to.eql({}); | ||
}); | ||
}); | ||
it('should skip keys with invalid json', () => { | ||
const p = (keys, cb) => cb(null, ['{1}', '{"hello": "world"}']) | ||
mgetSpy = sinon.spy(p) | ||
redisClient = mockRedisFactory({mget: mgetSpy})() | ||
const p = (keys, cb) => cb(null, ['{1}', '{"hello": "world"}']); | ||
mgetSpy = sinon.spy(p); | ||
redisClient = mockRedisFactory({ mget: mgetSpy })(); | ||
return readKeys(['key1', 'key2'], redisClient, dummyLog).then((result) => { | ||
expect(mgetSpy.called).to.equal(true) | ||
expect(result).to.eql({key2: {hello: 'world'}}) | ||
}) | ||
}) | ||
expect(mgetSpy.called).to.equal(true); | ||
expect(result).to.eql({ key2: { hello: 'world' } }); | ||
}); | ||
}); | ||
it('should reject when an error occurrs', () => { | ||
const p = (keys, cb) => cb(new Error('not ok'), null) | ||
mgetSpy = sinon.spy(p) | ||
redisClient = mockRedisFactory({mget: mgetSpy})() | ||
const p = (keys, cb) => cb(new Error('not ok'), null); | ||
mgetSpy = sinon.spy(p); | ||
redisClient = mockRedisFactory({ mget: mgetSpy })(); | ||
return readKeys(Object.keys(redisCache), redisClient, dummyLog).catch((err) => { | ||
expect(mgetSpy.called).to.equal(true) | ||
expect(err).to.be.an(Error) | ||
}) | ||
}) | ||
}) | ||
expect(mgetSpy.called).to.equal(true); | ||
expect(err).to.be.an(Error); | ||
}); | ||
}); | ||
}); | ||
@@ -178,5 +179,5 @@ describe('isSerializable', () => { | ||
} | ||
} | ||
expect(isSerializable(obj)).to.equal(true) | ||
}) | ||
}; | ||
expect(isSerializable(obj)).to.equal(true); | ||
}); | ||
@@ -186,5 +187,5 @@ it('should return false for objects with functions', () => { | ||
func: () => {} | ||
} | ||
expect(isSerializable(obj)).to.equal(false) | ||
}) | ||
}; | ||
expect(isSerializable(obj)).to.equal(false); | ||
}); | ||
@@ -195,6 +196,6 @@ it('should return false for objects with built in native objects', () => { | ||
date: new Date() | ||
} | ||
expect(isSerializable(obj)).to.equal(false) | ||
}) | ||
}) | ||
}) | ||
}; | ||
expect(isSerializable(obj)).to.equal(false); | ||
}); | ||
}); | ||
}); |
@@ -11,6 +11,6 @@ /** | ||
isSerializable | ||
} from './persistence-helpers.js' | ||
import pkg from '../../package.json' | ||
} from './persistence-helpers.js'; | ||
import pkg from '../../package.json'; | ||
const DEFAULT_GRACE = 60 * 60 * 24 * 1000 | ||
const DEFAULT_GRACE = 60 * 60 * 24 * 1000; | ||
@@ -25,37 +25,39 @@ /** | ||
**/ | ||
export default (redisFactory, | ||
{ | ||
doNotPersist = null, | ||
keySpace = '', | ||
grace = DEFAULT_GRACE, | ||
bootload = true | ||
} = {} | ||
export default ( | ||
redisFactory, | ||
{ doNotPersist = null, keySpace = '', grace = DEFAULT_GRACE, bootload = true } = {} | ||
) => (cacheInstance) => { | ||
const redisClient = redisFactory() | ||
const cacheKeyPrefix = `${pkg.name}-${keySpace}` | ||
const persisting = {} | ||
const redisClient = redisFactory(); | ||
const cacheKeyPrefix = `${pkg.name}-${keySpace}`; | ||
const persisting = {}; | ||
const persist = (key, val) => { | ||
if ((!doNotPersist || !doNotPersist.test(key)) && isSerializable(val) && !persisting[key]) { | ||
persisting[key] = true | ||
const redisKey = getRedisKey(cacheKeyPrefix, key) | ||
cacheInstance.log.debug(`Persist to key "${redisKey}"`) | ||
const valWithMeta = cacheInstance.get(key) | ||
redisClient.set(redisKey, JSON.stringify(valWithMeta), 'ex', Math.round((valWithMeta.TTL + grace) / 1000), (err) => { | ||
if (err) { | ||
cacheInstance.log.warn(err) | ||
persisting[key] = true; | ||
const redisKey = getRedisKey(cacheKeyPrefix, key); | ||
cacheInstance.log.debug(`Persist to key "${redisKey}"`); | ||
const valWithMeta = cacheInstance.get(key); | ||
redisClient.set( | ||
redisKey, | ||
JSON.stringify(valWithMeta), | ||
'ex', | ||
Math.round((valWithMeta.TTL + grace) / 1000), | ||
(err) => { | ||
if (err) { | ||
cacheInstance.log.warn(err); | ||
} | ||
delete persisting[key]; | ||
} | ||
delete persisting[key] | ||
}) | ||
); | ||
} else { | ||
cacheInstance.log.debug(`skipping persistence of promised object with key ${key}`) | ||
cacheInstance.log.debug(`skipping persistence of promised object with key ${key}`); | ||
} | ||
} | ||
}; | ||
const get = (...args) => { | ||
const next = args.pop() | ||
const next = args.pop(); | ||
return next(...args).then((val) => { | ||
const [key] = args | ||
const [key] = args; | ||
if (val.cache === 'miss') { | ||
persist(key, val) | ||
persist(key, val); | ||
// return valj | ||
@@ -77,61 +79,64 @@ // if ((!doNotPersist || !doNotPersist.test(key)) && isSerializable(obj) && !persisting[key]) { | ||
} | ||
return val | ||
}) | ||
} | ||
return val; | ||
}); | ||
}; | ||
const set = (...args) => { | ||
const next = args.pop() | ||
next(...args) | ||
const [key, val] = args | ||
persist(key, val) | ||
} | ||
const next = args.pop(); | ||
next(...args); | ||
const [key, val] = args; | ||
persist(key, val); | ||
}; | ||
const del = (key, next) => { | ||
return deleteKey(getRedisKey(cacheKeyPrefix, key), redisClient).then(() => { | ||
next(key) | ||
}) | ||
} | ||
next(key); | ||
}); | ||
}; | ||
const clear = (next) => { | ||
return deleteKeys(cacheKeyPrefix, redisClient).then(() => { | ||
next() | ||
}) | ||
} | ||
next(); | ||
}); | ||
}; | ||
const load = (nowDefault = Date.now()) => { | ||
const now = nowDefault | ||
return loadObjects(cacheKeyPrefix, redisClient, cacheInstance.log) | ||
.then((mapLoaded) => { | ||
Object.keys(mapLoaded).map((key) => { | ||
cacheInstance.set( | ||
extractKeyFromRedis(cacheKeyPrefix, key), | ||
mapLoaded[key].value, | ||
mapLoaded[key].TTL - (now - mapLoaded[key].created) | ||
) | ||
return key | ||
}) | ||
cacheInstance.log.info(`Read ${Object.keys(mapLoaded).length} keys from redis. Used ${Date.now() - now} ms`) | ||
}) | ||
} | ||
const now = nowDefault; | ||
return loadObjects(cacheKeyPrefix, redisClient, cacheInstance.log).then((mapLoaded) => { | ||
Object.keys(mapLoaded).map((key) => { | ||
cacheInstance.set( | ||
extractKeyFromRedis(cacheKeyPrefix, key), | ||
mapLoaded[key].value, | ||
mapLoaded[key].TTL - (now - mapLoaded[key].created) | ||
); | ||
return key; | ||
}); | ||
cacheInstance.log.info( | ||
`Read ${Object.keys(mapLoaded).length} keys from redis. Used ${Date.now() - now} ms` | ||
); | ||
}); | ||
}; | ||
const debug = (extraData, next) => { | ||
return next({cacheKeyPrefix, ...extraData}) | ||
} | ||
return next({ cacheKeyPrefix, ...extraData }); | ||
}; | ||
const onDispose = (key) => { | ||
deleteKey(getRedisKey(cacheKeyPrefix, key), redisClient).then(() => { | ||
cacheInstance.log.debug(`deleting key ${key} from redis (evicted by lru-cache)`) | ||
}).catch((err) => { | ||
cacheInstance.log.error(err) | ||
}) | ||
} | ||
deleteKey(getRedisKey(cacheKeyPrefix, key), redisClient) | ||
.then(() => { | ||
cacheInstance.log.debug(`deleting key ${key} from redis (evicted by lru-cache)`); | ||
}) | ||
.catch((err) => { | ||
cacheInstance.log.error(err); | ||
}); | ||
}; | ||
cacheInstance.addDisposer(onDispose) | ||
cacheInstance.addDisposer(onDispose); | ||
const destroy = () => { | ||
cacheInstance.removeDisposer(onDispose) | ||
} | ||
cacheInstance.removeDisposer(onDispose); | ||
}; | ||
if (bootload) { | ||
load().catch(cacheInstance.log.error) | ||
load().catch(cacheInstance.log.error); | ||
} | ||
@@ -147,3 +152,3 @@ | ||
destroy | ||
} | ||
} | ||
}; | ||
}; |
/** | ||
* @module | ||
**/ | ||
import isUndefined from 'lodash/isUndefined' | ||
import isNull from 'lodash/isNull' | ||
import isBoolean from 'lodash/isBoolean' | ||
import isNumber from 'lodash/isNumber' | ||
import isString from 'lodash/isString' | ||
import isArray from 'lodash/isArray' | ||
import isPlainObject from 'lodash/isPlainObject' | ||
import isUndefined from 'lodash/isUndefined'; | ||
import isNull from 'lodash/isNull'; | ||
import isBoolean from 'lodash/isBoolean'; | ||
import isNumber from 'lodash/isNumber'; | ||
import isString from 'lodash/isString'; | ||
import isArray from 'lodash/isArray'; | ||
import isPlainObject from 'lodash/isPlainObject'; | ||
const MAX_PAGE_SIZE = 100 | ||
const MAX_PAGE_SIZE = 100; | ||
export const extractKeyFromRedis = (prefix, key) => { | ||
return key.replace(new RegExp(`${prefix}-`), '') | ||
} | ||
return key.replace(new RegExp(`${prefix}-`), ''); | ||
}; | ||
export const getRedisKey = (prefix, key = '') => { | ||
return `${[prefix, key].join('-')}` | ||
} | ||
return `${[prefix, key].join('-')}`; | ||
}; | ||
export const readKeys = (keys, redisClient, log) => { | ||
if (keys.length === 0) { | ||
return Promise.resolve({}) | ||
return Promise.resolve({}); | ||
} | ||
const p = [] | ||
const p = []; | ||
for (let i = 0; i < keys.length; i = i + MAX_PAGE_SIZE) { | ||
const keysToRead = keys.slice(i, i + MAX_PAGE_SIZE) | ||
p.push(new Promise((resolve) => { | ||
redisClient.mget(keysToRead, (err, results) => { | ||
if (err) { | ||
log.warn(`could not read keys into cache, reason: ${err}`) | ||
resolve({}) | ||
return | ||
} | ||
resolve(keysToRead.reduce((acc, key, i) => { | ||
try { | ||
acc[key] = JSON.parse(results[i]) | ||
} catch (e) { | ||
log.warn(`could not parse value for ${key} as JSON. ${results[i]}`) | ||
const keysToRead = keys.slice(i, i + MAX_PAGE_SIZE); | ||
p.push( | ||
new Promise((resolve) => { | ||
redisClient.mget(keysToRead, (err, results) => { | ||
if (err) { | ||
log.warn(`could not read keys into cache, reason: ${err}`); | ||
resolve({}); | ||
return; | ||
} | ||
return acc | ||
}, {})) | ||
resolve( | ||
keysToRead.reduce((acc, key, i) => { | ||
try { | ||
acc[key] = JSON.parse(results[i]); | ||
} catch (e) { | ||
log.warn(`could not parse value for ${key} as JSON. ${results[i]}`); | ||
} | ||
return acc; | ||
}, {}) | ||
); | ||
}); | ||
}) | ||
})) | ||
); | ||
} | ||
return Promise.all(p).then((results) => { | ||
return results.reduce((acc, next) => { | ||
Object.assign(acc, next) | ||
return acc | ||
}, {}) | ||
}) | ||
} | ||
Object.assign(acc, next); | ||
return acc; | ||
}, {}); | ||
}); | ||
}; | ||
export const scanKeys = (cacheKeyPrefix, redisClient) => { | ||
const keys = [] | ||
const keys = []; | ||
return new Promise((resolve, reject) => { | ||
@@ -61,14 +65,14 @@ const stream = redisClient.scanStream({ | ||
count: 100 | ||
}) | ||
}); | ||
stream.on('data', (resultKeys) => { | ||
keys.push(...resultKeys) | ||
}) | ||
keys.push(...resultKeys); | ||
}); | ||
stream.on('end', () => { | ||
resolve(keys) | ||
}) | ||
resolve(keys); | ||
}); | ||
stream.on('error', (err) => { | ||
reject(err) | ||
}) | ||
}) | ||
} | ||
reject(err); | ||
}); | ||
}); | ||
}; | ||
@@ -79,22 +83,21 @@ export const deleteKey = (key, redisClient) => { | ||
if (err) { | ||
reject(err) | ||
return | ||
reject(err); | ||
return; | ||
} | ||
resolve(res) | ||
}) | ||
}) | ||
} | ||
resolve(res); | ||
}); | ||
}); | ||
}; | ||
export const deleteKeys = (cacheKeyPrefix, redisClient) => { | ||
return scanKeys(cacheKeyPrefix, redisClient).then((keys) => { | ||
return Promise.all(keys.map((key) => deleteKey(key, redisClient))) | ||
}) | ||
} | ||
return Promise.all(keys.map((key) => deleteKey(key, redisClient))); | ||
}); | ||
}; | ||
export const loadObjects = (cacheKeyPrefix, redisClient, log) => { | ||
return scanKeys(cacheKeyPrefix, redisClient) | ||
.then((keys) => { | ||
return readKeys(keys, redisClient, log) | ||
}) | ||
} | ||
return scanKeys(cacheKeyPrefix, redisClient).then((keys) => { | ||
return readKeys(keys, redisClient, log); | ||
}); | ||
}; | ||
@@ -104,13 +107,8 @@ // credits to https://stackoverflow.com/users/128816/treznik | ||
export const isSerializable = (obj) => { | ||
if (isUndefined(obj) || | ||
isNull(obj) || | ||
isBoolean(obj) || | ||
isNumber(obj) || | ||
isString(obj)) { | ||
return true | ||
if (isUndefined(obj) || isNull(obj) || isBoolean(obj) || isNumber(obj) || isString(obj)) { | ||
return true; | ||
} | ||
if (!isPlainObject(obj) && | ||
!isArray(obj)) { | ||
return false | ||
if (!isPlainObject(obj) && !isArray(obj)) { | ||
return false; | ||
} | ||
@@ -120,7 +118,7 @@ | ||
if (!isSerializable(obj[key])) { | ||
return false | ||
return false; | ||
} | ||
} | ||
return true | ||
} | ||
return true; | ||
}; |
@@ -8,5 +8,5 @@ import { | ||
isWaiting | ||
} from '../cache-helpers' | ||
import sinon from 'sinon' | ||
import expect from 'expect.js' | ||
} from '../cache-helpers'; | ||
import sinon from 'sinon'; | ||
import expect from 'expect.js'; | ||
@@ -16,3 +16,3 @@ describe('cache-helpers', () => { | ||
it('should create a wait object', () => { | ||
const waiting = createWait(100, 200) | ||
const waiting = createWait(100, 200); | ||
expect(waiting).to.eql({ | ||
@@ -22,104 +22,104 @@ started: 200, | ||
waitUntil: 300 | ||
}) | ||
}) | ||
}) | ||
}); | ||
}); | ||
}); | ||
describe('-> isWaiting', () => { | ||
it('should return true if waiting', () => { | ||
expect(isWaiting(createWait(1000))).to.equal(true) | ||
}) | ||
expect(isWaiting(createWait(1000))).to.equal(true); | ||
}); | ||
it('should return false if waiting is finished', () => { | ||
expect(isWaiting(createWait(-1000))).to.equal(false) | ||
}) | ||
expect(isWaiting(createWait(-1000))).to.equal(false); | ||
}); | ||
it('should return false if not waiting', () => { | ||
expect(isWaiting()).to.equal(false) | ||
}) | ||
}) | ||
expect(isWaiting()).to.equal(false); | ||
}); | ||
}); | ||
describe('-> isFresh', () => { | ||
it('should return true if TTL is not reached', () => { | ||
expect(isFresh(createEntry('yo', 1000))).to.equal(true) | ||
}) | ||
expect(isFresh(createEntry('yo', 1000))).to.equal(true); | ||
}); | ||
it('should return false if TTL is reached', () => { | ||
expect(isFresh(createEntry('yo', -1000))).to.equal(false) | ||
}) | ||
}) | ||
expect(isFresh(createEntry('yo', -1000))).to.equal(false); | ||
}); | ||
}); | ||
describe('-> createEntry', () => { | ||
it('should create a wrapper object with metadata for cached', () => { | ||
const obj = {hello: 'world'} | ||
const entry = createEntry(obj, 10) | ||
expect(entry).to.only.have.keys(['created', 'TTL', 'value']) | ||
expect(entry.value).to.eql(obj) | ||
}) | ||
}) | ||
const obj = { hello: 'world' }; | ||
const entry = createEntry(obj, 10); | ||
expect(entry).to.only.have.keys(['created', 'TTL', 'value']); | ||
expect(entry.value).to.eql(obj); | ||
}); | ||
}); | ||
describe('-> createObservable', () => { | ||
it('should create an Observable with subscription capabilities from promise', (done) => { | ||
const p = () => Promise.resolve() | ||
const spy = sinon.spy(p) | ||
const obs = createObservable(spy, 0) | ||
obs.subscribe(() => done()) | ||
}) | ||
const p = () => Promise.resolve(); | ||
const spy = sinon.spy(p); | ||
const obs = createObservable(spy, 0); | ||
obs.subscribe(() => done()); | ||
}); | ||
it('should create an Observable from promise with timeout support', (done) => { | ||
const p = () => new Promise((resolve) => setTimeout(() => resolve(), 10)) | ||
const spy = sinon.spy(p) | ||
const obs = createObservable(spy, 0) | ||
const p = () => new Promise((resolve) => setTimeout(() => resolve(), 10)); | ||
const spy = sinon.spy(p); | ||
const obs = createObservable(spy, 0); | ||
obs.subscribe(Function.prototype, (err) => { | ||
expect(err).to.be.an(Error) | ||
done() | ||
}) | ||
}) | ||
}) | ||
expect(err).to.be.an(Error); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
describe('-> createRegExp', () => { | ||
it('should create a regexp from a string', () => { | ||
const re = createRegExp('/houses/2') | ||
expect(re).to.eql(/\/houses\/2/) | ||
}) | ||
const re = createRegExp('/houses/2'); | ||
expect(re).to.eql(/\/houses\/2/); | ||
}); | ||
it('should support * to .* rewrite', () => { | ||
const re = createRegExp('/houses/2*') | ||
expect(re).to.eql(/\/houses\/2.*/) | ||
}) | ||
const re = createRegExp('/houses/2*'); | ||
expect(re).to.eql(/\/houses\/2.*/); | ||
}); | ||
it('should support ? to \\? rewrite', () => { | ||
const re = createRegExp('/houses/2?hallo') | ||
expect(re).to.eql(/\/houses\/2\?hallo/) | ||
}) | ||
const re = createRegExp('/houses/2?hallo'); | ||
expect(re).to.eql(/\/houses\/2\?hallo/); | ||
}); | ||
it('should support . to \\. rewrite', () => { | ||
const re = createRegExp('/houses/2.hallo') | ||
expect(re).to.eql(/\/houses\/2\.hallo/) | ||
}) | ||
const re = createRegExp('/houses/2.hallo'); | ||
expect(re).to.eql(/\/houses\/2\.hallo/); | ||
}); | ||
it('should support [ to \\[ rewrite', () => { | ||
const re = createRegExp('/houses/2hallo[') | ||
expect(re).to.eql(/\/houses\/2hallo\[/) | ||
}) | ||
const re = createRegExp('/houses/2hallo['); | ||
expect(re).to.eql(/\/houses\/2hallo\[/); | ||
}); | ||
it('should support ] to \\] rewrite', () => { | ||
const re = createRegExp('/houses/2hallo]') | ||
expect(re).to.eql(/\/houses\/2hallo\]/) | ||
}) | ||
const re = createRegExp('/houses/2hallo]'); | ||
expect(re).to.eql(/\/houses\/2hallo\]/); | ||
}); | ||
it('should support ^ to \\^ rewrite', () => { | ||
const re = createRegExp('/houses/2hallo^') | ||
expect(re).to.eql(/\/houses\/2hallo\^/) | ||
}) | ||
const re = createRegExp('/houses/2hallo^'); | ||
expect(re).to.eql(/\/houses\/2hallo\^/); | ||
}); | ||
it('should support $ to \\$ rewrite', () => { | ||
const re = createRegExp('/houses/2hallo$') | ||
expect(re).to.eql(/\/houses\/2hallo\$/) | ||
}) | ||
const re = createRegExp('/houses/2hallo$'); | ||
expect(re).to.eql(/\/houses\/2hallo\$/); | ||
}); | ||
it('should rewrite urls', () => { | ||
const re = createRegExp('http://localhost.no/?param1=yo¶m2=yo') | ||
expect(re).to.eql(/http:\/\/localhost\.no\/\?param1=yo¶m2=yo/) | ||
}) | ||
}) | ||
}) | ||
const re = createRegExp('http://localhost.no/?param1=yo¶m2=yo'); | ||
expect(re).to.eql(/http:\/\/localhost\.no\/\?param1=yo¶m2=yo/); | ||
}); | ||
}); | ||
}); |
@@ -1,11 +0,5 @@ | ||
import { | ||
getCacheInfo, | ||
buildKey | ||
} from '../debug' | ||
import expect from 'expect.js' | ||
import lruCache from 'lru-cache' | ||
import { | ||
createEntry, | ||
createWait | ||
} from '../cache-helpers' | ||
import { getCacheInfo, buildKey } from '../debug'; | ||
import expect from 'expect.js'; | ||
import lruCache from 'lru-cache'; | ||
import { createEntry, createWait } from '../cache-helpers'; | ||
@@ -16,4 +10,4 @@ describe('debug', () => { | ||
// const isWaiting = formatWait(createWait(10000, new Date('2017-09-05T08:00:00Z').getTime())) | ||
const waiting = createWait(10000, new Date('2017-09-04T08:01:00Z').getTime()) | ||
const key = 'hei' | ||
const waiting = createWait(10000, new Date('2017-09-04T08:01:00Z').getTime()); | ||
const key = 'hei'; | ||
const value = { | ||
@@ -23,5 +17,5 @@ value: 'verden', | ||
TTL: 60000 | ||
} | ||
const full = false | ||
const debugKey = buildKey({key, value, waiting, full}) | ||
}; | ||
const full = false; | ||
const debugKey = buildKey({ key, value, waiting, full }); | ||
expect(debugKey).to.eql({ | ||
@@ -36,7 +30,7 @@ key: 'hei', | ||
} | ||
}) | ||
}) | ||
}); | ||
}); | ||
it('should not print waiting if wait is not set', () => { | ||
const key = 'hei' | ||
const key = 'hei'; | ||
const value = { | ||
@@ -46,5 +40,5 @@ value: 'verden', | ||
TTL: 60000 | ||
} | ||
const full = false | ||
const debugKey = buildKey({key, value, full}) | ||
}; | ||
const full = false; | ||
const debugKey = buildKey({ key, value, full }); | ||
expect(debugKey).to.eql({ | ||
@@ -54,22 +48,22 @@ key: 'hei', | ||
expired: new Date('2017-09-04T08:01:00.000Z') | ||
}) | ||
}) | ||
}) | ||
}); | ||
}); | ||
}); | ||
describe('getCacheInfo', () => { | ||
it('should print debug info from cache', () => { | ||
const maxAge = 10000 | ||
const maxAge = 10000; | ||
const cache = lruCache({ | ||
max: 100, | ||
maxAge | ||
}) | ||
}); | ||
const entry = {...createEntry({hello: 'world'}, 12345), cache: 'hit'} | ||
const entry2 = {...createEntry({foo: 'bar'}, 12345), cache: 'hit'} | ||
const entry3 = {...createEntry({hello: 'world'}, -1), cache: 'hit'} | ||
cache.set('yo', entry) | ||
cache.set('notinresults', entry2) | ||
cache.set('yo2', entry3) | ||
const entry = { ...createEntry({ hello: 'world' }, 12345), cache: 'hit' }; | ||
const entry2 = { ...createEntry({ foo: 'bar' }, 12345), cache: 'hit' }; | ||
const entry3 = { ...createEntry({ hello: 'world' }, -1), cache: 'hit' }; | ||
cache.set('yo', entry); | ||
cache.set('notinresults', entry2); | ||
cache.set('yo2', entry3); | ||
// omit now and waiting field | ||
const {now, waiting, ...info} = getCacheInfo({ | ||
const { now, waiting, ...info } = getCacheInfo({ | ||
full: true, | ||
@@ -80,3 +74,3 @@ search: 'yo*', | ||
waiting: new Map() | ||
}) | ||
}); | ||
expect(info).to.eql({ | ||
@@ -96,16 +90,18 @@ full: true, | ||
], | ||
stale: [{ | ||
created: new Date(entry3.created), | ||
expired: new Date(entry3.created + entry3.TTL), | ||
key: 'yo2', | ||
value: { | ||
hello: 'world' | ||
stale: [ | ||
{ | ||
created: new Date(entry3.created), | ||
expired: new Date(entry3.created + entry3.TTL), | ||
key: 'yo2', | ||
value: { | ||
hello: 'world' | ||
} | ||
} | ||
}] | ||
] | ||
}, | ||
maxAge: '0.002777777777777778h', | ||
search: 'yo*' | ||
}) | ||
}) | ||
}) | ||
}) | ||
}); | ||
}); | ||
}); | ||
}); |
/** | ||
* @module | ||
**/ | ||
import {Observable} from 'rxjs/Observable' | ||
import 'rxjs/add/observable/fromPromise' | ||
import 'rxjs/add/operator/timeout' | ||
import { Observable } from 'rxjs/Observable'; | ||
import 'rxjs/add/observable/fromPromise'; | ||
import 'rxjs/add/operator/timeout'; | ||
export const createRegExp = (search) => { | ||
return new RegExp(search | ||
.replace(/\./g, '\\.') | ||
.replace(/\[/g, '\\[') | ||
.replace(/\]/g, '\\]') | ||
.replace(/\^/g, '\\^') | ||
.replace(/\$/g, '\\$') | ||
.replace(/\?/g, '\\?') | ||
.replace(/\*/g, '.*')) | ||
} | ||
return new RegExp( | ||
search | ||
.replace(/\./g, '\\.') | ||
.replace(/\[/g, '\\[') | ||
.replace(/\]/g, '\\]') | ||
.replace(/\^/g, '\\^') | ||
.replace(/\$/g, '\\$') | ||
.replace(/\?/g, '\\?') | ||
.replace(/\*/g, '.*') | ||
); | ||
}; | ||
export const isFresh = (entry, nowDefault) => { | ||
const now = nowDefault || Date.now() | ||
return entry.created + entry.TTL > now | ||
} | ||
const now = nowDefault || Date.now(); | ||
return entry.created + entry.TTL > now; | ||
}; | ||
export const isWaiting = (waiting, nowDefault) => { | ||
const now = nowDefault || Date.now() | ||
const now = nowDefault || Date.now(); | ||
if (waiting) { | ||
return waiting.waitUntil > now | ||
return waiting.waitUntil > now; | ||
} | ||
return false | ||
} | ||
return false; | ||
}; | ||
export const waitingForError = (key, wait = {}) => { | ||
return new Error(`Waiting for next run for ${key}, wait: ${JSON.stringify(wait, null, 2)}`) | ||
} | ||
return new Error(`Waiting for next run for ${key}, wait: ${JSON.stringify(wait, null, 2)}`); | ||
}; | ||
export const createEntry = (value, TTL, date) => { | ||
const created = date || Date.now() | ||
const created = date || Date.now(); | ||
return { | ||
@@ -42,7 +44,7 @@ created, | ||
value | ||
} | ||
} | ||
}; | ||
}; | ||
export const createWait = (wait, now) => { | ||
const started = now || Date.now() | ||
const started = now || Date.now(); | ||
return { | ||
@@ -52,14 +54,12 @@ started, | ||
waitUntil: started + wait | ||
} | ||
} | ||
}; | ||
}; | ||
export const createObservable = (promiseCreator, timeout, logger) => { | ||
const promise = promiseCreator().catch((err) => { | ||
logger.error('An error occured while executing worker promise', err) | ||
throw err | ||
}) | ||
logger.error('An error occured while executing worker promise', err); | ||
throw err; | ||
}); | ||
return Observable | ||
.fromPromise(promise) | ||
.timeout(timeout) | ||
} | ||
return Observable.fromPromise(promise).timeout(timeout); | ||
}; |
/** | ||
* @module | ||
**/ | ||
import { | ||
isFresh, | ||
createRegExp | ||
} from './cache-helpers' | ||
import { isFresh, createRegExp } from './cache-helpers'; | ||
export const buildKey = ({key, value, waiting, full}) => { | ||
const expire = value.created + value.TTL | ||
const expireKey = expire < Date.now() ? 'expired' : 'expires' | ||
return Object.assign({ | ||
key, | ||
created: new Date(value.created), | ||
[expireKey]: new Date(expire) | ||
}, | ||
waiting ? { | ||
waiting: { | ||
started: new Date(waiting.started), | ||
wait: waiting.wait, | ||
waitUntil: new Date(waiting.waitUntil) | ||
} | ||
} : {}, | ||
full ? {value: value.value} : {}) | ||
} | ||
export const buildKey = ({ key, value, waiting, full }) => { | ||
const expire = value.created + value.TTL; | ||
const expireKey = expire < Date.now() ? 'expired' : 'expires'; | ||
return Object.assign( | ||
{ | ||
key, | ||
created: new Date(value.created), | ||
[expireKey]: new Date(expire) | ||
}, | ||
waiting | ||
? { | ||
waiting: { | ||
started: new Date(waiting.started), | ||
wait: waiting.wait, | ||
waitUntil: new Date(waiting.waitUntil) | ||
} | ||
} | ||
: {}, | ||
full ? { value: value.value } : {} | ||
); | ||
}; | ||
const extractProps = (obj) => { | ||
const ret = {} | ||
const ret = {}; | ||
Object.keys(obj) | ||
.filter((key) => !/^log$|^cache$/.test(key)) | ||
.forEach((key) => { | ||
ret[key] = obj[key] | ||
}) | ||
return ret | ||
} | ||
ret[key] = obj[key]; | ||
}); | ||
return ret; | ||
}; | ||
export const getCacheInfo = (info) => { | ||
const { | ||
full, | ||
search = '*', | ||
cache, | ||
maxAge, | ||
waiting | ||
} = info | ||
const { full, search = '*', cache, maxAge, waiting } = info; | ||
const keys = { | ||
stale: [], | ||
hot: [] | ||
} | ||
const matcher = createRegExp(search) | ||
}; | ||
const matcher = createRegExp(search); | ||
cache.forEach((value, key) => { | ||
if (!matcher.test(key)) { | ||
return | ||
return; | ||
} | ||
const keyInfo = buildKey({key, value, waiting: waiting.get(key), full}) | ||
const keyInfo = buildKey({ key, value, waiting: waiting.get(key), full }); | ||
if (isFresh(value)) { | ||
keys.hot.push(keyInfo) | ||
keys.hot.push(keyInfo); | ||
} else { | ||
keys.stale.push(keyInfo) | ||
keys.stale.push(keyInfo); | ||
} | ||
}) | ||
}); | ||
return { | ||
@@ -67,3 +62,3 @@ now: new Date(), | ||
keys | ||
} | ||
} | ||
}; | ||
}; |
@@ -1,2 +0,2 @@ | ||
import sinon from 'sinon' | ||
import sinon from 'sinon'; | ||
@@ -8,2 +8,2 @@ export const dummyLog = { | ||
warn: sinon.spy(Function.prototype) | ||
} | ||
}; |
@@ -1,38 +0,40 @@ | ||
export const mockRedisFactory = (overrideMethods = {}, {events = {}} = {}) => { | ||
let instance | ||
export const mockRedisFactory = (overrideMethods = {}, { events = {} } = {}) => { | ||
let instance; | ||
return () => { | ||
const cbs = {} | ||
const namespaces = [] | ||
const cbs = {}; | ||
const namespaces = []; | ||
if (instance) { | ||
return instance | ||
return instance; | ||
} | ||
instance = Object.assign({ | ||
subscribe: (ns) => { | ||
namespaces.push(ns) | ||
}, | ||
publish: (ns, data) => { | ||
(cbs[ns] || []).forEach((cb) => cb(ns, data)) | ||
}, | ||
on: (event, cb) => { | ||
namespaces.forEach((ns) => { | ||
if (!cbs[ns]) { | ||
cbs[ns] = [] | ||
} | ||
cbs[ns].push(cb) | ||
}) | ||
}, | ||
scanStream: ({match, cound}) => { | ||
return { | ||
on: (event, cb) => { | ||
if (!events[event]) { | ||
events[event] = [] | ||
instance = Object.assign( | ||
{ | ||
subscribe: (ns) => { | ||
namespaces.push(ns); | ||
}, | ||
publish: (ns, data) => { | ||
(cbs[ns] || []).forEach((cb) => cb(ns, data)); | ||
}, | ||
on: (event, cb) => { | ||
namespaces.forEach((ns) => { | ||
if (!cbs[ns]) { | ||
cbs[ns] = []; | ||
} | ||
events[event].push(cb) | ||
} | ||
cbs[ns].push(cb); | ||
}); | ||
}, | ||
scanStream: () => { | ||
return { | ||
on: (event, cb) => { | ||
if (!events[event]) { | ||
events[event] = []; | ||
} | ||
events[event].push(cb); | ||
} | ||
}; | ||
} | ||
} | ||
}, overrideMethods) | ||
return instance | ||
} | ||
} | ||
}, | ||
overrideMethods | ||
); | ||
return instance; | ||
}; | ||
}; |
@@ -1,42 +0,44 @@ | ||
const {fork} = require('child_process') | ||
const cpuCount = require('os').cpus().length | ||
const path = require('path') | ||
const {log} = require('./log') | ||
const { fork } = require('child_process'); | ||
const cpuCount = require('os').cpus().length; | ||
const path = require('path'); | ||
const { log } = require('./log'); | ||
const worker = path.join(__dirname, 'cache.js') | ||
const worker = path.join(__dirname, 'cache.js'); | ||
const stats = {} | ||
const stats = {}; | ||
console.log(`Starting forked cache, using ${cpuCount} cpus`); // eslint-disable-line | ||
const promises = [] | ||
const promises = []; | ||
for (let i = 0; i < cpuCount; i++) { | ||
const p = new Promise((resolve) => { | ||
const child = fork(worker) | ||
child.on('message', ({used, iterations}) => { | ||
const child = fork(worker); | ||
child.on('message', ({ used, iterations }) => { | ||
if (!stats[iterations]) { | ||
stats[iterations] = [] | ||
stats[iterations] = []; | ||
} | ||
stats[iterations].push(used) | ||
}) | ||
child.on('exit', resolve) | ||
}) | ||
promises.push(p) | ||
stats[iterations].push(used); | ||
}); | ||
child.on('exit', resolve); | ||
}); | ||
promises.push(p); | ||
} | ||
Promise.all(promises).then(() => { | ||
const keys = Object.keys(stats) | ||
const processed = keys.map((stat) => { | ||
return { | ||
iterations: stat * stats[stat].length, | ||
used: stats[stat].reduce((a, b) => Math.max(a, b)) | ||
} | ||
Promise.all(promises) | ||
.then(() => { | ||
const keys = Object.keys(stats); | ||
const processed = keys.map((stat) => { | ||
return { | ||
iterations: stat * stats[stat].length, | ||
used: stats[stat].reduce((a, b) => Math.max(a, b)) | ||
}; | ||
}); | ||
processed.forEach(({ used, iterations }) => { | ||
log({ used, iterations }); | ||
}); | ||
}) | ||
processed.forEach(({used, iterations}) => { | ||
log({used, iterations}) | ||
}) | ||
}).catch((err) => { | ||
console.error(err); // eslint-disable-line | ||
throw err | ||
}) | ||
.catch((err) => { | ||
console.error(err); // eslint-disable-line | ||
throw err; | ||
}); |
@@ -1,27 +0,29 @@ | ||
const fs = require('fs') | ||
const path = require('path') | ||
const readFileAsync = require('util').promisify(fs.readFile) | ||
const testRunner = require('./test-runner') | ||
const {default: createCacheInstance} = require('../lib') | ||
const argv = require('yargs').argv | ||
const type = argv.type || 'sin' | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const readFileAsync = require('util').promisify(fs.readFile); | ||
const testRunner = require('./test-runner'); | ||
const { default: createCacheInstance } = require('../lib'); | ||
const argv = require('yargs').argv; | ||
const type = argv.type || 'sin'; | ||
const fileToParse = path.join(__dirname, 'newsfeed.json') | ||
const fileToParse = path.join(__dirname, 'newsfeed.json'); | ||
const ciOptions = { log: console, initial: {}, maxLength: 100, maxAge: 120 * 1000 } | ||
const workerOptions = { ttl: 60 * 1000, workerTimeout: 5 * 1000, deltaWait: 5 * 1000 } | ||
const ciOptions = { log: console, initial: {}, maxLength: 100, maxAge: 120 * 1000 }; | ||
const workerOptions = { ttl: 60 * 1000, workerTimeout: 5 * 1000, deltaWait: 5 * 1000 }; | ||
const ci = createCacheInstance(ciOptions) | ||
const ci = createCacheInstance(ciOptions); | ||
const cachePerfTest = (iterations) => { | ||
const now = Date.now() | ||
const promises = [] | ||
const now = Date.now(); | ||
const promises = []; | ||
for (let i = 0; i < iterations; i++) { | ||
promises.push(ci.get(fileToParse, workerOptions, () => { | ||
return readFileAsync(fileToParse, 'utf-8').then((data) => JSON.parse(data)) | ||
})) | ||
promises.push( | ||
ci.get(fileToParse, workerOptions, () => { | ||
return readFileAsync(fileToParse, 'utf-8').then((data) => JSON.parse(data)); | ||
}) | ||
); | ||
} | ||
return Promise.all(promises).then(() => ({ used: Date.now() - now, iterations })) | ||
} | ||
return Promise.all(promises).then(() => ({ used: Date.now() - now, iterations })); | ||
}; | ||
testRunner({perfTest: cachePerfTest, fileToParse, rounds: 15, max: 2400000, type}) | ||
testRunner({ perfTest: cachePerfTest, fileToParse, rounds: 15, max: 2400000, type }); |
@@ -1,22 +0,22 @@ | ||
const chalk = require('chalk') | ||
const THRESHOLD = 1000 | ||
const chalk = require('chalk'); | ||
const THRESHOLD = 1000; | ||
const isMaster = !process.send | ||
const isMaster = !process.send; | ||
const log = ({used, iterations}) => { | ||
let iterLabel = iterations | ||
const log = ({ used, iterations }) => { | ||
let iterLabel = iterations; | ||
if (iterations > 999) { | ||
iterLabel = `${(iterations / 1000).toFixed(2)}k` | ||
iterLabel = `${(iterations / 1000).toFixed(2)}k`; | ||
} | ||
if (iterations > 999999) { | ||
iterLabel = `${(iterations / 1000000).toFixed(2)}m` | ||
iterLabel = `${(iterations / 1000000).toFixed(2)}m`; | ||
} | ||
const usedLabel = chalk[used > THRESHOLD ? 'red' : 'green'](`${used}ms`) | ||
const usedLabel = chalk[used > THRESHOLD ? 'red' : 'green'](`${used}ms`); | ||
if (!isMaster) { | ||
// child process | ||
process.send({used, iterations}) | ||
process.send({ used, iterations }); | ||
} else { | ||
console.log(`Used ${usedLabel} to parse ${iterLabel} iterations`); // eslint-disable-line | ||
console.log(`Used ${usedLabel} to parse ${iterLabel} iterations`); // eslint-disable-line | ||
} | ||
} | ||
}; | ||
@@ -26,2 +26,2 @@ module.exports = { | ||
log | ||
} | ||
}; |
@@ -1,8 +0,8 @@ | ||
const fs = require('fs') | ||
const path = require('path') | ||
const testRunner = require('./test-runner') | ||
const argv = require('yargs').argv | ||
const type = argv.type || 'sin' | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const testRunner = require('./test-runner'); | ||
const argv = require('yargs').argv; | ||
const type = argv.type || 'sin'; | ||
const fileToParse = path.join(__dirname, 'newsfeed.json') | ||
const fileToParse = path.join(__dirname, 'newsfeed.json'); | ||
@@ -12,11 +12,10 @@ const data = fs.readFileSync(fileToParse, 'utf-8'); // eslint-disable-line | ||
const noCachePerfTest = (iterations) => { | ||
const now = Date.now() | ||
const promises = [] | ||
const now = Date.now(); | ||
const promises = []; | ||
for (let i = 0; i < iterations; i++) { | ||
promises.push(Promise.resolve(JSON.parse(data))) | ||
promises.push(Promise.resolve(JSON.parse(data))); | ||
} | ||
return Promise.all(promises) | ||
.then(() => ({ used: Date.now() - now, iterations })) | ||
} | ||
return Promise.all(promises).then(() => ({ used: Date.now() - now, iterations })); | ||
}; | ||
testRunner({perfTest: noCachePerfTest, fileToParse, rounds: 30, max: 1500, type}) | ||
testRunner({ perfTest: noCachePerfTest, fileToParse, rounds: 30, max: 1500, type }); |
@@ -1,17 +0,17 @@ | ||
const fs = require('fs') | ||
const path = require('path') | ||
const readFileAsync = require('util').promisify(fs.readFile) | ||
const testRunner = require('./test-runner') | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const readFileAsync = require('util').promisify(fs.readFile); | ||
const testRunner = require('./test-runner'); | ||
const fileToParse = path.join(__dirname, 'newsfeed.json') | ||
const fileToParse = path.join(__dirname, 'newsfeed.json'); | ||
const noCachePerfTest = (iterations) => { | ||
const now = Date.now() | ||
const promises = [] | ||
const now = Date.now(); | ||
const promises = []; | ||
for (let i = 0; i < iterations; i++) { | ||
promises.push(readFileAsync(fileToParse, 'utf-8').then((data) => JSON.parse(data))) | ||
promises.push(readFileAsync(fileToParse, 'utf-8').then((data) => JSON.parse(data))); | ||
} | ||
return Promise.all(promises).then(() => ({ used: Date.now() - now, iterations })) | ||
} | ||
return Promise.all(promises).then(() => ({ used: Date.now() - now, iterations })); | ||
}; | ||
testRunner({perfTest: noCachePerfTest, fileToParse, max: 1000}) | ||
testRunner({ perfTest: noCachePerfTest, fileToParse, max: 1000 }); |
@@ -1,14 +0,13 @@ | ||
/* eslint no-console: 0 */ | ||
const chalk = require('chalk') | ||
const fs = require('fs') | ||
const {THRESHOLD, log} = require('./log') | ||
const ROUNDS = 10 | ||
const chalk = require('chalk'); | ||
const fs = require('fs'); | ||
const { THRESHOLD, log } = require('./log'); | ||
const ROUNDS = 10; | ||
const getSinIterations = (max, i, rounds) => { | ||
return Math.round(max * Math.sin(i * (90 / rounds) * Math.PI / 180)) || 1 | ||
} | ||
return Math.round(max * Math.sin(i * (90 / rounds) * Math.PI / 180)) || 1; | ||
}; | ||
const getLinearIterations = (max, i, rounds) => { | ||
return Math.round((i / rounds) * max) || 1 | ||
} | ||
return Math.round(i / rounds * max) || 1; | ||
}; | ||
@@ -18,15 +17,17 @@ const iterationTypes = { | ||
sin: getSinIterations | ||
} | ||
}; | ||
const getLabel = (type, max) => { | ||
if (type === 'sin') { | ||
return `sin(0..90) * ${max}` | ||
return `sin(0..90) * ${max}`; | ||
} | ||
return `linear(0..${max})` | ||
} | ||
return `linear(0..${max})`; | ||
}; | ||
module.exports = ({perfTest, fileToParse, max, rounds = ROUNDS, type = 'sin'}) => { | ||
const fileSize = fs.statSync(fileToParse).size / 1024; // eslint-disable-line | ||
module.exports = ({ perfTest, fileToParse, max, rounds = ROUNDS, type = 'sin' }) => { | ||
const fileSize = fs.statSync(fileToParse).size / 1024; // eslint-disable-line | ||
if (!iterationTypes[type]) { | ||
throw new TypeError(`unsupported ${type}. Must be one of ${Object.keys(iterationTypes).join(', ')}`) | ||
throw new TypeError( | ||
`unsupported ${type}. Must be one of ${Object.keys(iterationTypes).join(', ')}` | ||
); | ||
} | ||
@@ -41,9 +42,9 @@ | ||
.reduce((prev, cur) => { | ||
return prev.then(() => cur()) | ||
return prev.then(() => cur()); | ||
}, Promise.resolve({})) | ||
.then(() => console.log(`completed testing ${max} * ${getLabel(type, max)}`)) // eslint-disable-line | ||
.catch((err) => { | ||
console.error(err); // eslint-disable-line | ||
throw err | ||
}) | ||
} | ||
.then(() => console.log(`completed testing ${max} * ${getLabel(type, max)}`)) // eslint-disable-line | ||
.catch((err) => { | ||
console.error(err); // eslint-disable-line | ||
throw err; | ||
}); | ||
}; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
674240
69
5935
17
3
+ Addedlodash@4.17.11(transitive)
+ Addedlru-cache@4.1.3(transitive)
- Removedlodash@4.17.4(transitive)
- Removedlru-cache@4.1.1(transitive)
Updatedlodash@4.17.11
Updatedlru-cache@4.1.3