You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
161 lines
4.4 KiB
161 lines
4.4 KiB
const CacheSemantics = require('http-cache-semantics') |
|
const Negotiator = require('negotiator') |
|
const ssri = require('ssri') |
|
|
|
// options passed to http-cache-semantics constructor |
|
const policyOptions = { |
|
shared: false, |
|
ignoreCargoCult: true, |
|
} |
|
|
|
// a fake empty response, used when only testing the |
|
// request for storability |
|
const emptyResponse = { status: 200, headers: {} } |
|
|
|
// returns a plain object representation of the Request |
|
const requestObject = (request) => { |
|
const _obj = { |
|
method: request.method, |
|
url: request.url, |
|
headers: {}, |
|
compress: request.compress, |
|
} |
|
|
|
request.headers.forEach((value, key) => { |
|
_obj.headers[key] = value |
|
}) |
|
|
|
return _obj |
|
} |
|
|
|
// returns a plain object representation of the Response |
|
const responseObject = (response) => { |
|
const _obj = { |
|
status: response.status, |
|
headers: {}, |
|
} |
|
|
|
response.headers.forEach((value, key) => { |
|
_obj.headers[key] = value |
|
}) |
|
|
|
return _obj |
|
} |
|
|
|
class CachePolicy { |
|
constructor ({ entry, request, response, options }) { |
|
this.entry = entry |
|
this.request = requestObject(request) |
|
this.response = responseObject(response) |
|
this.options = options |
|
this.policy = new CacheSemantics(this.request, this.response, policyOptions) |
|
|
|
if (this.entry) { |
|
// if we have an entry, copy the timestamp to the _responseTime |
|
// this is necessary because the CacheSemantics constructor forces |
|
// the value to Date.now() which means a policy created from a |
|
// cache entry is likely to always identify itself as stale |
|
this.policy._responseTime = this.entry.metadata.time |
|
} |
|
} |
|
|
|
// static method to quickly determine if a request alone is storable |
|
static storable (request, options) { |
|
// no cachePath means no caching |
|
if (!options.cachePath) { |
|
return false |
|
} |
|
|
|
// user explicitly asked not to cache |
|
if (options.cache === 'no-store') { |
|
return false |
|
} |
|
|
|
// we only cache GET and HEAD requests |
|
if (!['GET', 'HEAD'].includes(request.method)) { |
|
return false |
|
} |
|
|
|
// otherwise, let http-cache-semantics make the decision |
|
// based on the request's headers |
|
const policy = new CacheSemantics(requestObject(request), emptyResponse, policyOptions) |
|
return policy.storable() |
|
} |
|
|
|
// returns true if the policy satisfies the request |
|
satisfies (request) { |
|
const _req = requestObject(request) |
|
if (this.request.headers.host !== _req.headers.host) { |
|
return false |
|
} |
|
|
|
if (this.request.compress !== _req.compress) { |
|
return false |
|
} |
|
|
|
const negotiatorA = new Negotiator(this.request) |
|
const negotiatorB = new Negotiator(_req) |
|
|
|
if (JSON.stringify(negotiatorA.mediaTypes()) !== JSON.stringify(negotiatorB.mediaTypes())) { |
|
return false |
|
} |
|
|
|
if (JSON.stringify(negotiatorA.languages()) !== JSON.stringify(negotiatorB.languages())) { |
|
return false |
|
} |
|
|
|
if (JSON.stringify(negotiatorA.encodings()) !== JSON.stringify(negotiatorB.encodings())) { |
|
return false |
|
} |
|
|
|
if (this.options.integrity) { |
|
return ssri.parse(this.options.integrity).match(this.entry.integrity) |
|
} |
|
|
|
return true |
|
} |
|
|
|
// returns true if the request and response allow caching |
|
storable () { |
|
return this.policy.storable() |
|
} |
|
|
|
// NOTE: this is a hack to avoid parsing the cache-control |
|
// header ourselves, it returns true if the response's |
|
// cache-control contains must-revalidate |
|
get mustRevalidate () { |
|
return !!this.policy._rescc['must-revalidate'] |
|
} |
|
|
|
// returns true if the cached response requires revalidation |
|
// for the given request |
|
needsRevalidation (request) { |
|
const _req = requestObject(request) |
|
// force method to GET because we only cache GETs |
|
// but can serve a HEAD from a cached GET |
|
_req.method = 'GET' |
|
return !this.policy.satisfiesWithoutRevalidation(_req) |
|
} |
|
|
|
responseHeaders () { |
|
return this.policy.responseHeaders() |
|
} |
|
|
|
// returns a new object containing the appropriate headers |
|
// to send a revalidation request |
|
revalidationHeaders (request) { |
|
const _req = requestObject(request) |
|
return this.policy.revalidationHeaders(_req) |
|
} |
|
|
|
// returns true if the request/response was revalidated |
|
// successfully. returns false if a new response was received |
|
revalidated (request, response) { |
|
const _req = requestObject(request) |
|
const _res = responseObject(response) |
|
const policy = this.policy.revalidatedPolicy(_req, _res) |
|
return !policy.modified |
|
} |
|
} |
|
|
|
module.exports = CachePolicy
|
|
|