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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +24 -38
  2. package/docs/docs/api/Agent.md +14 -14
  3. package/docs/docs/api/BalancedPool.md +16 -16
  4. package/docs/docs/api/CacheStore.md +131 -0
  5. package/docs/docs/api/Client.md +12 -12
  6. package/docs/docs/api/Debug.md +1 -1
  7. package/docs/docs/api/Dispatcher.md +98 -193
  8. package/docs/docs/api/EnvHttpProxyAgent.md +12 -12
  9. package/docs/docs/api/MockAgent.md +5 -3
  10. package/docs/docs/api/MockClient.md +5 -5
  11. package/docs/docs/api/MockPool.md +4 -3
  12. package/docs/docs/api/Pool.md +15 -15
  13. package/docs/docs/api/PoolStats.md +1 -1
  14. package/docs/docs/api/ProxyAgent.md +3 -3
  15. package/docs/docs/api/RedirectHandler.md +1 -1
  16. package/docs/docs/api/RetryAgent.md +1 -1
  17. package/docs/docs/api/RetryHandler.md +4 -4
  18. package/docs/docs/api/WebSocket.md +46 -4
  19. package/docs/docs/api/api-lifecycle.md +11 -11
  20. package/docs/docs/best-practices/mocking-request.md +2 -2
  21. package/docs/docs/best-practices/proxy.md +1 -1
  22. package/index.d.ts +1 -1
  23. package/index.js +23 -3
  24. package/lib/api/abort-signal.js +2 -0
  25. package/lib/api/api-pipeline.js +4 -2
  26. package/lib/api/api-request.js +6 -4
  27. package/lib/api/api-stream.js +3 -1
  28. package/lib/api/api-upgrade.js +2 -2
  29. package/lib/api/readable.js +200 -47
  30. package/lib/api/util.js +2 -0
  31. package/lib/cache/memory-cache-store.js +177 -0
  32. package/lib/cache/sqlite-cache-store.js +446 -0
  33. package/lib/core/connect.js +54 -22
  34. package/lib/core/constants.js +35 -10
  35. package/lib/core/diagnostics.js +122 -128
  36. package/lib/core/errors.js +2 -2
  37. package/lib/core/request.js +6 -6
  38. package/lib/core/symbols.js +2 -0
  39. package/lib/core/tree.js +4 -2
  40. package/lib/core/util.js +238 -40
  41. package/lib/dispatcher/client-h1.js +405 -142
  42. package/lib/dispatcher/client-h2.js +212 -109
  43. package/lib/dispatcher/client.js +24 -7
  44. package/lib/dispatcher/dispatcher-base.js +4 -1
  45. package/lib/dispatcher/dispatcher.js +4 -0
  46. package/lib/dispatcher/fixed-queue.js +91 -49
  47. package/lib/dispatcher/pool-base.js +3 -3
  48. package/lib/dispatcher/pool-stats.js +2 -0
  49. package/lib/dispatcher/proxy-agent.js +3 -1
  50. package/lib/handler/cache-handler.js +393 -0
  51. package/lib/handler/cache-revalidation-handler.js +124 -0
  52. package/lib/handler/decorator-handler.js +3 -0
  53. package/lib/handler/redirect-handler.js +45 -59
  54. package/lib/handler/retry-handler.js +68 -109
  55. package/lib/handler/unwrap-handler.js +96 -0
  56. package/lib/handler/wrap-handler.js +98 -0
  57. package/lib/interceptor/cache.js +350 -0
  58. package/lib/interceptor/dns.js +375 -0
  59. package/lib/interceptor/response-error.js +15 -7
  60. package/lib/mock/mock-agent.js +5 -8
  61. package/lib/mock/mock-client.js +7 -2
  62. package/lib/mock/mock-errors.js +3 -1
  63. package/lib/mock/mock-interceptor.js +8 -6
  64. package/lib/mock/mock-pool.js +7 -2
  65. package/lib/mock/mock-symbols.js +2 -1
  66. package/lib/mock/mock-utils.js +33 -5
  67. package/lib/util/cache.js +360 -0
  68. package/lib/util/timers.js +50 -6
  69. package/lib/web/cache/cache.js +25 -21
  70. package/lib/web/cache/cachestorage.js +3 -1
  71. package/lib/web/cookies/index.js +18 -5
  72. package/lib/web/cookies/parse.js +6 -1
  73. package/lib/web/eventsource/eventsource.js +2 -0
  74. package/lib/web/fetch/body.js +43 -39
  75. package/lib/web/fetch/constants.js +45 -29
  76. package/lib/web/fetch/data-url.js +2 -2
  77. package/lib/web/fetch/formdata-parser.js +84 -46
  78. package/lib/web/fetch/formdata.js +42 -20
  79. package/lib/web/fetch/headers.js +119 -85
  80. package/lib/web/fetch/index.js +69 -65
  81. package/lib/web/fetch/request.js +132 -55
  82. package/lib/web/fetch/response.js +81 -36
  83. package/lib/web/fetch/util.js +274 -103
  84. package/lib/web/fetch/webidl.js +54 -18
  85. package/lib/web/websocket/connection.js +92 -15
  86. package/lib/web/websocket/constants.js +69 -9
  87. package/lib/web/websocket/events.js +8 -2
  88. package/lib/web/websocket/receiver.js +20 -26
  89. package/lib/web/websocket/stream/websocketerror.js +83 -0
  90. package/lib/web/websocket/stream/websocketstream.js +485 -0
  91. package/lib/web/websocket/util.js +115 -10
  92. package/lib/web/websocket/websocket.js +47 -170
  93. package/package.json +15 -11
  94. package/types/agent.d.ts +1 -1
  95. package/types/cache-interceptor.d.ts +172 -0
  96. package/types/cookies.d.ts +2 -0
  97. package/types/dispatcher.d.ts +29 -4
  98. package/types/env-http-proxy-agent.d.ts +1 -1
  99. package/types/fetch.d.ts +9 -8
  100. package/types/handlers.d.ts +4 -4
  101. package/types/index.d.ts +3 -1
  102. package/types/interceptors.d.ts +18 -1
  103. package/types/mock-agent.d.ts +4 -1
  104. package/types/mock-client.d.ts +1 -1
  105. package/types/mock-pool.d.ts +1 -1
  106. package/types/proxy-agent.d.ts +1 -1
  107. package/types/readable.d.ts +10 -7
  108. package/types/retry-handler.d.ts +3 -3
  109. package/types/webidl.d.ts +30 -4
  110. package/types/websocket.d.ts +33 -0
  111. package/lib/mock/pluralizer.js +0 -29
  112. package/lib/web/cache/symbols.js +0 -5
  113. package/lib/web/fetch/symbols.js +0 -8
