undici 7.0.0-alpha.1 → 7.0.0-alpha.10

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.
Files changed (113) hide show
  1. package/README.md +24 -38
  2. package/docs/docs/api/Agent.md +14 -14
  3. package/docs/docs/api/BalancedPool.md +16 -16
  4. package/docs/docs/api/CacheStore.md +131 -0
  5. package/docs/docs/api/Client.md +12 -12
  6. package/docs/docs/api/Debug.md +1 -1
  7. package/docs/docs/api/Dispatcher.md +98 -193
  8. package/docs/docs/api/EnvHttpProxyAgent.md +12 -12
  9. package/docs/docs/api/MockAgent.md +5 -3
  10. package/docs/docs/api/MockClient.md +5 -5
  11. package/docs/docs/api/MockPool.md +4 -3
  12. package/docs/docs/api/Pool.md +15 -15
  13. package/docs/docs/api/PoolStats.md +1 -1
  14. package/docs/docs/api/ProxyAgent.md +3 -3
  15. package/docs/docs/api/RedirectHandler.md +1 -1
  16. package/docs/docs/api/RetryAgent.md +1 -1
  17. package/docs/docs/api/RetryHandler.md +4 -4
  18. package/docs/docs/api/WebSocket.md +46 -4
  19. package/docs/docs/api/api-lifecycle.md +11 -11
  20. package/docs/docs/best-practices/mocking-request.md +2 -2
  21. package/docs/docs/best-practices/proxy.md +1 -1
  22. package/index.d.ts +1 -1
  23. package/index.js +23 -3
  24. package/lib/api/abort-signal.js +2 -0
  25. package/lib/api/api-pipeline.js +4 -2
  26. package/lib/api/api-request.js +6 -4
  27. package/lib/api/api-stream.js +3 -1
  28. package/lib/api/api-upgrade.js +2 -2
  29. package/lib/api/readable.js +200 -47
  30. package/lib/api/util.js +2 -0
  31. package/lib/cache/memory-cache-store.js +177 -0
  32. package/lib/cache/sqlite-cache-store.js +446 -0
  33. package/lib/core/connect.js +54 -22
  34. package/lib/core/constants.js +35 -10
  35. package/lib/core/diagnostics.js +122 -128
  36. package/lib/core/errors.js +2 -2
  37. package/lib/core/request.js +6 -6
  38. package/lib/core/symbols.js +2 -0
  39. package/lib/core/tree.js +4 -2
  40. package/lib/core/util.js +238 -40
  41. package/lib/dispatcher/client-h1.js +405 -142
  42. package/lib/dispatcher/client-h2.js +212 -109
  43. package/lib/dispatcher/client.js +24 -7
  44. package/lib/dispatcher/dispatcher-base.js +4 -1
  45. package/lib/dispatcher/dispatcher.js +4 -0
  46. package/lib/dispatcher/fixed-queue.js +91 -49
  47. package/lib/dispatcher/pool-base.js +3 -3
  48. package/lib/dispatcher/pool-stats.js +2 -0
  49. package/lib/dispatcher/proxy-agent.js +3 -1
  50. package/lib/handler/cache-handler.js +393 -0
  51. package/lib/handler/cache-revalidation-handler.js +124 -0
  52. package/lib/handler/decorator-handler.js +3 -0
  53. package/lib/handler/redirect-handler.js +45 -59
  54. package/lib/handler/retry-handler.js +68 -109
  55. package/lib/handler/unwrap-handler.js +96 -0
  56. package/lib/handler/wrap-handler.js +98 -0
  57. package/lib/interceptor/cache.js +350 -0
  58. package/lib/interceptor/dns.js +375 -0
  59. package/lib/interceptor/response-error.js +15 -7
  60. package/lib/mock/mock-agent.js +5 -8
  61. package/lib/mock/mock-client.js +7 -2
  62. package/lib/mock/mock-errors.js +3 -1
  63. package/lib/mock/mock-interceptor.js +8 -6
  64. package/lib/mock/mock-pool.js +7 -2
  65. package/lib/mock/mock-symbols.js +2 -1
  66. package/lib/mock/mock-utils.js +33 -5
  67. package/lib/util/cache.js +360 -0
  68. package/lib/util/timers.js +50 -6
  69. package/lib/web/cache/cache.js +25 -21
  70. package/lib/web/cache/cachestorage.js +3 -1
  71. package/lib/web/cookies/index.js +18 -5
  72. package/lib/web/cookies/parse.js +6 -1
  73. package/lib/web/eventsource/eventsource.js +2 -0
  74. package/lib/web/fetch/body.js +43 -39
  75. package/lib/web/fetch/constants.js +45 -29
  76. package/lib/web/fetch/data-url.js +2 -2
  77. package/lib/web/fetch/formdata-parser.js +84 -46
  78. package/lib/web/fetch/formdata.js +42 -20
  79. package/lib/web/fetch/headers.js +119 -85
  80. package/lib/web/fetch/index.js +69 -65
  81. package/lib/web/fetch/request.js +132 -55
  82. package/lib/web/fetch/response.js +81 -36
  83. package/lib/web/fetch/util.js +274 -103
  84. package/lib/web/fetch/webidl.js +54 -18
  85. package/lib/web/websocket/connection.js +92 -15
  86. package/lib/web/websocket/constants.js +69 -9
  87. package/lib/web/websocket/events.js +8 -2
  88. package/lib/web/websocket/receiver.js +20 -26
  89. package/lib/web/websocket/stream/websocketerror.js +83 -0
  90. package/lib/web/websocket/stream/websocketstream.js +485 -0
  91. package/lib/web/websocket/util.js +115 -10
  92. package/lib/web/websocket/websocket.js +47 -170
  93. package/package.json +15 -11
  94. package/types/agent.d.ts +1 -1
  95. package/types/cache-interceptor.d.ts +172 -0
  96. package/types/cookies.d.ts +2 -0
  97. package/types/dispatcher.d.ts +29 -4
  98. package/types/env-http-proxy-agent.d.ts +1 -1
  99. package/types/fetch.d.ts +9 -8
  100. package/types/handlers.d.ts +4 -4
  101. package/types/index.d.ts +3 -1
  102. package/types/interceptors.d.ts +18 -1
  103. package/types/mock-agent.d.ts +4 -1
  104. package/types/mock-client.d.ts +1 -1
  105. package/types/mock-pool.d.ts +1 -1
  106. package/types/proxy-agent.d.ts +1 -1
  107. package/types/readable.d.ts +10 -7
  108. package/types/retry-handler.d.ts +3 -3
  109. package/types/webidl.d.ts +30 -4
  110. package/types/websocket.d.ts +33 -0
  111. package/lib/mock/pluralizer.js +0 -29
  112. package/lib/web/cache/symbols.js +0 -5
  113. package/lib/web/fetch/symbols.js +0 -8
