undici 7.0.0-alpha.3 → 7.0.0-alpha.4

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.
@@ -1,33 +1,26 @@
1
1
  'use strict'
2
2
 
3
- const { Writable, Readable } = require('node:stream')
3
+ const { Writable } = require('node:stream')
4
4
 
5
5
  /**
6
6
  * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
7
7
  * @implements {CacheStore}
8
8
  *
9
9
  * @typedef {{
10
- * readers: number
11
- * readLock: boolean
12
- * writeLock: boolean
13
- * opts: import('../../types/cache-interceptor.d.ts').default.CacheStoreValue
14
- * body: Buffer[]
10
+ * locked: boolean
11
+ * opts: import('../../types/cache-interceptor.d.ts').default.CachedResponse
12
+ * body?: Buffer[]
15
13
  * }} MemoryStoreValue
16
14
  */
17
15
  class MemoryCacheStore {
18
- #maxEntries = Infinity
16
+ #maxCount = Infinity
19
17
 
20
18
  #maxEntrySize = Infinity
21
19
 
22
- /**
23
- * @type {((err) => void) | undefined}
24
- */
25
- #errorCallback = undefined
26
-
27
20
  #entryCount = 0
28
21
 
29
22
  /**
30
- * @type {Map<string, Map<string, MemoryStoreValue>>}
23
+ * @type {Map<string, Map<string, MemoryStoreValue[]>>}
31
24
  */
32
25
  #data = new Map()
33
26
 
@@ -40,15 +33,15 @@ class MemoryCacheStore {
40
33
  throw new TypeError('MemoryCacheStore options must be an object')
41
34
  }
42
35
 
43
- if (opts.maxEntries !== undefined) {
36
+ if (opts.maxCount !== undefined) {
44
37
  if (
45
- typeof opts.maxEntries !== 'number' ||
46
- !Number.isInteger(opts.maxEntries) ||
47
- opts.maxEntries < 0
38
+ typeof opts.maxCount !== 'number' ||
39
+ !Number.isInteger(opts.maxCount) ||
40
+ opts.maxCount < 0
48
41
  ) {
49
- throw new TypeError('MemoryCacheStore options.maxEntries must be a non-negative integer')
42
+ throw new TypeError('MemoryCacheStore options.maxCount must be a non-negative integer')
50
43
  }
51
- this.#maxEntries = opts.maxEntries
44
+ this.#maxCount = opts.maxCount
52
45
  }
53
46
 
54
47
  if (opts.maxEntrySize !== undefined) {
@@ -61,51 +54,44 @@ class MemoryCacheStore {
61
54
  }
62
55
  this.#maxEntrySize = opts.maxEntrySize
63
56
  }
64
-
65
- if (opts.errorCallback !== undefined) {
66
- if (typeof opts.errorCallback !== 'function') {
67
- throw new TypeError('MemoryCacheStore options.errorCallback must be a function')
68
- }
69
- this.#errorCallback = opts.errorCallback
70
- }
71
57
  }
72
58
  }
73
59
 
74
60
  get isFull () {
75
- return this.#entryCount >= this.#maxEntries
61
+ return this.#entryCount >= this.#maxCount
76
62
  }
77
63
 
78
64
  /**
79
- * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
80
- * @returns {import('../../types/cache-interceptor.d.ts').default.CacheStoreReadable | undefined}
65
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
66
+ * @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
81
67
  */
