undici 6.4.0 → 6.5.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.
- package/README.md +2 -0
- package/docs/api/EventSource.md +21 -0
- package/index.js +4 -0
- package/lib/api/readable.js +31 -21
- package/lib/eventsource/eventsource-stream.js +398 -0
- package/lib/eventsource/eventsource.js +473 -0
- package/lib/eventsource/util.js +29 -0
- package/lib/fetch/index.js +3 -0
- package/lib/fetch/response.js +11 -0
- package/package.json +5 -4
- package/types/eventsource.d.ts +61 -0
- package/types/index.d.ts +1 -0
- package/types/websocket.d.ts +22 -1
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ npm i undici
|
|
|
20
20
|
The benchmark is a simple `hello world` [example](benchmarks/benchmark.js) using a
|
|
21
21
|
50 TCP connections with a pipelining depth of 10 running on Node 20.10.0.
|
|
22
22
|
|
|
23
|
+
```
|
|
23
24
|
│ Tests │ Samples │ Result │ Tolerance │ Difference with slowest │
|
|
24
25
|
|─────────────────────|─────────|─────────────────|───────────|─────────────────────────|
|
|
25
26
|
│ got │ 45 │ 1661.71 req/sec │ ± 2.93 % │ - │
|
|
@@ -33,6 +34,7 @@ The benchmark is a simple `hello world` [example](benchmarks/benchmark.js) using
|
|
|
33
34
|
│ undici - request │ 55 │ 7773.98 req/sec │ ± 2.93 % │ + 367.83 % │
|
|
34
35
|
│ undici - stream │ 70 │ 8425.96 req/sec │ ± 2.91 % │ + 407.07 % │
|
|
35
36
|
│ undici - dispatch │ 50 │ 9488.99 req/sec │ ± 2.85 % │ + 471.04 % │
|
|
37
|
+
```
|
|
36
38
|
|
|
37
39
|
## Quick Start
|
|
38
40
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# EventSource
|
|
2
|
+
|
|
3
|
+
Undici exposes a WHATWG spec-compliant implementation of [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource)
|
|
4
|
+
for [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events).
|
|
5
|
+
|
|
6
|
+
## Instantiating EventSource
|
|
7
|
+
|
|
8
|
+
Undici exports a EventSource class. You can instantiate the EventSource as
|
|
9
|
+
follows:
|
|
10
|
+
|
|
11
|
+
```mjs
|
|
12
|
+
import { EventSource } from 'undici'
|
|
13
|
+
|
|
14
|
+
const evenSource = new EventSource('http://localhost:3000')
|
|
15
|
+
evenSource.onmessage = (event) => {
|
|
16
|
+
console.log(event.data)
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
More information about the EventSource API can be found on
|
|
21
|
+
[MDN](https://developer.mozilla.org/en-US/docs/Web/API/EventSource).
|
package/index.js
CHANGED
|
@@ -149,3 +149,7 @@ module.exports.MockClient = MockClient
|
|
|
149
149
|
module.exports.MockPool = MockPool
|
|
150
150
|
module.exports.MockAgent = MockAgent
|
|
151
151
|
module.exports.mockErrors = mockErrors
|
|
152
|
+
|
|
153
|
+
const { EventSource } = require('./lib/eventsource/eventsource')
|
|
154
|
+
|
|
155
|
+
module.exports.EventSource = EventSource
|
package/lib/api/readable.js
CHANGED
|
@@ -94,7 +94,7 @@ module.exports = class BodyReadable extends Readable {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
push (chunk) {
|
|
97
|
-
if (this[kConsume] && chunk !== null
|
|
97
|
+
if (this[kConsume] && chunk !== null) {
|
|
98
98
|
consumePush(this[kConsume], chunk)
|
|
99
99
|
return this[kReading] ? super.push(chunk) : true
|
|
100
100
|
}
|
|
@@ -215,26 +215,28 @@ async function consume (stream, type) {
|
|
|
215
215
|
reject(rState.errored ?? new TypeError('unusable'))
|
|
216
216
|
}
|
|
217
217
|
} else {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
218
|
+
queueMicrotask(() => {
|
|
219
|
+
stream[kConsume] = {
|
|
220
|
+
type,
|
|
221
|
+
stream,
|
|
222
|
+
resolve,
|
|
223
|
+
reject,
|
|
224
|
+
length: 0,
|
|
225
|
+
body: []
|
|
226
|
+
}
|
|
226
227
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
228
|
+
stream
|
|
229
|
+
.on('error', function (err) {
|
|
230
|
+
consumeFinish(this[kConsume], err)
|
|
231
|
+
})
|
|
232
|
+
.on('close', function () {
|
|
233
|
+
if (this[kConsume].body !== null) {
|
|
234
|
+
consumeFinish(this[kConsume], new RequestAbortedError())
|
|
235
|
+
}
|
|
236
|
+
})
|
|
236
237
|
|
|
237
|
-
|
|
238
|
+
consumeStart(stream[kConsume])
|
|
239
|
+
})
|
|
238
240
|
}
|
|
239
241
|
})
|
|
240
242
|
}
|
|
@@ -246,8 +248,16 @@ function consumeStart (consume) {
|
|
|
246
248
|
|
|
247
249
|
const { _readableState: state } = consume.stream
|
|
248
250
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
+
if (state.bufferIndex) {
|
|
252
|
+
const start = state.bufferIndex
|
|
253
|
+
const end = state.buffer.length
|
|
254
|
+
for (let n = start; n < end; n++) {
|
|
255
|
+
consumePush(consume, state.buffer[n])
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
for (const chunk of state.buffer) {
|
|
259
|
+
consumePush(consume, chunk)
|
|
260
|
+
}
|
|
251
261
|
}
|
|
252
262
|
|
|
253
263
|
if (state.endEmitted) {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { setTimeout } = require('node:timers/promises')
|
|
4
|
+
const { pipeline } = require('node:stream')
|
|
5
|
+
const { fetching } = require('../fetch')
|
|
6
|
+
const { makeRequest } = require('../fetch/request')
|
|
7
|
+
const { getGlobalOrigin } = require('../fetch/global')
|
|
8
|
+
const { webidl } = require('../fetch/webidl')
|
|
9
|
+
const { EventSourceStream } = require('./eventsource-stream')
|
|
10
|
+
const { parseMIMEType } = require('../fetch/dataURL')
|
|
11
|
+
const { MessageEvent } = require('../websocket/events')
|
|
12
|
+
const { isNetworkError } = require('../fetch/response')
|
|
13
|
+
const { getGlobalDispatcher } = require('../global')
|
|
14
|
+
|
|
15
|
+
let experimentalWarned = false
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A reconnection time, in milliseconds. This must initially be an implementation-defined value,
|
|
19
|
+
* probably in the region of a few seconds.
|
|
20
|
+
*
|
|
21
|
+
* In Comparison:
|
|
22
|
+
* - Chrome uses 3000ms.
|
|
23
|
+
* - Deno uses 5000ms.
|
|
24
|
+
*
|
|
25
|
+
* @type {3000}
|
|
26
|
+
*/
|
|
27
|
+
const defaultReconnectionTime = 3000
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The readyState attribute represents the state of the connection.
|
|
31
|
+
* @enum
|
|
32
|
+
* @readonly
|
|
33
|
+
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource-readystate-dev
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The connection has not yet been established, or it was closed and the user
|
|
38
|
+
* agent is reconnecting.
|
|
39
|
+
* @type {0}
|
|
40
|
+
*/
|
|
41
|
+
const CONNECTING = 0
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The user agent has an open connection and is dispatching events as it
|
|
45
|
+
* receives them.
|
|
46
|
+
* @type {1}
|
|
47
|
+
*/
|
|
48
|
+
const OPEN = 1
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The connection is not open, and the user agent is not trying to reconnect.
|
|
52
|
+
* @type {2}
|
|
53
|
+
*/
|
|
54
|
+
const CLOSED = 2
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Requests for the element will have their mode set to "cors" and their credentials mode set to "same-origin".
|
|
58
|
+
* @type {'anonymous'}
|
|
59
|
+
*/
|
|
60
|
+
const ANONYMOUS = 'anonymous'
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Requests for the element will have their mode set to "cors" and their credentials mode set to "include".
|
|
64
|
+
* @type {'use-credentials'}
|
|
65
|
+
*/
|
|
66
|
+
const USE_CREDENTIALS = 'use-credentials'
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @typedef {object} EventSourceInit
|
|
70
|
+
* @property {boolean} [withCredentials] indicates whether the request
|
|
71
|
+
* should include credentials.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The EventSource interface is used to receive server-sent events. It
|
|
76
|
+
* connects to a server over HTTP and receives events in text/event-stream
|
|
77
|
+
* format without closing the connection.
|
|
78
|
+
* @extends {EventTarget}
|
|
79
|
+
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
|
|
80
|
+
* @api public
|
|
81
|
+
*/
|
|
82
|
+
class EventSource extends EventTarget {
|
|
83
|
+
#events = {
|
|
84
|
+
open: null,
|
|
85
|
+
error: null,
|
|
86
|
+
message: null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#url = null
|
|
90
|
+
#withCredentials = false
|
|
91
|
+
|
|
92
|
+
#readyState = CONNECTING
|
|
93
|
+
|
|
94
|
+
#request = null
|
|
95
|
+
#controller = null
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @type {object}
|
|
99
|
+
* @property {string} lastEventId
|
|
100
|
+
* @property {number} reconnectionTime
|
|
101
|
+
* @property {any} reconnectionTimer
|
|
102
|
+
*/
|
|
103
|
+
#settings = null
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Creates a new EventSource object.
|
|
107
|
+
* @param {string} url
|
|
108
|
+
* @param {EventSourceInit} [eventSourceInitDict]
|
|
109
|
+
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface
|
|
110
|
+
*/
|
|
111
|
+
constructor (url, eventSourceInitDict = {}) {
|
|
112
|
+
// 1. Let ev be a new EventSource object.
|
|
113
|
+
super()
|
|
114
|
+
|
|
115
|
+
webidl.argumentLengthCheck(arguments, 1, { header: 'EventSource constructor' })
|
|
116
|
+
|
|
117
|
+
if (!experimentalWarned) {
|
|
118
|
+
experimentalWarned = true
|
|
119
|
+
process.emitWarning('EventSource is experimental, expect them to change at any time.', {
|
|
120
|
+
code: 'UNDICI-ES'
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
url = webidl.converters.USVString(url)
|
|
125
|
+
eventSourceInitDict = webidl.converters.EventSourceInitDict(eventSourceInitDict)
|
|
126
|
+
|
|
127
|
+
// 2. Let settings be ev's relevant settings object.
|
|
128
|
+
// https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object
|
|
129
|
+
this.#settings = {
|
|
130
|
+
origin: getGlobalOrigin(),
|
|
131
|
+
policyContainer: {
|
|
132
|
+
referrerPolicy: 'no-referrer'
|
|
133
|
+
},
|
|
134
|
+
lastEventId: '',
|
|
135
|
+
reconnectionTime: defaultReconnectionTime
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let urlRecord
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// 3. Let urlRecord be the result of encoding-parsing a URL given url, relative to settings.
|
|
142
|
+
urlRecord = new URL(url, this.#settings.origin)
|
|
143
|
+
this.#settings.origin = urlRecord.origin
|
|
144
|
+
} catch (e) {
|
|
145
|
+
// 4. If urlRecord is failure, then throw a "SyntaxError" DOMException.
|
|
146
|
+
throw new DOMException(e, 'SyntaxError')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 5. Set ev's url to urlRecord.
|
|
150
|
+
this.#url = urlRecord.href
|
|
151
|
+
|
|
152
|
+
// 6. Let corsAttributeState be Anonymous.
|
|
153
|
+
let corsAttributeState = ANONYMOUS
|
|
154
|
+
|
|
155
|
+
// 7. If the value of eventSourceInitDict's withCredentials member is true,
|
|
156
|
+
// then set corsAttributeState to Use Credentials and set ev's
|
|
157
|
+
// withCredentials attribute to true.
|
|
158
|
+
if (eventSourceInitDict.withCredentials) {
|
|
159
|
+
corsAttributeState = USE_CREDENTIALS
|
|
160
|
+
this.#withCredentials = true
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 8. Let request be the result of creating a potential-CORS request given
|
|
164
|
+
// urlRecord, the empty string, and corsAttributeState.
|
|
165
|
+
const initRequest = {
|
|
166
|
+
redirect: 'follow',
|
|
167
|
+
keepalive: true,
|
|
168
|
+
// @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
|
|
169
|
+
mode: 'cors',
|
|
170
|
+
credentials: corsAttributeState === 'anonymous'
|
|
171
|
+
? 'same-origin'
|
|
172
|
+
: 'omit',
|
|
173
|
+
referrer: 'no-referrer'
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 9. Set request's client to settings.
|
|
177
|
+
initRequest.client = this.#settings
|
|
178
|
+
|
|
179
|
+
// 10. User agents may set (`Accept`, `text/event-stream`) in request's header list.
|
|
180
|
+
initRequest.headersList = [['accept', { name: 'accept', value: 'text/event-stream' }]]
|
|
181
|
+
|
|
182
|
+
// 11. Set request's cache mode to "no-store".
|
|
183
|
+
initRequest.cache = 'no-store'
|
|
184
|
+
|
|
185
|
+
// 12. Set request's initiator type to "other".
|
|
186
|
+
initRequest.initiator = 'other'
|
|
187
|
+
|
|
188
|
+
initRequest.urlList = [new URL(this.#url)]
|
|
189
|
+
|
|
190
|
+
// 13. Set ev's request to request.
|
|
191
|
+
this.#request = makeRequest(initRequest)
|
|
192
|
+
|
|
193
|
+
this.#connect()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Returns the state of this EventSource object's connection. It can have the
|
|
198
|
+
* values described below.
|
|
199
|
+
* @returns {0|1|2}
|
|
200
|
+
* @readonly
|
|
201
|
+
*/
|
|
202
|
+
get readyState () {
|
|
203
|
+
return this.#readyState
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Returns the URL providing the event stream.
|
|
208
|
+
* @readonly
|
|
209
|
+
* @returns {string}
|
|
210
|
+
*/
|
|
211
|
+
get url () {
|
|
212
|
+
return this.#url
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Returns a boolean indicating whether the EventSource object was
|
|
217
|
+
* instantiated with CORS credentials set (true), or not (false, the default).
|
|
218
|
+
*/
|
|
219
|
+
get withCredentials () {
|
|
220
|
+
return this.#withCredentials
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#connect () {
|
|
224
|
+
if (this.#readyState === CLOSED) return
|
|
225
|
+
|
|
226
|
+
this.#readyState = CONNECTING
|
|
227
|
+
|
|
228
|
+
const fetchParam = {
|
|
229
|
+
request: this.#request
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 14. Let processEventSourceEndOfBody given response res be the following step: if res is not a network error, then reestablish the connection.
|
|
233
|
+
const processEventSourceEndOfBody = (response) => {
|
|
234
|
+
if (isNetworkError(response)) {
|
|
235
|
+
this.dispatchEvent(new Event('error'))
|
|
236
|
+
this.close()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.#reconnect()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 15. Fetch request, with processResponseEndOfBody set to processEventSourceEndOfBody...
|
|
243
|
+
fetchParam.processResponseEndOfBody = processEventSourceEndOfBody
|
|
244
|
+
|
|
245
|
+
// and processResponse set to the following steps given response res:
|
|
246
|
+
fetchParam.processResponse = (response) => {
|
|
247
|
+
// 1. If res is an aborted network error, then fail the connection.
|
|
248
|
+
|
|
249
|
+
if (isNetworkError(response)) {
|
|
250
|
+
// 1. When a user agent is to fail the connection, the user agent
|
|
251
|
+
// must queue a task which, if the readyState attribute is set to a
|
|
252
|
+
// value other than CLOSED, sets the readyState attribute to CLOSED
|
|
253
|
+
// and fires an event named error at the EventSource object. Once the
|
|
254
|
+
// user agent has failed the connection, it does not attempt to
|
|
255
|
+
// reconnect.
|
|
256
|
+
if (response.aborted) {
|
|
257
|
+
this.close()
|
|
258
|
+
this.dispatchEvent(new Event('error'))
|
|
259
|
+
return
|
|
260
|
+
// 2. Otherwise, if res is a network error, then reestablish the
|
|
261
|
+
// connection, unless the user agent knows that to be futile, in
|
|
262
|
+
// which case the user agent may fail the connection.
|
|
263
|
+
} else {
|
|
264
|
+
this.#reconnect()
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 3. Otherwise, if res's status is not 200, or if res's `Content-Type`
|
|
270
|
+
// is not `text/event-stream`, then fail the connection.
|
|
271
|
+
const contentType = response.headersList.get('content-type', true)
|
|
272
|
+
const mimeType = contentType !== null ? parseMIMEType(contentType) : 'failure'
|
|
273
|
+
const contentTypeValid = mimeType !== 'failure' && mimeType.essence === 'text/event-stream'
|
|
274
|
+
if (
|
|
275
|
+
response.status !== 200 ||
|
|
276
|
+
contentTypeValid === false
|
|
277
|
+
) {
|
|
278
|
+
this.close()
|
|
279
|
+
this.dispatchEvent(new Event('error'))
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 4. Otherwise, announce the connection and interpret res's body
|
|
284
|
+
// line by line.
|
|
285
|
+
|
|
286
|
+
// When a user agent is to announce the connection, the user agent
|
|
287
|
+
// must queue a task which, if the readyState attribute is set to a
|
|
288
|
+
// value other than CLOSED, sets the readyState attribute to OPEN
|
|
289
|
+
// and fires an event named open at the EventSource object.
|
|
290
|
+
// @see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model
|
|
291
|
+
this.#readyState = OPEN
|
|
292
|
+
this.dispatchEvent(new Event('open'))
|
|
293
|
+
|
|
294
|
+
// If redirected to a different origin, set the origin to the new origin.
|
|
295
|
+
this.#settings.origin = response.urlList[response.urlList.length - 1].origin
|
|
296
|
+
|
|
297
|
+
const eventSourceStream = new EventSourceStream({
|
|
298
|
+
eventSourceSettings: this.#settings,
|
|
299
|
+
push: (event) => {
|
|
300
|
+
this.dispatchEvent(new MessageEvent(
|
|
301
|
+
event.type,
|
|
302
|
+
event.options
|
|
303
|
+
))
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
pipeline(response.body.stream,
|
|
308
|
+
eventSourceStream,
|
|
309
|
+
(error) => {
|
|
310
|
+
if (
|
|
311
|
+
error?.aborted === false
|
|
312
|
+
) {
|
|
313
|
+
this.close()
|
|
314
|
+
this.dispatchEvent(new Event('error'))
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
this.#controller = fetching({
|
|
320
|
+
...fetchParam,
|
|
321
|
+
dispatcher: getGlobalDispatcher()
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model
|
|
327
|
+
* @returns {Promise<void>}
|
|
328
|
+
*/
|
|
329
|
+
async #reconnect () {
|
|
330
|
+
// When a user agent is to reestablish the connection, the user agent must
|
|
331
|
+
// run the following steps. These steps are run in parallel, not as part of
|
|
332
|
+
// a task. (The tasks that it queues, of course, are run like normal tasks
|
|
333
|
+
// and not themselves in parallel.)
|
|
334
|
+
|
|
335
|
+
// 1. Queue a task to run the following steps:
|
|
336
|
+
|
|
337
|
+
// 1. If the readyState attribute is set to CLOSED, abort the task.
|
|
338
|
+
if (this.#readyState === CLOSED) return
|
|
339
|
+
|
|
340
|
+
// 2. Set the readyState attribute to CONNECTING.
|
|
341
|
+
this.#readyState = CONNECTING
|
|
342
|
+
|
|
343
|
+
// 3. Fire an event named error at the EventSource object.
|
|
344
|
+
this.dispatchEvent(new Event('error'))
|
|
345
|
+
|
|
346
|
+
// 2. Wait a delay equal to the reconnection time of the event source.
|
|
347
|
+
await setTimeout(this.#settings.reconnectionTime, { ref: false })
|
|
348
|
+
|
|
349
|
+
// 5. Queue a task to run the following steps:
|
|
350
|
+
|
|
351
|
+
// 1. If the EventSource object's readyState attribute is not set to
|
|
352
|
+
// CONNECTING, then return.
|
|
353
|
+
if (this.#readyState !== CONNECTING) return
|
|
354
|
+
|
|
355
|
+
// 2. Let request be the EventSource object's request.
|
|
356
|
+
// 3. If the EventSource object's last event ID string is not the empty
|
|
357
|
+
// string, then:
|
|
358
|
+
// 1. Let lastEventIDValue be the EventSource object's last event ID
|
|
359
|
+
// string, encoded as UTF-8.
|
|
360
|
+
// 2. Set (`Last-Event-ID`, lastEventIDValue) in request's header
|
|
361
|
+
// list.
|
|
362
|
+
if (this.#settings.lastEventId !== '') {
|
|
363
|
+
this.#request.headersList.set('last-event-id', this.#settings.lastEventId, true)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 4. Fetch request and process the response obtained in this fashion, if any, as described earlier in this section.
|
|
367
|
+
this.#connect()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Closes the connection, if any, and sets the readyState attribute to
|
|
372
|
+
* CLOSED.
|
|
373
|
+
*/
|
|
374
|
+
close () {
|
|
375
|
+
webidl.brandCheck(this, EventSource)
|
|
376
|
+
|
|
377
|
+
if (this.#readyState === CLOSED) return
|
|
378
|
+
this.#readyState = CLOSED
|
|
379
|
+
clearTimeout(this.#settings.reconnectionTimer)
|
|
380
|
+
this.#controller.abort()
|
|
381
|
+
|
|
382
|
+
if (this.#request) {
|
|
383
|
+
this.#request = null
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
get onopen () {
|
|
388
|
+
return this.#events.open
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
set onopen (fn) {
|
|
392
|
+
if (this.#events.open) {
|
|
393
|
+
this.removeEventListener('open', this.#events.open)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (typeof fn === 'function') {
|
|
397
|
+
this.#events.open = fn
|
|
398
|
+
this.addEventListener('open', fn)
|
|
399
|
+
} else {
|
|
400
|
+
this.#events.open = null
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
get onmessage () {
|
|
405
|
+
return this.#events.message
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
set onmessage (fn) {
|
|
409
|
+
if (this.#events.message) {
|
|
410
|
+
this.removeEventListener('message', this.#events.message)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (typeof fn === 'function') {
|
|
414
|
+
this.#events.message = fn
|
|
415
|
+
this.addEventListener('message', fn)
|
|
416
|
+
} else {
|
|
417
|
+
this.#events.message = null
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
get onerror () {
|
|
422
|
+
return this.#events.error
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
set onerror (fn) {
|
|
426
|
+
if (this.#events.error) {
|
|
427
|
+
this.removeEventListener('error', this.#events.error)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (typeof fn === 'function') {
|
|
431
|
+
this.#events.error = fn
|
|
432
|
+
this.addEventListener('error', fn)
|
|
433
|
+
} else {
|
|
434
|
+
this.#events.error = null
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const constantsPropertyDescriptors = {
|
|
440
|
+
CONNECTING: {
|
|
441
|
+
__proto__: null,
|
|
442
|
+
configurable: false,
|
|
443
|
+
enumerable: true,
|
|
444
|
+
value: CONNECTING,
|
|
445
|
+
writable: false
|
|
446
|
+
},
|
|
447
|
+
OPEN: {
|
|
448
|
+
__proto__: null,
|
|
449
|
+
configurable: false,
|
|
450
|
+
enumerable: true,
|
|
451
|
+
value: OPEN,
|
|
452
|
+
writable: false
|
|
453
|
+
},
|
|
454
|
+
CLOSED: {
|
|
455
|
+
__proto__: null,
|
|
456
|
+
configurable: false,
|
|
457
|
+
enumerable: true,
|
|
458
|
+
value: CLOSED,
|
|
459
|
+
writable: false
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
Object.defineProperties(EventSource, constantsPropertyDescriptors)
|
|
464
|
+
Object.defineProperties(EventSource.prototype, constantsPropertyDescriptors)
|
|
465
|
+
|
|
466
|
+
webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([
|
|
467
|
+
{ key: 'withCredentials', converter: webidl.converters.boolean, defaultValue: false }
|
|
468
|
+
])
|
|
469
|
+
|
|
470
|
+
module.exports = {
|
|
471
|
+
EventSource,
|
|
472
|
+
defaultReconnectionTime
|
|
473
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if the given value is a valid LastEventId.
|
|
5
|
+
* @param {string} value
|
|
6
|
+
* @returns {boolean}
|
|
7
|
+
*/
|
|
8
|
+
function isValidLastEventId (value) {
|
|
9
|
+
// LastEventId should not contain U+0000 NULL
|
|
10
|
+
return value.indexOf('\u0000') === -1
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Checks if the given value is a base 10 digit.
|
|
15
|
+
* @param {string} value
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
function isASCIINumber (value) {
|
|
19
|
+
if (value.length === 0) return false
|
|
20
|
+
for (let i = 0; i < value.length; i++) {
|
|
21
|
+
if (value.charCodeAt(i) < 0x30 || value.charCodeAt(i) > 0x39) return false
|
|
22
|
+
}
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
isValidLastEventId,
|
|
28
|
+
isASCIINumber
|
|
29
|
+
}
|
package/lib/fetch/index.js
CHANGED
|
@@ -374,6 +374,9 @@ function fetching ({
|
|
|
374
374
|
useParallelQueue = false,
|
|
375
375
|
dispatcher // undici
|
|
376
376
|
}) {
|
|
377
|
+
// This has bitten me in the ass more times than I'd like to admit.
|
|
378
|
+
assert(dispatcher)
|
|
379
|
+
|
|
377
380
|
// 1. Let taskDestination be null.
|
|
378
381
|
let taskDestination = null
|
|
379
382
|
|
package/lib/fetch/response.js
CHANGED
|
@@ -364,6 +364,16 @@ function makeNetworkError (reason) {
|
|
|
364
364
|
})
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
// @see https://fetch.spec.whatwg.org/#concept-network-error
|
|
368
|
+
function isNetworkError (response) {
|
|
369
|
+
return (
|
|
370
|
+
// A network error is a response whose type is "error",
|
|
371
|
+
response.type === 'error' &&
|
|
372
|
+
// status is 0
|
|
373
|
+
response.status === 0
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
367
377
|
function makeFilteredResponse (response, state) {
|
|
368
378
|
state = {
|
|
369
379
|
internalResponse: response,
|
|
@@ -572,6 +582,7 @@ webidl.converters.ResponseInit = webidl.dictionaryConverter([
|
|
|
572
582
|
])
|
|
573
583
|
|
|
574
584
|
module.exports = {
|
|
585
|
+
isNetworkError,
|
|
575
586
|
makeNetworkError,
|
|
576
587
|
makeResponse,
|
|
577
588
|
makeAppropriateNetworkError,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.5.0",
|
|
4
4
|
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
|
5
5
|
"homepage": "https://undici.nodejs.org",
|
|
6
6
|
"bugs": {
|
|
@@ -75,9 +75,10 @@
|
|
|
75
75
|
"build:wasm": "node build/wasm.js --docker",
|
|
76
76
|
"lint": "standard | snazzy",
|
|
77
77
|
"lint:fix": "standard --fix | snazzy",
|
|
78
|
-
"test": "node scripts/generate-pem && npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:wpt && npm run test:websocket && npm run test:jest && npm run test:typescript && npm run test:node-test",
|
|
78
|
+
"test": "node scripts/generate-pem && npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:jest && npm run test:typescript && npm run test:node-test",
|
|
79
79
|
"test:cookies": "borp --coverage -p \"test/cookie/*.js\"",
|
|
80
80
|
"test:node-fetch": "mocha --exit test/node-fetch",
|
|
81
|
+
"test:eventsource": "npm run build:node && borp --expose-gc --coverage -p \"test/eventsource/*.js\"",
|
|
81
82
|
"test:fetch": "npm run build:node && borp --expose-gc --coverage -p \"test/fetch/*.js\" && borp --coverage -p \"test/webidl/*.js\"",
|
|
82
83
|
"test:jest": "jest",
|
|
83
84
|
"test:tap": "tap test/*.js",
|
|
@@ -86,7 +87,7 @@
|
|
|
86
87
|
"test:tdd:node-test": "borp -p \"test/node-test/**/*.js\" -w",
|
|
87
88
|
"test:typescript": "tsd && tsc --skipLibCheck test/imports/undici-import.ts",
|
|
88
89
|
"test:websocket": "borp --coverage -p \"test/websocket/*.js\"",
|
|
89
|
-
"test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs",
|
|
90
|
+
"test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
|
|
90
91
|
"coverage": "nyc --reporter=text --reporter=html npm run test",
|
|
91
92
|
"coverage:ci": "nyc --reporter=lcov npm run test",
|
|
92
93
|
"bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run",
|
|
@@ -119,7 +120,7 @@
|
|
|
119
120
|
"husky": "^8.0.1",
|
|
120
121
|
"import-fresh": "^3.3.0",
|
|
121
122
|
"jest": "^29.0.2",
|
|
122
|
-
"jsdom": "^
|
|
123
|
+
"jsdom": "^24.0.0",
|
|
123
124
|
"jsfuzz": "^1.0.15",
|
|
124
125
|
"mitata": "^0.1.6",
|
|
125
126
|
"mocha": "^10.0.0",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { MessageEvent, ErrorEvent } from './websocket'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
EventTarget,
|
|
5
|
+
Event,
|
|
6
|
+
EventListenerOptions,
|
|
7
|
+
AddEventListenerOptions,
|
|
8
|
+
EventListenerOrEventListenerObject
|
|
9
|
+
} from './patch'
|
|
10
|
+
|
|
11
|
+
interface EventSourceEventMap {
|
|
12
|
+
error: ErrorEvent
|
|
13
|
+
message: MessageEvent
|
|
14
|
+
open: Event
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface EventSource extends EventTarget {
|
|
18
|
+
close(): void
|
|
19
|
+
readonly CLOSED: 2
|
|
20
|
+
readonly CONNECTING: 0
|
|
21
|
+
readonly OPEN: 1
|
|
22
|
+
onerror: (this: EventSource, ev: ErrorEvent) => any
|
|
23
|
+
onmessage: (this: EventSource, ev: MessageEvent) => any
|
|
24
|
+
onopen: (this: EventSource, ev: Event) => any
|
|
25
|
+
readonly readyState: 0 | 1 | 2
|
|
26
|
+
readonly url: string
|
|
27
|
+
readonly withCredentials: boolean
|
|
28
|
+
|
|
29
|
+
addEventListener<K extends keyof EventSourceEventMap>(
|
|
30
|
+
type: K,
|
|
31
|
+
listener: (this: EventSource, ev: EventSourceEventMap[K]) => any,
|
|
32
|
+
options?: boolean | AddEventListenerOptions
|
|
33
|
+
): void
|
|
34
|
+
addEventListener(
|
|
35
|
+
type: string,
|
|
36
|
+
listener: EventListenerOrEventListenerObject,
|
|
37
|
+
options?: boolean | AddEventListenerOptions
|
|
38
|
+
): void
|
|
39
|
+
removeEventListener<K extends keyof EventSourceEventMap>(
|
|
40
|
+
type: K,
|
|
41
|
+
listener: (this: EventSource, ev: EventSourceEventMap[K]) => any,
|
|
42
|
+
options?: boolean | EventListenerOptions
|
|
43
|
+
): void
|
|
44
|
+
removeEventListener(
|
|
45
|
+
type: string,
|
|
46
|
+
listener: EventListenerOrEventListenerObject,
|
|
47
|
+
options?: boolean | EventListenerOptions
|
|
48
|
+
): void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export declare const EventSource: {
|
|
52
|
+
prototype: EventSource
|
|
53
|
+
new (url: string | URL, init: EventSourceInit): EventSource
|
|
54
|
+
readonly CLOSED: 2
|
|
55
|
+
readonly CONNECTING: 0
|
|
56
|
+
readonly OPEN: 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface EventSourceInit {
|
|
60
|
+
withCredentials?: boolean
|
|
61
|
+
}
|
package/types/index.d.ts
CHANGED
package/types/websocket.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type BinaryType = 'blob' | 'arraybuffer'
|
|
|
17
17
|
|
|
18
18
|
interface WebSocketEventMap {
|
|
19
19
|
close: CloseEvent
|
|
20
|
-
error:
|
|
20
|
+
error: ErrorEvent
|
|
21
21
|
message: MessageEvent
|
|
22
22
|
open: Event
|
|
23
23
|
}
|
|
@@ -124,6 +124,27 @@ export declare const MessageEvent: {
|
|
|
124
124
|
new<T>(type: string, eventInitDict?: MessageEventInit<T>): MessageEvent<T>
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
interface ErrorEventInit extends EventInit {
|
|
128
|
+
message?: string
|
|
129
|
+
filename?: string
|
|
130
|
+
lineno?: number
|
|
131
|
+
colno?: number
|
|
132
|
+
error?: any
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface ErrorEvent extends Event {
|
|
136
|
+
readonly message: string
|
|
137
|
+
readonly filename: string
|
|
138
|
+
readonly lineno: number
|
|
139
|
+
readonly colno: number
|
|
140
|
+
readonly error: any
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export declare const ErrorEvent: {
|
|
144
|
+
prototype: ErrorEvent
|
|
145
|
+
new (type: string, eventInitDict?: ErrorEventInit): ErrorEvent
|
|
146
|
+
}
|
|
147
|
+
|
|
127
148
|
interface WebSocketInit {
|
|
128
149
|
protocols?: string | string[],
|
|
129
150
|
dispatcher?: Dispatcher,
|