flashheart
Advanced tools
Comparing version 2.7.1 to 2.8.0
@@ -8,2 +8,3 @@ var _ = require('lodash'); | ||
this.cache = opts.cache; | ||
this.staleIfError = opts.staleIfError; | ||
this.doNotVary = opts.doNotVary || []; | ||
@@ -16,3 +17,6 @@ } | ||
if (cacheControl) { | ||
return !cacheControl['no-cache'] && !cacheControl.private && cacheControl['max-age'] > 0; | ||
const hasMaxAge = cacheControl['max-age'] > 0; | ||
const hasStaleIfError = cacheControl['stale-if-error'] > 0; | ||
return !cacheControl['no-cache'] && !cacheControl.private && (hasMaxAge || hasStaleIfError); | ||
} | ||
@@ -23,13 +27,20 @@ | ||
function getMaxAge(res) { | ||
var maxAge = -1; | ||
function getCacheTime(res, directive) { | ||
var cacheControl = getCacheControl(res); | ||
if (cacheControl) { | ||
maxAge = (cacheControl['max-age'] || 0) * 1000; | ||
return (cacheControl[directive] || 0) * 1000; | ||
} | ||
return maxAge; | ||
return -1; | ||
} | ||
function getMaxAge(res) { | ||
return getCacheTime(res, 'max-age'); | ||
} | ||
function getStaleIfError(res) { | ||
return getCacheTime(res, 'stale-if-error'); | ||
} | ||
function getCacheControl(res) { | ||
@@ -66,2 +77,6 @@ var cacheControl = res.headers['cache-control']; | ||
function createStaleKeyObject(url, opts, doNotVary) { | ||
return _.extend({}, createKeyObject(url, opts, doNotVary), { stale: true }); | ||
} | ||
CachingClient.prototype.get = function (url, opts, cb) { | ||
@@ -80,5 +95,7 @@ if (typeof opts === 'function') { | ||
var maxAge; | ||
var staleIfError; | ||
if (!servedFromCache && res && isCacheable(res)) { | ||
maxAge = getMaxAge(res); | ||
staleIfError = getStaleIfError(res); | ||
cachedObject = {}; | ||
@@ -101,3 +118,9 @@ | ||
cache.set(createKeyObject(url, opts, doNotVary), cachedObject, maxAge, client._handleCacheError.bind(client)); | ||
if (maxAge) { | ||
cache.set(createKeyObject(url, opts, doNotVary), cachedObject, maxAge, client._handleCacheError.bind(client)); | ||
} | ||
if (client.staleIfError && staleIfError) { | ||
cache.set(createStaleKeyObject(url, opts, doNotVary), cachedObject, staleIfError, client._handleCacheError.bind(client)); | ||
} | ||
} | ||
@@ -109,2 +132,12 @@ | ||
function createErrorFromCache(cachedError) { | ||
var err = new Error(cachedError.message); | ||
err.statusCode = cachedError.statusCode; | ||
err.body = cachedError.body; | ||
err.headers = cachedError.headers; | ||
return err; | ||
} | ||
CachingClient.prototype._getCachedOrFetch = function (url, opts, cb) { | ||
@@ -120,3 +153,2 @@ var client = this; | ||
var cacheHit = cached && cached.item && (cached.item.body || cached.item.error); | ||
var cachedError; | ||
@@ -127,7 +159,3 @@ if (cacheHit) { | ||
if (cached.item.error) { | ||
cachedError = cached.item.error; | ||
err = new Error(cachedError.message); | ||
err.statusCode = cachedError.statusCode; | ||
err.body = cachedError.body; | ||
err.headers = cachedError.headers; | ||
err = createErrorFromCache(cached.item.error); | ||
} | ||
@@ -140,3 +168,17 @@ | ||
delegate.getWithResponse(url, opts, cb); | ||
delegate.getWithResponse(url, opts, function (err, body, res) { | ||
if (err && client.staleIfError) { | ||
var originalErr = err; | ||
return cache.get(createStaleKeyObject(url, opts, doNotVary), function (err, cached) { | ||
if (err) client._handleCacheError(err); | ||
if (!cached) return cb(originalErr, body, res); | ||
if (delegate.stats) delegate.stats.increment(delegate.name + '.cache.stale'); | ||
cb(err, cached.item.body, cached.item.response, true); | ||
}); | ||
} | ||
cb(err, body, res); | ||
}); | ||
}); | ||
@@ -143,0 +185,0 @@ }; |
{ | ||
"name": "flashheart", | ||
"version": "2.7.1", | ||
"version": "2.8.0", | ||
"description": "A fully-featured REST client built for ease-of-use and resilience", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -90,2 +90,17 @@ # flashheart | ||
Optionally, you can enable `staleIfError` which will start listening to the `stale-if-error` directive as well. This stores the response for the duration of the `stale-if-error` directive as well as the `max-age` and will try to retrieve them in this order: | ||
* `max-age` stored version | ||
* fresh version | ||
* `stale-if-error` version | ||
This is enabled simply by passing in the `staleIfError` parameter to `createClient`: | ||
```js | ||
const client = require('flashheart').createClient({ | ||
cache: storage, | ||
staleIfError: true | ||
}); | ||
``` | ||
The cache varies on _all_ request options (and therefore, headers) by default. If you don't want to vary on a particular header, use the `doNotVary` option: | ||
@@ -92,0 +107,0 @@ |
@@ -19,3 +19,3 @@ var async = require('async'); | ||
headers: { | ||
'cache-control': 'max-age=60', | ||
'cache-control': 'max-age=60,stale-if-error=7200', | ||
'content-type': 'application/json' | ||
@@ -30,6 +30,7 @@ }, | ||
var client; | ||
var staleClient; | ||
var catbox; | ||
var headers = { | ||
'cache-control': 'max-age=60' | ||
'cache-control': 'max-age=60,stale-if-error=7200' | ||
}; | ||
@@ -42,2 +43,8 @@ | ||
var expectedStaleKey = { | ||
segment: 'flashheart:' + require('../package').version, | ||
id: url, | ||
stale: true | ||
}; | ||
beforeEach(function () { | ||
@@ -66,2 +73,9 @@ nock.cleanAll(); | ||
}); | ||
staleClient = Client.createClient({ | ||
cache: catbox, | ||
stats: stats, | ||
logger: logger, | ||
retries: 0, | ||
staleIfError: true | ||
}); | ||
}); | ||
@@ -80,2 +94,13 @@ | ||
it('caches the stale value if stale-if-error is set', function (done) { | ||
staleClient.get(url, function (err) { | ||
assert.ifError(err); | ||
sinon.assert.calledWith(catbox.set, expectedStaleKey, { | ||
body: responseBody, | ||
response: expectedCachedResponse | ||
}, 7200000); | ||
done(); | ||
}); | ||
}); | ||
it('returns the response from the cache if it exists', function (done) { | ||
@@ -136,2 +161,70 @@ var cachedResponseBody = { | ||
it('attempts to serve stale if error received and staleIfError is enabled', function (done) { | ||
var cachedResponseBody = { | ||
foo: 'baz' | ||
}; | ||
var errorResponseCode = 503; | ||
var errorResponseBody = { | ||
error: 'An error' | ||
}; | ||
var errorHeaders = { | ||
'cache-control': 'max-age=5', | ||
'content-type': 'application/json', | ||
'www-authenticate': 'Bearer realm="/"' | ||
}; | ||
catbox.get.withArgs(expectedStaleKey).yields(null, { | ||
item: { | ||
body: cachedResponseBody, | ||
response: expectedCachedResponse | ||
} | ||
}); | ||
nock.cleanAll(); | ||
api.get('/').reply(errorResponseCode, errorResponseBody, errorHeaders); | ||
staleClient.get(url, function (err, body, res) { | ||
assert.ifError(err); | ||
assert.deepEqual(body, cachedResponseBody); | ||
assert.deepEqual(res, expectedCachedResponse); | ||
sinon.assert.notCalled(catbox.set); | ||
sinon.assert.calledTwice(catbox.get); | ||
done(); | ||
}); | ||
}); | ||
it('does not store stale cache if staleIfError is not enabled', function (done) { | ||
client.get(url, function (err) { | ||
assert.ifError(err); | ||
sinon.assert.calledOnce(catbox.set); | ||
sinon.assert.calledWith(catbox.set, expectedKey, { | ||
body: responseBody, | ||
response: expectedCachedResponse | ||
}, 60000); | ||
done(); | ||
}); | ||
}); | ||
it('does not read stale cache if staleIfError is not enabled', function (done) { | ||
var errorResponseCode = 503; | ||
var errorResponseBody = { | ||
error: 'An error' | ||
}; | ||
var errorHeaders = { | ||
'cache-control': 'max-age=5', | ||
'content-type': 'application/json', | ||
'www-authenticate': 'Bearer realm="/"' | ||
}; | ||
nock.cleanAll(); | ||
api.get('/').reply(errorResponseCode, errorResponseBody, errorHeaders); | ||
client.get(url, function (err) { | ||
assert.ok(err); | ||
assert.equal(err.statusCode, 503); | ||
sinon.assert.calledOnce(catbox.get); | ||
sinon.assert.calledWith(catbox.get, expectedKey); | ||
done(); | ||
}); | ||
}); | ||
it('returns an error from the cache if it exists', function (done) { | ||
@@ -292,2 +385,28 @@ var errorResponseBody = { | ||
it('stores content for stale duration if staleIfError enabled and max-age value is zero', function (done) { | ||
var headers = { | ||
'cache-control': 'max-age=0,stale-if-error=7200' | ||
}; | ||
var expectedCachedResponse = { | ||
statusCode: 200, | ||
headers: { | ||
'cache-control': 'max-age=0,stale-if-error=7200', | ||
'content-type': 'application/json' | ||
}, | ||
elapsedTime: nockElapsedTime | ||
}; | ||
nock.cleanAll(); | ||
api.get('/').reply(200, responseBody, headers); | ||
staleClient.get(url, function (err) { | ||
assert.ifError(err); | ||
sinon.assert.calledOnce(catbox.set); | ||
sinon.assert.calledWith(catbox.set, expectedStaleKey, { | ||
body: responseBody, | ||
response: expectedCachedResponse | ||
}, 7200000); | ||
done(); | ||
}); | ||
}); | ||
it('does not cache the response if the max-age value is invalid', function (done) { | ||
@@ -307,2 +426,28 @@ var headers = { | ||
it('does not store a stale value if stale-if-error missing', function (done) { | ||
var headers = { | ||
'cache-control': 'max-age=60' | ||
}; | ||
var expectedCachedResponse = { | ||
statusCode: 200, | ||
headers: { | ||
'cache-control': 'max-age=60', | ||
'content-type': 'application/json' | ||
}, | ||
elapsedTime: nockElapsedTime | ||
}; | ||
nock.cleanAll(); | ||
api.get('/').reply(200, responseBody, headers); | ||
staleClient.get(url, function (err) { | ||
assert.ifError(err); | ||
sinon.assert.calledOnce(catbox.set); | ||
sinon.assert.calledWith(catbox.set, expectedKey, { | ||
body: responseBody, | ||
response: expectedCachedResponse | ||
}, 60000); | ||
done(); | ||
}); | ||
}); | ||
it('respects the doNotVary option when creating a cache key', function (done) { | ||
@@ -427,2 +572,35 @@ var reqheaders = { | ||
it('increments a counter when a stale response is sent', function (done) { | ||
var cachedResponseBody = { | ||
foo: 'baz' | ||
}; | ||
var errorResponseCode = 503; | ||
var errorResponseBody = { | ||
error: 'An error' | ||
}; | ||
var errorHeaders = { | ||
'cache-control': 'max-age=5', | ||
'content-type': 'application/json', | ||
'www-authenticate': 'Bearer realm="/"' | ||
}; | ||
catbox.get.withArgs(expectedStaleKey).yields(null, { | ||
item: { | ||
body: cachedResponseBody, | ||
response: expectedCachedResponse | ||
} | ||
}); | ||
nock.cleanAll(); | ||
api.get('/').reply(errorResponseCode, errorResponseBody, errorHeaders); | ||
staleClient.get(url, function (err, body, res) { | ||
assert.ifError(err); | ||
assert.deepEqual(body, cachedResponseBody); | ||
assert.deepEqual(res, expectedCachedResponse); | ||
sinon.assert.calledWith(stats.increment, 'http.cache.stale'); | ||
done(); | ||
}); | ||
}); | ||
it('increments a counter for each cache miss', function (done) { | ||
@@ -429,0 +607,0 @@ client.get(url, function (err) { |
281551
1806
346