@@ -50,6 +50,7 @@ function isClosed (readyState) {
50
50
  * @param {EventTarget} target
51
51
  * @param {(...args: ConstructorParameters<typeof Event>) => Event} eventFactory
52
52
  * @param {EventInit | undefined} eventInitDict
53
+ * @returns {void}
53
54
  */
54
55
  function fireEvent (e, target, eventFactory = (type, init) => new Event(type, init), eventInitDict = {}) {
55
56
  // 1. If eventConstructor is not given, then let eventConstructor be Event.
@@ -72,11 +73,16 @@ function fireEvent (e, target, eventFactory = (type, init) => new Event(type, in
72
73
  * @param {import('./websocket').Handler} handler
73
74
  * @param {number} type Opcode
74
75
  * @param {Buffer} data application data
76
+ * @returns {void}
75
77
  */
76
78
  function websocketMessageReceived (handler, type, data) {
77
79
  handler.onMessage(type, data)
78
80
  }
79
81
 
82
+ /**
83
+ * @param {Buffer} buffer
84
+ * @returns {ArrayBuffer}
85
+ */
80
86
  function toArrayBuffer (buffer) {
81
87
  if (buffer.byteLength === buffer.buffer.byteLength) {
82
88
  return buffer.buffer
@@ -89,6 +95,7 @@ function toArrayBuffer (buffer) {
89
95
  * @see https://datatracker.ietf.org/doc/html/rfc2616
90
96
  * @see https://bugs.chromium.org/p/chromium/issues/detail?id=398407
91
97
  * @param {string} protocol
98
+ * @returns {boolean}
92
99
  */
93
100
  function isValidSubprotocol (protocol) {
94
101
  // If present, this value indicates one
@@ -135,6 +142,7 @@ function isValidSubprotocol (protocol) {
135
142
  /**
136
143
  * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7-4
137
144
  * @param {number} code
145
+ * @returns {boolean}
138
146
  */
139
147
  function isValidStatusCode (code) {
140
148
  if (code >= 1000 && code < 1015) {
@@ -150,15 +158,34 @@ function isValidStatusCode (code) {
150
158
 
151
159
  /**
152
160
  * @param {import('./websocket').Handler} handler
161
+ * @param {number} code
153
162
  * @param {string|undefined} reason
163
+ * @returns {void}
154
164
  */
155
- function failWebsocketConnection (handler, reason) {
156
- handler.onFail(reason)
165
+ function failWebsocketConnection (handler, code, reason) {
166
+ // If _The WebSocket Connection is Established_ prior to the point where
167
+ // the endpoint is required to _Fail the WebSocket Connection_, the
168
+ // endpoint SHOULD send a Close frame with an appropriate status code
169
+ // (Section 7.4) before proceeding to _Close the WebSocket Connection_.
170
+ if (isEstablished(handler.readyState)) {
171
+ // avoid circular require - performance is not important here
172
+ const { closeWebSocketConnection } = require('./connection')
173
+ closeWebSocketConnection(handler, code, reason, false)
174
+ }
175
+
176
+ handler.controller.abort()
177
+
178
+ if (handler.socket?.destroyed === false) {
179
+ handler.socket.destroy()
180
+ }
181
+
182
+ handler.onFail(code, reason)
157
183
  }
158
184
 
159
185
  /**
160
186
  * @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5
161
187
  * @param {number} opcode
188
+ * @returns {boolean}
162
189
  */
163
190
  function isControlFrame (opcode) {
164
191
  return (
@@ -168,14 +195,27 @@ function isControlFrame (opcode) {
168
195
  )
169
196
  }
170
197
 
198
+ /**
199
+ * @param {number} opcode
200
+ * @returns {boolean}
201
+ */
171
202
  function isContinuationFrame (opcode) {
172
203
  return opcode === opcodes.CONTINUATION
173
204
  }
174
205
 
206
+ /**
207
+ * @param {number} opcode
208
+ * @returns {boolean}
209
+ */
175
210
  function isTextBinaryFrame (opcode) {
176
211
  return opcode === opcodes.TEXT || opcode === opcodes.BINARY
177
212
  }
178
213
 
214
+ /**
215
+ *
216
+ * @param {number} opcode
217
+ * @returns {boolean}
218
+ */
179
219
  function isValidOpcode (opcode) {
180
220
  return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
181
221
  }
@@ -209,6 +249,7 @@ function parseExtensions (extensions) {
209
249
  * @see https://www.rfc-editor.org/rfc/rfc7692#section-7.1.2.2
210
250
  * @description "client-max-window-bits = 1*DIGIT"
211
251
  * @param {string} value
252
+ * @returns {boolean}
212
253
  */
213
254
  function isValidClientWindowBits (value) {
214
255
  for (let i = 0; i < value.length; i++) {
@@ -222,22 +263,84 @@ function isValidClientWindowBits (value) {
222
263
  return true
223
264
  }
224
265
 
225
- // https://nodejs.org/api/intl.html#detecting-internationalization-support
226
- const hasIntl = typeof process.versions.icu === 'string'
227
- const fatalDecoder = hasIntl ? new TextDecoder('utf-8', { fatal: true }) : undefined
266
+ /**
267
+ * @see https://whatpr.org/websockets/48/7b748d3...d5570f3.html#get-a-url-record
268
+ * @param {string} url
269
+ * @param {string} [baseURL]
270
+ */
271
+ function getURLRecord (url, baseURL) {
272
+ // 1. Let urlRecord be the result of applying the URL parser to url with baseURL .
273
+ // 2. If urlRecord is failure, then throw a " SyntaxError " DOMException .
274
+ let urlRecord
275
+
276
+ try {
277
+ urlRecord = new URL(url, baseURL)
278
+ } catch (e) {
279
+ throw new DOMException(e, 'SyntaxError')
280
+ }
281
+
282
+ // 3. If urlRecord ’s scheme is " http ", then set urlRecord ’s scheme to " ws ".
283
+ // 4. Otherwise, if urlRecord ’s scheme is " https ", set urlRecord ’s scheme to " wss ".
284
+ if (urlRecord.protocol === 'http:') {
285
+ urlRecord.protocol = 'ws:'
286
+ } else if (urlRecord.protocol === 'https:') {
287
+ urlRecord.protocol = 'wss:'
288
+ }
289
+
290
+ // 5. If urlRecord ’s scheme is not " ws " or " wss ", then throw a " SyntaxError " DOMException .
291
+ if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') {
292
+ throw new DOMException('expected a ws: or wss: url', 'SyntaxError')
293
+ }
294
+
295
+ // If urlRecord ’s fragment is non-null, then throw a " SyntaxError " DOMException .
296
+ if (urlRecord.hash.length || urlRecord.href.endsWith('#')) {
297
+ throw new DOMException('hash', 'SyntaxError')
298
+ }
299
+
300
+ // Return urlRecord .
301
+ return urlRecord
302
+ }
303
+
304
+ // https://whatpr.org/websockets/48.html#validate-close-code-and-reason
305
+ function validateCloseCodeAndReason (code, reason) {
306
+ // 1. If code is not null, but is neither an integer equal to
307
+ // 1000 nor an integer in the range 3000 to 4999, inclusive,
308
+ // throw an "InvalidAccessError" DOMException.
309
+ if (code !== null) {
310
+ if (code !== 1000 && (code < 3000 || code > 4999)) {
311
+ throw new DOMException('invalid code', 'InvalidAccessError')
312
+ }
313
+ }
314
+
315
+ // 2. If reason is not null, then:
316
+ if (reason !== null) {
317
+ // 2.1. Let reasonBytes be the result of UTF-8 encoding reason.
318
+ // 2.2. If reasonBytes is longer than 123 bytes, then throw a
319
+ // "SyntaxError" DOMException.
320
+ const reasonBytesLength = Buffer.byteLength(reason)
321
+
322
+ if (reasonBytesLength > 123) {
323
+ throw new DOMException(`Reason must be less than 123 bytes; received ${reasonBytesLength}`, 'SyntaxError')
324
+ }
325
+ }
326
+ }
228
327
 
229
328
  /**
230
329
  * Converts a Buffer to utf-8, even on platforms without icu.
231
- * @param {Buffer} buffer
330
+ * @type {(buffer: Buffer) => string}
232
331
  */
233
- const utf8Decode = hasIntl
234
- ? fatalDecoder.decode.bind(fatalDecoder)
235
- : function (buffer) {
332
+ const utf8Decode = (() => {
333
+ if (typeof process.versions.icu === 'string') {
334
+ const fatalDecoder = new TextDecoder('utf-8', { fatal: true })
335
+ return fatalDecoder.decode.bind(fatalDecoder)
336
+ }
337
+ return function (buffer) {
236
338
  if (isUtf8(buffer)) {
237
339
  return buffer.toString('utf-8')
238
340
  }
239
341
  throw new TypeError('Invalid utf-8 received.')
240
342
  }
343
+ })()
241
344
 
242
345
  module.exports = {
243
346
  isConnecting,
@@ -256,5 +359,7 @@ module.exports = {
256
359
  isValidOpcode,
257
360
  parseExtensions,
258
361
  isValidClientWindowBits,
259
- toArrayBuffer
362
+ toArrayBuffer,
363
+ getURLRecord,
364
+ validateCloseCodeAndReason
260
365
  }
@@ -3,7 +3,7 @@
3
3
  const { webidl } = require('../fetch/webidl')
4
4
  const { URLSerializer } = require('../fetch/data-url')
5
5
  const { environmentSettingsObject } = require('../fetch/util')
6
- const { staticPropertyDescriptors, states, sentCloseFrameState, sendHints, opcodes, emptyBuffer } = require('./constants')
6
+ const { staticPropertyDescriptors, states, sentCloseFrameState, sendHints, opcodes } = require('./constants')
7
7
  const {
8
8
  isConnecting,
9
9
  isEstablished,
@@ -13,7 +13,7 @@ const {
13
13
  failWebsocketConnection,
14
14
  utf8Decode,
15
15
  toArrayBuffer,
16
- isClosed
16
+ getURLRecord
17
17
  } = require('./util')
18
18
  const { establishWebSocketConnection, closeWebSocketConnection } = require('./connection')
19
19
  const { ByteParser } = require('./receiver')
@@ -22,15 +22,13 @@ const { getGlobalDispatcher } = require('../../global')
22
22
  const { types } = require('node:util')
23
23
  const { ErrorEvent, CloseEvent, createFastMessageEvent } = require('./events')
24
24
  const { SendQueue } = require('./sender')
25
- const { WebsocketFrameSend } = require('./frame')
26
25
  const { channels } = require('../../core/diagnostics')
27
26
 
28
27
  /**
29
28
  * @typedef {object} Handler
30
29
  * @property {(response: any, extensions?: string[]) => void} onConnectionEstablished
31
- * @property {(reason: any) => void} onFail
30
+ * @property {(code: number, reason: any) => void} onFail
32
31
  * @property {(opcode: number, data: Buffer) => void} onMessage
33
- * @property {(code: number, reason: any, reasonByteLength: number) => void} onClose
34
32
  * @property {(error: Error) => void} onParserError
35
33
  * @property {() => void} onParserDrain
36
34
  * @property {(chunk: Buffer) => void} onSocketData
@@ -39,8 +37,9 @@ const { channels } = require('../../core/diagnostics')
39
37
  *
40
38
  * @property {number} readyState
41
39
  * @property {import('stream').Duplex} socket
42
- * @property {number} closeState
43
- * @property {boolean} receivedClose
40
+ * @property {Set<number>} closeState
41
+ * @property {import('../fetch/index').Fetch} controller
42
+ * @property {boolean} [wasEverConnected=false]
44
43
  */
45
44
 
46
45
  // https://websockets.spec.whatwg.org/#interface-definition
@@ -62,10 +61,9 @@ class WebSocket extends EventTarget {
62
61
  /** @type {Handler} */
63
62
  #handler = {
64
63
  onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
65
- onFail: (reason) => this.#onFail(reason),
64
+ onFail: (code, reason) => this.#onFail(code, reason),
66
65
  onMessage: (opcode, data) => this.#onMessage(opcode, data),
67
- onClose: (code, reason, reasonByteLength) => this.#onClose(code, reason, reasonByteLength),
68
- onParserError: (err) => this.#onParserError(err),
66
+ onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message),
69
67
  onParserDrain: () => this.#onParserDrain(),
70
68
  onSocketData: (chunk) => {
71
69
  if (!this.#parser.write(chunk)) {
@@ -85,12 +83,12 @@ class WebSocket extends EventTarget {
85
83
 
86
84
  readyState: states.CONNECTING,
87
85
  socket: null,
88
- closeState: sentCloseFrameState.NOT_SENT,
89
- receivedClose: false
86
+ closeState: new Set(),
87
+ controller: null,
88
+ wasEverConnected: false
90
89
  }
91
90
 
92
91
  #url
93
- #controller
94
92
  #binaryType
95
93
  /** @type {import('./receiver').ByteParser} */
96
94
  #parser
@@ -102,6 +100,8 @@ class WebSocket extends EventTarget {
102
100
  constructor (url, protocols = []) {
103
101
  super()
104
102
 
103
+ webidl.util.markAsUncloneable(this)
104
+
105
105
  const prefix = 'WebSocket constructor'
106
106
  webidl.argumentLengthCheck(arguments, 1, prefix)
107
107
 
@@ -113,45 +113,16 @@ class WebSocket extends EventTarget {
113
113
  // 1. Let baseURL be this's relevant settings object's API base URL.
114
114
  const baseURL = environmentSettingsObject.settingsObject.baseUrl
115
115
 
116
- // 1. Let urlRecord be the result of applying the URL parser to url with baseURL.
117
- let urlRecord
118
-
119
- try {
120
- urlRecord = new URL(url, baseURL)
121
- } catch (e) {
122
- // 3. If urlRecord is failure, then throw a "SyntaxError" DOMException.
123
- throw new DOMException(e, 'SyntaxError')
124
- }
125
-
126
- // 4. If urlRecord’s scheme is "http", then set urlRecord’s scheme to "ws".
127
- if (urlRecord.protocol === 'http:') {
128
- urlRecord.protocol = 'ws:'
129
- } else if (urlRecord.protocol === 'https:') {
130
- // 5. Otherwise, if urlRecord’s scheme is "https", set urlRecord’s scheme to "wss".
131
- urlRecord.protocol = 'wss:'
132
- }
133
-
134
- // 6. If urlRecord’s scheme is not "ws" or "wss", then throw a "SyntaxError" DOMException.
135
- if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') {
136
- throw new DOMException(
137
- `Expected a ws: or wss: protocol, got ${urlRecord.protocol}`,
138
- 'SyntaxError'
139
- )
140
- }
141
-
142
- // 7. If urlRecord’s fragment is non-null, then throw a "SyntaxError"
143
- // DOMException.
144
- if (urlRecord.hash || urlRecord.href.endsWith('#')) {
145
- throw new DOMException('Got fragment', 'SyntaxError')
146
- }
116
+ // 2. Let urlRecord be the result of getting a URL record given url and baseURL.
117
+ const urlRecord = getURLRecord(url, baseURL)
147
118
 
148
- // 8. If protocols is a string, set protocols to a sequence consisting
119
+ // 3. If protocols is a string, set protocols to a sequence consisting
149
120
  // of just that string.
150
121
  if (typeof protocols === 'string') {
151
122
  protocols = [protocols]
152
123
  }
153
124
 
154
- // 9. If any of the values in protocols occur more than once or otherwise
125
+ // 4. If any of the values in protocols occur more than once or otherwise
155
126
  // fail to match the requirements for elements that comprise the value
156
127
  // of `Sec-WebSocket-Protocol` fields as defined by The WebSocket
157
128
  // protocol, then throw a "SyntaxError" DOMException.
@@ -163,17 +134,16 @@ class WebSocket extends EventTarget {
163
134
  throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
164
135
  }
165
136
 
166
- // 10. Set this's url to urlRecord.
137
+ // 5. Set this's url to urlRecord.
167
138
  this.#url = new URL(urlRecord.href)
168
139
 
169
- // 11. Let client be this's relevant settings object.
140
+ // 6. Let client be this's relevant settings object.
170
141
  const client = environmentSettingsObject.settingsObject
171
142
 
172
- // 12. Run this step in parallel:
173
-
174
- // 1. Establish a WebSocket connection given urlRecord, protocols,
175
- // and client.
176
- this.#controller = establishWebSocketConnection(
143
+ // 7. Run this step in parallel:
144
+ // 7.1. Establish a WebSocket connection given urlRecord, protocols,
145
+ // and client.
146
+ this.#handler.controller = establishWebSocketConnection(
177
147
  urlRecord,
178
148
  protocols,
179
149
  client,
@@ -186,8 +156,6 @@ class WebSocket extends EventTarget {
186
156
  // be CONNECTING (0).
187
157
  this.#handler.readyState = WebSocket.CONNECTING
188
158
 
189
- this.#handler.closeState = sentCloseFrameState.NOT_SENT
190
-
191
159
  // The extensions attribute must initially return the empty string.
192
160
 
193
161
  // The protocol attribute must initially return the empty string.
@@ -215,34 +183,14 @@ class WebSocket extends EventTarget {
215
183
  reason = webidl.converters.USVString(reason)
216
184
  }
217
185
 
218
- // 1. If code is present, but is neither an integer equal to 1000 nor an
219
- // integer in the range 3000 to 4999, inclusive, throw an
220
- // "InvalidAccessError" DOMException.
221
- if (code !== undefined) {
222
- if (code !== 1000 && (code < 3000 || code > 4999)) {
223
- throw new DOMException('invalid code', 'InvalidAccessError')
224
- }
225
- }
226
-
227
- let reasonByteLength = 0
186
+ // 1. If code is the special value "missing", then set code to null.
187
+ code ??= null
228
188
 
229
- // 2. If reason is present, then run these substeps:
230
- if (reason !== undefined) {
231
- // 1. Let reasonBytes be the result of encoding reason.
232
- // 2. If reasonBytes is longer than 123 bytes, then throw a
233
- // "SyntaxError" DOMException.
234
- reasonByteLength = Buffer.byteLength(reason)
235
-
236
- if (reasonByteLength > 123) {
237
- throw new DOMException(
238
- `Reason must be less than 123 bytes; received ${reasonByteLength}`,
239
- 'SyntaxError'
240
- )
241
- }
242
- }
189
+ // 2. If reason is the special value "missing", then set reason to the empty string.
190
+ reason ??= ''
243
191
 
244
- // 3. Run the first matching steps from the following list:
245
- closeWebSocketConnection(this.#handler, code, reason, reasonByteLength)
192
+ // 3. Close the WebSocket with this, code, and reason.
193
+ closeWebSocketConnection(this.#handler, code, reason, true)
246
194
  }
247
195
 
248
196
  /**
@@ -324,7 +272,7 @@ class WebSocket extends EventTarget {
324
272
  this.#sendQueue.add(data, () => {
325
273
  this.#bufferedAmount -= data.byteLength
326
274
  }, sendHints.typedArray)
327
- } else if (data instanceof Blob) {
275
+ } else if (webidl.is.Blob(data)) {
328
276
  // If the WebSocket connection is established, and the WebSocket
329
277
  // closing handshake has not yet started, then the user agent must
330
278
  // send a WebSocket Message comprised of data using a binary frame
@@ -515,15 +463,7 @@ class WebSocket extends EventTarget {
515
463
  fireEvent('open', this)
516
464
  }
517
465
 
518
- #onFail (reason) {
519
- this.#controller.abort()
520
-
521
- if (this.#handler.socket && !this.#handler.socket.destroyed) {
522
- this.#handler.socket.destroy()
523
- }
524
-
525
- this.#handler.readyState = states.CLOSED
526
-
466
+ #onFail (code, reason) {
527
467
  if (reason) {
528
468
  // TODO: process.nextTick
529
469
  fireEvent('error', this, (type, init) => new ErrorEvent(type, init), {
@@ -531,6 +471,16 @@ class WebSocket extends EventTarget {
531
471
  message: reason
532
472
  })
533
473
  }
474
+
475
+ if (!this.#handler.wasEverConnected) {
476
+ this.#handler.readyState = states.CLOSED
477
+
478
+ // If the WebSocket connection could not be established, it is also said
479
+ // that _The WebSocket Connection is Closed_, but not _cleanly_.
480
+ fireEvent('close', this, (type, init) => new CloseEvent(type, init), {
481
+ wasClean: false, code, reason
482
+ })
483
+ }
534
484
  }
535
485
 
536
486
  #onMessage (type, data) {
@@ -548,7 +498,7 @@ class WebSocket extends EventTarget {
548
498
  try {
549
499
  dataForEvent = utf8Decode(data)
550
500
  } catch {
551
- failWebsocketConnection(this.#handler, 'Received invalid UTF-8 in text frame.')
501
+ failWebsocketConnection(this.#handler, 1007, 'Received invalid UTF-8 in text frame.')
552
502
  return
553
503
  }
554
504
  } else if (type === opcodes.BINARY) {
@@ -574,81 +524,6 @@ class WebSocket extends EventTarget {
574
524
  })
575
525
  }
576
526
 
577
- #onClose (code, reason, reasonByteLength) {
578
- if (isClosing(this.#handler.readyState) || isClosed(this.#handler.readyState)) {
579
- // If this's ready state is CLOSING (2) or CLOSED (3)
580
- // Do nothing.
581
- } else if (!isEstablished(this.#handler.readyState)) {
582
- // If the WebSocket connection is not yet established
583
- // Fail the WebSocket connection and set this's ready state
584
- // to CLOSING (2).
585
- failWebsocketConnection(this.#handler, 'Connection was closed before it was established.')
586
- this.#handler.readyState = states.CLOSING
587
- } else if (this.#handler.closeState === sentCloseFrameState.NOT_SENT) {
588
- // If the WebSocket closing handshake has not yet been started
589
- // Start the WebSocket closing handshake and set this's ready
590
- // state to CLOSING (2).
591
- // - If neither code nor reason is present, the WebSocket Close
592
- // message must not have a body.
593
- // - If code is present, then the status code to use in the
594
- // WebSocket Close message must be the integer given by code.
595
- // - If reason is also present, then reasonBytes must be
596
- // provided in the Close message after the status code.
597
-
598
- this.#handler.closeState = sentCloseFrameState.PROCESSING
599
-
600
- const frame = new WebsocketFrameSend()
601
-
602
- // If neither code nor reason is present, the WebSocket Close
603
- // message must not have a body.
604
-
605
- // If code is present, then the status code to use in the
606
- // WebSocket Close message must be the integer given by code.
607
- if (code !== undefined && reason === undefined) {
608
- frame.frameData = Buffer.allocUnsafe(2)
609
- frame.frameData.writeUInt16BE(code, 0)
610
- } else if (code !== undefined && reason !== undefined) {
611
- // If reason is also present, then reasonBytes must be
612
- // provided in the Close message after the status code.
613
- frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength)
614
- frame.frameData.writeUInt16BE(code, 0)
615
- // the body MAY contain UTF-8-encoded data with value /reason/
616
- frame.frameData.write(reason, 2, 'utf-8')
617
- } else {
618
- frame.frameData = emptyBuffer
619
- }
620
-
621
- this.#handler.socket.write(frame.createFrame(opcodes.CLOSE))
622
-
623
- this.#handler.closeState = sentCloseFrameState.SENT
624
-
625
- // Upon either sending or receiving a Close control frame, it is said
626
- // that _The WebSocket Closing Handshake is Started_ and that the
627
- // WebSocket connection is in the CLOSING state.
628
- this.#handler.readyState = states.CLOSING
629
- } else {
630
- // Otherwise
631
- // Set this's ready state to CLOSING (2).
632
- this.#handler.readyState = states.CLOSING
633
- }
634
- }
635
-
636
- #onParserError (err) {
637
- let message
638
- let code
639
-
640
- if (err instanceof CloseEvent) {
641
- message = err.reason
642
- code = err.code
643
- } else {
644
- message = err.message
645
- }
646
-
647
- fireEvent('error', this, () => new ErrorEvent('error', { error: err, message }))
648
-
649
- closeWebSocketConnection(this.#handler, code)
650
- }
651
-
652
527
  #onParserDrain () {
653
528
  this.#handler.socket.resume()
654
529
  }
@@ -661,7 +536,9 @@ class WebSocket extends EventTarget {
661
536
  // If the TCP connection was closed after the
662
537
  // WebSocket closing handshake was completed, the WebSocket connection
663
538
  // is said to have been closed _cleanly_.
664
- const wasClean = this.#handler.closeState === sentCloseFrameState.SENT && this.#handler.receivedClose
539
+ const wasClean =
540
+ this.#handler.closeState.has(sentCloseFrameState.SENT) &&
541
+ this.#handler.closeState.has(sentCloseFrameState.RECEIVED)
665
542
 
666
543
  let code = 1005
667
544
  let reason = ''
@@ -671,7 +548,7 @@ class WebSocket extends EventTarget {
671
548
  if (result && !result.error) {
672
549
  code = result.code ?? 1005
673
550
  reason = result.reason
674
- } else if (!this.#handler.receivedClose) {
551
+ } else if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
675
552
  // If _The WebSocket
676
553
  // Connection is Closed_ and no Close control frame was received by the
677
554
  // endpoint (such as could occur if the underlying transport connection
@@ -793,7 +670,7 @@ webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'] = functio
793
670
 
794
671
  webidl.converters.WebSocketSendData = function (V) {
795
672
  if (webidl.util.Type(V) === webidl.util.Types.OBJECT) {
796
- if (V instanceof Blob) {
673
+ if (webidl.is.Blob(V)) {
797
674
  return V
798
675
  }
799
676
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "7.0.0-alpha.1",
3
+ "version": "7.0.0-alpha.10",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
@@ -62,22 +62,26 @@
62
62
  "main": "index.js",
63
63
  "types": "index.d.ts",
64
64
  "scripts": {
65
- "build:node": "npx esbuild@0.19.10 index-fetch.js --bundle --platform=node --outfile=undici-fetch.js --define:esbuildDetection=1 --keep-names && node scripts/strip-comments.js",
66
- "prebuild:wasm": "node build/wasm.js --prebuild",
65
+ "build:node": "esbuild index-fetch.js --bundle --platform=node --outfile=undici-fetch.js --define:esbuildDetection=1 --keep-names && node scripts/strip-comments.js",
67
66
  "build:wasm": "node build/wasm.js --docker",
68
67
  "generate-pem": "node scripts/generate-pem.js",
69
68
  "lint": "eslint --cache",
70
69
  "lint:fix": "eslint --fix --cache",
71
70
  "test": "npm run test:javascript && cross-env NODE_V8_COVERAGE= npm run test:typescript",
72
71
  "test:javascript": "npm run test:javascript:no-jest && npm run test:jest",
73
- "test:javascript:no-jest": "npm run generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:cache && npm run test:interceptors && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:node-test",
72
+ "test:javascript:no-jest": "npm run generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:cache && npm run test:cache-interceptor && npm run test:interceptors && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:node-test",
74
73
  "test:javascript:without-intl": "npm run test:javascript:no-jest",
75
74
  "test:busboy": "borp -p \"test/busboy/*.js\"",
76
75
  "test:cache": "borp -p \"test/cache/*.js\"",
76
+ "test:sqlite": "NODE_OPTIONS=--experimental-sqlite borp -p \"test/cache-interceptor/*.js\"",
77
+ "test:cache-interceptor": "borp -p \"test/cache-interceptor/*.js\"",
77
78
  "test:cookies": "borp -p \"test/cookie/*.js\"",
78
79
  "test:eventsource": "npm run build:node && borp --expose-gc -p \"test/eventsource/*.js\"",
79
80
  "test:fuzzing": "node test/fuzzing/fuzzing.test.js",
80
81
  "test:fetch": "npm run build:node && borp --timeout 180000 --expose-gc --concurrency 1 -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy",
82
+ "test:h2": "npm run test:h2:core && npm run test:h2:fetch",
83
+ "test:h2:core": "borp -p \"test/http2*.js\"",
84
+ "test:h2:fetch": "npm run build:node && borp -p \"test/fetch/http2*.js\"",
81
85
  "test:interceptors": "borp -p \"test/interceptors/*.js\"",
82
86
  "test:jest": "cross-env NODE_V8_COVERAGE= jest",
83
87
  "test:unit": "borp --expose-gc -p \"test/*.js\"",
@@ -104,13 +108,14 @@
104
108
  "devDependencies": {
105
109
  "@fastify/busboy": "3.0.0",
106
110
  "@matteo.collina/tspl": "^0.1.1",
107
- "@sinonjs/fake-timers": "^11.1.0",
108
- "@types/node": "~18.17.19",
111
+ "@sinonjs/fake-timers": "^12.0.0",
112
+ "@types/node": "^18.19.50",
109
113
  "abort-controller": "^3.0.0",
110
- "borp": "^0.17.0",
114
+ "borp": "^0.19.0",
111
115
  "c8": "^10.0.0",
112
116
  "cross-env": "^7.0.3",
113
117
  "dns-packet": "^5.4.0",
118
+ "esbuild": "^0.24.0",
114
119
  "eslint": "^9.9.0",
115
120
  "fast-check": "^3.17.1",
116
121
  "https-pem": "^3.0.0",
@@ -118,14 +123,13 @@
118
123
  "jest": "^29.0.2",
119
124
  "neostandard": "^0.11.2",
120
125
  "node-forge": "^1.3.1",
121
- "pre-commit": "^1.2.2",
122
126
  "proxy": "^2.1.1",
123
- "tsd": "^0.31.0",
124
- "typescript": "^5.0.2",
127
+ "tsd": "^0.31.2",
128
+ "typescript": "^5.6.2",
125
129
  "ws": "^8.11.0"
126
130
  },
127
131
  "engines": {
128
- "node": ">=18.17"
132
+ "node": ">=20.18.1"
129
133
  },
130
134
  "tsd": {
131
135
  "directory": "test/types",
package/types/agent.d.ts CHANGED
@@ -11,7 +11,7 @@ declare class Agent extends Dispatcher {
11
11
  /** `true` after `dispatcher.destroyed()` has been called or `dispatcher.close()` has been called and the dispatcher shutdown has completed. */
12
12
  destroyed: boolean
13
13
  /** Dispatches a request. */
14
- dispatch (options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean
14
+ dispatch (options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean
15
15
  }
16
16
 
17
17
  declare namespace Agent {