undici 6.19.7 → 6.20.0

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,99 +1,423 @@
1
1
  'use strict'
2
2
 
3
- const TICK_MS = 499
3
+ /**
4
+ * This module offers an optimized timer implementation designed for scenarios
5
+ * where high precision is not critical.
6
+ *
7
+ * The timer achieves faster performance by using a low-resolution approach,
8
+ * with an accuracy target of within 500ms. This makes it particularly useful
9
+ * for timers with delays of 1 second or more, where exact timing is less
10
+ * crucial.
11
+ *
12
+ * It's important to note that Node.js timers are inherently imprecise, as
13
+ * delays can occur due to the event loop being blocked by other operations.
14
+ * Consequently, timers may trigger later than their scheduled time.
15
+ */
4
16
 
5
- let fastNow = Date.now()
17
+ /**
18
+ * The fastNow variable contains the internal fast timer clock value.
19
+ *
20
+ * @type {number}
21
+ */
22
+ let fastNow = 0
23
+
24
+ /**
25
+ * RESOLUTION_MS represents the target resolution time in milliseconds.
26
+ *
27
+ * @type {number}
28
+ * @default 1000
29
+ */
30
+ const RESOLUTION_MS = 1e3
31
+
32
+ /**
33
+ * TICK_MS defines the desired interval in milliseconds between each tick.
34
+ * The target value is set to half the resolution time, minus 1 ms, to account
35
+ * for potential event loop overhead.
36
+ *
37
+ * @type {number}
38
+ * @default 499
39
+ */
40
+ const TICK_MS = (RESOLUTION_MS >> 1) - 1
41
+
42
+ /**
43
+ * fastNowTimeout is a Node.js timer used to manage and process
44
+ * the FastTimers stored in the `fastTimers` array.
45
+ *
46
+ * @type {NodeJS.Timeout}
47
+ */
6
48
  let fastNowTimeout
7
49
 
50
+ /**
51
+ * The kFastTimer symbol is used to identify FastTimer instances.
52
+ *
53
+ * @type {Symbol}
54
+ */
55
+ const kFastTimer = Symbol('kFastTimer')
56
+
57
+ /**
58
+ * The fastTimers array contains all active FastTimers.
59
+ *
60
+ * @type {FastTimer[]}
61
+ */
8
62
  const fastTimers = []
9
63
 