82
- createReadStream (req) {
83
- if (typeof req !== 'object') {
84
- throw new TypeError(`expected req to be object, got ${typeof req}`)
68
+ get (key) {
69
+ if (typeof key !== 'object') {
70
+ throw new TypeError(`expected key to be object, got ${typeof key}`)
85
71
  }
86
72
 
87
- const values = this.#getValuesForRequest(req, false)
73
+ const values = this.#getValuesForRequest(key, false)
88
74
  if (!values) {
89
75
  return undefined
90
76
  }
91
77
 
92
- const value = this.#findValue(req, values)
78
+ const value = this.#findValue(key, values)
93
79
 
94
- if (!value || value.readLock) {
80
+ if (!value || value.locked) {
95
81
  return undefined
96
82
  }
97
83
 
98
- return new MemoryStoreReadableStream(value)
84
+ return { ...value.opts, body: value.body }
99
85
  }
100
86
 
101
87
  /**
102
- * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
103
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreValue} opts
104
- * @returns {import('../../types/cache-interceptor.d.ts').default.CacheStoreWriteable | undefined}
88
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
89
+ * @param {import('../../types/cache-interceptor.d.ts').default.CachedResponse} opts
90
+ * @returns {Writable | undefined}
105
91
  */
106
- createWriteStream (req, opts) {
107
- if (typeof req !== 'object') {
108
- throw new TypeError(`expected req to be object, got ${typeof req}`)
92
+ createWriteStream (key, opts) {
93
+ if (typeof key !== 'object') {
94
+ throw new TypeError(`expected key to be object, got ${typeof key}`)
109
95
  }
110
96
  if (typeof opts !== 'object') {
111
97
  throw new TypeError(`expected value to be object, got ${typeof opts}`)
@@ -115,9 +101,13 @@ class MemoryCacheStore {
115
101
  return undefined
116
102
  }
117
103
 
118
- const values = this.#getValuesForRequest(req, true)
104
+ const values = this.#getValuesForRequest(key, true)
119
105
 
120
- let value = this.#findValue(req, values)
106
+ /**
107
+ * @type {(MemoryStoreValue & { index: number }) | undefined}
108
+ */
109
+ let value = this.#findValue(key, values)
110
+ let valueIndex = value?.index
121
111
  if (!value) {
122
112
  // The value doesn't already exist, meaning we haven't cached this
123
113
  // response before. Let's assign it a value and insert it into our data
@@ -131,11 +121,8 @@ class MemoryCacheStore {
131
121
  this.#entryCount++
132
122
 
133
123
  value = {
134
- readers: 0,
135
- readLock: false,
136
- writeLock: false,
137
- opts,
138
- body: []
124
+ locked: true,
125
+ opts
139
126
  }
140
127
 
141
128
  // We want to sort our responses in decending order by their deleteAt
@@ -147,9 +134,11 @@ class MemoryCacheStore {
147
134
  // Our value is either the only response for this path or our deleteAt
148
135
  // time is sooner than all the other responses
149
136
  values.push(value)
137
+ valueIndex = values.length - 1
150
138
  } else if (opts.deleteAt >= values[0].deleteAt) {
151
139
  // Our deleteAt is later than everyone elses
152
140
  values.unshift(value)
141
+ valueIndex = 0
153
142
  } else {
154
143
  // We're neither in the front or the end, let's just binary search to
155
144
  // find our stop we need to be in
@@ -165,6 +154,7 @@ class MemoryCacheStore {
165
154
  const middleValue = values[middleIndex]
166
155
  if (opts.deleteAt === middleIndex) {
167
156
  values.splice(middleIndex, 0, value)
157
+ valueIndex = middleIndex
168
158
  break
169
159
  } else if (opts.deleteAt > middleValue.opts.deleteAt) {
170
160
  endIndex = middleIndex
@@ -178,7 +168,7 @@ class MemoryCacheStore {
178
168
  } else {
179
169
  // Check if there's already another request writing to the value or
180
170
  // a request reading from it
181
- if (value.writeLock || value.readLock) {
171
+ if (value.locked) {
182
172
  return undefined
183
173
  }
184
174
 
@@ -186,70 +176,98 @@ class MemoryCacheStore {
186
176
  value.body = []
187
177
  }
188
178
 
189
- const writable = new MemoryStoreWritableStream(
190
- value,
191
- this.#maxEntrySize
192
- )
179
+ let currentSize = 0
180
+ /**
181
+ * @type {Buffer[] | null}
182
+ */
183
+ let body = key.method !== 'HEAD' ? [] : null
184
+ const maxEntrySize = this.#maxEntrySize
193
185
 
194
- // Remove the value if there was some error
195
- writable.on('error', (err) => {
196
- values.filter(current => value !== current)
197
- if (this.#errorCallback) {
198
- this.#errorCallback(err)
199
- }
200
- })
186
+ const writable = new Writable({
187
+ write (chunk, encoding, callback) {
188
+ if (key.method === 'HEAD') {
189
+ throw new Error('HEAD request shouldn\'t have a body')
190
+ }
191
+
192
+ if (!body) {
193
+ return callback()
194
+ }
195
+
196
+ if (typeof chunk === 'string') {
197
+ chunk = Buffer.from(chunk, encoding)
198
+ }
199
+
200
+ currentSize += chunk.byteLength
201
+
202
+ if (currentSize >= maxEntrySize) {
203
+ body = null
204
+ this.end()
205
+ shiftAtIndex(values, valueIndex)
206
+ return callback()
207
+ }
208
+
209
+ body.push(chunk)
210
+ callback()
211
+ },
212
+ final (callback) {
213
+ value.locked = false
214
+ if (body !== null) {
215
+ value.body = body
216
+ }
201
217
 
202
- writable.on('bodyOversized', () => {
203
- values.filter(current => value !== current)
218
+ callback()
219
+ }
204
220
  })
205
221
 
206
222
  return writable
207
223
  }
208
224
 
209
225
  /**
210
- * @param {string} origin
226
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
211
227
  */
212
- deleteByOrigin (origin) {
213
- this.#data.delete(origin)
228
+ delete (key) {
229
+ this.#data.delete(`${key.origin}:${key.path}`)
214
230
  }
215
231
 
216
232
  /**
217
233
  * Gets all of the requests of the same origin, path, and method. Does not
218
234
  * take the `vary` property into account.
219
- * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
235
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
220
236
  * @param {boolean} [makeIfDoesntExist=false]
237
+ * @returns {MemoryStoreValue[] | undefined}
221
238
  */
222
- #getValuesForRequest (req, makeIfDoesntExist) {
239
+ #getValuesForRequest (key, makeIfDoesntExist) {
223
240
  // https://www.rfc-editor.org/rfc/rfc9111.html#section-2-3
224
- let cachedPaths = this.#data.get(req.origin)
241
+ const topLevelKey = `${key.origin}:${key.path}`
242
+ let cachedPaths = this.#data.get(topLevelKey)
225
243
  if (!cachedPaths) {
226
244
  if (!makeIfDoesntExist) {
227
245
  return undefined
228
246
  }
229
247
 
230
248
  cachedPaths = new Map()
231
- this.#data.set(req.origin, cachedPaths)
249
+ this.#data.set(topLevelKey, cachedPaths)
232
250
  }
233
251
 
234
- let values = cachedPaths.get(`${req.path}:${req.method}`)
235
- if (!values && makeIfDoesntExist) {
236
- values = []
237
- cachedPaths.set(`${req.path}:${req.method}`, values)
252
+ let value = cachedPaths.get(key.method)
253
+ if (!value && makeIfDoesntExist) {
254
+ value = []
255
+ cachedPaths.set(key.method, value)
238
256
  }
239
257
 
240
- return values
258
+ return value
241
259
  }
242
260
 
243
261
  /**
244
262
  * Given a list of values of a certain request, this decides the best value
245
263
  * to respond with.
246
- * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
264
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req
247
265
  * @param {MemoryStoreValue[]} values
248
- * @returns {MemoryStoreValue | undefined}
266
+ * @returns {(MemoryStoreValue & { index: number }) | undefined}
249
267
  */
250
268
  #findValue (req, values) {
251
269
  /**
252
- * @type {MemoryStoreValue}
270
+ * @type {MemoryStoreValue | undefined}
253
271
  */
254
272
  let value
255
273
  const now = Date.now()
@@ -280,7 +298,10 @@ class MemoryCacheStore {
280
298
  }
281
299
 
282
300
  if (matches) {
283
- value = current
301
+ value = {
302
+ ...current,
303
+ index: i
304
+ }
284
305
  break
285
306
  }
286
307
  }
@@ -289,129 +310,16 @@ class MemoryCacheStore {
289
310
  }
290
311
  }
291
312
 
292
- class MemoryStoreReadableStream extends Readable {
293
- /**
294
- * @type {MemoryStoreValue}
295
- */
296
- #value
297
- /**
298
- * @type {Buffer[]}
299
- */
300
- #chunksToSend = []
301
-
302
- /**
303
- * @param {MemoryStoreValue} value
304
- */
305
- constructor (value) {
306
- super()
307
-
308
- if (value.readLock) {
309
- throw new Error('can\'t read a locked value')
310
- }
311
-
312
- this.#value = value
313
- this.#chunksToSend = value?.body ? [...value.body, null] : [null]
314
-
315
- this.#value.readers++
316
- this.#value.writeLock = true
317
-
318
- this.on('close', () => {
319
- this.#value.readers--
320
-
321
- if (this.#value.readers === 0) {
322
- this.#value.writeLock = false
323
- }
324
- })
325
- }
326
-
327
- get value () {
328
- return this.#value.opts
329
- }
330
-
331
- /**
332
- * @param {number} size
333
- */
334
- _read (size) {
335
- if (this.#chunksToSend.length === 0) {
336
- throw new Error('no chunks left to read, stream should have closed')
337
- }
338
-
339
- if (size > this.#chunksToSend.length) {
340
- size = this.#chunksToSend.length
341
- }
342
-
343
- for (let i = 0; i < size; i++) {
344
- this.push(this.#chunksToSend.shift())
345
- }
346
- }
347
- }
348
-
349
- class MemoryStoreWritableStream extends Writable {
350
- /**
351
- * @type {MemoryStoreValue}
352
- */
353
- #value
354
- #currentSize = 0
355
- #maxEntrySize = 0
356
- /**
357
- * @type {Buffer[]|null}
358
- */
359
- #body = []
360
-
361
- /**
362
- * @param {MemoryStoreValue} value
363
- * @param {number} maxEntrySize
364
- */
365
- constructor (value, maxEntrySize) {
366
- super()
367
- this.#value = value
368
- this.#value.readLock = true
369
- this.#maxEntrySize = maxEntrySize ?? Infinity
370
- }
371
-
372
- get rawTrailers () {
373
- return this.#value.opts.rawTrailers
374
- }
375
-
376
- /**
377
- * @param {string[] | undefined} trailers
378
- */
379
- set rawTrailers (trailers) {
380
- this.#value.opts.rawTrailers = trailers
381
- }
382
-
383
- /**
384
- * @param {Buffer} chunk
385
- * @param {string} encoding
386
- * @param {BufferEncoding} encoding
387
- */
388
- _write (chunk, encoding, callback) {
389
- if (typeof chunk === 'string') {
390
- chunk = Buffer.from(chunk, encoding)
391
- }
392
-
393
- this.#currentSize += chunk.byteLength
394
- if (this.#currentSize < this.#maxEntrySize) {
395
- this.#body.push(chunk)
396
- } else {
397
- this.#body = null // release memory as early as possible
398
- this.emit('bodyOversized')
399
- }
400
-
401
- callback()
313
+ /**
314
+ * @param {any[]} array Array to modify
315
+ * @param {number} idx Index to delete
316
+ */
317
+ function shiftAtIndex (array, idx) {
318
+ for (let i = idx + 1; idx < array.length; i++) {
319
+ array[i - 1] = array[i]
402
320
  }
403
321
 
404
- /**
405
- * @param {() => void} callback
406
- */
407
- _final (callback) {
408
- if (this.#currentSize < this.#maxEntrySize) {
409
- this.#value.readLock = false
410
- this.#value.body = this.#body
411
- }
412
-
413
- callback()
414
- }
322
+ array.length--
415
323
  }
416
324
 
417
325
  module.exports = MemoryCacheStore
@@ -220,6 +220,11 @@ const setupConnectTimeout = process.platform === 'win32'
220
220
  * @param {number} opts.port
221
221
  */
222
222
  function onConnectTimeout (socket, opts) {
223
+ // The socket could be already garbage collected
224
+ if (socket == null) {
225
+ return
226
+ }
227
+
223
228
  let message = 'Connect Timeout Error'
224
229
  if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
225
230
  message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
@@ -130,7 +130,6 @@ class Request {
130
130
  }
131
131
 
132
132
  this.completed = false
133
-
134
133
  this.aborted = false
135
134
 
136
135
  this.upgrade = upgrade || null
@@ -143,7 +142,7 @@ class Request {
143
142
  ? method === 'HEAD' || method === 'GET'
144
143
  : idempotent
145
144
 
146
- this.blocking = blocking == null ? false : blocking
145
+ this.blocking = blocking ?? this.method !== 'HEAD'
147
146
 
148
147
  this.reset = reset == null ? null : reset
149
148
 
@@ -272,6 +271,7 @@ class Request {
272
271
  this.onFinally()
273
272
 
274
273
  assert(!this.aborted)
274
+ assert(!this.completed)
275
275
 
276
276
  this.completed = true
277
277
  if (channels.trailers.hasSubscribers) {
package/lib/core/util.js CHANGED
@@ -10,7 +10,7 @@ const nodeUtil = require('node:util')
10
10
  const { stringify } = require('node:querystring')
11
11
  const { EventEmitter: EE } = require('node:events')
12
12
  const { InvalidArgumentError } = require('./errors')
13
- const { headerNameLowerCasedRecord, getHeaderNameAsBuffer } = require('./constants')
13
+ const { headerNameLowerCasedRecord } = require('./constants')
14
14
  const { tree } = require('./tree')
15
15
 
16
16
  const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
@@ -436,44 +436,6 @@ function parseHeaders (headers, obj) {
436
436
  return obj
437
437
  }
438
438
 
439
- /**
440
- * @param {Record<string, string | string[]>} headers
441
- * @returns {(Buffer | Buffer[])[]}
442
- */
443
- function encodeHeaders (headers) {
444
- const headerNames = Object.keys(headers)
445
-
446
- /**
447
- * @type {Buffer[]|Buffer[][]}
448
- */
449
- const rawHeaders = new Array(headerNames.length * 2)
450
-
451
- let rawHeadersIndex = 0
452
- for (const header of headerNames) {
453
- let rawValue
454
- const value = headers[header]
455
- if (Array.isArray(value)) {
456
- rawValue = new Array(value.length)
457
-
458
- for (let i = 0; i < value.length; i++) {
459
- rawValue[i] = Buffer.from(value[i])
460
- }
461
- } else {
462
- rawValue = Buffer.from(value)
463
- }
464
-
465
- const headerBuffer = getHeaderNameAsBuffer(header)
466
-
467
- rawHeaders[rawHeadersIndex] = headerBuffer
468
- rawHeadersIndex++
469
-
470
- rawHeaders[rawHeadersIndex] = rawValue
471
- rawHeadersIndex++
472
- }
473
-
474
- return rawHeaders
475
- }
476
-
477
439
  /**
478
440
  * @param {Buffer[]} headers
479
441
  * @returns {string[]}
@@ -516,6 +478,17 @@ function parseRawHeaders (headers) {
516
478
  return ret
517
479
  }
518
480
 
481
+ /**
482
+ * @param {string[]} headers
483
+ * @param {Buffer[]} headers
484
+ */
485
+ function encodeRawHeaders (headers) {
486
+ if (!Array.isArray(headers)) {
487
+ throw new TypeError('expected headers to be an array')
488
+ }
489
+ return headers.map(x => Buffer.from(x))
490
+ }
491
+
519
492
  /**
520
493
  * @param {*} buffer
521
494
  * @returns {buffer is Buffer}
@@ -901,8 +874,8 @@ module.exports = {
901
874
  removeAllListeners,
902
875
  errorRequest,
903
876
  parseRawHeaders,
877
+ encodeRawHeaders,
904
878
  parseHeaders,
905
- encodeHeaders,
906
879
  parseKeepAliveTimeout,
907
880
  destroy,
908
881
  bodyLength,