codekingpro/portable-devtools
114k
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 