10
- function onTimeout () {
11
- fastNow = Date.now()
64
+ /**
65
+ * These constants represent the various states of a FastTimer.
66
+ */
12
67
 
13
- let len = fastTimers.length
68
+ /**
69
+ * The `NOT_IN_LIST` constant indicates that the FastTimer is not included
70
+ * in the `fastTimers` array. Timers with this status will not be processed
71
+ * during the next tick by the `onTick` function.
72
+ *
73
+ * A FastTimer can be re-added to the `fastTimers` array by invoking the
74
+ * `refresh` method on the FastTimer instance.
75
+ *
76
+ * @type {-2}
77
+ */
78
+ const NOT_IN_LIST = -2
79
+
80
+ /**
81
+ * The `TO_BE_CLEARED` constant indicates that the FastTimer is scheduled
82
+ * for removal from the `fastTimers` array. A FastTimer in this state will
83
+ * be removed in the next tick by the `onTick` function and will no longer
84
+ * be processed.
85
+ *
86
+ * This status is also set when the `clear` method is called on the FastTimer instance.
87
+ *
88
+ * @type {-1}
89
+ */
90
+ const TO_BE_CLEARED = -1
91
+
92
+ /**
93
+ * The `PENDING` constant signifies that the FastTimer is awaiting processing
94
+ * in the next tick by the `onTick` function. Timers with this status will have
95
+ * their `_idleStart` value set and their status updated to `ACTIVE` in the next tick.
96
+ *
97
+ * @type {0}
98
+ */
99
+ const PENDING = 0
100
+
101
+ /**
102
+ * The `ACTIVE` constant indicates that the FastTimer is active and waiting
103
+ * for its timer to expire. During the next tick, the `onTick` function will
104
+ * check if the timer has expired, and if so, it will execute the associated callback.
105
+ *
106
+ * @type {1}
107
+ */
108
+ const ACTIVE = 1
109
+
110
+ /**
111
+ * The onTick function processes the fastTimers array.
112
+ *
113
+ * @returns {void}
114
+ */
115
+ function onTick () {
116
+ /**
117
+ * Increment the fastNow value by the TICK_MS value, despite the actual time
118
+ * that has passed since the last tick. This approach ensures independence
119
+ * from the system clock and delays caused by a blocked event loop.
120
+ *
121
+ * @type {number}
122
+ */
123
+ fastNow += TICK_MS
124
+
125
+ /**
126
+ * The `idx` variable is used to iterate over the `fastTimers` array.
127
+ * Expired timers are removed by replacing them with the last element in the array.
128
+ * Consequently, `idx` is only incremented when the current element is not removed.
129
+ *
130
+ * @type {number}
131
+ */
14
132
  let idx = 0
133
+
134
+ /**
135
+ * The len variable will contain the length of the fastTimers array
136
+ * and will be decremented when a FastTimer should be removed from the
137
+ * fastTimers array.
138
+ *
139
+ * @type {number}
140
+ */
141
+ let len = fastTimers.length
142
+
15
143
  while (idx < len) {
144
+ /**
145
+ * @type {FastTimer}
146
+ */
16
147
  const timer = fastTimers[idx]
17
148
 
18
- if (timer.state === 0) {
19
- timer.state = fastNow + timer.delay - TICK_MS
20
- } else if (timer.state > 0 && fastNow >= timer.state) {
21
- timer.state = -1
22
- timer.callback(timer.opaque)
149
+ // If the timer is in the ACTIVE state and the timer has expired, it will
150
+ // be processed in the next tick.
151
+ if (timer._state === PENDING) {
152
+ // Set the _idleStart value to the fastNow value minus the TICK_MS value
153
+ // to account for the time the timer was in the PENDING state.
154
+ timer._idleStart = fastNow - TICK_MS
155
+ timer._state = ACTIVE
156
+ } else if (
157
+ timer._state === ACTIVE &&
158
+ fastNow >= timer._idleStart + timer._idleTimeout
159
+ ) {
160
+ timer._state = TO_BE_CLEARED
161
+ timer._idleStart = -1
162
+ timer._onTimeout(timer._timerArg)
23
163
  }
24
164
 
25
- if (timer.state === -1) {
26
- timer.state = -2
27
- if (idx !== len - 1) {
28
- fastTimers[idx] = fastTimers.pop()
29
- } else {
30
- fastTimers.pop()
165
+ if (timer._state === TO_BE_CLEARED) {
166
+ timer._state = NOT_IN_LIST
167
+
168
+ // Move the last element to the current index and decrement len if it is
169
+ // not the only element in the array.
170
+ if (--len !== 0) {
171
+ fastTimers[idx] = fastTimers[len]
31
172
  }
32
- len -= 1
33
173
  } else {
34
- idx += 1
174
+ ++idx
35
175
  }
36
176
  }
37
177
 
38
- if (fastTimers.length > 0) {
178
+ // Set the length of the fastTimers array to the new length and thus
179
+ // removing the excess FastTimers elements from the array.
180
+ fastTimers.length = len
181
+
182
+ // If there are still active FastTimers in the array, refresh the Timer.
183
+ // If there are no active FastTimers, the timer will be refreshed again
184
+ // when a new FastTimer is instantiated.
185
+ if (fastTimers.length !== 0) {
39
186
  refreshTimeout()
40
187
  }
41
188
  }
42
189
 
43
190
  function refreshTimeout () {
44
- if (fastNowTimeout?.refresh) {
191
+ // If the fastNowTimeout is already set, refresh it.
192
+ if (fastNowTimeout) {
45
193
  fastNowTimeout.refresh()
194
+ // fastNowTimeout is not instantiated yet, create a new Timer.
46
195
  } else {
47
196
  clearTimeout(fastNowTimeout)
48
- fastNowTimeout = setTimeout(onTimeout, TICK_MS)
197
+ fastNowTimeout = setTimeout(onTick, TICK_MS)
198
+
199
+ // If the Timer has an unref method, call it to allow the process to exit if
200
+ // there are no other active handles.
49
201
  if (fastNowTimeout.unref) {
50
202
  fastNowTimeout.unref()
51
203
  }
52
204
  }
53
205
  }
54
206
 
55
- class Timeout {
56
- constructor (callback, delay, opaque) {
57
- this.callback = callback
58
- this.delay = delay
59
- this.opaque = opaque
207
+ /**
208
+ * The `FastTimer` class is a data structure designed to store and manage
209
+ * timer information.
210
+ */
211
+ class FastTimer {
212
+ [kFastTimer] = true
213
+
214
+ /**
215
+ * The state of the timer, which can be one of the following:
216
+ * - NOT_IN_LIST (-2)
217
+ * - TO_BE_CLEARED (-1)
218
+ * - PENDING (0)
219
+ * - ACTIVE (1)
220
+ *
221
+ * @type {-2|-1|0|1}
222
+ * @private
223
+ */
224
+ _state = NOT_IN_LIST
60
225
 
61
- // -2 not in timer list
62
- // -1 in timer list but inactive
63
- // 0 in timer list waiting for time
64
- // > 0 in timer list waiting for time to expire
65
- this.state = -2
226
+ /**
227
+ * The number of milliseconds to wait before calling the callback.
228
+ *
229
+ * @type {number}
230
+ * @private
231
+ */
232
+ _idleTimeout = -1
233
+
234
+ /**
235
+ * The time in milliseconds when the timer was started. This value is used to
236
+ * calculate when the timer should expire.
237
+ *
238
+ * @type {number}
239
+ * @default -1
240
+ * @private
241
+ */
242
+ _idleStart = -1
243
+
244
+ /**
245
+ * The function to be executed when the timer expires.
246
+ * @type {Function}
247
+ * @private
248
+ */
249
+ _onTimeout
250
+
251
+ /**
252
+ * The argument to be passed to the callback when the timer expires.
253
+ *
254
+ * @type {*}
255
+ * @private
256
+ */
257
+ _timerArg
258
+
259
+ /**
260
+ * @constructor
261
+ * @param {Function} callback A function to be executed after the timer
262
+ * expires.
263
+ * @param {number} delay The time, in milliseconds that the timer should wait
264
+ * before the specified function or code is executed.
265
+ * @param {*} arg
266
+ */
267
+ constructor (callback, delay, arg) {
268
+ this._onTimeout = callback
269
+ this._idleTimeout = delay
270
+ this._timerArg = arg
66
271
 
67
272
  this.refresh()
68
273
  }
69
274
 
275
+ /**
276
+ * Sets the timer's start time to the current time, and reschedules the timer
277
+ * to call its callback at the previously specified duration adjusted to the
278
+ * current time.
279
+ * Using this on a timer that has already called its callback will reactivate
280
+ * the timer.
281
+ *
282
+ * @returns {void}
283
+ */
70
284
  refresh () {
71
- if (this.state === -2) {
285
+ // In the special case that the timer is not in the list of active timers,
286
+ // add it back to the array to be processed in the next tick by the onTick
287
+ // function.
288
+ if (this._state === NOT_IN_LIST) {
72
289
  fastTimers.push(this)
73
- if (!fastNowTimeout || fastTimers.length === 1) {
74
- refreshTimeout()
75
- }
76
290
  }
77
291
 
78
- this.state = 0
292
+ // If the timer is the only active timer, refresh the fastNowTimeout for
293
+ // better resolution.
294
+ if (!fastNowTimeout || fastTimers.length === 1) {
295
+ refreshTimeout()
296
+ }
297
+
298
+ // Setting the state to PENDING will cause the timer to be reset in the
299
+ // next tick by the onTick function.
300
+ this._state = PENDING
79
301
  }
80
302
 
303
+ /**
304
+ * The `clear` method cancels the timer, preventing it from executing.
305
+ *
306
+ * @returns {void}
307
+ * @private
308
+ */
81
309
  clear () {
82
- this.state = -1
310
+ // Set the state to TO_BE_CLEARED to mark the timer for removal in the next
311
+ // tick by the onTick function.
312
+ this._state = TO_BE_CLEARED
313
+
314
+ // Reset the _idleStart value to -1 to indicate that the timer is no longer
315
+ // active.
316
+ this._idleStart = -1
83
317
  }
84
318
  }
85
319
 
320
+ /**
321
+ * This module exports a setTimeout and clearTimeout function that can be
322
+ * used as a drop-in replacement for the native functions.
323
+ */
86
324
  module.exports = {
87
- setTimeout (callback, delay, opaque) {
88
- return delay <= 1e3
89
- ? setTimeout(callback, delay, opaque)
90
- : new Timeout(callback, delay, opaque)
325
+ /**
326
+ * The setTimeout() method sets a timer which executes a function once the
327
+ * timer expires.
328
+ * @param {Function} callback A function to be executed after the timer
329
+ * expires.
330
+ * @param {number} delay The time, in milliseconds that the timer should
331
+ * wait before the specified function or code is executed.
332
+ * @param {*} [arg] An optional argument to be passed to the callback function
333
+ * when the timer expires.
334
+ * @returns {NodeJS.Timeout|FastTimer}
335
+ */
336
+ setTimeout (callback, delay, arg) {
337
+ // If the delay is less than or equal to the RESOLUTION_MS value return a
338
+ // native Node.js Timer instance.
339
+ return delay <= RESOLUTION_MS
340
+ ? setTimeout(callback, delay, arg)
341
+ : new FastTimer(callback, delay, arg)
91
342
  },
343
+ /**
344
+ * The clearTimeout method cancels an instantiated Timer previously created
345
+ * by calling setTimeout.
346
+ *
347
+ * @param {NodeJS.Timeout|FastTimer} timeout
348
+ */
92
349
  clearTimeout (timeout) {
93
- if (timeout instanceof Timeout) {
350
+ // If the timeout is a FastTimer, call its own clear method.
351
+ if (timeout[kFastTimer]) {
352
+ /**
353
+ * @type {FastTimer}
354
+ */
94
355
  timeout.clear()
356
+ // Otherwise it is an instance of a native NodeJS.Timeout, so call the
357
+ // Node.js native clearTimeout function.
95
358
  } else {
96
359
  clearTimeout(timeout)
97
360
  }
98
- }
361
+ },
362
+ /**
363
+ * The setFastTimeout() method sets a fastTimer which executes a function once
364
+ * the timer expires.
365
+ * @param {Function} callback A function to be executed after the timer
366
+ * expires.
367
+ * @param {number} delay The time, in milliseconds that the timer should
368
+ * wait before the specified function or code is executed.
369
+ * @param {*} [arg] An optional argument to be passed to the callback function
370
+ * when the timer expires.
371
+ * @returns {FastTimer}
372
+ */
373
+ setFastTimeout (callback, delay, arg) {
374
+ return new FastTimer(callback, delay, arg)
375
+ },
376
+ /**
377
+ * The clearTimeout method cancels an instantiated FastTimer previously
378
+ * created by calling setFastTimeout.
379
+ *
380
+ * @param {FastTimer} timeout
381
+ */
382
+ clearFastTimeout (timeout) {
383
+ timeout.clear()
384
+ },
385
+ /**
386
+ * The now method returns the value of the internal fast timer clock.
387
+ *
388
+ * @returns {number}
389
+ */
390
+ now () {
391
+ return fastNow
392
+ },
393
+ /**
394
+ * Trigger the onTick function to process the fastTimers array.
395
+ * Exported for testing purposes only.
396
+ * Marking as deprecated to discourage any use outside of testing.
397
+ * @deprecated
398
+ * @param {number} [delay=0] The delay in milliseconds to add to the now value.
399
+ */
400
+ tick (delay = 0) {
401
+ fastNow += delay - RESOLUTION_MS + 1
402
+ onTick()
403
+ onTick()
404
+ },
405
+ /**
406
+ * Reset FastTimers.
407
+ * Exported for testing purposes only.
408
+ * Marking as deprecated to discourage any use outside of testing.
409
+ * @deprecated
410
+ */
411
+ reset () {
412
+ fastNow = 0
413
+ fastTimers.length = 0
414
+ clearTimeout(fastNowTimeout)
415
+ fastNowTimeout = null
416
+ },
417
+ /**
418
+ * Exporting for testing purposes only.
419
+ * Marking as deprecated to discourage any use outside of testing.
420
+ * @deprecated
421
+ */
422
+ kFastTimer
99
423
  }
@@ -16,12 +16,25 @@ const { kState } = require('./symbols')
16
16
  const { webidl } = require('./webidl')
17
17
  const { Blob } = require('node:buffer')
18
18
  const assert = require('node:assert')
19
- const { isErrored } = require('../../core/util')
19
+ const { isErrored, isDisturbed } = require('node:stream')
20
20
  const { isArrayBuffer } = require('node:util/types')
21
21
  const { serializeAMimeType } = require('./data-url')
22
22
  const { multipartFormDataParser } = require('./formdata-parser')
23
23
 
24
24
  const textEncoder = new TextEncoder()
25
+ function noop () {}
26
+
27
+ const hasFinalizationRegistry = globalThis.FinalizationRegistry && process.version.indexOf('v18') !== 0
28
+ let streamRegistry
29
+
30
+ if (hasFinalizationRegistry) {
31
+ streamRegistry = new FinalizationRegistry((weakRef) => {
32
+ const stream = weakRef.deref()
33
+ if (stream && !stream.locked && !isDisturbed(stream) && !isErrored(stream)) {
34
+ stream.cancel('Response object has been garbage collected').catch(noop)
35
+ }
36
+ })
37
+ }
25
38
 
26
39
  // https://fetch.spec.whatwg.org/#concept-bodyinit-extract
27
40
  function extractBody (object, keepalive = false) {
@@ -264,7 +277,7 @@ function safelyExtractBody (object, keepalive = false) {
264
277
  return extractBody(object, keepalive)
265
278
  }
266
279
 
267
- function cloneBody (body) {
280
+ function cloneBody (instance, body) {
268
281
  // To clone a body body, run these steps:
269
282
 
270
283
  // https://fetch.spec.whatwg.org/#concept-body-clone
@@ -272,6 +285,10 @@ function cloneBody (body) {
272
285
  // 1. Let « out1, out2 » be the result of teeing body’s stream.
273
286
  const [out1, out2] = body.stream.tee()
274
287
 
288
+ if (hasFinalizationRegistry) {
289
+ streamRegistry.register(instance, new WeakRef(out1))
290
+ }
291
+
275
292
  // 2. Set body’s stream to out1.
276
293
  body.stream = out1
277
294
 
@@ -414,7 +431,7 @@ async function consumeBody (object, convertBytesToJSValue, instance) {
414
431
 
415
432
  // 1. If object is unusable, then return a promise rejected
416
433
  // with a TypeError.
417
- if (bodyUnusable(object[kState].body)) {
434
+ if (bodyUnusable(object)) {
418
435
  throw new TypeError('Body is unusable: Body has already been read')
419
436
  }
420
437
 
@@ -454,7 +471,9 @@ async function consumeBody (object, convertBytesToJSValue, instance) {
454
471
  }
455
472
 
456
473
  // https://fetch.spec.whatwg.org/#body-unusable
457
- function bodyUnusable (body) {
474
+ function bodyUnusable (object) {
475
+ const body = object[kState].body
476
+
458
477
  // An object including the Body interface mixin is
459
478
  // said to be unusable if its body is non-null and
460
479
  // its body’s stream is disturbed or locked.
@@ -496,5 +515,8 @@ module.exports = {
496
515
  extractBody,
497
516
  safelyExtractBody,
498
517
  cloneBody,
499
- mixinBody
518
+ mixinBody,
519
+ streamRegistry,
520
+ hasFinalizationRegistry,
521
+ bodyUnusable
500
522
  }
@@ -87,11 +87,21 @@ function multipartFormDataParser (input, mimeType) {
87
87
  // the first byte.
88
88
  const position = { position: 0 }
89
89
 
90
- // Note: undici addition, allow \r\n before the body.
91
- if (input[0] === 0x0d && input[1] === 0x0a) {
90
+ // Note: undici addition, allows leading and trailing CRLFs.
91
+ while (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) {
92
92
  position.position += 2
93
93
  }
94
94
 
95
+ let trailing = input.length
96
+
97
+ while (input[trailing - 1] === 0x0a && input[trailing - 2] === 0x0d) {
98
+ trailing -= 2
99
+ }
100
+
101
+ if (trailing !== input.length) {
102
+ input = input.subarray(0, trailing)
103
+ }
104
+
95
105
  // 5. While true:
96
106
  while (true) {
97
107
  // 5.1. If position points to a sequence of bytes starting with 0x2D 0x2D
@@ -2150,9 +2150,15 @@ async function httpNetworkFetch (
2150
2150
  finishFlush: zlib.constants.Z_SYNC_FLUSH
2151
2151
  }))
2152
2152
  } else if (coding === 'deflate') {
2153
- decoders.push(createInflate())
2153
+ decoders.push(createInflate({
2154
+ flush: zlib.constants.Z_SYNC_FLUSH,
2155
+ finishFlush: zlib.constants.Z_SYNC_FLUSH
2156
+ }))
2154
2157
  } else if (coding === 'br') {
2155
- decoders.push(zlib.createBrotliDecompress())
2158
+ decoders.push(zlib.createBrotliDecompress({
2159
+ flush: zlib.constants.BROTLI_OPERATION_FLUSH,
2160
+ finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
2161
+ }))
2156
2162
  } else {
2157
2163
  decoders.length = 0
2158
2164
  break
@@ -2160,13 +2166,19 @@ async function httpNetworkFetch (
2160
2166
  }
2161
2167
  }
2162
2168
 
2169
+ const onError = this.onError.bind(this)
2170
+
2163
2171
  resolve({
2164
2172
  status,
2165
2173
  statusText,
2166
2174
  headersList,
2167
2175
  body: decoders.length
2168
- ? pipeline(this.body, ...decoders, () => { })
2169
- : this.body.on('error', () => { })
2176
+ ? pipeline(this.body, ...decoders, (err) => {
2177
+ if (err) {
2178
+ this.onError(err)
2179
+ }
2180
+ }).on('error', onError)
2181
+ : this.body.on('error', onError)
2170
2182
  })
2171
2183
 
2172
2184
  return true
@@ -2,7 +2,7 @@
2
2
 
3
3
  'use strict'
4
4
 
5
- const { extractBody, mixinBody, cloneBody } = require('./body')
5
+ const { extractBody, mixinBody, cloneBody, bodyUnusable } = require('./body')
6
6
  const { Headers, fill: fillHeaders, HeadersList, setHeadersGuard, getHeadersGuard, setHeadersList, getHeadersList } = require('./headers')
7
7
  const { FinalizationRegistry } = require('./dispatcher-weakref')()
8
8
  const util = require('../../core/util')
@@ -557,7 +557,7 @@ class Request {
557
557
  // 40. If initBody is null and inputBody is non-null, then:
558
558
  if (initBody == null && inputBody != null) {
559
559
  // 1. If input is unusable, then throw a TypeError.
560
- if (util.isDisturbed(inputBody.stream) || inputBody.stream.locked) {
560
+ if (bodyUnusable(input)) {
561
561
  throw new TypeError(
562
562
  'Cannot construct a Request with a Request object that has already been used.'
563
563
  )
@@ -759,7 +759,7 @@ class Request {
759
759
  webidl.brandCheck(this, Request)
760
760
 
761
761
  // 1. If this is unusable, then throw a TypeError.
762
- if (this.bodyUsed || this.body?.locked) {
762
+ if (bodyUnusable(this)) {
763
763
  throw new TypeError('unusable')
764
764
  }
765
765
 
@@ -877,7 +877,7 @@ function cloneRequest (request) {
877
877
  // 2. If request’s body is non-null, set newRequest’s body to the
878
878
  // result of cloning request’s body.
879
879
  if (request.body != null) {
880
- newRequest.body = cloneBody(request.body)
880
+ newRequest.body = cloneBody(newRequest, request.body)
881
881
  }
882
882
 
883
883
  // 3. Return newRequest.