undici 7.0.0-alpha.2 → 7.0.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/docs/api/CacheStore.md +116 -0
- package/docs/docs/api/Dispatcher.md +10 -0
- package/index.js +6 -1
- package/lib/api/api-request.js +1 -1
- package/lib/api/readable.js +6 -6
- package/lib/cache/memory-cache-store.js +417 -0
- package/lib/core/constants.js +24 -1
- package/lib/core/util.js +41 -2
- package/lib/dispatcher/client-h1.js +100 -87
- package/lib/dispatcher/client-h2.js +127 -75
- package/lib/dispatcher/pool-base.js +3 -3
- package/lib/handler/cache-handler.js +359 -0
- package/lib/handler/cache-revalidation-handler.js +119 -0
- package/lib/interceptor/cache.js +171 -0
- package/lib/util/cache.js +224 -0
- package/lib/web/cache/cache.js +1 -0
- package/lib/web/cache/cachestorage.js +2 -0
- package/lib/web/eventsource/eventsource.js +2 -0
- package/lib/web/fetch/constants.js +12 -5
- package/lib/web/fetch/data-url.js +2 -2
- package/lib/web/fetch/formdata.js +3 -1
- package/lib/web/fetch/headers.js +2 -0
- package/lib/web/fetch/request.js +3 -1
- package/lib/web/fetch/response.js +3 -1
- package/lib/web/fetch/util.js +171 -47
- package/lib/web/fetch/webidl.js +16 -12
- package/lib/web/websocket/constants.js +67 -6
- package/lib/web/websocket/events.js +4 -0
- package/lib/web/websocket/stream/websocketerror.js +1 -1
- package/lib/web/websocket/websocket.js +2 -0
- package/package.json +7 -3
- package/types/cache-interceptor.d.ts +97 -0
- package/types/fetch.d.ts +9 -8
- package/types/index.d.ts +3 -0
- package/types/interceptors.d.ts +4 -0
- package/types/webidl.d.ts +7 -1
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const util = require('../core/util')
|
|
4
|
+
const DecoratorHandler = require('../handler/decorator-handler')
|
|
5
|
+
const {
|
|
6
|
+
parseCacheControlHeader,
|
|
7
|
+
parseVaryHeader
|
|
8
|
+
} = require('../util/cache')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Writes a response to a CacheStore and then passes it on to the next handler
|
|
12
|
+
*/
|
|
13
|
+
class CacheHandler extends DecoratorHandler {
|
|
14
|
+
/**
|
|
15
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
|
|
16
|
+
*/
|
|
17
|
+
#store
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @type {import('../../types/dispatcher.d.ts').default.RequestOptions}
|
|
21
|
+
*/
|
|
22
|
+
#requestOptions
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @type {import('../../types/dispatcher.d.ts').default.DispatchHandlers}
|
|
26
|
+
*/
|
|
27
|
+
#handler
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheStoreWriteable | undefined}
|
|
31
|
+
*/
|
|
32
|
+
#writeStream
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} opts
|
|
36
|
+
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} requestOptions
|
|
37
|
+
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
|
|
38
|
+
*/
|
|
39
|
+
constructor (opts, requestOptions, handler) {
|
|
40
|
+
const { store } = opts
|
|
41
|
+
|
|
42
|
+
super(handler)
|
|
43
|
+
|
|
44
|
+
this.#store = store
|
|
45
|
+
this.#requestOptions = requestOptions
|
|
46
|
+
this.#handler = handler
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @see {DispatchHandlers.onHeaders}
|
|
51
|
+
*
|
|
52
|
+
* @param {number} statusCode
|
|
53
|
+
* @param {Buffer[]} rawHeaders
|
|
54
|
+
* @param {() => void} resume
|
|
55
|
+
* @param {string} statusMessage
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
onHeaders (
|
|
59
|
+
statusCode,
|
|
60
|
+
rawHeaders,
|
|
61
|
+
resume,
|
|
62
|
+
statusMessage
|
|
63
|
+
) {
|
|
64
|
+
const downstreamOnHeaders = () => this.#handler.onHeaders(
|
|
65
|
+
statusCode,
|
|
66
|
+
rawHeaders,
|
|
67
|
+
resume,
|
|
68
|
+
statusMessage
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
!util.safeHTTPMethods.includes(this.#requestOptions.method) &&
|
|
73
|
+
statusCode >= 200 &&
|
|
74
|
+
statusCode <= 399
|
|
75
|
+
) {
|
|
76
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-respons
|
|
77
|
+
// Try/catch for if it's synchronous
|
|
78
|
+
try {
|
|
79
|
+
const result = this.#store.deleteByOrigin(this.#requestOptions.origin)
|
|
80
|
+
if (
|
|
81
|
+
result &&
|
|
82
|
+
typeof result.catch === 'function' &&
|
|
83
|
+
typeof this.#handler.onError === 'function'
|
|
84
|
+
) {
|
|
85
|
+
// Fail silently
|
|
86
|
+
result.catch(_ => {})
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// Fail silently
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return downstreamOnHeaders()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const headers = util.parseHeaders(rawHeaders)
|
|
96
|
+
|
|
97
|
+
const cacheControlHeader = headers['cache-control']
|
|
98
|
+
const contentLengthHeader = headers['content-length']
|
|
99
|
+
|
|
100
|
+
if (!cacheControlHeader || !contentLengthHeader || this.#store.isFull) {
|
|
101
|
+
// Don't have the headers we need, can't cache
|
|
102
|
+
return downstreamOnHeaders()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const contentLength = Number(contentLengthHeader)
|
|
106
|
+
if (!Number.isInteger(contentLength)) {
|
|
107
|
+
return downstreamOnHeaders()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const cacheControlDirectives = parseCacheControlHeader(cacheControlHeader)
|
|
111
|
+
if (!canCacheResponse(statusCode, headers, cacheControlDirectives)) {
|
|
112
|
+
return downstreamOnHeaders()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const now = Date.now()
|
|
116
|
+
const staleAt = determineStaleAt(now, headers, cacheControlDirectives)
|
|
117
|
+
if (staleAt) {
|
|
118
|
+
const varyDirectives = headers.vary
|
|
119
|
+
? parseVaryHeader(headers.vary, this.#requestOptions.headers)
|
|
120
|
+
: undefined
|
|
121
|
+
const deleteAt = determineDeleteAt(now, cacheControlDirectives, staleAt)
|
|
122
|
+
|
|
123
|
+
const strippedHeaders = stripNecessaryHeaders(
|
|
124
|
+
rawHeaders,
|
|
125
|
+
headers,
|
|
126
|
+
cacheControlDirectives
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
this.#writeStream = this.#store.createWriteStream(this.#requestOptions, {
|
|
130
|
+
statusCode,
|
|
131
|
+
statusMessage,
|
|
132
|
+
rawHeaders: strippedHeaders,
|
|
133
|
+
vary: varyDirectives,
|
|
134
|
+
cachedAt: now,
|
|
135
|
+
staleAt,
|
|
136
|
+
deleteAt
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
if (this.#writeStream) {
|
|
140
|
+
this.#writeStream.on('drain', resume)
|
|
141
|
+
this.#writeStream.on('error', () => {
|
|
142
|
+
this.#writeStream = undefined
|
|
143
|
+
resume()
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (typeof this.#handler.onHeaders === 'function') {
|
|
149
|
+
return downstreamOnHeaders()
|
|
150
|
+
}
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @see {DispatchHandlers.onData}
|
|
156
|
+
*
|
|
157
|
+
* @param {Buffer} chunk
|
|
158
|
+
* @returns {boolean}
|
|
159
|
+
*/
|
|
160
|
+
onData (chunk) {
|
|
161
|
+
let paused = false
|
|
162
|
+
|
|
163
|
+
if (this.#writeStream) {
|
|
164
|
+
paused ||= this.#writeStream.write(chunk) === false
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (typeof this.#handler.onData === 'function') {
|
|
168
|
+
paused ||= this.#handler.onData(chunk) === false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return !paused
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @see {DispatchHandlers.onComplete}
|
|
176
|
+
*
|
|
177
|
+
* @param {string[] | null} rawTrailers
|
|
178
|
+
*/
|
|
179
|
+
onComplete (rawTrailers) {
|
|
180
|
+
if (this.#writeStream) {
|
|
181
|
+
if (rawTrailers) {
|
|
182
|
+
this.#writeStream.rawTrailers = rawTrailers
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.#writeStream.end()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (typeof this.#handler.onComplete === 'function') {
|
|
189
|
+
return this.#handler.onComplete(rawTrailers)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @see {DispatchHandlers.onError}
|
|
195
|
+
*
|
|
196
|
+
* @param {Error} err
|
|
197
|
+
*/
|
|
198
|
+
onError (err) {
|
|
199
|
+
if (this.#writeStream) {
|
|
200
|
+
this.#writeStream.destroy(err)
|
|
201
|
+
this.#writeStream = undefined
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (typeof this.#handler.onError === 'function') {
|
|
205
|
+
this.#handler.onError(err)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
212
|
+
*
|
|
213
|
+
* @param {number} statusCode
|
|
214
|
+
* @param {Record<string, string>} headers
|
|
215
|
+
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
|
|
216
|
+
*/
|
|
217
|
+
function canCacheResponse (statusCode, headers, cacheControlDirectives) {
|
|
218
|
+
if (
|
|
219
|
+
statusCode !== 200 &&
|
|
220
|
+
statusCode !== 307
|
|
221
|
+
) {
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (
|
|
226
|
+
!cacheControlDirectives.public ||
|
|
227
|
+
cacheControlDirectives.private === true ||
|
|
228
|
+
cacheControlDirectives['no-cache'] === true ||
|
|
229
|
+
cacheControlDirectives['no-store']
|
|
230
|
+
) {
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
|
|
235
|
+
if (headers.vary === '*') {
|
|
236
|
+
return false
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
240
|
+
if (headers['authorization']) {
|
|
241
|
+
if (
|
|
242
|
+
Array.isArray(cacheControlDirectives['no-cache']) &&
|
|
243
|
+
cacheControlDirectives['no-cache'].includes('authorization')
|
|
244
|
+
) {
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (
|
|
249
|
+
Array.isArray(cacheControlDirectives['private']) &&
|
|
250
|
+
cacheControlDirectives['private'].includes('authorization')
|
|
251
|
+
) {
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return true
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {number} now
|
|
261
|
+
* @param {Record<string, string | string[]>} headers
|
|
262
|
+
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
|
|
263
|
+
*
|
|
264
|
+
* @returns {number | undefined} time that the value is stale at or undefined if it shouldn't be cached
|
|
265
|
+
*/
|
|
266
|
+
function determineStaleAt (now, headers, cacheControlDirectives) {
|
|
267
|
+
// Prioritize s-maxage since we're a shared cache
|
|
268
|
+
// s-maxage > max-age > Expire
|
|
269
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
|
|
270
|
+
const sMaxAge = cacheControlDirectives['s-maxage']
|
|
271
|
+
if (sMaxAge) {
|
|
272
|
+
return now + (sMaxAge * 1000)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (cacheControlDirectives.immutable) {
|
|
276
|
+
// https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
|
|
277
|
+
return now + 31536000
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const maxAge = cacheControlDirectives['max-age']
|
|
281
|
+
if (maxAge) {
|
|
282
|
+
return now + (maxAge * 1000)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (headers.expire) {
|
|
286
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
|
|
287
|
+
return now + (Date.now() - new Date(headers.expire).getTime())
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return undefined
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @param {number} now
|
|
295
|
+
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
|
|
296
|
+
* @param {number} staleAt
|
|
297
|
+
*/
|
|
298
|
+
function determineDeleteAt (now, cacheControlDirectives, staleAt) {
|
|
299
|
+
if (cacheControlDirectives['stale-while-revalidate']) {
|
|
300
|
+
return now + (cacheControlDirectives['stale-while-revalidate'] * 1000)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return staleAt
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Strips headers required to be removed in cached responses
|
|
308
|
+
* @param {Buffer[]} rawHeaders
|
|
309
|
+
* @param {Record<string, string | string[]>} parsedHeaders
|
|
310
|
+
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
|
|
311
|
+
* @returns {(Buffer|Buffer[])[]}
|
|
312
|
+
*/
|
|
313
|
+
function stripNecessaryHeaders (rawHeaders, parsedHeaders, cacheControlDirectives) {
|
|
314
|
+
const headersToRemove = ['connection']
|
|
315
|
+
|
|
316
|
+
if (Array.isArray(cacheControlDirectives['no-cache'])) {
|
|
317
|
+
headersToRemove.push(...cacheControlDirectives['no-cache'])
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (Array.isArray(cacheControlDirectives['private'])) {
|
|
321
|
+
headersToRemove.push(...cacheControlDirectives['private'])
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* These are the headers that are okay to cache. If this is assigned, we need
|
|
326
|
+
* to remake the buffer representation of the headers
|
|
327
|
+
* @type {Record<string, string | string[]> | undefined}
|
|
328
|
+
*/
|
|
329
|
+
let strippedHeaders
|
|
330
|
+
|
|
331
|
+
const headerNames = Object.keys(parsedHeaders)
|
|
332
|
+
for (let i = 0; i < headerNames.length; i++) {
|
|
333
|
+
const header = headerNames[i]
|
|
334
|
+
|
|
335
|
+
if (headersToRemove.indexOf(header) !== -1) {
|
|
336
|
+
// We have a at least one header we want to remove
|
|
337
|
+
if (!strippedHeaders) {
|
|
338
|
+
// This is the first header we want to remove, let's create the object
|
|
339
|
+
// and backfill the previous headers into it
|
|
340
|
+
strippedHeaders = {}
|
|
341
|
+
|
|
342
|
+
for (let j = 0; j < i; j++) {
|
|
343
|
+
strippedHeaders[headerNames[j]] = parsedHeaders[headerNames[j]]
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// This header is fine. Let's add it to strippedHeaders if it exists.
|
|
351
|
+
if (strippedHeaders) {
|
|
352
|
+
strippedHeaders[header] = parsedHeaders[header]
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return strippedHeaders ? util.encodeHeaders(strippedHeaders) : rawHeaders
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
module.exports = CacheHandler
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const DecoratorHandler = require('../handler/decorator-handler')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This takes care of revalidation requests we send to the origin. If we get
|
|
7
|
+
* a response indicating that what we have is cached (via a HTTP 304), we can
|
|
8
|
+
* continue using the cached value. Otherwise, we'll receive the new response
|
|
9
|
+
* here, which we then just pass on to the next handler (most likely a
|
|
10
|
+
* CacheHandler). Note that this assumes the proper headers were already
|
|
11
|
+
* included in the request to tell the origin that we want to revalidate the
|
|
12
|
+
* response (i.e. if-modified-since).
|
|
13
|
+
*
|
|
14
|
+
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-validation
|
|
15
|
+
*
|
|
16
|
+
* @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandlers} DispatchHandlers
|
|
17
|
+
* @implements {DispatchHandlers}
|
|
18
|
+
*/
|
|
19
|
+
class CacheRevalidationHandler extends DecoratorHandler {
|
|
20
|
+
#successful = false
|
|
21
|
+
/**
|
|
22
|
+
* @type {(() => void)}
|
|
23
|
+
*/
|
|
24
|
+
#successCallback
|
|
25
|
+
/**
|
|
26
|
+
* @type {(import('../../types/dispatcher.d.ts').default.DispatchHandlers)}
|
|
27
|
+
*/
|
|
28
|
+
#handler
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {() => void} successCallback Function to call if the cached value is valid
|
|
32
|
+
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
|
|
33
|
+
*/
|
|
34
|
+
constructor (successCallback, handler) {
|
|
35
|
+
if (typeof successCallback !== 'function') {
|
|
36
|
+
throw new TypeError('successCallback must be a function')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
super(handler)
|
|
40
|
+
|
|
41
|
+
this.#successCallback = successCallback
|
|
42
|
+
this.#handler = handler
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @see {DispatchHandlers.onHeaders}
|
|
47
|
+
*
|
|
48
|
+
* @param {number} statusCode
|
|
49
|
+
* @param {Buffer[]} rawHeaders
|
|
50
|
+
* @param {() => void} resume
|
|
51
|
+
* @param {string} statusMessage
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
onHeaders (
|
|
55
|
+
statusCode,
|
|
56
|
+
rawHeaders,
|
|
57
|
+
resume,
|
|
58
|
+
statusMessage
|
|
59
|
+
) {
|
|
60
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo
|
|
61
|
+
if (statusCode === 304) {
|
|
62
|
+
this.#successful = true
|
|
63
|
+
this.#successCallback()
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof this.#handler.onHeaders === 'function') {
|
|
68
|
+
return this.#handler.onHeaders(
|
|
69
|
+
statusCode,
|
|
70
|
+
rawHeaders,
|
|
71
|
+
resume,
|
|
72
|
+
statusMessage
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @see {DispatchHandlers.onData}
|
|
80
|
+
*
|
|
81
|
+
* @param {Buffer} chunk
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
onData (chunk) {
|
|
85
|
+
if (this.#successful) {
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof this.#handler.onData === 'function') {
|
|
90
|
+
return this.#handler.onData(chunk)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @see {DispatchHandlers.onComplete}
|
|
98
|
+
*
|
|
99
|
+
* @param {string[] | null} rawTrailers
|
|
100
|
+
*/
|
|
101
|
+
onComplete (rawTrailers) {
|
|
102
|
+
if (!this.#successful && typeof this.#handler.onComplete === 'function') {
|
|
103
|
+
this.#handler.onComplete(rawTrailers)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @see {DispatchHandlers.onError}
|
|
109
|
+
*
|
|
110
|
+
* @param {Error} err
|
|
111
|
+
*/
|
|
112
|
+
onError (err) {
|
|
113
|
+
if (typeof this.#handler.onError === 'function') {
|
|
114
|
+
this.#handler.onError(err)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = CacheRevalidationHandler
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const util = require('../core/util')
|
|
4
|
+
const CacheHandler = require('../handler/cache-handler')
|
|
5
|
+
const MemoryCacheStore = require('../cache/memory-cache-store')
|
|
6
|
+
const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
|
|
7
|
+
const { assertCacheStore, assertCacheMethods } = require('../util/cache.js')
|
|
8
|
+
|
|
9
|
+
const AGE_HEADER = Buffer.from('age')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} [opts]
|
|
13
|
+
* @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
|
|
14
|
+
*/
|
|
15
|
+
module.exports = (opts = {}) => {
|
|
16
|
+
const {
|
|
17
|
+
store = new MemoryCacheStore(),
|
|
18
|
+
methods = ['GET']
|
|
19
|
+
} = opts
|
|
20
|
+
|
|
21
|
+
if (typeof opts !== 'object' || opts === null) {
|
|
22
|
+
throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
assertCacheStore(store, 'opts.store')
|
|
26
|
+
assertCacheMethods(methods, 'opts.methods')
|
|
27
|
+
|
|
28
|
+
const globalOpts = {
|
|
29
|
+
store,
|
|
30
|
+
methods
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const safeMethodsToNotCache = util.safeHTTPMethods.filter(method => methods.includes(method) === false)
|
|
34
|
+
|
|
35
|
+
return dispatch => {
|
|
36
|
+
return (opts, handler) => {
|
|
37
|
+
if (!opts.origin || safeMethodsToNotCache.includes(opts.method)) {
|
|
38
|
+
// Not a method we want to cache or we don't have the origin, skip
|
|
39
|
+
return dispatch(opts, handler)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const stream = store.createReadStream(opts)
|
|
43
|
+
if (!stream) {
|
|
44
|
+
// Request isn't cached
|
|
45
|
+
return dispatch(opts, new CacheHandler(globalOpts, opts, handler))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let onErrorCalled = false
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreReadable} stream
|
|
52
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreValue} value
|
|
53
|
+
*/
|
|
54
|
+
const respondWithCachedValue = (stream, value) => {
|
|
55
|
+
const ac = new AbortController()
|
|
56
|
+
const signal = ac.signal
|
|
57
|
+
|
|
58
|
+
signal.onabort = (_, err) => {
|
|
59
|
+
stream.destroy()
|
|
60
|
+
if (!onErrorCalled) {
|
|
61
|
+
handler.onError(err)
|
|
62
|
+
onErrorCalled = true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
stream.on('error', (err) => {
|
|
67
|
+
if (!onErrorCalled) {
|
|
68
|
+
handler.onError(err)
|
|
69
|
+
onErrorCalled = true
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
if (typeof handler.onConnect === 'function') {
|
|
75
|
+
handler.onConnect(ac.abort)
|
|
76
|
+
signal.throwIfAborted()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof handler.onHeaders === 'function') {
|
|
80
|
+
// Add the age header
|
|
81
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-age
|
|
82
|
+
const age = Math.round((Date.now() - value.cachedAt) / 1000)
|
|
83
|
+
|
|
84
|
+
value.rawHeaders.push(AGE_HEADER, Buffer.from(`${age}`))
|
|
85
|
+
|
|
86
|
+
handler.onHeaders(value.statusCode, value.rawHeaders, stream.resume, value.statusMessage)
|
|
87
|
+
signal.throwIfAborted()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (opts.method === 'HEAD') {
|
|
91
|
+
if (typeof handler.onComplete === 'function') {
|
|
92
|
+
handler.onComplete(null)
|
|
93
|
+
stream.destroy()
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
if (typeof handler.onData === 'function') {
|
|
97
|
+
stream.on('data', chunk => {
|
|
98
|
+
if (!handler.onData(chunk)) {
|
|
99
|
+
stream.pause()
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof handler.onComplete === 'function') {
|
|
105
|
+
stream.on('end', () => {
|
|
106
|
+
handler.onComplete(value.rawTrailers ?? [])
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
stream.destroy(err)
|
|
112
|
+
if (!onErrorCalled && typeof handler.onError === 'function') {
|
|
113
|
+
handler.onError(err)
|
|
114
|
+
onErrorCalled = true
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreReadable | undefined} stream
|
|
121
|
+
*/
|
|
122
|
+
const handleStream = (stream) => {
|
|
123
|
+
if (!stream) {
|
|
124
|
+
// Request isn't cached
|
|
125
|
+
return dispatch(opts, new CacheHandler(globalOpts, opts, handler))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { value } = stream
|
|
129
|
+
|
|
130
|
+
// Dump body on error
|
|
131
|
+
if (util.isStream(opts.body)) {
|
|
132
|
+
opts.body?.on('error', () => {}).resume()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if the response is stale
|
|
136
|
+
const now = Date.now()
|
|
137
|
+
if (now >= value.staleAt) {
|
|
138
|
+
if (now >= value.deleteAt) {
|
|
139
|
+
// Safety check in case the store gave us a response that should've been
|
|
140
|
+
// deleted already
|
|
141
|
+
dispatch(opts, new CacheHandler(globalOpts, opts, handler))
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!opts.headers) {
|
|
146
|
+
opts.headers = {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
opts.headers['if-modified-since'] = new Date(value.cachedAt).toUTCString()
|
|
150
|
+
|
|
151
|
+
// Need to revalidate the response
|
|
152
|
+
dispatch(
|
|
153
|
+
opts,
|
|
154
|
+
new CacheRevalidationHandler(
|
|
155
|
+
() => respondWithCachedValue(stream, value),
|
|
156
|
+
new CacheHandler(globalOpts, opts, handler)
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
respondWithCachedValue(stream, value)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
Promise.resolve(stream).then(handleStream).catch(handler.onError)
|
|
167
|
+
|
|
168
|
+
return true
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|