@@ -19,7 +19,20 @@ const kBytesRead = Symbol('kBytesRead')
19
19
 
20
20
  const noop = () => {}
21
21
 
22
+ /**
23
+ * @class
24
+ * @extends {Readable}
25
+ * @see https://fetch.spec.whatwg.org/#body
26
+ */
22
27
  class BodyReadable extends Readable {
28
+ /**
29
+ * @param {object} opts
30
+ * @param {(this: Readable, size: number) => void} opts.resume
31
+ * @param {() => (void | null)} opts.abort
32
+ * @param {string} [opts.contentType = '']
33
+ * @param {number} [opts.contentLength]
34
+ * @param {number} [opts.highWaterMark = 64 * 1024]
35
+ */
23
36
  constructor ({
24
37
  resume,
25
38
  abort,
@@ -36,8 +49,15 @@ class BodyReadable extends Readable {
36
49
  this._readableState.dataEmitted = false
37
50
 
38
51
  this[kAbort] = abort
52
+
53
+ /**
54
+ * @type {Consume | null}
55
+ */
39
56
  this[kConsume] = null
40
57
  this[kBytesRead] = 0
58
+ /**
59
+ * @type {ReadableStream|null}
60
+ */
41
61
  this[kBody] = null
42
62
  this[kUsed] = false
43
63
  this[kContentType] = contentType
@@ -50,6 +70,11 @@ class BodyReadable extends Readable {
50
70
  this[kReading] = false
51
71
  }
52
72
 
73
+ /**
74
+ * @param {Error|null} err
75
+ * @param {(error:(Error|null)) => void} callback
76
+ * @returns {void}
77
+ */
53
78
  _destroy (err, callback) {
54
79
  if (!err && !this._readableState.endEmitted) {
55
80
  err = new RequestAbortedError()
@@ -61,7 +86,7 @@ class BodyReadable extends Readable {
61
86
 
62
87
  // Workaround for Node "bug". If the stream is destroyed in same
63
88
  // tick as it is created, then a user who is waiting for a
64
- // promise (i.e micro tick) for installing a 'error' listener will
89
+ // promise (i.e micro tick) for installing an 'error' listener will
65
90
  // never get a chance and will always encounter an unhandled exception.
66
91
  if (!this[kUsed]) {
67
92
  setImmediate(() => {
@@ -72,21 +97,36 @@ class BodyReadable extends Readable {
72
97
  }
73
98
  }
74
99
 
75
- on (ev, ...args) {
76
- if (ev === 'data' || ev === 'readable') {
100
+ /**
101
+ * @param {string} event
102
+ * @param {(...args: any[]) => void} listener
103
+ * @returns {this}
104
+ */
105
+ on (event, listener) {
106
+ if (event === 'data' || event === 'readable') {
77
107
  this[kReading] = true
78
108
  this[kUsed] = true
79
109
  }
80
- return super.on(ev, ...args)
110
+ return super.on(event, listener)
81
111
  }
82
112
 
83
- addListener (ev, ...args) {
84
- return this.on(ev, ...args)
113
+ /**
114
+ * @param {string} event
115
+ * @param {(...args: any[]) => void} listener
116
+ * @returns {this}
117
+ */
118
+ addListener (event, listener) {
119
+ return this.on(event, listener)
85
120
  }
86
121
 
87
- off (ev, ...args) {
88
- const ret = super.off(ev, ...args)
89
- if (ev === 'data' || ev === 'readable') {
122
+ /**
123
+ * @param {string|symbol} event
124
+ * @param {(...args: any[]) => void} listener
125
+ * @returns {this}
126
+ */
127
+ off (event, listener) {
128
+ const ret = super.off(event, listener)
129
+ if (event === 'data' || event === 'readable') {
90
130
  this[kReading] = (
91
131
  this.listenerCount('data') > 0 ||
92
132
  this.listenerCount('readable') > 0
@@ -95,10 +135,19 @@ class BodyReadable extends Readable {
95
135
  return ret
96
136
  }
97
137
 
98
- removeListener (ev, ...args) {
99
- return this.off(ev, ...args)
138
+ /**
139
+ * @param {string|symbol} event
140
+ * @param {(...args: any[]) => void} listener
141
+ * @returns {this}
142
+ */
143
+ removeListener (event, listener) {
144
+ return this.off(event, listener)
100
145
  }
101
146
 
147
+ /**
148
+ * @param {Buffer|null} chunk
149
+ * @returns {boolean}
150
+ */
102
151
  push (chunk) {
103
152
  this[kBytesRead] += chunk ? chunk.length : 0
104
153
 
@@ -109,43 +158,84 @@ class BodyReadable extends Readable {
109
158
  return super.push(chunk)
110
159
  }
111
160
 
112
- // https://fetch.spec.whatwg.org/#dom-body-text
113
- async text () {
161
+ /**
162
+ * Consumes and returns the body as a string.
163
+ *
164
+ * @see https://fetch.spec.whatwg.org/#dom-body-text
165
+ * @returns {Promise<string>}
166
+ */
167
+ text () {
114
168
  return consume(this, 'text')
115
169
  }
116
170
 
117
- // https://fetch.spec.whatwg.org/#dom-body-json
118
- async json () {
171
+ /**
172
+ * Consumes and returns the body as a JavaScript Object.
173
+ *
174
+ * @see https://fetch.spec.whatwg.org/#dom-body-json
175
+ * @returns {Promise<unknown>}
176
+ */
177
+ json () {
119
178
  return consume(this, 'json')
120
179
  }
121
180
 
122
- // https://fetch.spec.whatwg.org/#dom-body-blob
123
- async blob () {
181
+ /**
182
+ * Consumes and returns the body as a Blob
183
+ *
184
+ * @see https://fetch.spec.whatwg.org/#dom-body-blob
185
+ * @returns {Promise<Blob>}
186
+ */
187
+ blob () {
124
188
  return consume(this, 'blob')
125
189
  }
126
190
 
127
- // https://fetch.spec.whatwg.org/#dom-body-bytes
128
- async bytes () {
191
+ /**
192
+ * Consumes and returns the body as an Uint8Array.
193
+ *
194
+ * @see https://fetch.spec.whatwg.org/#dom-body-bytes
195
+ * @returns {Promise<Uint8Array>}
196
+ */
197
+ bytes () {
129
198
  return consume(this, 'bytes')
130
199
  }
131
200
 
132
- // https://fetch.spec.whatwg.org/#dom-body-arraybuffer
133
- async arrayBuffer () {
201
+ /**
202
+ * Consumes and returns the body as an ArrayBuffer.
203
+ *
204
+ * @see https://fetch.spec.whatwg.org/#dom-body-arraybuffer
205
+ * @returns {Promise<ArrayBuffer>}
206
+ */
207
+ arrayBuffer () {
134
208
  return consume(this, 'arrayBuffer')
135
209
  }
136
210
 
137
- // https://fetch.spec.whatwg.org/#dom-body-formdata
211
+ /**
212
+ * Not implemented
213
+ *
214
+ * @see https://fetch.spec.whatwg.org/#dom-body-formdata
215
+ * @throws {NotSupportedError}
216
+ */
138
217
  async formData () {
139
218
  // TODO: Implement.
140
219
  throw new NotSupportedError()
141
220
  }
142
221
 
143
- // https://fetch.spec.whatwg.org/#dom-body-bodyused
222
+ /**
223
+ * Returns true if the body is not null and the body has been consumed.
224
+ * Otherwise, returns false.
225
+ *
226
+ * @see https://fetch.spec.whatwg.org/#dom-body-bodyused
227
+ * @readonly
228
+ * @returns {boolean}
229
+ */
144
230
  get bodyUsed () {
145
231
  return util.isDisturbed(this)
146
232
  }
147
233
 
148
- // https://fetch.spec.whatwg.org/#dom-body-body
234
+ /**
235
+ * @see https://fetch.spec.whatwg.org/#dom-body-body
236
+ * @readonly
237
+ * @returns {ReadableStream}
238
+ */
149
239
  get body () {
150
240
  if (!this[kBody]) {
151
241
  this[kBody] = ReadableStreamFrom(this)
@@ -158,6 +248,13 @@ class BodyReadable extends Readable {
158
248
  return this[kBody]
159
249
  }
160
250
 
251
+ /**
252
+ * Dumps the response body by reading `limit` number of bytes.
253
+ * @param {object} opts
254
+ * @param {number} [opts.limit = 131072] Number of bytes to read.
255
+ * @param {AbortSignal} [opts.signal] An AbortSignal to cancel the dump.
256
+ * @returns {Promise<null>}
257
+ */
161
258
  async dump (opts) {
162
259
  const signal = opts?.signal
163
260
 
@@ -165,7 +262,9 @@ class BodyReadable extends Readable {
165
262
  throw new InvalidArgumentError('signal must be an AbortSignal')
166
263
  }
167
264
 
168
- const limit = Number.isFinite(opts?.limit) ? opts.limit : 128 * 1024
265
+ const limit = opts?.limit && Number.isFinite(opts.limit)
266
+ ? opts.limit
267
+ : 128 * 1024
169
268
 
170
269
  signal?.throwIfAborted()
171
270
 
@@ -174,26 +273,34 @@ class BodyReadable extends Readable {
174
273
  }
175
274
 
176
275
  return await new Promise((resolve, reject) => {
177
- if (this[kContentLength] > limit || this[kBytesRead] > limit) {
276
+ if (
277
+ (this[kContentLength] && (this[kContentLength] > limit)) ||
278
+ this[kBytesRead] > limit
279
+ ) {
178
280
  this.destroy(new AbortError())
179
281
  }
180
282
 
181
- const onAbort = () => {
182
- this.destroy(signal.reason ?? new AbortError())
283
+ if (signal) {
284
+ const onAbort = () => {
285
+ this.destroy(signal.reason ?? new AbortError())
286
+ }
287
+ signal.addEventListener('abort', onAbort)
288
+ this
289
+ .on('close', function () {
290
+ signal.removeEventListener('abort', onAbort)
291
+ if (signal.aborted) {
292
+ reject(signal.reason ?? new AbortError())
293
+ } else {
294
+ resolve(null)
295
+ }
296
+ })
297
+ } else {
298
+ this.on('close', resolve)
183
299
  }
184
- signal?.addEventListener('abort', onAbort)
185
300
 
186
301
  this
187
- .on('close', function () {
188
- signal?.removeEventListener('abort', onAbort)
189
- if (signal?.aborted) {
190
- reject(signal.reason ?? new AbortError())
191
- } else {
192
- resolve(null)
193
- }
194
- })
195
302
  .on('error', noop)
196
- .on('data', function (chunk) {
303
+ .on('data', () => {
197
304
  if (this[kBytesRead] > limit) {
198
305
  this.destroy()
199
306
  }
@@ -204,7 +311,7 @@ class BodyReadable extends Readable {
204
311
 
205
312
  /**
206
313
  * @param {BufferEncoding} encoding
207
- * @returns {BodyReadable}
314
+ * @returns {this}
208
315
  */
209
316
  setEncoding (encoding) {
210
317
  if (Buffer.isEncoding(encoding)) {
@@ -214,18 +321,41 @@ class BodyReadable extends Readable {
214
321
  }
215
322
  }
216
323
 
217
- // https://streams.spec.whatwg.org/#readablestream-locked
218
- function isLocked (self) {
324
+ /**
325
+ * @see https://streams.spec.whatwg.org/#readablestream-locked
326
+ * @param {BodyReadable} bodyReadable
327
+ * @returns {boolean}
328
+ */
329
+ function isLocked (bodyReadable) {
219
330
  // Consume is an implicit lock.
220
- return (self[kBody] && self[kBody].locked === true) || self[kConsume]
331
+ return bodyReadable[kBody]?.locked === true || bodyReadable[kConsume] !== null
221
332
  }
222
333
 
223
- // https://fetch.spec.whatwg.org/#body-unusable
224
- function isUnusable (self) {
225
- return util.isDisturbed(self) || isLocked(self)
334
+ /**
335
+ * @see https://fetch.spec.whatwg.org/#body-unusable
336
+ * @param {BodyReadable} bodyReadable
337
+ * @returns {boolean}
338
+ */
339
+ function isUnusable (bodyReadable) {
340
+ return util.isDisturbed(bodyReadable) || isLocked(bodyReadable)
226
341
  }
227
342
 
228
- async function consume (stream, type) {
343
+ /**
344
+ * @typedef {object} Consume
345
+ * @property {string} type
346
+ * @property {BodyReadable} stream
347
+ * @property {((value?: any) => void)} resolve
348
+ * @property {((err: Error) => void)} reject
349
+ * @property {number} length
350
+ * @property {Buffer[]} body
351
+ */
352
+
353
+ /**
354
+ * @param {BodyReadable} stream
355
+ * @param {string} type
356
+ * @returns {Promise<any>}
357
+ */
358
+ function consume (stream, type) {
229
359
  assert(!stream[kConsume])
230
360
 
231
361
  return new Promise((resolve, reject) => {
@@ -269,6 +399,10 @@ async function consume (stream, type) {
269
399
  })
270
400
  }
271
401
 
402
+ /**
403
+ * @param {Consume} consume
404
+ * @returns {void}
405
+ */
272
406
  function consumeStart (consume) {
273
407
  if (consume.body === null) {
274
408
  return
@@ -356,6 +490,11 @@ function chunksConcat (chunks, length) {
356
490
  return buffer
357
491
  }
358
492
 
493
+ /**
494
+ * @param {Consume} consume
495
+ * @param {BufferEncoding} encoding
496
+ * @returns {void}
497
+ */
359
498
  function consumeEnd (consume, encoding) {
360
499
  const { type, body, resolve, stream, length } = consume
361
500
 
@@ -378,11 +517,21 @@ function consumeEnd (consume, encoding) {
378
517
  }
379
518
  }
380
519
 
520
+ /**
521
+ * @param {Consume} consume
522
+ * @param {Buffer} chunk
523
+ * @returns {void}
524
+ */
381
525
  function consumePush (consume, chunk) {
382
526
  consume.length += chunk.length
383
527
  consume.body.push(chunk)
384
528
  }
385
529
 
530
+ /**
531
+ * @param {Consume} consume
532
+ * @param {Error} [err]
533
+ * @returns {void}
534
+ */
386
535
  function consumeFinish (consume, err) {
387
536
  if (consume.body === null) {
388
537
  return
@@ -394,6 +543,7 @@ function consumeFinish (consume, err) {
394
543
  consume.resolve()
395
544
  }
396
545
 
546
+ // Reset the consume object to allow for garbage collection.
397
547
  consume.type = null
398
548
  consume.stream = null
399
549
  consume.resolve = null
@@ -402,4 +552,7 @@ function consumeFinish (consume, err) {
402
552
  consume.body = null
403
553
  }
404
554
 
405
- module.exports = { Readable: BodyReadable, chunksDecode }
555
+ module.exports = {
556
+ Readable: BodyReadable,
557
+ chunksDecode
558
+ }
package/lib/api/util.js CHANGED
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const assert = require('node:assert')
2
4
  const {
3
5
  ResponseStatusCodeError
@@ -0,0 +1,177 @@
1
+ 'use strict'
2
+
3
+ const { Writable } = require('node:stream')
4
+ const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
5
+
6
+ /**
7
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey
8
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheValue} CacheValue
9
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
10
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.GetResult} GetResult
11
+ */
12
+
13
+ /**
14
+ * @implements {CacheStore}
15
+ */
16
+ class MemoryCacheStore {
17
+ #maxCount = Infinity
18
+ #maxSize = Infinity
19
+ #maxEntrySize = Infinity
20
+
21
+ #size = 0
22
+ #count = 0
23
+ #entries = new Map()
24
+
25
+ /**
26
+ * @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts]
27
+ */
28
+ constructor (opts) {
29
+ if (opts) {
30
+ if (typeof opts !== 'object') {
31
+ throw new TypeError('MemoryCacheStore options must be an object')
32
+ }
33
+
34
+ if (opts.maxCount !== undefined) {
35
+ if (
36
+ typeof opts.maxCount !== 'number' ||
37
+ !Number.isInteger(opts.maxCount) ||
38
+ opts.maxCount < 0
39
+ ) {
40
+ throw new TypeError('MemoryCacheStore options.maxCount must be a non-negative integer')
41
+ }
42
+ this.#maxCount = opts.maxCount
43
+ }
44
+
45
+ if (opts.maxSize !== undefined) {
46
+ if (
47
+ typeof opts.maxSize !== 'number' ||
48
+ !Number.isInteger(opts.maxSize) ||
49
+ opts.maxSize < 0
50
+ ) {
51
+ throw new TypeError('MemoryCacheStore options.maxSize must be a non-negative integer')
52
+ }
53
+ this.#maxSize = opts.maxSize
54
+ }
55
+
56
+ if (opts.maxEntrySize !== undefined) {
57
+ if (
58
+ typeof opts.maxEntrySize !== 'number' ||
59
+ !Number.isInteger(opts.maxEntrySize) ||
60
+ opts.maxEntrySize < 0
61
+ ) {
62
+ throw new TypeError('MemoryCacheStore options.maxEntrySize must be a non-negative integer')
63
+ }
64
+ this.#maxEntrySize = opts.maxEntrySize
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req
71
+ * @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
72
+ */
73
+ get (key) {
74
+ assertCacheKey(key)
75
+
76
+ const topLevelKey = `${key.origin}:${key.path}`
77
+
78
+ const now = Date.now()
79
+ const entry = this.#entries.get(topLevelKey)?.find((entry) => (
80
+ entry.deleteAt > now &&
81
+ entry.method === key.method &&
82
+ (entry.vary == null || Object.keys(entry.vary).every(headerName => entry.vary[headerName] === key.headers?.[headerName]))
83
+ ))
84
+
85
+ return entry == null
86
+ ? undefined
87
+ : {
88
+ statusMessage: entry.statusMessage,
89
+ statusCode: entry.statusCode,
90
+ headers: entry.headers,
91
+ body: entry.body,
92
+ vary: entry.vary ? entry.vary : undefined,
93
+ etag: entry.etag,
94
+ cacheControlDirectives: entry.cacheControlDirectives,
95
+ cachedAt: entry.cachedAt,
96
+ staleAt: entry.staleAt,
97
+ deleteAt: entry.deleteAt
98
+ }
99
+ }
100
+
101
+ /**
102
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
103
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} val
104
+ * @returns {Writable | undefined}
105
+ */
106
+ createWriteStream (key, val) {
107
+ assertCacheKey(key)
108
+ assertCacheValue(val)
109
+
110
+ const topLevelKey = `${key.origin}:${key.path}`
111
+
112
+ const store = this
113
+ const entry = { ...key, ...val, body: [], size: 0 }
114
+
115
+ return new Writable({
116
+ write (chunk, encoding, callback) {
117
+ if (typeof chunk === 'string') {
118
+ chunk = Buffer.from(chunk, encoding)
119
+ }
120
+
121
+ entry.size += chunk.byteLength
122
+
123
+ if (entry.size >= store.#maxEntrySize) {
124
+ this.destroy()
125
+ } else {
126
+ entry.body.push(chunk)
127
+ }
128
+
129
+ callback(null)
130
+ },
131
+ final (callback) {
132
+ let entries = store.#entries.get(topLevelKey)
133
+ if (!entries) {
134
+ entries = []
135
+ store.#entries.set(topLevelKey, entries)
136
+ }
137
+ entries.push(entry)
138
+
139
+ store.#size += entry.size
140
+ store.#count += 1
141
+
142
+ if (store.#size > store.#maxSize || store.#count > store.#maxCount) {
143
+ for (const [key, entries] of store.#entries) {
144
+ for (const entry of entries.splice(0, entries.length / 2)) {
145
+ store.#size -= entry.size
146
+ store.#count -= 1
147
+ }
148
+ if (entries.length === 0) {
149
+ store.#entries.delete(key)
150
+ }
151
+ }
152
+ }
153
+
154
+ callback(null)
155
+ }
156
+ })
157
+ }
158
+
159
+ /**
160
+ * @param {CacheKey} key
161
+ */
162
+ delete (key) {
163
+ if (typeof key !== 'object') {
164
+ throw new TypeError(`expected key to be object, got ${typeof key}`)
165
+ }
166
+
167
+ const topLevelKey = `${key.origin}:${key.path}`
168
+
169
+ for (const entry of this.#entries.get(topLevelKey) ?? []) {
170
+ this.#size -= entry.size
171
+ this.#count -= 1
172
+ }
173
+ this.#entries.delete(topLevelKey)
174
+ }
175
+ }
176
+
177
+ module.exports = MemoryCacheStore