undici 7.0.0-alpha.7 → 7.0.0-alpha.8
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/docs/docs/api/Dispatcher.md +2 -0
- package/lib/cache/memory-cache-store.js +2 -0
- package/lib/cache/sqlite-cache-store.js +32 -43
- package/lib/handler/cache-handler.js +144 -45
- package/lib/handler/cache-revalidation-handler.js +16 -5
- package/lib/handler/decorator-handler.js +3 -0
- package/lib/handler/redirect-handler.js +15 -38
- package/lib/handler/unwrap-handler.js +1 -1
- package/lib/interceptor/cache.js +253 -201
- package/lib/util/cache.js +42 -31
- package/package.json +4 -3
- package/types/cache-interceptor.d.ts +40 -0
|
@@ -1260,6 +1260,8 @@ The `cache` interceptor implements client-side response caching as described in
|
|
|
1260
1260
|
|
|
1261
1261
|
- `store` - The [`CacheStore`](/docs/docs/api/CacheStore.md) to store and retrieve responses from. Default is [`MemoryCacheStore`](/docs/docs/api/CacheStore.md#memorycachestore).
|
|
1262
1262
|
- `methods` - The [**safe** HTTP methods](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1) to cache the response of.
|
|
1263
|
+
- `cacheByDefault` - The default expiration time to cache responses by if they don't have an explicit expiration. If this isn't present, responses without explicit expiration will not be cached. Default `undefined`.
|
|
1264
|
+
- `type` - The type of cache for Undici to act as. Can be `shared` or `private`. Default `shared`.
|
|
1263
1265
|
|
|
1264
1266
|
## Instance Events
|
|
1265
1267
|
|
|
@@ -89,7 +89,9 @@ class MemoryCacheStore {
|
|
|
89
89
|
statusCode: entry.statusCode,
|
|
90
90
|
headers: entry.headers,
|
|
91
91
|
body: entry.body,
|
|
92
|
+
vary: entry.vary ? entry.vary : undefined,
|
|
92
93
|
etag: entry.etag,
|
|
94
|
+
cacheControlDirectives: entry.cacheControlDirectives,
|
|
93
95
|
cachedAt: entry.cachedAt,
|
|
94
96
|
staleAt: entry.staleAt,
|
|
95
97
|
deleteAt: entry.deleteAt
|
|
@@ -4,7 +4,10 @@ const { DatabaseSync } = require('node:sqlite')
|
|
|
4
4
|
const { Writable } = require('stream')
|
|
5
5
|
const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
|
|
6
6
|
|
|
7
|
-
const VERSION =
|
|
7
|
+
const VERSION = 3
|
|
8
|
+
|
|
9
|
+
// 2gb
|
|
10
|
+
const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
|
|
@@ -17,8 +20,8 @@ const VERSION = 2
|
|
|
17
20
|
* body: string
|
|
18
21
|
* } & import('../../types/cache-interceptor.d.ts').default.CacheValue} SqliteStoreValue
|
|
19
22
|
*/
|
|
20
|
-
class SqliteCacheStore {
|
|
21
|
-
#maxEntrySize =
|
|
23
|
+
module.exports = class SqliteCacheStore {
|
|
24
|
+
#maxEntrySize = MAX_ENTRY_SIZE
|
|
22
25
|
#maxCount = Infinity
|
|
23
26
|
|
|
24
27
|
/**
|
|
@@ -78,6 +81,11 @@ class SqliteCacheStore {
|
|
|
78
81
|
) {
|
|
79
82
|
throw new TypeError('SqliteCacheStore options.maxEntrySize must be a non-negative integer')
|
|
80
83
|
}
|
|
84
|
+
|
|
85
|
+
if (opts.maxEntrySize > MAX_ENTRY_SIZE) {
|
|
86
|
+
throw new TypeError('SqliteCacheStore options.maxEntrySize must be less than 2gb')
|
|
87
|
+
}
|
|
88
|
+
|
|
81
89
|
this.#maxEntrySize = opts.maxEntrySize
|
|
82
90
|
}
|
|
83
91
|
|
|
@@ -103,11 +111,12 @@ class SqliteCacheStore {
|
|
|
103
111
|
method TEXT NOT NULL,
|
|
104
112
|
|
|
105
113
|
-- Data returned to the interceptor
|
|
106
|
-
body
|
|
114
|
+
body BUF NULL,
|
|
107
115
|
deleteAt INTEGER NOT NULL,
|
|
108
116
|
statusCode INTEGER NOT NULL,
|
|
109
117
|
statusMessage TEXT NOT NULL,
|
|
110
118
|
headers TEXT NULL,
|
|
119
|
+
cacheControlDirectives TEXT NULL,
|
|
111
120
|
etag TEXT NULL,
|
|
112
121
|
vary TEXT NULL,
|
|
113
122
|
cachedAt INTEGER NOT NULL,
|
|
@@ -128,6 +137,7 @@ class SqliteCacheStore {
|
|
|
128
137
|
statusMessage,
|
|
129
138
|
headers,
|
|
130
139
|
etag,
|
|
140
|
+
cacheControlDirectives,
|
|
131
141
|
vary,
|
|
132
142
|
cachedAt,
|
|
133
143
|
staleAt
|
|
@@ -147,6 +157,7 @@ class SqliteCacheStore {
|
|
|
147
157
|
statusMessage = ?,
|
|
148
158
|
headers = ?,
|
|
149
159
|
etag = ?,
|
|
160
|
+
cacheControlDirectives = ?,
|
|
150
161
|
cachedAt = ?,
|
|
151
162
|
staleAt = ?,
|
|
152
163
|
deleteAt = ?
|
|
@@ -164,11 +175,12 @@ class SqliteCacheStore {
|
|
|
164
175
|
statusMessage,
|
|
165
176
|
headers,
|
|
166
177
|
etag,
|
|
178
|
+
cacheControlDirectives,
|
|
167
179
|
vary,
|
|
168
180
|
cachedAt,
|
|
169
181
|
staleAt,
|
|
170
182
|
deleteAt
|
|
171
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
183
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
172
184
|
`)
|
|
173
185
|
|
|
174
186
|
this.#deleteByUrlQuery = this.#db.prepare(
|
|
@@ -218,11 +230,15 @@ class SqliteCacheStore {
|
|
|
218
230
|
* @type {import('../../types/cache-interceptor.d.ts').default.GetResult}
|
|
219
231
|
*/
|
|
220
232
|
const result = {
|
|
221
|
-
body:
|
|
233
|
+
body: Buffer.from(value.body),
|
|
222
234
|
statusCode: value.statusCode,
|
|
223
235
|
statusMessage: value.statusMessage,
|
|
224
236
|
headers: value.headers ? JSON.parse(value.headers) : undefined,
|
|
225
237
|
etag: value.etag ? value.etag : undefined,
|
|
238
|
+
vary: value.vary ?? undefined,
|
|
239
|
+
cacheControlDirectives: value.cacheControlDirectives
|
|
240
|
+
? JSON.parse(value.cacheControlDirectives)
|
|
241
|
+
: undefined,
|
|
226
242
|
cachedAt: value.cachedAt,
|
|
227
243
|
staleAt: value.staleAt,
|
|
228
244
|
deleteAt: value.deleteAt
|
|
@@ -269,12 +285,13 @@ class SqliteCacheStore {
|
|
|
269
285
|
if (existingValue) {
|
|
270
286
|
// Updating an existing response, let's overwrite it
|
|
271
287
|
store.#updateValueQuery.run(
|
|
272
|
-
|
|
288
|
+
Buffer.concat(body),
|
|
273
289
|
value.deleteAt,
|
|
274
290
|
value.statusCode,
|
|
275
291
|
value.statusMessage,
|
|
276
292
|
value.headers ? JSON.stringify(value.headers) : null,
|
|
277
|
-
value.etag,
|
|
293
|
+
value.etag ? value.etag : null,
|
|
294
|
+
value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
|
|
278
295
|
value.cachedAt,
|
|
279
296
|
value.staleAt,
|
|
280
297
|
value.deleteAt,
|
|
@@ -286,12 +303,13 @@ class SqliteCacheStore {
|
|
|
286
303
|
store.#insertValueQuery.run(
|
|
287
304
|
url,
|
|
288
305
|
key.method,
|
|
289
|
-
|
|
306
|
+
Buffer.concat(body),
|
|
290
307
|
value.deleteAt,
|
|
291
308
|
value.statusCode,
|
|
292
309
|
value.statusMessage,
|
|
293
310
|
value.headers ? JSON.stringify(value.headers) : null,
|
|
294
311
|
value.etag ? value.etag : null,
|
|
312
|
+
value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
|
|
295
313
|
value.vary ? JSON.stringify(value.vary) : null,
|
|
296
314
|
value.cachedAt,
|
|
297
315
|
value.staleAt,
|
|
@@ -316,7 +334,7 @@ class SqliteCacheStore {
|
|
|
316
334
|
}
|
|
317
335
|
|
|
318
336
|
#prune () {
|
|
319
|
-
if (this
|
|
337
|
+
if (this.size <= this.#maxCount) {
|
|
320
338
|
return 0
|
|
321
339
|
}
|
|
322
340
|
|
|
@@ -341,7 +359,7 @@ class SqliteCacheStore {
|
|
|
341
359
|
* Counts the number of rows in the cache
|
|
342
360
|
* @returns {Number}
|
|
343
361
|
*/
|
|
344
|
-
get
|
|
362
|
+
get size () {
|
|
345
363
|
const { total } = this.#countEntriesQuery.get()
|
|
346
364
|
return total
|
|
347
365
|
}
|
|
@@ -385,10 +403,10 @@ class SqliteCacheStore {
|
|
|
385
403
|
return undefined
|
|
386
404
|
}
|
|
387
405
|
|
|
388
|
-
|
|
406
|
+
value.vary = JSON.parse(value.vary)
|
|
389
407
|
|
|
390
|
-
for (const header in vary) {
|
|
391
|
-
if (headerValueEquals(headers[header], vary[header])) {
|
|
408
|
+
for (const header in value.vary) {
|
|
409
|
+
if (!headerValueEquals(headers[header], value.vary[header])) {
|
|
392
410
|
matches = false
|
|
393
411
|
break
|
|
394
412
|
}
|
|
@@ -426,32 +444,3 @@ function headerValueEquals (lhs, rhs) {
|
|
|
426
444
|
|
|
427
445
|
return lhs === rhs
|
|
428
446
|
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* @param {Buffer[]} buffers
|
|
432
|
-
* @returns {string[]}
|
|
433
|
-
*/
|
|
434
|
-
function stringifyBufferArray (buffers) {
|
|
435
|
-
const output = new Array(buffers.length)
|
|
436
|
-
for (let i = 0; i < buffers.length; i++) {
|
|
437
|
-
output[i] = buffers[i].toString()
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return output
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* @param {string[]} strings
|
|
445
|
-
* @returns {Buffer[]}
|
|
446
|
-
*/
|
|
447
|
-
function parseBufferArray (strings) {
|
|
448
|
-
const output = new Array(strings.length)
|
|
449
|
-
|
|
450
|
-
for (let i = 0; i < strings.length; i++) {
|
|
451
|
-
output[i] = Buffer.from(strings[i])
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return output
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
module.exports = SqliteCacheStore
|
|
@@ -10,7 +10,9 @@ const {
|
|
|
10
10
|
function noop () {}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* @
|
|
13
|
+
* @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
|
|
14
|
+
*
|
|
15
|
+
* @implements {DispatchHandler}
|
|
14
16
|
*/
|
|
15
17
|
class CacheHandler {
|
|
16
18
|
/**
|
|
@@ -18,6 +20,16 @@ class CacheHandler {
|
|
|
18
20
|
*/
|
|
19
21
|
#cacheKey
|
|
20
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions['type']}
|
|
25
|
+
*/
|
|
26
|
+
#cacheType
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @type {number | undefined}
|
|
30
|
+
*/
|
|
31
|
+
#cacheByDefault
|
|
32
|
+
|
|
21
33
|
/**
|
|
22
34
|
* @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
|
|
23
35
|
*/
|
|
@@ -38,10 +50,10 @@ class CacheHandler {
|
|
|
38
50
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
|
|
39
51
|
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
|
|
40
52
|
*/
|
|
41
|
-
constructor (
|
|
42
|
-
const { store } = opts
|
|
43
|
-
|
|
53
|
+
constructor ({ store, type, cacheByDefault }, cacheKey, handler) {
|
|
44
54
|
this.#store = store
|
|
55
|
+
this.#cacheType = type
|
|
56
|
+
this.#cacheByDefault = cacheByDefault
|
|
45
57
|
this.#cacheKey = cacheKey
|
|
46
58
|
this.#handler = handler
|
|
47
59
|
}
|
|
@@ -85,24 +97,47 @@ class CacheHandler {
|
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
const cacheControlHeader = headers['cache-control']
|
|
88
|
-
if (!cacheControlHeader) {
|
|
100
|
+
if (!cacheControlHeader && !headers['expires'] && !this.#cacheByDefault) {
|
|
89
101
|
// Don't have the cache control header or the cache is full
|
|
90
102
|
return downstreamOnHeaders()
|
|
91
103
|
}
|
|
92
104
|
|
|
93
|
-
const cacheControlDirectives = parseCacheControlHeader(cacheControlHeader)
|
|
94
|
-
if (!canCacheResponse(statusCode, headers, cacheControlDirectives)) {
|
|
105
|
+
const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
|
|
106
|
+
if (!canCacheResponse(this.#cacheType, statusCode, headers, cacheControlDirectives)) {
|
|
95
107
|
return downstreamOnHeaders()
|
|
96
108
|
}
|
|
97
109
|
|
|
110
|
+
const age = getAge(headers)
|
|
111
|
+
|
|
98
112
|
const now = Date.now()
|
|
99
|
-
const staleAt = determineStaleAt(now, headers, cacheControlDirectives)
|
|
113
|
+
const staleAt = determineStaleAt(this.#cacheType, now, headers, cacheControlDirectives) ?? this.#cacheByDefault
|
|
100
114
|
if (staleAt) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
let baseTime = now
|
|
116
|
+
if (headers['date']) {
|
|
117
|
+
const parsedDate = parseInt(headers['date'])
|
|
118
|
+
const date = new Date(isNaN(parsedDate) ? headers['date'] : parsedDate)
|
|
119
|
+
if (date instanceof Date && !isNaN(date)) {
|
|
120
|
+
baseTime = date.getTime()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const absoluteStaleAt = staleAt + baseTime
|
|
125
|
+
|
|
126
|
+
if (now >= absoluteStaleAt || (age && age >= staleAt)) {
|
|
127
|
+
// Response is already stale
|
|
128
|
+
return downstreamOnHeaders()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let varyDirectives
|
|
132
|
+
if (this.#cacheKey.headers && headers.vary) {
|
|
133
|
+
varyDirectives = parseVaryHeader(headers.vary, this.#cacheKey.headers)
|
|
134
|
+
if (!varyDirectives) {
|
|
135
|
+
// Parse error
|
|
136
|
+
return downstreamOnHeaders()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
105
139
|
|
|
140
|
+
const deleteAt = determineDeleteAt(cacheControlDirectives, absoluteStaleAt)
|
|
106
141
|
const strippedHeaders = stripNecessaryHeaders(headers, cacheControlDirectives)
|
|
107
142
|
|
|
108
143
|
/**
|
|
@@ -113,8 +148,9 @@ class CacheHandler {
|
|
|
113
148
|
statusMessage,
|
|
114
149
|
headers: strippedHeaders,
|
|
115
150
|
vary: varyDirectives,
|
|
116
|
-
|
|
117
|
-
|
|
151
|
+
cacheControlDirectives,
|
|
152
|
+
cachedAt: age ? now - (age * 1000) : now,
|
|
153
|
+
staleAt: absoluteStaleAt,
|
|
118
154
|
deleteAt
|
|
119
155
|
}
|
|
120
156
|
|
|
@@ -130,6 +166,7 @@ class CacheHandler {
|
|
|
130
166
|
.on('drain', () => controller.resume())
|
|
131
167
|
.on('error', function () {
|
|
132
168
|
// TODO (fix): Make error somehow observable?
|
|
169
|
+
handler.#writeStream = undefined
|
|
133
170
|
})
|
|
134
171
|
.on('close', function () {
|
|
135
172
|
if (handler.#writeStream === this) {
|
|
@@ -168,25 +205,29 @@ class CacheHandler {
|
|
|
168
205
|
/**
|
|
169
206
|
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
170
207
|
*
|
|
208
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
|
|
171
209
|
* @param {number} statusCode
|
|
172
210
|
* @param {Record<string, string | string[]>} headers
|
|
173
|
-
* @param {import('
|
|
211
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
174
212
|
*/
|
|
175
|
-
function canCacheResponse (statusCode, headers, cacheControlDirectives) {
|
|
213
|
+
function canCacheResponse (cacheType, statusCode, headers, cacheControlDirectives) {
|
|
176
214
|
if (statusCode !== 200 && statusCode !== 307) {
|
|
177
215
|
return false
|
|
178
216
|
}
|
|
179
217
|
|
|
180
218
|
if (
|
|
181
|
-
cacheControlDirectives.private === true ||
|
|
182
219
|
cacheControlDirectives['no-cache'] === true ||
|
|
183
220
|
cacheControlDirectives['no-store']
|
|
184
221
|
) {
|
|
185
222
|
return false
|
|
186
223
|
}
|
|
187
224
|
|
|
225
|
+
if (cacheType === 'shared' && cacheControlDirectives.private === true) {
|
|
226
|
+
return false
|
|
227
|
+
}
|
|
228
|
+
|
|
188
229
|
// https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
|
|
189
|
-
if (headers.vary
|
|
230
|
+
if (headers.vary?.includes('*')) {
|
|
190
231
|
return false
|
|
191
232
|
}
|
|
192
233
|
|
|
@@ -215,63 +256,120 @@ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
|
|
|
215
256
|
}
|
|
216
257
|
|
|
217
258
|
/**
|
|
259
|
+
* @param {Record<string, string | string[]>} headers
|
|
260
|
+
* @returns {number | undefined}
|
|
261
|
+
*/
|
|
262
|
+
function getAge (headers) {
|
|
263
|
+
if (!headers.age) {
|
|
264
|
+
return undefined
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const age = parseInt(Array.isArray(headers.age) ? headers.age[0] : headers.age)
|
|
268
|
+
if (isNaN(age) || age >= 2147483647) {
|
|
269
|
+
return undefined
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return age
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
|
|
218
277
|
* @param {number} now
|
|
219
278
|
* @param {Record<string, string | string[]>} headers
|
|
220
|
-
* @param {import('
|
|
279
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
221
280
|
*
|
|
222
281
|
* @returns {number | undefined} time that the value is stale at or undefined if it shouldn't be cached
|
|
223
282
|
*/
|
|
224
|
-
function determineStaleAt (now, headers, cacheControlDirectives) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (cacheControlDirectives.immutable) {
|
|
234
|
-
// https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
|
|
235
|
-
return now + 31536000
|
|
283
|
+
function determineStaleAt (cacheType, now, headers, cacheControlDirectives) {
|
|
284
|
+
if (cacheType === 'shared') {
|
|
285
|
+
// Prioritize s-maxage since we're a shared cache
|
|
286
|
+
// s-maxage > max-age > Expire
|
|
287
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
|
|
288
|
+
const sMaxAge = cacheControlDirectives['s-maxage']
|
|
289
|
+
if (sMaxAge) {
|
|
290
|
+
return sMaxAge * 1000
|
|
291
|
+
}
|
|
236
292
|
}
|
|
237
293
|
|
|
238
294
|
const maxAge = cacheControlDirectives['max-age']
|
|
239
295
|
if (maxAge) {
|
|
240
|
-
return
|
|
296
|
+
return maxAge * 1000
|
|
241
297
|
}
|
|
242
298
|
|
|
243
|
-
if (headers.
|
|
299
|
+
if (headers.expires && typeof headers.expires === 'string') {
|
|
244
300
|
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
|
|
245
|
-
const expiresDate = new Date(headers.
|
|
301
|
+
const expiresDate = new Date(headers.expires)
|
|
246
302
|
if (expiresDate instanceof Date && Number.isFinite(expiresDate.valueOf())) {
|
|
247
|
-
|
|
303
|
+
if (now >= expiresDate.getTime()) {
|
|
304
|
+
return undefined
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return expiresDate.getTime() - now
|
|
248
308
|
}
|
|
249
309
|
}
|
|
250
310
|
|
|
311
|
+
if (cacheControlDirectives.immutable) {
|
|
312
|
+
// https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
|
|
313
|
+
return 31536000
|
|
314
|
+
}
|
|
315
|
+
|
|
251
316
|
return undefined
|
|
252
317
|
}
|
|
253
318
|
|
|
254
319
|
/**
|
|
255
|
-
* @param {
|
|
256
|
-
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
|
|
320
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
257
321
|
* @param {number} staleAt
|
|
258
322
|
*/
|
|
259
|
-
function determineDeleteAt (
|
|
323
|
+
function determineDeleteAt (cacheControlDirectives, staleAt) {
|
|
324
|
+
let staleWhileRevalidate = -Infinity
|
|
325
|
+
let staleIfError = -Infinity
|
|
326
|
+
let immutable = -Infinity
|
|
327
|
+
|
|
260
328
|
if (cacheControlDirectives['stale-while-revalidate']) {
|
|
261
|
-
|
|
329
|
+
staleWhileRevalidate = staleAt + (cacheControlDirectives['stale-while-revalidate'] * 1000)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (cacheControlDirectives['stale-if-error']) {
|
|
333
|
+
staleIfError = staleAt + (cacheControlDirectives['stale-if-error'] * 1000)
|
|
262
334
|
}
|
|
263
335
|
|
|
264
|
-
|
|
336
|
+
if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
|
|
337
|
+
immutable = 31536000
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
|
|
265
341
|
}
|
|
266
342
|
|
|
267
343
|
/**
|
|
268
344
|
* Strips headers required to be removed in cached responses
|
|
269
345
|
* @param {Record<string, string | string[]>} headers
|
|
270
|
-
* @param {import('
|
|
346
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
271
347
|
* @returns {Record<string, string | string []>}
|
|
272
348
|
*/
|
|
273
349
|
function stripNecessaryHeaders (headers, cacheControlDirectives) {
|
|
274
|
-
const headersToRemove = [
|
|
350
|
+
const headersToRemove = [
|
|
351
|
+
'connection',
|
|
352
|
+
'proxy-authenticate',
|
|
353
|
+
'proxy-authentication-info',
|
|
354
|
+
'proxy-authorization',
|
|
355
|
+
'proxy-connection',
|
|
356
|
+
'te',
|
|
357
|
+
'transfer-encoding',
|
|
358
|
+
'upgrade',
|
|
359
|
+
// We'll add age back when serving it
|
|
360
|
+
'age'
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
if (headers['connection']) {
|
|
364
|
+
if (Array.isArray(headers['connection'])) {
|
|
365
|
+
// connection: a
|
|
366
|
+
// connection: b
|
|
367
|
+
headersToRemove.push(...headers['connection'].map(header => header.trim()))
|
|
368
|
+
} else {
|
|
369
|
+
// connection: a, b
|
|
370
|
+
headersToRemove.push(...headers['connection'].split(',').map(header => header.trim()))
|
|
371
|
+
}
|
|
372
|
+
}
|
|
275
373
|
|
|
276
374
|
if (Array.isArray(cacheControlDirectives['no-cache'])) {
|
|
277
375
|
headersToRemove.push(...cacheControlDirectives['no-cache'])
|
|
@@ -282,12 +380,13 @@ function stripNecessaryHeaders (headers, cacheControlDirectives) {
|
|
|
282
380
|
}
|
|
283
381
|
|
|
284
382
|
let strippedHeaders
|
|
285
|
-
for (const headerName of
|
|
286
|
-
if (
|
|
383
|
+
for (const headerName of headersToRemove) {
|
|
384
|
+
if (headers[headerName]) {
|
|
287
385
|
strippedHeaders ??= { ...headers }
|
|
288
|
-
delete
|
|
386
|
+
delete strippedHeaders[headerName]
|
|
289
387
|
}
|
|
290
388
|
}
|
|
389
|
+
|
|
291
390
|
return strippedHeaders ?? headers
|
|
292
391
|
}
|
|
293
392
|
|
|
@@ -17,10 +17,12 @@ const assert = require('node:assert')
|
|
|
17
17
|
*/
|
|
18
18
|
class CacheRevalidationHandler {
|
|
19
19
|
#successful = false
|
|
20
|
+
|
|
20
21
|
/**
|
|
21
22
|
* @type {((boolean, any) => void) | null}
|
|
22
23
|
*/
|
|
23
24
|
#callback
|
|
25
|
+
|
|
24
26
|
/**
|
|
25
27
|
* @type {(import('../../types/dispatcher.d.ts').default.DispatchHandler)}
|
|
26
28
|
*/
|
|
@@ -29,19 +31,26 @@ class CacheRevalidationHandler {
|
|
|
29
31
|
#context
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
|
-
* @
|
|
33
|
-
|
|
34
|
+
* @type {boolean}
|
|
35
|
+
*/
|
|
36
|
+
#allowErrorStatusCodes
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {(boolean) => void} callback Function to call if the cached value is valid
|
|
40
|
+
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
|
|
41
|
+
* @param {boolean} allowErrorStatusCodes
|
|
34
42
|
*/
|
|
35
|
-
constructor (callback, handler) {
|
|
43
|
+
constructor (callback, handler, allowErrorStatusCodes) {
|
|
36
44
|
if (typeof callback !== 'function') {
|
|
37
45
|
throw new TypeError('callback must be a function')
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
this.#callback = callback
|
|
41
49
|
this.#handler = handler
|
|
50
|
+
this.#allowErrorStatusCodes = allowErrorStatusCodes
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
onRequestStart (
|
|
53
|
+
onRequestStart (_, context) {
|
|
45
54
|
this.#successful = false
|
|
46
55
|
this.#context = context
|
|
47
56
|
}
|
|
@@ -59,7 +68,9 @@ class CacheRevalidationHandler {
|
|
|
59
68
|
assert(this.#callback != null)
|
|
60
69
|
|
|
61
70
|
// https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo
|
|
62
|
-
|
|
71
|
+
// https://datatracker.ietf.org/doc/html/rfc5861#section-4
|
|
72
|
+
this.#successful = statusCode === 304 ||
|
|
73
|
+
(this.#allowErrorStatusCodes && statusCode >= 500 && statusCode <= 504)
|
|
63
74
|
this.#callback(this.#successful, this.#context)
|
|
64
75
|
this.#callback = null
|
|
65
76
|
|