• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1const CacheSemantics = require('http-cache-semantics')
2const Negotiator = require('negotiator')
3const ssri = require('ssri')
4
5// options passed to http-cache-semantics constructor
6const policyOptions = {
7  shared: false,
8  ignoreCargoCult: true,
9}
10
11// a fake empty response, used when only testing the
12// request for storability
13const emptyResponse = { status: 200, headers: {} }
14
15// returns a plain object representation of the Request
16const requestObject = (request) => {
17  const _obj = {
18    method: request.method,
19    url: request.url,
20    headers: {},
21    compress: request.compress,
22  }
23
24  request.headers.forEach((value, key) => {
25    _obj.headers[key] = value
26  })
27
28  return _obj
29}
30
31// returns a plain object representation of the Response
32const responseObject = (response) => {
33  const _obj = {
34    status: response.status,
35    headers: {},
36  }
37
38  response.headers.forEach((value, key) => {
39    _obj.headers[key] = value
40  })
41
42  return _obj
43}
44
45class CachePolicy {
46  constructor ({ entry, request, response, options }) {
47    this.entry = entry
48    this.request = requestObject(request)
49    this.response = responseObject(response)
50    this.options = options
51    this.policy = new CacheSemantics(this.request, this.response, policyOptions)
52
53    if (this.entry) {
54      // if we have an entry, copy the timestamp to the _responseTime
55      // this is necessary because the CacheSemantics constructor forces
56      // the value to Date.now() which means a policy created from a
57      // cache entry is likely to always identify itself as stale
58      this.policy._responseTime = this.entry.metadata.time
59    }
60  }
61
62  // static method to quickly determine if a request alone is storable
63  static storable (request, options) {
64    // no cachePath means no caching
65    if (!options.cachePath) {
66      return false
67    }
68
69    // user explicitly asked not to cache
70    if (options.cache === 'no-store') {
71      return false
72    }
73
74    // we only cache GET and HEAD requests
75    if (!['GET', 'HEAD'].includes(request.method)) {
76      return false
77    }
78
79    // otherwise, let http-cache-semantics make the decision
80    // based on the request's headers
81    const policy = new CacheSemantics(requestObject(request), emptyResponse, policyOptions)
82    return policy.storable()
83  }
84
85  // returns true if the policy satisfies the request
86  satisfies (request) {
87    const _req = requestObject(request)
88    if (this.request.headers.host !== _req.headers.host) {
89      return false
90    }
91
92    if (this.request.compress !== _req.compress) {
93      return false
94    }
95
96    const negotiatorA = new Negotiator(this.request)
97    const negotiatorB = new Negotiator(_req)
98
99    if (JSON.stringify(negotiatorA.mediaTypes()) !== JSON.stringify(negotiatorB.mediaTypes())) {
100      return false
101    }
102
103    if (JSON.stringify(negotiatorA.languages()) !== JSON.stringify(negotiatorB.languages())) {
104      return false
105    }
106
107    if (JSON.stringify(negotiatorA.encodings()) !== JSON.stringify(negotiatorB.encodings())) {
108      return false
109    }
110
111    if (this.options.integrity) {
112      return ssri.parse(this.options.integrity).match(this.entry.integrity)
113    }
114
115    return true
116  }
117
118  // returns true if the request and response allow caching
119  storable () {
120    return this.policy.storable()
121  }
122
123  // NOTE: this is a hack to avoid parsing the cache-control
124  // header ourselves, it returns true if the response's
125  // cache-control contains must-revalidate
126  get mustRevalidate () {
127    return !!this.policy._rescc['must-revalidate']
128  }
129
130  // returns true if the cached response requires revalidation
131  // for the given request
132  needsRevalidation (request) {
133    const _req = requestObject(request)
134    // force method to GET because we only cache GETs
135    // but can serve a HEAD from a cached GET
136    _req.method = 'GET'
137    return !this.policy.satisfiesWithoutRevalidation(_req)
138  }
139
140  responseHeaders () {
141    return this.policy.responseHeaders()
142  }
143
144  // returns a new object containing the appropriate headers
145  // to send a revalidation request
146  revalidationHeaders (request) {
147    const _req = requestObject(request)
148    return this.policy.revalidationHeaders(_req)
149  }
150
151  // returns true if the request/response was revalidated
152  // successfully. returns false if a new response was received
153  revalidated (request, response) {
154    const _req = requestObject(request)
155    const _res = responseObject(response)
156    const policy = this.policy.revalidatedPolicy(_req, _res)
157    return !policy.modified
158  }
159}
160
161module.exports = CachePolicy
162