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
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { types, inspect } = require('node:util')
4
+ const { markAsUncloneable } = require('node:worker_threads')
4
5
  const { toUSVString } = require('../../core/util')
5
6
 
6
7
  const UNDEFINED = 1
@@ -12,11 +13,15 @@ const BIGINT = 6
12
13
  const NULL = 7
13
14
  const OBJECT = 8 // function and object
14
15
 
16
+ const FunctionPrototypeSymbolHasInstance = Function.call.bind(Function.prototype[Symbol.hasInstance])
17
+
15
18
  /** @type {import('../../../types/webidl').Webidl} */
16
- const webidl = {}
17
- webidl.converters = {}
18
- webidl.util = {}
19
- webidl.errors = {}
19
+ const webidl = {
20
+ converters: {},
21
+ util: {},
22
+ errors: {},
23
+ is: {}
24
+ }
20
25
 
21
26
  webidl.errors.exception = function (message) {
22
27
  return new TypeError(`${message.header}: ${message.message}`)
@@ -43,18 +48,22 @@ webidl.errors.invalidArgument = function (context) {
43
48
 
44
49
  // https://webidl.spec.whatwg.org/#implements
45
50
  webidl.brandCheck = function (V, I) {
46
- if (!(V instanceof I)) {
51
+ if (!FunctionPrototypeSymbolHasInstance(I, V)) {
47
52
  const err = new TypeError('Illegal invocation')
48
53
  err.code = 'ERR_INVALID_THIS' // node compat.
49
54
  throw err
50
55
  }
51
56
  }
52
57
 
53
- webidl.brandCheckMultiple = function (V, List) {
54
- if (List.every((clazz) => !(V instanceof clazz))) {
55
- const err = new TypeError('Illegal invocation')
56
- err.code = 'ERR_INVALID_THIS' // node compat.
57
- throw err
58
+ webidl.brandCheckMultiple = function (List) {
59
+ const prototypes = List.map((c) => webidl.util.MakeTypeAssertion(c))
60
+
61
+ return (V) => {
62
+ if (prototypes.every(typeCheck => !typeCheck(V))) {
63
+ const err = new TypeError('Illegal invocation')
64
+ err.code = 'ERR_INVALID_THIS' // node compat.
65
+ throw err
66
+ }
58
67
  }
59
68
  }
60
69
 
@@ -75,6 +84,10 @@ webidl.illegalConstructor = function () {
75
84
  })
76
85
  }
77
86
 
87
+ webidl.util.MakeTypeAssertion = function (I) {
88
+ return (O) => FunctionPrototypeSymbolHasInstance(I, O)
89
+ }
90
+
78
91
  // https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values
79
92
  webidl.util.Type = function (V) {
80
93
  switch (typeof V) {
@@ -119,6 +132,8 @@ webidl.util.TypeValueToString = function (o) {
119
132
  }
120
133
  }
121
134
 
135
+ webidl.util.markAsUncloneable = markAsUncloneable || (() => {})
136
+
122
137
  // https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
123
138
  webidl.util.ConvertToInt = function (V, bitLength, signedness, opts) {
124
139
  let upperBound
@@ -330,12 +345,14 @@ webidl.recordConverter = function (keyConverter, valueConverter) {
330
345
  const keys = [...Object.getOwnPropertyNames(O), ...Object.getOwnPropertySymbols(O)]
331
346
 
332
347
  for (const key of keys) {
348
+ const keyName = webidl.util.Stringify(key)
349
+
333
350
  // 1. Let typedKey be key converted to an IDL value of type K.
334
- const typedKey = keyConverter(key, prefix, argument)
351
+ const typedKey = keyConverter(key, prefix, `Key ${keyName} in ${argument}`)
335
352
 
336
353
  // 2. Let value be ? Get(O, key).
337
354
  // 3. Let typedValue be value converted to an IDL value of type V.
338
- const typedValue = valueConverter(O[key], prefix, argument)
355
+ const typedValue = valueConverter(O[key], prefix, `${argument}[${keyName}]`)
339
356
 
340
357
  // 4. Set result[typedKey] to typedValue.
341
358
  result[typedKey] = typedValue
@@ -372,12 +389,12 @@ webidl.recordConverter = function (keyConverter, valueConverter) {
372
389
  }
373
390
  }
374
391
 
375
- webidl.interfaceConverter = function (i) {
392
+ webidl.interfaceConverter = function (TypeCheck, name) {
376
393
  return (V, prefix, argument) => {
377
- if (!(V instanceof i)) {
394
+ if (!TypeCheck(V)) {
378
395
  throw webidl.errors.exception({
379
396
  header: prefix,
380
- message: `Expected ${argument} ("${webidl.util.Stringify(V)}") to be an instance of ${i.name}.`
397
+ message: `Expected ${argument} ("${webidl.util.Stringify(V)}") to be an instance of ${name}.`
381
398
  })
382
399
  }
383
400
 
@@ -451,6 +468,14 @@ webidl.nullableConverter = function (converter) {
451
468
  }
452
469
  }
453
470
 
471
+ webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream)
472
+ webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob)
473
+ webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams)
474
+ webidl.is.File = webidl.util.MakeTypeAssertion(globalThis.File ?? require('node:buffer').File)
475
+ webidl.is.URL = webidl.util.MakeTypeAssertion(URL)
476
+ webidl.is.AbortSignal = webidl.util.MakeTypeAssertion(AbortSignal)
477
+ webidl.is.MessagePort = webidl.util.MakeTypeAssertion(MessagePort)
478
+
454
479
  // https://webidl.spec.whatwg.org/#es-DOMString
455
480
  webidl.converters.DOMString = function (V, prefix, argument, opts) {
456
481
  // 1. If V is null and the conversion is to an IDL type
@@ -478,8 +503,14 @@ webidl.converters.DOMString = function (V, prefix, argument, opts) {
478
503
  // https://webidl.spec.whatwg.org/#es-ByteString
479
504
  webidl.converters.ByteString = function (V, prefix, argument) {
480
505
  // 1. Let x be ? ToString(V).
481
- // Note: DOMString converter perform ? ToString(V)
482
- const x = webidl.converters.DOMString(V, prefix, argument)
506
+ if (typeof V === 'symbol') {
507
+ throw webidl.errors.exception({
508
+ header: prefix,
509
+ message: `${argument} is a symbol, which cannot be converted to a ByteString.`
510
+ })
511
+ }
512
+
513
+ const x = String(V)
483
514
 
484
515
  // 2. If the value of any element of x is greater than
485
516
  // 255, then throw a TypeError.
@@ -697,7 +728,12 @@ webidl.converters['record<ByteString, ByteString>'] = webidl.recordConverter(
697
728
  webidl.converters.ByteString
698
729
  )
699
730
 
700
- webidl.converters.Blob = webidl.interfaceConverter(Blob)
731
+ webidl.converters.Blob = webidl.interfaceConverter(webidl.is.Blob, 'Blob')
732
+
733
+ webidl.converters.AbortSignal = webidl.interfaceConverter(
734
+ webidl.is.AbortSignal,
735
+ 'AbortSignal'
736
+ )
701
737
 
702
738
  module.exports = {
703
739
  webidl
@@ -1,12 +1,14 @@
1
1
  'use strict'
2
2
 
3
- const { uid } = require('./constants')
4
- const { failWebsocketConnection, parseExtensions } = require('./util')
3
+ const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants')
4
+ const { failWebsocketConnection, parseExtensions, isClosed, isClosing, isEstablished, validateCloseCodeAndReason } = require('./util')
5
5
  const { channels } = require('../../core/diagnostics')
6
6
  const { makeRequest } = require('../fetch/request')
7
7
  const { fetching } = require('../fetch/index')
8
8
  const { Headers, getHeadersList } = require('../fetch/headers')
9
9
  const { getDecodeSplit } = require('../fetch/util')
10
+ const { WebsocketFrameSend } = require('./frame')
11
+ const assert = require('node:assert')
10
12
 
11
13
  /** @type {import('crypto')} */
12
14
  let crypto
@@ -94,10 +96,16 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
94
96
  useParallelQueue: true,
95
97
  dispatcher: options.dispatcher,
96
98
  processResponse (response) {
99
+ if (response.type === 'error') {
100
+ // If the WebSocket connection could not be established, it is also said
101
+ // that _The WebSocket Connection is Closed_, but not _cleanly_.
102
+ handler.readyState = states.CLOSED
103
+ }
104
+
97
105
  // 1. If response is a network error or its status is not 101,
98
106
  // fail the WebSocket connection.
99
107
  if (response.type === 'error' || response.status !== 101) {
100
- failWebsocketConnection(handler, 'Received network error or non-101 status code.')
108
+ failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.')
101
109
  return
102
110
  }
103
111
 
@@ -106,7 +114,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
106
114
  // header list results in null, failure, or the empty byte
107
115
  // sequence, then fail the WebSocket connection.
108
116
  if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) {
109
- failWebsocketConnection(handler, 'Server did not respond with sent protocols.')
117
+ failWebsocketConnection(handler, 1002, 'Server did not respond with sent protocols.')
110
118
  return
111
119
  }
112
120
 
@@ -121,7 +129,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
121
129
  // insensitive match for the value "websocket", the client MUST
122
130
  // _Fail the WebSocket Connection_.
123
131
  if (response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') {
124
- failWebsocketConnection(handler, 'Server did not set Upgrade header to "websocket".')
132
+ failWebsocketConnection(handler, 1002, 'Server did not set Upgrade header to "websocket".')
125
133
  return
126
134
  }
127
135
 
@@ -130,7 +138,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
130
138
  // ASCII case-insensitive match for the value "Upgrade", the client
131
139
  // MUST _Fail the WebSocket Connection_.
132
140
  if (response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') {
133
- failWebsocketConnection(handler, 'Server did not set Connection header to "upgrade".')
141
+ failWebsocketConnection(handler, 1002, 'Server did not set Connection header to "upgrade".')
134
142
  return
135
143
  }
136
144
 
@@ -144,7 +152,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
144
152
  const secWSAccept = response.headersList.get('Sec-WebSocket-Accept')
145
153
  const digest = crypto.createHash('sha1').update(keyValue + uid).digest('base64')
146
154
  if (secWSAccept !== digest) {
147
- failWebsocketConnection(handler, 'Incorrect hash received in Sec-WebSocket-Accept header.')
155
+ failWebsocketConnection(handler, 1002, 'Incorrect hash received in Sec-WebSocket-Accept header.')
148
156
  return
149
157
  }
150
158
 
@@ -162,7 +170,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
162
170
  extensions = parseExtensions(secExtension)
163
171
 
164
172
  if (!extensions.has('permessage-deflate')) {
165
- failWebsocketConnection(handler, 'Sec-WebSocket-Extensions header does not match.')
173
+ failWebsocketConnection(handler, 1002, 'Sec-WebSocket-Extensions header does not match.')
166
174
  return
167
175
  }
168
176
  }
@@ -183,7 +191,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
183
191
  // the selected subprotocol values in its response for the connection to
184
192
  // be established.
185
193
  if (!requestProtocols.includes(secProtocol)) {
186
- failWebsocketConnection(handler, 'Protocol was not set in the opening handshake.')
194
+ failWebsocketConnection(handler, 1002, 'Protocol was not set in the opening handshake.')
187
195
  return
188
196
  }
189
197
  }
@@ -200,6 +208,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
200
208
  })
201
209
  }
202
210
 
211
+ handler.wasEverConnected = true
203
212
  handler.onConnectionEstablished(response, extensions)
204
213
  }
205
214
  })
@@ -208,13 +217,81 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
208
217
  }
209
218
 
210
219
  /**
211
- * @param {import('./websocket').Handler} handler
212
- * @param {number} code
213
- * @param {any} reason
214
- * @param {number} reasonByteLength
220
+ * @see https://whatpr.org/websockets/48.html#close-the-websocket
221
+ * @param {import('./websocket').Handler} object
222
+ * @param {number} [code=null]
223
+ * @param {string} [reason='']
215
224
  */
216
- function closeWebSocketConnection (handler, code, reason, reasonByteLength) {
217
- handler.onClose(code, reason, reasonByteLength)
225
+ function closeWebSocketConnection (object, code, reason, validate = false) {
226
+ // 1. If code was not supplied, let code be null.
227
+ code ??= null
228
+
229
+ // 2. If reason was not supplied, let reason be the empty string.
230
+ reason ??= ''
231
+
232
+ // 3. Validate close code and reason with code and reason.
233
+ if (validate) validateCloseCodeAndReason(code, reason)
234
+
235
+ // 4. Run the first matching steps from the following list:
236
+ // - If object’s ready state is CLOSING (2) or CLOSED (3)
237
+ // - If the WebSocket connection is not yet established [WSP]
238
+ // - If the WebSocket closing handshake has not yet been started [WSP]
239
+ // - Otherwise
240
+ if (isClosed(object.readyState) || isClosing(object.readyState)) {
241
+ // Do nothing.
242
+ } else if (!isEstablished(object.readyState)) {
243
+ // Fail the WebSocket connection and set object’s ready state to CLOSING (2). [WSP]
244
+ failWebsocketConnection(object)
245
+ object.readyState = states.CLOSING
246
+ } else if (!object.closeState.has(sentCloseFrameState.SENT) && !object.closeState.has(sentCloseFrameState.RECEIVED)) {
247
+ // Upon either sending or receiving a Close control frame, it is said
248
+ // that _The WebSocket Closing Handshake is Started_ and that the
249
+ // WebSocket connection is in the CLOSING state.
250
+
251
+ const frame = new WebsocketFrameSend()
252
+
253
+ // If neither code nor reason is present, the WebSocket Close
254
+ // message must not have a body.
255
+
256
+ // If code is present, then the status code to use in the
257
+ // WebSocket Close message must be the integer given by code.
258
+ // If code is null and reason is the empty string, the WebSocket Close frame must not have a body.
259
+ // If reason is non-empty but code is null, then set code to 1000 ("Normal Closure").
260
+ if (reason.length !== 0 && code === null) {
261
+ code = 1000
262
+ }
263
+
264
+ // If code is set, then the status code to use in the WebSocket Close frame must be the integer given by code.
265
+ assert(code === null || Number.isInteger(code))
266
+
267
+ if (code === null && reason.length === 0) {
268
+ frame.frameData = emptyBuffer
269
+ } else if (code !== null && reason === null) {
270
+ frame.frameData = Buffer.allocUnsafe(2)
271
+ frame.frameData.writeUInt16BE(code, 0)
272
+ } else if (code !== null && reason !== null) {
273
+ // If reason is also present, then reasonBytes must be
274
+ // provided in the Close message after the status code.
275
+ frame.frameData = Buffer.allocUnsafe(2 + Buffer.byteLength(reason))
276
+ frame.frameData.writeUInt16BE(code, 0)
277
+ // the body MAY contain UTF-8-encoded data with value /reason/
278
+ frame.frameData.write(reason, 2, 'utf-8')
279
+ } else {
280
+ frame.frameData = emptyBuffer
281
+ }
282
+
283
+ object.socket.write(frame.createFrame(opcodes.CLOSE))
284
+
285
+ object.closeState.add(sentCloseFrameState.SENT)
286
+
287
+ // Upon either sending or receiving a Close control frame, it is said
288
+ // that _The WebSocket Closing Handshake is Started_ and that the
289
+ // WebSocket connection is in the CLOSING state.
290
+ object.readyState = states.CLOSING
291
+ } else {
292
+ // Set object’s ready state to CLOSING (2).
293
+ object.readyState = states.CLOSING
294
+ }
218
295
  }
219
296
 
220
297
  module.exports = {
@@ -1,18 +1,32 @@
1
1
  'use strict'
2
2
 
3
- // This is a Globally Unique Identifier unique used
4
- // to validate that the endpoint accepts websocket
5
- // connections.
6
- // See https://www.rfc-editor.org/rfc/rfc6455.html#section-1.3
3
+ /**
4
+ * This is a Globally Unique Identifier unique used to validate that the
5
+ * endpoint accepts websocket connections.
6
+ * @see https://www.rfc-editor.org/rfc/rfc6455.html#section-1.3
7
+ * @type {'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'}
8
+ */
7
9
  const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
8
10
 
9
- /** @type {PropertyDescriptor} */
11
+ /**
12
+ * @type {PropertyDescriptor}
13
+ */
10
14
  const staticPropertyDescriptors = {
11
15
  enumerable: true,
12
16
  writable: false,
13
17
  configurable: false
14
18
  }
15
19
 
20
+ /**
21
+ * The states of the WebSocket connection.
22
+ *
23
+ * @readonly
24
+ * @enum
25
+ * @property {0} CONNECTING
26
+ * @property {1} OPEN
27
+ * @property {2} CLOSING
28
+ * @property {3} CLOSED
29
+ */
16
30
  const states = {
17
31
  CONNECTING: 0,
18
32
  OPEN: 1,
@@ -20,12 +34,31 @@ const states = {
20
34
  CLOSED: 3
21
35
  }
22
36
 
37
+ /**
38
+ * @readonly
39
+ * @enum
40
+ * @property {0} NOT_SENT
41
+ * @property {1} PROCESSING
42
+ * @property {2} SENT
43
+ */
23
44
  const sentCloseFrameState = {
24
- NOT_SENT: 0,
25
- PROCESSING: 1,
26
- SENT: 2
45
+ SENT: 1,
46
+ RECEIVED: 2
27
47
  }
28
48
 
49
+ /**
50
+ * The WebSocket opcodes.
51
+ *
52
+ * @readonly
53
+ * @enum
54
+ * @property {0x0} CONTINUATION
55
+ * @property {0x1} TEXT
56
+ * @property {0x2} BINARY
57
+ * @property {0x8} CLOSE
58
+ * @property {0x9} PING
59
+ * @property {0xA} PONG
60
+ * @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
61
+ */
29
62
  const opcodes = {
30
63
  CONTINUATION: 0x0,
31
64
  TEXT: 0x1,
@@ -35,8 +68,23 @@ const opcodes = {
35
68
  PONG: 0xA
36
69
  }
37
70
 
38
- const maxUnsigned16Bit = 2 ** 16 - 1 // 65535
71
+ /**
72
+ * The maximum value for an unsigned 16-bit integer.
73
+ *
74
+ * @type {65535} 2 ** 16 - 1
75
+ */
76
+ const maxUnsigned16Bit = 65535
39
77
 
78
+ /**
79
+ * The states of the parser.
80
+ *
81
+ * @readonly
82
+ * @enum
83
+ * @property {0} INFO
84
+ * @property {2} PAYLOADLENGTH_16
85
+ * @property {3} PAYLOADLENGTH_64
86
+ * @property {4} READ_DATA
87
+ */
40
88
  const parserStates = {
41
89
  INFO: 0,
42
90
  PAYLOADLENGTH_16: 2,
@@ -44,8 +92,20 @@ const parserStates = {
44
92
  READ_DATA: 4
45
93
  }
46
94
 
95
+ /**
96
+ * An empty buffer.
97
+ *
98
+ * @type {Buffer}
99
+ */
47
100
  const emptyBuffer = Buffer.allocUnsafe(0)
48
101
 
102
+ /**
103
+ * @readonly
104
+ * @property {1} text
105
+ * @property {2} typedArray
106
+ * @property {3} arrayBuffer
107
+ * @property {4} blob
108
+ */
49
109
  const sendHints = {
50
110
  text: 1,
51
111
  typedArray: 2,
@@ -3,7 +3,6 @@
3
3
  const { webidl } = require('../fetch/webidl')
4
4
  const { kEnumerableProperty } = require('../../core/util')
5
5
  const { kConstruct } = require('../../core/symbols')
6
- const { MessagePort } = require('node:worker_threads')
7
6
 
8
7
  /**
9
8
  * @see https://html.spec.whatwg.org/multipage/comms.html#messageevent
@@ -14,6 +13,7 @@ class MessageEvent extends Event {
14
13
  constructor (type, eventInitDict = {}) {
15
14
  if (type === kConstruct) {
16
15
  super(arguments[1], arguments[2])
16
+ webidl.util.markAsUncloneable(this)
17
17
  return
18
18
  }
19
19
 
@@ -26,6 +26,7 @@ class MessageEvent extends Event {
26
26
  super(type, eventInitDict)
27
27
 
28
28
  this.#eventInit = eventInitDict
29
+ webidl.util.markAsUncloneable(this)
29
30
  }
30
31
 
31
32
  get data () {
@@ -112,6 +113,7 @@ class CloseEvent extends Event {
112
113
  super(type, eventInitDict)
113
114
 
114
115
  this.#eventInit = eventInitDict
116
+ webidl.util.markAsUncloneable(this)
115
117
  }
116
118
 
117
119
  get wasClean () {
@@ -142,6 +144,7 @@ class ErrorEvent extends Event {
142
144
  webidl.argumentLengthCheck(arguments, 1, prefix)
143
145
 
144
146
  super(type, eventInitDict)
147
+ webidl.util.markAsUncloneable(this)
145
148
 
146
149
  type = webidl.converters.DOMString(type, prefix, 'type')
147
150
  eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {})
@@ -215,7 +218,10 @@ Object.defineProperties(ErrorEvent.prototype, {
215
218
  error: kEnumerableProperty
216
219
  })
217
220
 
218
- webidl.converters.MessagePort = webidl.interfaceConverter(MessagePort)
221
+ webidl.converters.MessagePort = webidl.interfaceConverter(
222
+ webidl.is.MessagePort,
223
+ 'MessagePort'
224
+ )
219
225
 
220
226
  webidl.converters['sequence<MessagePort>'] = webidl.sequenceConverter(
221
227
  webidl.converters.MessagePort
@@ -15,7 +15,6 @@ const {
15
15
  isContinuationFrame
16
16
  } = require('./util')
17
17
  const { WebsocketFrameSend } = require('./frame')
18
- const { closeWebSocketConnection } = require('./connection')
19
18
  const { PerMessageDeflate } = require('./permessage-deflate')
20
19
 
21
20
  // This code was influenced by ws released under the MIT license.
@@ -88,12 +87,12 @@ class ByteParser extends Writable {
88
87
  const rsv3 = buffer[0] & 0x10
89
88
 
90
89
  if (!isValidOpcode(opcode)) {
91
- failWebsocketConnection(this.#handler, 'Invalid opcode received')
90
+ failWebsocketConnection(this.#handler, 1002, 'Invalid opcode received')
92
91
  return callback()
93
92
  }
94
93
 
95
94
  if (masked) {
96
- failWebsocketConnection(this.#handler, 'Frame cannot be masked')
95
+ failWebsocketConnection(this.#handler, 1002, 'Frame cannot be masked')
97
96
  return callback()
98
97
  }
99
98
 
@@ -107,43 +106,43 @@ class ByteParser extends Writable {
107
106
  // WebSocket connection where a PMCE is in use, this bit indicates
108
107
  // whether a message is compressed or not.
109
108
  if (rsv1 !== 0 && !this.#extensions.has('permessage-deflate')) {
110
- failWebsocketConnection(this.#handler, 'Expected RSV1 to be clear.')
109
+ failWebsocketConnection(this.#handler, 1002, 'Expected RSV1 to be clear.')
111
110
  return
112
111
  }
113
112
 
114
113
  if (rsv2 !== 0 || rsv3 !== 0) {
115
- failWebsocketConnection(this.#handler, 'RSV1, RSV2, RSV3 must be clear')
114
+ failWebsocketConnection(this.#handler, 1002, 'RSV1, RSV2, RSV3 must be clear')
116
115
  return
117
116
  }
118
117
 
119
118
  if (fragmented && !isTextBinaryFrame(opcode)) {
120
119
  // Only text and binary frames can be fragmented
121
- failWebsocketConnection(this.#handler, 'Invalid frame type was fragmented.')
120
+ failWebsocketConnection(this.#handler, 1002, 'Invalid frame type was fragmented.')
122
121
  return
123
122
  }
124
123
 
125
124
  // If we are already parsing a text/binary frame and do not receive either
126
125
  // a continuation frame or close frame, fail the connection.
127
126
  if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) {
128
- failWebsocketConnection(this.#handler, 'Expected continuation frame')
127
+ failWebsocketConnection(this.#handler, 1002, 'Expected continuation frame')
129
128
  return
130
129
  }
131
130
 
132
131
  if (this.#info.fragmented && fragmented) {
133
132
  // A fragmented frame can't be fragmented itself
134
- failWebsocketConnection(this.#handler, 'Fragmented frame exceeded 125 bytes.')
133
+ failWebsocketConnection(this.#handler, 1002, 'Fragmented frame exceeded 125 bytes.')
135
134
  return
136
135
  }
137
136
 
138
137
  // "All control frames MUST have a payload length of 125 bytes or less
139
138
  // and MUST NOT be fragmented."
140
139
  if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) {
141
- failWebsocketConnection(this.#handler, 'Control frame either too large or fragmented')
140
+ failWebsocketConnection(this.#handler, 1002, 'Control frame either too large or fragmented')
142
141
  return
143
142
  }
144
143
 
145
144
  if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) {
146
- failWebsocketConnection(this.#handler, 'Unexpected continuation frame')
145
+ failWebsocketConnection(this.#handler, 1002, 'Unexpected continuation frame')
147
146
  return
148
147
  }
149
148
 
@@ -189,7 +188,7 @@ class ByteParser extends Writable {
189
188
  // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
190
189
  // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
191
190
  if (upper > 2 ** 31 - 1) {
192
- failWebsocketConnection(this.#handler, 'Received payload length > 2^31 bytes.')
191
+ failWebsocketConnection(this.#handler, 1009, 'Received payload length > 2^31 bytes.')
193
192
  return
194
193
  }
195
194
 
@@ -225,7 +224,7 @@ class ByteParser extends Writable {
225
224
  } else {
226
225
  this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
227
226
  if (error) {
228
- closeWebSocketConnection(this.#handler, 1007, error.message, error.message.length)
227
+ failWebsocketConnection(this.#handler, 1007, error.message)
229
228
  return
230
229
  }
231
230
 
@@ -341,7 +340,7 @@ class ByteParser extends Writable {
341
340
 
342
341
  if (opcode === opcodes.CLOSE) {
343
342
  if (payloadLength === 1) {
344
- failWebsocketConnection(this.#handler, 'Received close frame with a 1-byte body.')
343
+ failWebsocketConnection(this.#handler, 1002, 'Received close frame with a 1-byte body.')
345
344
  return false
346
345
  }
347
346
 
@@ -350,12 +349,13 @@ class ByteParser extends Writable {
350
349
  if (this.#info.closeInfo.error) {
351
350
  const { code, reason } = this.#info.closeInfo
352
351
 
353
- closeWebSocketConnection(this.#handler, code, reason, reason.length)
354
- failWebsocketConnection(this.#handler, reason)
352
+ failWebsocketConnection(this.#handler, code, reason)
355
353
  return false
356
354
  }
357
355
 
358
- if (this.#handler.closeState !== sentCloseFrameState.SENT) {
356
+ // Upon receiving such a frame, the other peer sends a
357
+ // Close frame in response, if it hasn't already sent one.
358
+ if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
359
359
  // If an endpoint receives a Close frame and did not previously send a
360
360
  // Close frame, the endpoint MUST send a Close frame in response. (When
361
361
  // sending a Close frame in response, the endpoint typically echos the
@@ -367,21 +367,15 @@ class ByteParser extends Writable {
367
367
  }
368
368
  const closeFrame = new WebsocketFrameSend(body)
369
369
 
370
- this.#handler.socket.write(
371
- closeFrame.createFrame(opcodes.CLOSE),
372
- (err) => {
373
- if (!err) {
374
- this.#handler.closeState = sentCloseFrameState.SENT
375
- }
376
- }
377
- )
370
+ this.#handler.socket.write(closeFrame.createFrame(opcodes.CLOSE))
371
+ this.#handler.closeState.add(sentCloseFrameState.SENT)
378
372
  }
379
373
 
380
374
  // Upon either sending or receiving a Close control frame, it is said
381
375
  // that _The WebSocket Closing Handshake is Started_ and that the
382
376
  // WebSocket connection is in the CLOSING state.
383
377
  this.#handler.readyState = states.CLOSING
384
- this.#handler.receivedClose = true
378
+ this.#handler.closeState.add(sentCloseFrameState.RECEIVED)
385
379
 
386
380
  return false
387
381
  } else if (opcode === opcodes.PING) {
@@ -390,7 +384,7 @@ class ByteParser extends Writable {
390
384
  // A Pong frame sent in response to a Ping frame must have identical
391
385
  // "Application data"
392
386
 
393
- if (!this.#handler.receivedClose) {
387
+ if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
394
388
  const frame = new WebsocketFrameSend(body)
395
389
 
396
390
  this.#handler.socket.write(frame.createFrame(opcodes.PONG))