@@ -10,14 +10,26 @@ const {
10
10
function noop ( ) { }
11
11
12
12
/**
13
- * @implements {import('../../types/dispatcher.d.ts').default.DispatchHandler}
13
+ * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler } DispatchHandler
14
+ *
15
+ * @implements {DispatchHandler}
14
16
*/
15
17
class CacheHandler {
16
18
/**
17
19
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey }
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,8 +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 ( { store } , cacheKey , handler ) {
53
+ constructor ( { store, type , cacheByDefault } , cacheKey , handler ) {
42
54
this . #store = store
55
+ this . #cacheType = type
56
+ this . #cacheByDefault = cacheByDefault
43
57
this . #cacheKey = cacheKey
44
58
this . #handler = handler
45
59
}
@@ -83,24 +97,47 @@ class CacheHandler {
83
97
}
84
98
85
99
const cacheControlHeader = headers [ 'cache-control' ]
86
- if ( ! cacheControlHeader ) {
100
+ if ( ! cacheControlHeader && ! headers [ 'expires' ] && ! this . #cacheByDefault ) {
87
101
// Don't have the cache control header or the cache is full
88
102
return downstreamOnHeaders ( )
89
103
}
90
104
91
- const cacheControlDirectives = parseCacheControlHeader ( cacheControlHeader )
92
- if ( ! canCacheResponse ( statusCode , headers , cacheControlDirectives ) ) {
105
+ const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader ( cacheControlHeader ) : { }
106
+ if ( ! canCacheResponse ( this . #cacheType , statusCode , headers , cacheControlDirectives ) ) {
93
107
return downstreamOnHeaders ( )
94
108
}
95
109
110
+ const age = getAge ( headers )
111
+
96
112
const now = Date . now ( )
97
- const staleAt = determineStaleAt ( now , headers , cacheControlDirectives )
113
+ const staleAt = determineStaleAt ( this . #cacheType , now , headers , cacheControlDirectives ) ?? this . #cacheByDefault
98
114
if ( staleAt ) {
99
- const varyDirectives = this . #cacheKey. headers && headers . vary
100
- ? parseVaryHeader ( headers . vary , this . #cacheKey. headers )
101
- : undefined
102
- const deleteAt = determineDeleteAt ( now , cacheControlDirectives , staleAt )
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
+ }
103
139
140
+ const deleteAt = determineDeleteAt ( cacheControlDirectives , absoluteStaleAt )
104
141
const strippedHeaders = stripNecessaryHeaders ( headers , cacheControlDirectives )
105
142
106
143
/**
@@ -112,8 +149,8 @@ class CacheHandler {
112
149
headers : strippedHeaders ,
113
150
vary : varyDirectives ,
114
151
cacheControlDirectives,
115
- cachedAt : now ,
116
- staleAt,
152
+ cachedAt : age ? now - ( age * 1000 ) : now ,
153
+ staleAt : absoluteStaleAt ,
117
154
deleteAt
118
155
}
119
156
@@ -129,6 +166,7 @@ class CacheHandler {
129
166
. on ( 'drain' , ( ) => controller . resume ( ) )
130
167
. on ( 'error' , function ( ) {
131
168
// TODO (fix): Make error somehow observable?
169
+ handler . #writeStream = undefined
132
170
} )
133
171
. on ( 'close' , function ( ) {
134
172
if ( handler . #writeStream === this ) {
@@ -167,25 +205,29 @@ class CacheHandler {
167
205
/**
168
206
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
169
207
*
208
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type'] } cacheType
170
209
* @param {number } statusCode
171
210
* @param {Record<string, string | string[]> } headers
172
211
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives } cacheControlDirectives
173
212
*/
174
- function canCacheResponse ( statusCode , headers , cacheControlDirectives ) {
213
+ function canCacheResponse ( cacheType , statusCode , headers , cacheControlDirectives ) {
175
214
if ( statusCode !== 200 && statusCode !== 307 ) {
176
215
return false
177
216
}
178
217
179
218
if (
180
- cacheControlDirectives . private === true ||
181
219
cacheControlDirectives [ 'no-cache' ] === true ||
182
220
cacheControlDirectives [ 'no-store' ]
183
221
) {
184
222
return false
185
223
}
186
224
225
+ if ( cacheType === 'shared' && cacheControlDirectives . private === true ) {
226
+ return false
227
+ }
228
+
187
229
// https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
188
- if ( headers . vary === '*' ) {
230
+ if ( headers . vary ?. includes ( '*' ) ) {
189
231
return false
190
232
}
191
233
@@ -214,60 +256,88 @@ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
214
256
}
215
257
216
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
217
277
* @param {number } now
218
278
* @param {Record<string, string | string[]> } headers
219
279
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives } cacheControlDirectives
220
280
*
221
281
* @returns {number | undefined } time that the value is stale at or undefined if it shouldn't be cached
222
282
*/
223
- function determineStaleAt ( now , headers , cacheControlDirectives ) {
224
- // Prioritize s-maxage since we're a shared cache
225
- // s-maxage > max-age > Expire
226
- // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
227
- const sMaxAge = cacheControlDirectives [ 's-maxage' ]
228
- if ( sMaxAge ) {
229
- return now + ( sMaxAge * 1000 )
230
- }
231
-
232
- if ( cacheControlDirectives . immutable ) {
233
- // https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
234
- 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
+ }
235
292
}
236
293
237
294
const maxAge = cacheControlDirectives [ 'max-age' ]
238
295
if ( maxAge ) {
239
- return now + ( maxAge * 1000 )
296
+ return maxAge * 1000
240
297
}
241
298
242
- if ( headers . expire && typeof headers . expire === 'string' ) {
299
+ if ( headers . expires && typeof headers . expires === 'string' ) {
243
300
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
244
- const expiresDate = new Date ( headers . expire )
301
+ const expiresDate = new Date ( headers . expires )
245
302
if ( expiresDate instanceof Date && Number . isFinite ( expiresDate . valueOf ( ) ) ) {
246
- return now + ( Date . now ( ) - expiresDate . getTime ( ) )
303
+ if ( now >= expiresDate . getTime ( ) ) {
304
+ return undefined
305
+ }
306
+
307
+ return expiresDate . getTime ( ) - now
247
308
}
248
309
}
249
310
311
+ if ( cacheControlDirectives . immutable ) {
312
+ // https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
313
+ return 31536000
314
+ }
315
+
250
316
return undefined
251
317
}
252
318
253
319
/**
254
- * @param {number } now
255
320
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives } cacheControlDirectives
256
321
* @param {number } staleAt
257
322
*/
258
- function determineDeleteAt ( now , cacheControlDirectives , staleAt ) {
323
+ function determineDeleteAt ( cacheControlDirectives , staleAt ) {
259
324
let staleWhileRevalidate = - Infinity
260
325
let staleIfError = - Infinity
326
+ let immutable = - Infinity
261
327
262
328
if ( cacheControlDirectives [ 'stale-while-revalidate' ] ) {
263
- staleWhileRevalidate = now + ( cacheControlDirectives [ 'stale-while-revalidate' ] * 1000 )
329
+ staleWhileRevalidate = staleAt + ( cacheControlDirectives [ 'stale-while-revalidate' ] * 1000 )
264
330
}
265
331
266
332
if ( cacheControlDirectives [ 'stale-if-error' ] ) {
267
- staleIfError = now + ( cacheControlDirectives [ 'stale-if-error' ] * 1000 )
333
+ staleIfError = staleAt + ( cacheControlDirectives [ 'stale-if-error' ] * 1000 )
268
334
}
269
335
270
- return Math . max ( staleAt , staleWhileRevalidate , staleIfError )
336
+ if ( staleWhileRevalidate === - Infinity && staleIfError === - Infinity ) {
337
+ immutable = 31536000
338
+ }
339
+
340
+ return Math . max ( staleAt , staleWhileRevalidate , staleIfError , immutable )
271
341
}
272
342
273
343
/**
@@ -277,7 +347,29 @@ function determineDeleteAt (now, cacheControlDirectives, staleAt) {
277
347
* @returns {Record<string, string | string []> }
278
348
*/
279
349
function stripNecessaryHeaders ( headers , cacheControlDirectives ) {
280
- const headersToRemove = [ 'connection' ]
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
+ }
281
373
282
374
if ( Array . isArray ( cacheControlDirectives [ 'no-cache' ] ) ) {
283
375
headersToRemove . push ( ...cacheControlDirectives [ 'no-cache' ] )
@@ -288,12 +380,13 @@ function stripNecessaryHeaders (headers, cacheControlDirectives) {
288
380
}
289
381
290
382
let strippedHeaders
291
- for ( const headerName of Object . keys ( headers ) ) {
292
- if ( headersToRemove . includes ( headerName ) ) {
383
+ for ( const headerName of headersToRemove ) {
384
+ if ( headers [ headerName ] ) {
293
385
strippedHeaders ??= { ...headers }
294
- delete headers [ headerName ]
386
+ delete strippedHeaders [ headerName ]
295
387
}
296
388
}
389
+
297
390
return strippedHeaders ?? headers
298
391
}
299
392
0 commit comments