undici 6.4.0 → 6.6.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.
Files changed (60) hide show
  1. package/README.md +2 -0
  2. package/docs/api/EventSource.md +21 -0
  3. package/docs/best-practices/client-certificate.md +3 -3
  4. package/index.js +4 -0
  5. package/lib/agent.js +5 -7
  6. package/lib/api/api-connect.js +2 -2
  7. package/lib/api/api-pipeline.js +4 -4
  8. package/lib/api/api-request.js +2 -2
  9. package/lib/api/api-stream.js +4 -4
  10. package/lib/api/api-upgrade.js +3 -3
  11. package/lib/api/readable.js +33 -23
  12. package/lib/api/util.js +1 -1
  13. package/lib/balanced-pool.js +1 -1
  14. package/lib/cache/cache.js +4 -10
  15. package/lib/cache/util.js +1 -1
  16. package/lib/client.js +14 -14
  17. package/lib/cookies/parse.js +1 -1
  18. package/lib/cookies/util.js +1 -1
  19. package/lib/core/connect.js +3 -3
  20. package/lib/core/diagnostics.js +2 -2
  21. package/lib/core/request.js +18 -13
  22. package/lib/core/tree.js +3 -5
  23. package/lib/core/util.js +15 -15
  24. package/lib/dispatcher.js +1 -1
  25. package/lib/eventsource/eventsource-stream.js +398 -0
  26. package/lib/eventsource/eventsource.js +473 -0
  27. package/lib/eventsource/util.js +37 -0
  28. package/lib/fetch/body.js +29 -21
  29. package/lib/fetch/dataURL.js +97 -17
  30. package/lib/fetch/file.js +5 -5
  31. package/lib/fetch/formdata.js +1 -1
  32. package/lib/fetch/headers.js +1 -1
  33. package/lib/fetch/index.js +31 -37
  34. package/lib/fetch/request.js +3 -2
  35. package/lib/fetch/response.js +42 -39
  36. package/lib/fetch/util.js +196 -36
  37. package/lib/fetch/webidl.js +1 -1
  38. package/lib/fileapi/util.js +2 -2
  39. package/lib/handler/RedirectHandler.js +2 -2
  40. package/lib/handler/RetryHandler.js +3 -3
  41. package/lib/llhttp/llhttp-wasm.js +3 -1
  42. package/lib/llhttp/llhttp_simd-wasm.js +3 -1
  43. package/lib/mock/mock-agent.js +2 -2
  44. package/lib/mock/mock-client.js +1 -1
  45. package/lib/mock/mock-pool.js +1 -1
  46. package/lib/mock/mock-utils.js +2 -2
  47. package/lib/mock/pending-interceptors-formatter.js +2 -2
  48. package/lib/pool.js +7 -8
  49. package/lib/proxy-agent.js +2 -2
  50. package/lib/timers.js +1 -1
  51. package/lib/websocket/connection.js +1 -1
  52. package/lib/websocket/events.js +1 -1
  53. package/lib/websocket/frame.js +1 -1
  54. package/lib/websocket/receiver.js +7 -6
  55. package/lib/websocket/util.js +1 -1
  56. package/lib/websocket/websocket.js +1 -1
  57. package/package.json +8 -10
  58. package/types/eventsource.d.ts +61 -0
  59. package/types/index.d.ts +1 -0
  60. package/types/websocket.d.ts +22 -1
