Browse Source

Better Vary

master
Kornel Lesiński 9 years ago
parent
commit
35eb7ef912
  1. 9
      README.md
  2. 41
      index.js
  3. 70
      test/varytest.js

9
README.md

@ -19,9 +19,10 @@ Cacheability of response depends on how it was requested, so both request and re
```js
const request = {
url: '/',
method: 'GET',
headers: {
'accept': '*/*',
accept: '*/*',
},
};
@ -39,6 +40,12 @@ const options = {
If `options.shared` is true (default), then response is evaluated from perspective of a shared cache (i.e. `private` is not cacheable and `s-maxage` is respected). If `options.shared` is false, then response is evaluated from perspective of a single-user cache (i.e. `private` is cacheable and `s-maxage` is ignored).
### `satisfiesWithoutRevalidation(request)`
If it returns `true`, then the given `request` matches the response this cache policy has been created with, and the existing response can be used without contacting the server.
If it returns `false`, then the response may not be matching at all (e.g. it's different URL or method), or may require to be refreshed first.
### `storable()`
Returns `true` if the response can be stored in a cache. If it's `false` then you MUST NOT store either request or the response.

41
index.js

@ -88,21 +88,50 @@ CachePolicy.prototype = {
this._res.headers.expires;
},
satisfiesWithoutRevalidation(req) {
if (!req || !req.headers) {
throw Error("Request headers missing");
}
// When presented with a request, a cache MUST NOT reuse a stored response, unless:
// the presented request does not contain the no-cache pragma (Section 5.4), nor the no-cache cache directive,
// unless the stored response is successfully validated (Section 4.3), and
const requestCC = parseCacheControl(req.headers['cache-control']);
if (requestCC['no-cache'] || /no-cache/.test(req.headers.pragma)) {
return false;
}
// The presented effective request URI and that of the stored response match, and
return (!this._req.url || this._req.url === req.url) &&
// the request method associated with the stored response allows it to be used for the presented request, and
(!this._req.method || this._req.method === req.method) &&
// selecting header fields nominated by the stored response (if any) match those presented, and
this._varyMatches(req) &&
// the stored response is either:
// fresh, or allowed to be served stale
!this.stale() // TODO: allow stale
},
_allowsStoringAuthenticated() {
// following Cache-Control response directives (Section 5.2.2) have such an effect: must-revalidate, public, and s-maxage.
return this._rescc['must-revalidate'] || this._rescc['public'] || this._rescc['s-maxage'];
},
_varyKeyForRequest(req) {
if (!this._res.headers.vary) return '';
_varyMatches(req) {
if (!this._res.headers.vary) {
return true;
}
// A Vary header field-value of "*" always fails to match
if (this._req.headers.vary === '*') {
return false;
}
let key = '';
const fields = this._res.headers.vary.toLowerCase().split(/\s*,\s*/);
fields.sort();
for(const name of fields) {
key += `${name}:${req.headers[name] || '÷'}\n`;
if (req.headers[name] !== this._req.headers[name]) return false;
}
return key;
return true;
},
/**

70
test/varytest.js

@ -5,71 +5,63 @@ const CachePolicy = require('..');
describe('Vary', function() {
it('Basic', function() {
const cache1 = new CachePolicy({method:'GET',headers:{'weather': 'nice'}}, {headers:{'vary':'weather'}});
const cache2 = new CachePolicy({method:'GET',headers:{'weather': 'bad'}}, {headers:{'vary':'WEATHER'}});
const policy = new CachePolicy({headers:{'weather': 'nice'}}, {headers:{'cache-control':'max-age=5','vary':'weather'}});
assert.equal(cache1.cacheKey(), cache1.cacheKey());
assert.equal(cache2.cacheKey(), cache2.cacheKey());
assert.notEqual(cache1.cacheKey(), cache2.cacheKey());
assert(policy.satisfiesWithoutRevalidation({headers:{'weather': 'nice'}}));
assert(!policy.satisfiesWithoutRevalidation({headers:{'weather': 'bad'}}));
});
it("* doesn't match other", function() {
const cache1 = new CachePolicy({method:'GET',headers:{'weather': 'ok'}}, {headers:{'vary':'*'}});
const cache2 = new CachePolicy({method:'GET',headers:{'weather': 'ok'}}, {headers:{'vary':'weather'}});
it("* doesn't match", function() {
const policy = new CachePolicy({headers:{'weather': 'ok'}}, {headers:{'cache-control':'max-age=5','vary':'*'}});
assert.equal(cache2.cacheKey(), cache2.cacheKey());
assert.notEqual(cache1.cacheKey(), cache2.cacheKey());
assert(!policy.satisfiesWithoutRevalidation({headers:{'weather': 'ok'}}));
});
it("* is stale", function() {
const cache1 = new CachePolicy({method:'GET',headers:{'weather': 'ok'}}, {headers:{'cache-control':'public,max-age=99', 'vary':'*'}});
const cache2 = new CachePolicy({method:'GET',headers:{'weather': 'ok'}}, {headers:{'cache-control':'public,max-age=99', 'vary':'weather'}});
const policy1 = new CachePolicy({headers:{'weather': 'ok'}}, {headers:{'cache-control':'public,max-age=99', 'vary':'*'}});
const policy2 = new CachePolicy({headers:{'weather': 'ok'}}, {headers:{'cache-control':'public,max-age=99', 'vary':'weather'}});
assert.notEqual(cache1.cacheKey(), cache2.cacheKey());
assert(cache1.stale());
assert(!cache2.stale());
assert(policy1.stale());
assert(!policy2.stale());
});
it('Values are case-sensitive', function() {
const cache1 = new CachePolicy({method:'GET',headers:{'weather': 'BAD'}}, {headers:{'vary':'weather'}});
const cache2 = new CachePolicy({method:'GET',headers:{'weather': 'bad'}}, {headers:{'vary':'weather'}});
const policy = new CachePolicy({headers:{'weather': 'BAD'}}, {headers:{'cache-control':'max-age=5','vary':'Weather'}});
assert.notEqual(cache1.cacheKey(), cache2.cacheKey());
assert(policy.satisfiesWithoutRevalidation({headers:{'weather': 'BAD'}}));
assert(!policy.satisfiesWithoutRevalidation({headers:{'weather': 'bad'}}));
});
it('Irrelevant headers ignored', function() {
const cache1 = new CachePolicy({method:'GET',headers:{'weather': 'nice'}}, {headers:{'vary':'moon-phase'}});
const cache2 = new CachePolicy({method:'GET',headers:{'weather': 'bad'}}, {headers:{'vary':'moon-phase'}});
const policy = new CachePolicy({headers:{'weather': 'nice'}}, {headers:{'cache-control':'max-age=5','vary':'moon-phase'}});
assert.equal(cache1.cacheKey(), cache1.cacheKey());
assert.equal(cache1.cacheKey(), cache2.cacheKey());
assert(policy.satisfiesWithoutRevalidation({headers:{'weather': 'bad'}}));
assert(policy.satisfiesWithoutRevalidation({headers:{'sun': 'shining'}}));
assert(!policy.satisfiesWithoutRevalidation({headers:{'moon-phase': 'full'}}));
});
it('Absence is meaningful', function() {
const cache1 = new CachePolicy({method:'GET',headers:{'weather': 'nice'}}, {headers:{'vary':'moon-phase'}});
const cache2 = new CachePolicy({method:'GET',headers:{'weather': 'bad'}}, {headers:{'vary':'sunshine'}});
const policy = new CachePolicy({headers:{'weather': 'nice'}}, {headers:{'cache-control':'max-age=5','vary':'moon-phase, weather'}});
assert.equal(cache2.cacheKey(), cache2.cacheKey());
assert.notEqual(cache1.cacheKey(), cache2.cacheKey());
assert(policy.satisfiesWithoutRevalidation({headers:{'weather': 'nice'}}));
assert(!policy.satisfiesWithoutRevalidation({headers:{'weather': 'nice', 'moon-phase': ''}}));
assert(!policy.satisfiesWithoutRevalidation({headers:{}}));
});
it('All values must match', function() {
const cache1 = new CachePolicy({method:'GET',headers:{'sun': 'shining', 'weather': 'nice'}}, {headers:{'vary':'weather, sun'}});
const cache2 = new CachePolicy({method:'GET',headers:{'sun': 'shining', 'weather': 'bad'}}, {headers:{'vary':'weather, sun'}});
assert.notEqual(cache1.cacheKey(), cache2.cacheKey());
const policy = new CachePolicy({headers:{'sun': 'shining', 'weather': 'nice'}}, {headers:{'cache-control':'max-age=5','vary':'weather, sun'}});
assert(policy.satisfiesWithoutRevalidation({headers:{'sun': 'shining', 'weather': 'nice'}}));
assert(!policy.satisfiesWithoutRevalidation({headers:{'sun': 'shining', 'weather': 'bad'}}));
});
it('Order is irrelevant', function() {
const cache1 = new CachePolicy({method:'GET',headers:{'weather': 'nice'}}, {headers:{'vary':'moon-phase, SUNSHINE'}});
const cache2 = new CachePolicy({method:'GET',headers:{'weather': 'bad'}}, {headers:{'vary':'sunshine, moon-phase'}});
assert.equal(cache1.cacheKey(), cache2.cacheKey());
const cache3 = new CachePolicy({method:'GET',headers:{'weather': 'nice'}}, {headers:{'vary':'moon-phase, weather'}});
const cache4 = new CachePolicy({method:'GET',headers:{'weather': 'nice'}}, {headers:{'vary':'weather, moon-phase'}});
assert.equal(cache3.cacheKey(), cache4.cacheKey());
const policy1 = new CachePolicy({headers:{'sun': 'shining', 'weather': 'nice'}}, {headers:{'cache-control':'max-age=5','vary':'weather, sun'}});
const policy2 = new CachePolicy({headers:{'sun': 'shining', 'weather': 'nice'}}, {headers:{'cache-control':'max-age=5','vary':'sun, weather'}});
assert.notEqual(cache1.cacheKey(), cache3.cacheKey());
assert.notEqual(cache2.cacheKey(), cache4.cacheKey());
assert(policy1.satisfiesWithoutRevalidation({headers:{'weather': 'nice', 'sun': 'shining'}}));
assert(policy1.satisfiesWithoutRevalidation({headers:{'sun': 'shining', 'weather': 'nice'}}));
assert(policy2.satisfiesWithoutRevalidation({headers:{'weather': 'nice', 'sun': 'shining'}}));
assert(policy2.satisfiesWithoutRevalidation({headers:{'sun': 'shining', 'weather': 'nice'}}));
});
});

Loading…
Cancel
Save