Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

cache-manager

Package Overview
Dependencies
Maintainers
1
Versions
110
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

cache-manager - npm Package Compare versions

Comparing version 3.1.0 to 3.2.0

3

History.md

@@ -0,1 +1,4 @@

- 3.2.0 2020-03-13
- Background refresh of expiring entries (#138). - @marcoreni
- 3.1.0 2020-02-24

@@ -2,0 +5,0 @@ - Pass updateAgeOnGet to lru-cache in memory-store (#136). - @BastianKubaile

@@ -33,2 +33,3 @@ /** @module cacheManager/caching */

self.ignoreCacheErrors = args.ignoreCacheErrors || false;
self.refreshThreshold = args.refreshThreshold || false;

@@ -38,2 +39,3 @@ var Promise = args.promiseDependency || global.Promise;

var callbackFiller = new CallbackFiller();
var backgroundQueue = new Set();

@@ -69,3 +71,57 @@ if (typeof args.isCacheableValue === 'function') {

function handleBackgroundRefresh(key, work, options) {
if (!backgroundQueue.has(key)) {
backgroundQueue.add(key);
self.checkRefreshThreshold(key, function(err, isExpiring) {
if (err) {
backgroundQueue.delete(key);
return;
}
if (isExpiring) {
work(function(err, data) {
if (err || !self._isCacheableValue(data)) {
backgroundQueue.delete(key);
return;
}
if (options && typeof options.ttl === 'function') {
options.ttl = options.ttl(data);
}
self.store.set(key, data, options, function() {
backgroundQueue.delete(key);
});
});
}
});
}
}
/**
* Checks if the current key is expiring. I.e., if a refreshThreshold is set for this cache
* and if the cache supports the ttl method, this method checks if the remaining ttl is
* less than the refreshThreshold.
* In all other cases this method's callback will contain "false" (ie. not expiring).
*
* @function
* @name checkRefreshThreshold
*
* @param {string} key - The cache key to check.
* @param {function} cb
*/
self.checkRefreshThreshold = function(key, cb) {
if (self.refreshThreshold && typeof self.store.ttl === 'function') {
return self.store.ttl(key, function(ttlErr, ttl) {
if (ttlErr || typeof ttl !== 'number') {
return cb(new Error('Invalid TTL response'));
}
if (self.refreshThreshold > ttl) {
return cb(null, true);
}
return cb(null, false);
});
} else {
return cb(new Error('Unhandled refresh'));
}
};
/**
* Wraps a function in cache. I.e., the first time the function is run,

@@ -133,2 +189,3 @@ * its results are stored in cache so subsequent calls retrieve from cache

} else if (self._isCacheableValue(result)) {
handleBackgroundRefresh(key, work, options);
callbackFiller.fill(key, null, result);

@@ -135,0 +192,0 @@ } else {

@@ -32,2 +32,3 @@ /** @module cacheManager/multiCaching */

var callbackFiller = new CallbackFiller();
var backgroundQueue = new Set();

@@ -327,2 +328,29 @@ if (typeof options.isCacheableValue === 'function') {

function handleBackgroundRefresh(caches, index, key, work) {
if (!backgroundQueue.has(key)) {
backgroundQueue.add(key);
caches[index].checkRefreshThreshold(key, function(err, isExpiring) {
if (err) {
backgroundQueue.delete(key);
return;
}
if (isExpiring) {
work(function(workErr, workData) {
if (workErr || !self._isCacheableValue(workData)) {
backgroundQueue.delete(key);
return;
}
if (options && typeof options.ttl === 'function') {
options.ttl = options.ttl(workData);
}
var args = [caches, key, workData, options, function() {
backgroundQueue.delete(key);
}];
setInMultipleCaches.apply(null, args);
});
}
});
}
}
/**

@@ -378,2 +406,3 @@ * Wraps a function in one or more caches.

} else if (self._isCacheableValue(result)) {
handleBackgroundRefresh(caches, index, key, work);
var cachesToUpdate = caches.slice(0, index);

@@ -380,0 +409,0 @@ var args = [cachesToUpdate, key, result, options, function(err) {

2

package.json
{
"name": "cache-manager",
"version": "3.1.0",
"version": "3.2.0",
"description": "Cache module for Node.js",

@@ -5,0 +5,0 @@ "main": "index.js",

@@ -429,2 +429,29 @@ [![build status](https://secure.travis-ci.org/BryanDonovan/node-cache-manager.svg)](http://travis-ci.org/BryanDonovan/node-cache-manager)

### Refresh cache keys in background
Both the `caching` and `multicaching` modules support a mechanism to refresh expiring cache keys in background when using `wrap` function.
This is done by adding a `refreshThreshold` attribute while creating the caching store.
If `refreshThreshold` is set and if the `ttl` method is available for the used store, after retrieving a value from cache TTL will be checked.
If the remaining TTL is less than `refreshThreshold`, the system will spawn a background worker to update the value, following same rules as standard fetching. In the meantime, the system will return the old value until expiration.
In case of multicaching, the store that will be used for refresh is the one where the key will be found first (highest priority). The value will then be set in all the stores.
NOTES:
* In case of multicaching, the store that will be checked for refresh is the one where the key will be found first (highest priority).
* If the threshold is low and the worker function is slow, the key may expire and you may encounter a racing condition with updating values.
* The background refresh mechanism currently does not support providing multiple keys to `wrap` function.
* The caching store needs to provide the `ttl` method.
For example, pass the refreshThreshold to `caching` like this:
```javascript
var redisStore = require('cache-manager-ioredis');
var redisCache = cacheManager.caching({store: redisStore, refreshThreshold: 3, isCacheableValue: isCacheableValue});
```
When a value will be retrieved from Redis with a remaining TTL < 3sec, the value will be updated in background.
### Development environment

@@ -431,0 +458,0 @@ You may disable real caching but still get all the callback functionality working by setting `none` store.

@@ -1108,2 +1108,186 @@ // TODO: These are really a mix of unit and integration tests.

describe("using tweaked memory (lru-cache) store with a refreshThreshold", function() {
var memoryStoreStub;
var opts;
beforeEach(function() {
opts = {ttl: 10};
memoryStoreStub = memoryStore.create(opts);
sinon.stub(memoryStore, 'create').returns(memoryStoreStub);
cache = caching({store: 'memory', ttl: opts.ttl, refreshThreshold: 2, ignoreCacheErrors: false});
key = support.random.string(20);
name = support.random.string();
});
afterEach(function() {
memoryStore.create.restore();
});
context("when result is already cached", function() {
function getCachedWidget(name, cb) {
cache.wrap(key, function(cacheCb) {
methods.getWidget(name, cacheCb);
}, opts, cb);
}
beforeEach(function(done) {
memoryStoreStub.ttl = function(key, cb) {
return cb(null, 5);
};
sinon.spy(memoryStoreStub, 'ttl');
getCachedWidget(name, function(err, widget) {
checkErr(err);
assert.ok(widget);
memoryStoreStub.get(key, function(err, result) {
checkErr(err);
assert.ok(result);
sinon.spy(memoryStoreStub, 'get');
sinon.spy(memoryStoreStub, 'set');
done();
});
});
});
afterEach(function() {
memoryStoreStub.get.restore();
memoryStoreStub.set.restore();
memoryStoreStub.ttl.restore();
});
it("retrieves data from cache and checks ttl without refreshing", function(done) {
var funcCalled = false;
cache.wrap(key, function(cb) {
funcCalled = true;
methods.getWidget(name, function(err, result) {
cb(err, result);
});
}, function(err, widget) {
checkErr(err);
assert.deepEqual(widget, {name: name});
assert.ok(memoryStoreStub.get.calledWith(key));
assert.ok(memoryStoreStub.ttl.calledWith(key));
assert.equal(funcCalled, false);
assert.equal(memoryStoreStub.set.callCount, 0);
done();
});
});
});
context("when result is already cached but expiring", function() {
function getCachedWidget(name, cb) {
cache.wrap(key, function(cacheCb) {
methods.getWidget(name, cacheCb);
}, opts, cb);
}
beforeEach(function(done) {
memoryStoreStub.ttl = function(key, cb) {
return cb(null, 1);
};
sinon.spy(memoryStoreStub, 'ttl');
getCachedWidget(name, function(err, widget) {
checkErr(err);
assert.ok(widget);
memoryStoreStub.get(key, function(err, result) {
checkErr(err);
assert.ok(result);
sinon.spy(memoryStoreStub, 'get');
sinon.spy(memoryStoreStub, 'set');
done();
});
});
});
afterEach(function() {
memoryStoreStub.get.restore();
memoryStoreStub.set.restore();
memoryStoreStub.ttl.restore();
});
it("returns value and invokes worker in background", function(done) {
var funcCalled = false;
cache.wrap(key, function(cb) {
funcCalled = true;
methods.getWidget(name, function(err, result) {
cb(err, result);
});
}, function(err, widget) {
// Wait for just a bit, to be sure that the callback is called.
setTimeout(function() {
checkErr(err);
assert.deepEqual(widget, {name: name});
assert.ok(memoryStoreStub.get.calledWith(key));
assert.ok(memoryStoreStub.ttl.calledWith(key));
assert.equal(funcCalled, true);
assert.equal(memoryStoreStub.set.callCount, 1);
done();
}, 500);
});
});
it("returns value and invokes worker in background once when called multiple times", function(done) {
var funcCalled = 0;
var values = [];
for (var i = 0; i < 2; i++) {
values.push(i);
}
async.each(values, function(val, next) {
cache.wrap(key, function(cb) {
funcCalled += 1;
var timeout = support.random.number(100);
setTimeout(function() {
methods.getWidget(name, function(err, result) {
cb(err, result);
});
}, timeout);
}, function(err, widget) {
checkErr(err);
assert.deepEqual(widget, {name: name});
next(err);
});
}, function(err) {
// Wait for just a bit, to be sure that the callback is called.
setTimeout(function() {
checkErr(err);
assert.ok(memoryStoreStub.get.calledWith(key));
assert.equal(memoryStoreStub.ttl.callCount, 1);
assert.equal(funcCalled, 1);
assert.equal(memoryStoreStub.set.callCount, 1);
done();
}, 500);
});
});
it("returns value and invokes worker in background discarding result if error", function(done) {
var funcCalled = false;
var fakeError = new Error(support.random.string());
cache.wrap(key, function(cb) {
funcCalled = true;
cb(fakeError);
}, function(err, widget) {
// Wait for just a bit, to be sure that the callback is called.
setTimeout(function() {
checkErr(err);
assert.deepEqual(widget, {name: name});
assert.ok(memoryStoreStub.get.calledWith(key));
assert.ok(memoryStoreStub.ttl.calledWith(key));
assert.equal(funcCalled, true);
assert.equal(memoryStoreStub.set.callCount, 0);
done();
}, 500);
});
});
});
});
describe("when called multiple times in parallel with same key", function() {

@@ -1110,0 +1294,0 @@ var construct;

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc