1
1
const { Request, Response } = require ( 'minipass-fetch' )
2
2
const Minipass = require ( 'minipass' )
3
- const MinipassCollect = require ( 'minipass-collect' )
4
3
const MinipassFlush = require ( 'minipass-flush' )
5
- const MinipassPipeline = require ( 'minipass-pipeline' )
6
4
const cacache = require ( 'cacache' )
7
5
const url = require ( 'url' )
8
6
7
+ const CachingMinipassPipeline = require ( '../pipeline.js' )
9
8
const CachePolicy = require ( './policy.js' )
10
9
const cacheKey = require ( './key.js' )
11
10
const remote = require ( '../remote.js' )
12
11
13
12
const hasOwnProperty = ( obj , prop ) => Object . prototype . hasOwnProperty . call ( obj , prop )
14
13
15
- // maximum amount of data we will buffer into memory
16
- // if we'll exceed this, we switch to streaming
17
- const MAX_MEM_SIZE = 5 * 1024 * 1024 // 5MB
18
-
19
14
// allow list for request headers that will be written to the cache index
20
15
// note: we will also store any request headers
21
16
// that are named in a response's vary header
@@ -256,13 +251,12 @@ class CacheEntry {
256
251
}
257
252
258
253
const size = this . response . headers . get ( 'content-length' )
259
- const fitsInMemory = ! ! size && Number ( size ) < MAX_MEM_SIZE
260
- const shouldBuffer = this . options . memoize !== false && fitsInMemory
261
254
const cacheOpts = {
262
255
algorithms : this . options . algorithms ,
263
256
metadata : getMetadata ( this . request , this . response , this . options ) ,
264
257
size,
265
- memoize : fitsInMemory && this . options . memoize ,
258
+ integrity : this . options . integrity ,
259
+ integrityEmitter : this . response . body . hasIntegrityEmitter && this . response . body ,
266
260
}
267
261
268
262
let body = null
@@ -275,52 +269,31 @@ class CacheEntry {
275
269
cacheWriteReject = reject
276
270
} )
277
271
278
- body = new MinipassPipeline ( new MinipassFlush ( {
272
+ body = new CachingMinipassPipeline ( { events : [ 'integrity' , 'size' ] } , new MinipassFlush ( {
279
273
flush ( ) {
280
274
return cacheWritePromise
281
275
} ,
282
276
} ) )
283
-
284
- let abortStream , onResume
285
- if ( shouldBuffer ) {
286
- // if the result fits in memory, use a collect stream to gather
287
- // the response and write it to cacache while also passing it through
288
- // to the user
289
- onResume = ( ) => {
290
- const collector = new MinipassCollect . PassThrough ( )
291
- abortStream = collector
292
- collector . on ( 'collect' , ( data ) => {
293
- // TODO if the cache write fails, log a warning but return the response anyway
294
- cacache . put ( this . options . cachePath , this . key , data , cacheOpts )
295
- . then ( cacheWriteResolve , cacheWriteReject )
296
- } )
297
- body . unshift ( collector )
298
- body . unshift ( this . response . body )
299
- }
300
- } else {
301
- // if it does not fit in memory, create a tee stream and use
302
- // that to pipe to both the cache and the user simultaneously
303
- onResume = ( ) => {
304
- const tee = new Minipass ( )
305
- const cacheStream = cacache . put . stream ( this . options . cachePath , this . key , cacheOpts )
306
- abortStream = cacheStream
307
- tee . pipe ( cacheStream )
308
- // TODO if the cache write fails, log a warning but return the response anyway
309
- cacheStream . promise ( ) . then ( cacheWriteResolve , cacheWriteReject )
310
- body . unshift ( tee )
311
- body . unshift ( this . response . body )
312
- }
277
+ // this is always true since if we aren't reusing the one from the remote fetch, we
278
+ // are using the one from cacache
279
+ body . hasIntegrityEmitter = true
280
+
281
+ const onResume = ( ) => {
282
+ const tee = new Minipass ( )
283
+ const cacheStream = cacache . put . stream ( this . options . cachePath , this . key , cacheOpts )
284
+ // re-emit the integrity and size events on our new response body so they can be reused
285
+ cacheStream . on ( 'integrity' , i => body . emit ( 'integrity' , i ) )
286
+ cacheStream . on ( 'size' , s => body . emit ( 'size' , s ) )
287
+ // stick a flag on here so downstream users will know if they can expect integrity events
288
+ tee . pipe ( cacheStream )
289
+ // TODO if the cache write fails, log a warning but return the response anyway
290
+ cacheStream . promise ( ) . then ( cacheWriteResolve , cacheWriteReject )
291
+ body . unshift ( tee )
292
+ body . unshift ( this . response . body )
313
293
}
314
294
315
295
body . once ( 'resume' , onResume )
316
296
body . once ( 'end' , ( ) => body . removeListener ( 'resume' , onResume ) )
317
- this . response . body . on ( 'error' , ( err ) => {
318
- // the abortStream will either be a MinipassCollect if we buffer
319
- // or a cacache write stream, either way be sure to listen for
320
- // errors from the actual response and avoid writing data that we
321
- // know to be invalid to the cache
322
- abortStream . destroy ( err )
323
- } )
324
297
} else {
325
298
await cacache . index . insert ( this . options . cachePath , this . key , null , cacheOpts )
326
299
}
@@ -331,7 +304,7 @@ class CacheEntry {
331
304
// the header anyway
332
305
this . response . headers . set ( 'x-local-cache' , encodeURIComponent ( this . options . cachePath ) )
333
306
this . response . headers . set ( 'x-local-cache-key' , encodeURIComponent ( this . key ) )
334
- this . response . headers . set ( 'x-local-cache-mode' , shouldBuffer ? 'buffer' : 'stream' )
307
+ this . response . headers . set ( 'x-local-cache-mode' , 'stream' )
335
308
this . response . headers . set ( 'x-local-cache-status' , status )
336
309
this . response . headers . set ( 'x-local-cache-time' , new Date ( ) . toISOString ( ) )
337
310
const newResponse = new Response ( body , {
@@ -346,9 +319,6 @@ class CacheEntry {
346
319
// use the cached data to create a response and return it
347
320
async respond ( method , options , status ) {
348
321
let response
349
- const size = Number ( this . response . headers . get ( 'content-length' ) )
350
- const fitsInMemory = ! ! size && size < MAX_MEM_SIZE
351
- const shouldBuffer = this . options . memoize !== false && fitsInMemory
352
322
if ( method === 'HEAD' || [ 301 , 308 ] . includes ( this . response . status ) ) {
353
323
// if the request is a HEAD, or the response is a redirect,
354
324
// then the metadata in the entry already includes everything
@@ -358,66 +328,44 @@ class CacheEntry {
358
328
// we're responding with a full cached response, so create a body
359
329
// that reads from cacache and attach it to a new Response
360
330
const body = new Minipass ( )
361
- const removeOnResume = ( ) => body . removeListener ( 'resume' , onResume )
362
- let onResume
363
- if ( shouldBuffer ) {
364
- onResume = async ( ) => {
365
- removeOnResume ( )
366
- try {
367
- const content = await cacache . get . byDigest (
331
+ const headers = { ...this . policy . responseHeaders ( ) }
332
+ const onResume = ( ) => {
333
+ const cacheStream = cacache . get . stream . byDigest (
334
+ this . options . cachePath , this . entry . integrity , { memoize : this . options . memoize }
335
+ )
336
+ cacheStream . on ( 'error' , async ( err ) => {
337
+ cacheStream . pause ( )
338
+ if ( err . code === 'EINTEGRITY' ) {
339
+ await cacache . rm . content (
368
340
this . options . cachePath , this . entry . integrity , { memoize : this . options . memoize }
369
341
)
370
- body . end ( content )
371
- } catch ( err ) {
372
- if ( err . code === 'EINTEGRITY' ) {
373
- await cacache . rm . content (
374
- this . options . cachePath , this . entry . integrity , { memoize : this . options . memoize }
375
- )
376
- }
377
- if ( err . code === 'ENOENT' || err . code === 'EINTEGRITY' ) {
378
- await CacheEntry . invalidate ( this . request , this . options )
379
- }
380
- body . emit ( 'error' , err )
381
342
}
382
- }
383
- } else {
384
- onResume = ( ) => {
385
- const cacheStream = cacache . get . stream . byDigest (
386
- this . options . cachePath , this . entry . integrity , { memoize : this . options . memoize }
387
- )
388
- cacheStream . on ( 'error' , async ( err ) => {
389
- cacheStream . pause ( )
390
- if ( err . code === 'EINTEGRITY' ) {
391
- await cacache . rm . content (
392
- this . options . cachePath , this . entry . integrity , { memoize : this . options . memoize }
393
- )
394
- }
395
- if ( err . code === 'ENOENT' || err . code === 'EINTEGRITY' ) {
396
- await CacheEntry . invalidate ( this . request , this . options )
397
- }
398
- body . emit ( 'error' , err )
399
- cacheStream . resume ( )
400
- } )
401
- cacheStream . pipe ( body )
402
- }
343
+ if ( err . code === 'ENOENT' || err . code === 'EINTEGRITY' ) {
344
+ await CacheEntry . invalidate ( this . request , this . options )
345
+ }
346
+ body . emit ( 'error' , err )
347
+ cacheStream . resume ( )
348
+ } )
349
+ // emit the integrity and size events based on our metadata so we're consistent
350
+ body . emit ( 'integrity' , this . entry . integrity )
351
+ body . emit ( 'size' , Number ( headers [ 'content-length' ] ) )
352
+ cacheStream . pipe ( body )
403
353
}
404
354
405
355
body . once ( 'resume' , onResume )
406
- body . once ( 'end' , removeOnResume )
356
+ body . once ( 'end' , ( ) => body . removeListener ( 'resume' , onResume ) )
407
357
response = new Response ( body , {
408
358
url : this . entry . metadata . url ,
409
359
counter : options . counter ,
410
360
status : 200 ,
411
- headers : {
412
- ...this . policy . responseHeaders ( ) ,
413
- } ,
361
+ headers,
414
362
} )
415
363
}
416
364
417
365
response . headers . set ( 'x-local-cache' , encodeURIComponent ( this . options . cachePath ) )
418
366
response . headers . set ( 'x-local-cache-hash' , encodeURIComponent ( this . entry . integrity ) )
419
367
response . headers . set ( 'x-local-cache-key' , encodeURIComponent ( this . key ) )
420
- response . headers . set ( 'x-local-cache-mode' , shouldBuffer ? 'buffer' : 'stream' )
368
+ response . headers . set ( 'x-local-cache-mode' , 'stream' )
421
369
response . headers . set ( 'x-local-cache-status' , status )
422
370
response . headers . set ( 'x-local-cache-time' , new Date ( this . entry . metadata . time ) . toUTCString ( ) )
423
371
return response
0 commit comments