@@ -0,0 +1,398 @@
1
+ 'use strict'
2
+ const { Transform } = require('node:stream')
3
+ const { isASCIINumber, isValidLastEventId } = require('./util')
4
+
5
+ /**
6
+ * @type {number[]} BOM
7
+ */
8
+ const BOM = [0xEF, 0xBB, 0xBF]
9
+ /**
10
+ * @type {10} LF
11
+ */
12
+ const LF = 0x0A
13
+ /**
14
+ * @type {13} CR
15
+ */
16
+ const CR = 0x0D
17
+ /**
18
+ * @type {58} COLON
19
+ */
20
+ const COLON = 0x3A
21
+ /**
22
+ * @type {32} SPACE
23
+ */
24
+ const SPACE = 0x20
25
+
26
+ /**
27
+ * @typedef {object} EventSourceStreamEvent
28
+ * @type {object}
29
+ * @property {string} [event] The event type.
30
+ * @property {string} [data] The data of the message.
31
+ * @property {string} [id] A unique ID for the event.
32
+ * @property {string} [retry] The reconnection time, in milliseconds.
33
+ */
34
+
35
+ /**
36
+ * @typedef eventSourceSettings
37
+ * @type {object}
38
+ * @property {string} lastEventId The last event ID received from the server.
39
+ * @property {string} origin The origin of the event source.
40
+ * @property {number} reconnectionTime The reconnection time, in milliseconds.
41
+ */
42
+
43
+ class EventSourceStream extends Transform {
44
+ /**
45
+ * @type {eventSourceSettings}
46
+ */
47
+ state = null
48
+
49
+ /**
50
+ * Leading byte-order-mark check.
51
+ * @type {boolean}
52
+ */
53
+ checkBOM = true
54
+
55
+ /**
56
+ * @type {boolean}
57
+ */
58
+ crlfCheck = false
59
+
60
+ /**
61
+ * @type {boolean}
62
+ */
63
+ eventEndCheck = false
64
+
65
+ /**
66
+ * @type {Buffer}
67
+ */
68
+ buffer = null
69
+
70
+ pos = 0
71
+
72
+ event = {
73
+ data: undefined,
74
+ event: undefined,
75
+ id: undefined,
76
+ retry: undefined
77
+ }
78
+
79
+ /**
80
+ * @param {object} options
81
+ * @param {eventSourceSettings} options.eventSourceSettings
82
+ * @param {Function} [options.push]
83
+ */
84
+ constructor (options = {}) {
85
+ // Enable object mode as EventSourceStream emits objects of shape
86
+ // EventSourceStreamEvent
87
+ options.readableObjectMode = true
88
+
89
+ super(options)
90
+
91
+ this.state = options.eventSourceSettings || {}
92
+ if (options.push) {
93
+ this.push = options.push
94
+ }
95
+ }
96
+
97
+ /**
98
+ * @param {Buffer} chunk
99
+ * @param {string} _encoding
100
+ * @param {Function} callback
101
+ * @returns {void}
102
+ */
103
+ _transform (chunk, _encoding, callback) {
104
+ if (chunk.length === 0) {
105
+ callback()
106
+ return
107
+ }
108
+
109
+ // Cache the chunk in the buffer, as the data might not be complete while
110
+ // processing it
111
+ // TODO: Investigate if there is a more performant way to handle
112
+ // incoming chunks
113
+ // see: https://github.com/nodejs/undici/issues/2630
114
+ if (this.buffer) {
115
+ this.buffer = Buffer.concat([this.buffer, chunk])
116
+ } else {
117
+ this.buffer = chunk
118
+ }
119
+
120
+ // Strip leading byte-order-mark if we opened the stream and started
121
+ // the processing of the incoming data
122
+ if (this.checkBOM) {
123
+ switch (this.buffer.length) {
124
+ case 1:
125
+ // Check if the first byte is the same as the first byte of the BOM
126
+ if (this.buffer[0] === BOM[0]) {
127
+ // If it is, we need to wait for more data
128
+ callback()
129
+ return
130
+ }
131
+ // Set the checkBOM flag to false as we don't need to check for the
132
+ // BOM anymore
133
+ this.checkBOM = false
134
+
135
+ // The buffer only contains one byte so we need to wait for more data
136
+ callback()
137
+ return
138
+ case 2:
139
+ // Check if the first two bytes are the same as the first two bytes
140
+ // of the BOM
141
+ if (
142
+ this.buffer[0] === BOM[0] &&
143
+ this.buffer[1] === BOM[1]
144
+ ) {
145
+ // If it is, we need to wait for more data, because the third byte
146
+ // is needed to determine if it is the BOM or not
147
+ callback()
148
+ return
149
+ }
150
+
151
+ // Set the checkBOM flag to false as we don't need to check for the
152
+ // BOM anymore
153
+ this.checkBOM = false
154
+ break
155
+ case 3:
156
+ // Check if the first three bytes are the same as the first three
157
+ // bytes of the BOM
158
+ if (
159
+ this.buffer[0] === BOM[0] &&
160
+ this.buffer[1] === BOM[1] &&
161
+ this.buffer[2] === BOM[2]
162
+ ) {
163
+ // If it is, we can drop the buffered data, as it is only the BOM
164
+ this.buffer = Buffer.alloc(0)
165
+ // Set the checkBOM flag to false as we don't need to check for the
166
+ // BOM anymore
167
+ this.checkBOM = false
168
+
169
+ // Await more data
170
+ callback()
171
+ return
172
+ }
173
+ // If it is not the BOM, we can start processing the data
174
+ this.checkBOM = false
175
+ break
176
+ default:
177
+ // The buffer is longer than 3 bytes, so we can drop the BOM if it is
178
+ // present
179
+ if (
180
+ this.buffer[0] === BOM[0] &&
181
+ this.buffer[1] === BOM[1] &&
182
+ this.buffer[2] === BOM[2]
183
+ ) {
184
+ // Remove the BOM from the buffer
185
+ this.buffer = this.buffer.subarray(3)
186
+ }
187
+
188
+ // Set the checkBOM flag to false as we don't need to check for the
189
+ this.checkBOM = false
190
+ break
191
+ }
192
+ }
193
+
194
+ while (this.pos < this.buffer.length) {
195
+ // If the previous line ended with an end-of-line, we need to check
196
+ // if the next character is also an end-of-line.
197
+ if (this.eventEndCheck) {
198
+ // If the the current character is an end-of-line, then the event
199
+ // is finished and we can process it
200
+
201
+ // If the previous line ended with a carriage return, we need to
202
+ // check if the current character is a line feed and remove it
203
+ // from the buffer.
204
+ if (this.crlfCheck) {
205
+ // If the current character is a line feed, we can remove it
206
+ // from the buffer and reset the crlfCheck flag
207
+ if (this.buffer[this.pos] === LF) {
208
+ this.buffer = this.buffer.subarray(this.pos + 1)
209
+ this.pos = 0
210
+ this.crlfCheck = false
211
+
212
+ // It is possible that the line feed is not the end of the
213
+ // event. We need to check if the next character is an
214
+ // end-of-line character to determine if the event is
215
+ // finished. We simply continue the loop to check the next
216
+ // character.
217
+
218
+ // As we removed the line feed from the buffer and set the
219
+ // crlfCheck flag to false, we basically don't make any
220
+ // distinction between a line feed and a carriage return.
221
+ continue
222
+ }
223
+ this.crlfCheck = false
224
+ }
225
+
226
+ if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
227
+ // If the current character is a carriage return, we need to
228
+ // set the crlfCheck flag to true, as we need to check if the
229
+ // next character is a line feed so we can remove it from the
230
+ // buffer
231
+ if (this.buffer[this.pos] === CR) {
232
+ this.crlfCheck = true
233
+ }
234
+
235
+ this.buffer = this.buffer.subarray(this.pos + 1)
236
+ this.pos = 0
237
+ if (
238
+ this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) {
239
+ this.processEvent(this.event)
240
+ }
241
+ this.clearEvent()
242
+ continue
243
+ }
244
+ // If the current character is not an end-of-line, then the event
245
+ // is not finished and we have to reset the eventEndCheck flag
246
+ this.eventEndCheck = false
247
+ continue
248
+ }
249
+
250
+ // If the current character is an end-of-line, we can process the
251
+ // line
252
+ if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
253
+ // If the current character is a carriage return, we need to
254
+ // set the crlfCheck flag to true, as we need to check if the
255
+ // next character is a line feed
256
+ if (this.buffer[this.pos] === CR) {
257
+ this.crlfCheck = true
258
+ }
259
+
260
+ // In any case, we can process the line as we reached an
261
+ // end-of-line character
262
+ this.parseLine(this.buffer.subarray(0, this.pos), this.event)
263
+
264
+ // Remove the processed line from the buffer
265
+ this.buffer = this.buffer.subarray(this.pos + 1)
266
+ // Reset the position as we removed the processed line from the buffer
267
+ this.pos = 0
268
+ // A line was processed and this could be the end of the event. We need
269
+ // to check if the next line is empty to determine if the event is
270
+ // finished.
271
+ this.eventEndCheck = true
272
+ continue
273
+ }
274
+
275
+ this.pos++
276
+ }
277
+
278
+ callback()
279
+ }
280
+
281
+ /**
282
+ * @param {Buffer} line
283
+ * @param {EventStreamEvent} event
284
+ */
285
+ parseLine (line, event) {
286
+ // If the line is empty (a blank line)
287
+ // Dispatch the event, as defined below.
288
+ // This will be handled in the _transform method
289
+ if (line.length === 0) {
290
+ return
291
+ }
292
+
293
+ // If the line starts with a U+003A COLON character (:)
294
+ // Ignore the line.
295
+ const colonPosition = line.indexOf(COLON)
296
+ if (colonPosition === 0) {
297
+ return
298
+ }
299
+
300
+ let field = ''
301
+ let value = ''
302
+
303
+ // If the line contains a U+003A COLON character (:)
304
+ if (colonPosition !== -1) {
305
+ // Collect the characters on the line before the first U+003A COLON
306
+ // character (:), and let field be that string.
307
+ // TODO: Investigate if there is a more performant way to extract the
308
+ // field
309
+ // see: https://github.com/nodejs/undici/issues/2630
310
+ field = line.subarray(0, colonPosition).toString('utf8')
311
+
312
+ // Collect the characters on the line after the first U+003A COLON
313
+ // character (:), and let value be that string.
314
+ // If value starts with a U+0020 SPACE character, remove it from value.
315
+ let valueStart = colonPosition + 1
316
+ if (line[valueStart] === SPACE) {
317
+ ++valueStart
318
+ }
319
+ // TODO: Investigate if there is a more performant way to extract the
320
+ // value
321
+ // see: https://github.com/nodejs/undici/issues/2630
322
+ value = line.subarray(valueStart).toString('utf8')
323
+
324
+ // Otherwise, the string is not empty but does not contain a U+003A COLON
325
+ // character (:)
326
+ } else {
327
+ // Process the field using the steps described below, using the whole
328
+ // line as the field name, and the empty string as the field value.
329
+ field = line.toString('utf8')
330
+ value = ''
331
+ }
332
+
333
+ // Modify the event with the field name and value. The value is also
334
+ // decoded as UTF-8
335
+ switch (field) {
336
+ case 'data':
337
+ if (event[field] === undefined) {
338
+ event[field] = value
339
+ } else {
340
+ event[field] += `\n${value}`
341
+ }
342
+ break
343
+ case 'retry':
344
+ if (isASCIINumber(value)) {
345
+ event[field] = value
346
+ }
347
+ break
348
+ case 'id':
349
+ if (isValidLastEventId(value)) {
350
+ event[field] = value
351
+ }
352
+ break
353
+ case 'event':
354
+ if (value.length > 0) {
355
+ event[field] = value
356
+ }
357
+ break
358
+ }
359
+ }
360
+
361
+ /**
362
+ * @param {EventSourceStreamEvent} event
363
+ */
364
+ processEvent (event) {
365
+ if (event.retry && isASCIINumber(event.retry)) {
366
+ this.state.reconnectionTime = parseInt(event.retry, 10)
367
+ }
368
+
369
+ if (event.id && isValidLastEventId(event.id)) {
370
+ this.state.lastEventId = event.id
371
+ }
372
+
373
+ // only dispatch event, when data is provided
374
+ if (event.data !== undefined) {
375
+ this.push({
376
+ type: event.event || 'message',
377
+ options: {
378
+ data: event.data,
379
+ lastEventId: this.state.lastEventId,
380
+ origin: this.state.origin
381
+ }
382
+ })
383
+ }
384
+ }
385
+
386
+ clearEvent () {
387
+ this.event = {
388
+ data: undefined,
389
+ event: undefined,
390
+ id: undefined,
391
+ retry: undefined
392
+ }
393
+ }
394
+ }
395
+
396
+ module.exports = {
397
+ EventSourceStream
398
+ }