undici 6.17.0 → 6.18.1

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 CHANGED
@@ -123,7 +123,7 @@ This section documents our most commonly used API methods. Additional APIs are d
123
123
  Arguments:
124
124
 
125
125
  * **url** `string | URL | UrlObject`
126
- * **options** [`RequestOptions`](./docs/api/Dispatcher.md#parameter-requestoptions)
126
+ * **options** [`RequestOptions`](./docs/docs/api/Dispatcher.md#parameter-requestoptions)
127
127
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
128
128
  * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
129
129
  * **maxRedirections** `Integer` - Default: `0`
@@ -132,14 +132,14 @@ Returns a promise with the result of the `Dispatcher.request` method.
132
132
 
133
133
  Calls `options.dispatcher.request(options)`.
134
134
 
135
- See [Dispatcher.request](./docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./examples/README.md) for examples.
135
+ See [Dispatcher.request](./docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./examples/README.md) for examples.
136
136
 
137
137
  ### `undici.stream([url, options, ]factory): Promise`
138
138
 
139
139
  Arguments:
140
140
 
141
141
  * **url** `string | URL | UrlObject`
142
- * **options** [`StreamOptions`](./docs/api/Dispatcher.md#parameter-streamoptions)
142
+ * **options** [`StreamOptions`](./docs/docs/api/Dispatcher.md#parameter-streamoptions)
143
143
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
144
144
  * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
145
145
  * **maxRedirections** `Integer` - Default: `0`
@@ -149,14 +149,14 @@ Returns a promise with the result of the `Dispatcher.stream` method.
149
149
 
150
150
  Calls `options.dispatcher.stream(options, factory)`.
151
151
 
152
- See [Dispatcher.stream](./docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details.
152
+ See [Dispatcher.stream](./docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details.
153
153
 
154
154
  ### `undici.pipeline([url, options, ]handler): Duplex`
155
155
 
156
156
  Arguments:
157
157
 
158
158
  * **url** `string | URL | UrlObject`
159
- * **options** [`PipelineOptions`](./docs/api/Dispatcher.md#parameter-pipelineoptions)
159
+ * **options** [`PipelineOptions`](./docs/docs/api/Dispatcher.md#parameter-pipelineoptions)
160
160
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
161
161
  * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
162
162
  * **maxRedirections** `Integer` - Default: `0`
@@ -166,7 +166,7 @@ Returns: `stream.Duplex`
166
166
 
167
167
  Calls `options.dispatch.pipeline(options, handler)`.
168
168
 
169
- See [Dispatcher.pipeline](./docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details.
169
+ See [Dispatcher.pipeline](./docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details.
170
170
 
171
171
  ### `undici.connect([url, options]): Promise`
172
172
 
@@ -175,7 +175,7 @@ Starts two-way communications with the requested resource using [HTTP CONNECT](h
175
175
  Arguments:
176
176
 
177
177
  * **url** `string | URL | UrlObject`
178
- * **options** [`ConnectOptions`](./docs/api/Dispatcher.md#parameter-connectoptions)
178
+ * **options** [`ConnectOptions`](./docs/docs/api/Dispatcher.md#parameter-connectoptions)
179
179
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
180
180
  * **maxRedirections** `Integer` - Default: `0`
181
181
  * **callback** `(err: Error | null, data: ConnectData | null) => void` (optional)
@@ -184,7 +184,7 @@ Returns a promise with the result of the `Dispatcher.connect` method.
184
184
 
185
185
  Calls `options.dispatch.connect(options)`.
186
186
 
187
- See [Dispatcher.connect](./docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details.
187
+ See [Dispatcher.connect](./docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details.
188
188
 
189
189
  ### `undici.fetch(input[, init]): Promise`
190
190
 
@@ -335,7 +335,7 @@ Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](h
335
335
  Arguments:
336
336
 
337
337
  * **url** `string | URL | UrlObject`
338
- * **options** [`UpgradeOptions`](./docs/api/Dispatcher.md#parameter-upgradeoptions)
338
+ * **options** [`UpgradeOptions`](./docs/docs/api/Dispatcher.md#parameter-upgradeoptions)
339
339
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
340
340
  * **maxRedirections** `Integer` - Default: `0`
341
341
  * **callback** `(error: Error | null, data: UpgradeData) => void` (optional)
@@ -344,7 +344,7 @@ Returns a promise with the result of the `Dispatcher.upgrade` method.
344
344
 
345
345
  Calls `options.dispatcher.upgrade(options)`.
346
346
 
347
- See [Dispatcher.upgrade](./docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details.
347
+ See [Dispatcher.upgrade](./docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details.
348
348
 
349
349
  ### `undici.setGlobalDispatcher(dispatcher)`
350
350
 
@@ -737,6 +737,7 @@ module.exports = {
737
737
  collectAnHTTPQuotedString,
738
738
  serializeAMimeType,
739
739
  removeChars,
740
+ removeHTTPWhitespace,
740
741
  minimizeSupportedMimeType,
741
742
  HTTP_TOKEN_CODEPOINTS,
742
743
  isomorphicDecode
@@ -626,10 +626,6 @@ Reflect.deleteProperty(Headers, 'setHeadersGuard')
626
626
  Reflect.deleteProperty(Headers, 'getHeadersList')
627
627
  Reflect.deleteProperty(Headers, 'setHeadersList')
628
628
 
629
- Object.defineProperty(Headers.prototype, util.inspect.custom, {
630
- enumerable: false
631
- })
632
-
633
629
  iteratorMixin('Headers', Headers, kHeadersSortedMap, 0, 1)
634
630
 
635
631
  Object.defineProperties(Headers.prototype, {
@@ -642,6 +638,17 @@ Object.defineProperties(Headers.prototype, {
642
638
  [Symbol.toStringTag]: {
643
639
  value: 'Headers',
644
640
  configurable: true
641
+ },
642
+ [util.inspect.custom]: {
643
+ enumerable: false
644
+ },
645
+ // Compatibility for global headers
646
+ [Symbol('headers list')]: {
647
+ configurable: false,
648
+ enumerable: false,
649
+ get: function () {
650
+ return getHeadersList(this)
651
+ }
645
652
  }
646
653
  })
647
654
 
@@ -8,7 +8,7 @@ const {
8
8
  kReceivedClose,
9
9
  kResponse
10
10
  } = require('./symbols')
11
- const { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished } = require('./util')
11
+ const { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished, parseExtensions } = require('./util')
12
12
  const { channels } = require('../../core/diagnostics')
13
13
  const { CloseEvent } = require('./events')
14
14
  const { makeRequest } = require('../fetch/request')
@@ -31,7 +31,7 @@ try {
31
31
  * @param {URL} url
32
32
  * @param {string|string[]} protocols
33
33
  * @param {import('./websocket').WebSocket} ws
34
- * @param {(response: any) => void} onEstablish
34
+ * @param {(response: any, extensions: string[] | undefined) => void} onEstablish
35
35
  * @param {Partial<import('../../types/websocket').WebSocketInit>} options
36
36
  */
37
37
  function establishWebSocketConnection (url, protocols, client, ws, onEstablish, options) {
@@ -91,12 +91,11 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
91
91
  // 9. Let permessageDeflate be a user-agent defined
92
92
  // "permessage-deflate" extension header value.
93
93
  // https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673
94
- // TODO: enable once permessage-deflate is supported
95
- const permessageDeflate = '' // 'permessage-deflate; 15'
94
+ const permessageDeflate = 'permessage-deflate; client_max_window_bits'
96
95
 
97
96
  // 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to
98
97
  // request’s header list.
99
- // request.headersList.append('sec-websocket-extensions', permessageDeflate)
98
+ request.headersList.append('sec-websocket-extensions', permessageDeflate)
100
99
 
101
100
  // 11. Fetch request with useParallelQueue set to true, and
102
101
  // processResponse given response being these steps:
@@ -167,10 +166,15 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
167
166
  // header field to determine which extensions are requested is
168
167
  // discussed in Section 9.1.)
169
168
  const secExtension = response.headersList.get('Sec-WebSocket-Extensions')
169
+ let extensions
170
170
 
171
- if (secExtension !== null && secExtension !== permessageDeflate) {
172
- failWebsocketConnection(ws, 'Received different permessage-deflate than the one set.')
173
- return
171
+ if (secExtension !== null) {
172
+ extensions = parseExtensions(secExtension)
173
+
174
+ if (!extensions.has('permessage-deflate')) {
175
+ failWebsocketConnection(ws, 'Sec-WebSocket-Extensions header does not match.')
176
+ return
177
+ }
174
178
  }
175
179
 
176
180
  // 6. If the response includes a |Sec-WebSocket-Protocol| header field
@@ -206,7 +210,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
206
210
  })
207
211
  }
208
212
 
209
- onEstablish(response)
213
+ onEstablish(response, extensions)
210
214
  }
211
215
  })
212
216
 
@@ -290,6 +294,11 @@ function onSocketData (chunk) {
290
294
  */
291
295
  function onSocketClose () {
292
296
  const { ws } = this
297
+ const { [kResponse]: response } = ws
298
+
299
+ response.socket.off('data', onSocketData)
300
+ response.socket.off('close', onSocketClose)
301
+ response.socket.off('error', onSocketError)
293
302
 
294
303
  // If the TCP connection was closed after the
295
304
  // WebSocket closing handshake was completed, the WebSocket connection
@@ -46,6 +46,13 @@ const parserStates = {
46
46
 
47
47
  const emptyBuffer = Buffer.allocUnsafe(0)
48
48
 
49
+ const sendHints = {
50
+ string: 1,
51
+ typedArray: 2,
52
+ arrayBuffer: 3,
53
+ blob: 4
54
+ }
55
+
49
56
  module.exports = {
50
57
  uid,
51
58
  sentCloseFrameState,
@@ -54,5 +61,6 @@ module.exports = {
54
61
  opcodes,
55
62
  maxUnsigned16Bit,
56
63
  parserStates,
57
- emptyBuffer
64
+ emptyBuffer,
65
+ sendHints
58
66
  }
@@ -0,0 +1,70 @@
1
+ 'use strict'
2
+
3
+ const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib')
4
+ const { isValidClientWindowBits } = require('./util')
5
+
6
+ const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
7
+ const kBuffer = Symbol('kBuffer')
8
+ const kLength = Symbol('kLength')
9
+
10
+ class PerMessageDeflate {
11
+ /** @type {import('node:zlib').InflateRaw} */
12
+ #inflate
13
+
14
+ #options = {}
15
+
16
+ constructor (extensions) {
17
+ this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
18
+ this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
19
+ }
20
+
21
+ decompress (chunk, fin, callback) {
22
+ // An endpoint uses the following algorithm to decompress a message.
23
+ // 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
24
+ // payload of the message.
25
+ // 2. Decompress the resulting data using DEFLATE.
26
+
27
+ if (!this.#inflate) {
28
+ let windowBits = Z_DEFAULT_WINDOWBITS
29
+
30
+ if (this.#options.serverMaxWindowBits) { // empty values default to Z_DEFAULT_WINDOWBITS
31
+ if (!isValidClientWindowBits(this.#options.serverMaxWindowBits)) {
32
+ callback(new Error('Invalid server_max_window_bits'))
33
+ return
34
+ }
35
+
36
+ windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
37
+ }
38
+
39
+ this.#inflate = createInflateRaw({ windowBits })
40
+ this.#inflate[kBuffer] = []
41
+ this.#inflate[kLength] = 0
42
+
43
+ this.#inflate.on('data', (data) => {
44
+ this.#inflate[kBuffer].push(data)
45
+ this.#inflate[kLength] += data.length
46
+ })
47
+
48
+ this.#inflate.on('error', (err) => {
49
+ this.#inflate = null
50
+ callback(err)
51
+ })
52
+ }
53
+
54
+ this.#inflate.write(chunk)
55
+ if (fin) {
56
+ this.#inflate.write(tail)
57
+ }
58
+
59
+ this.#inflate.flush(() => {
60
+ const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
61
+
62
+ this.#inflate[kBuffer].length = 0
63
+ this.#inflate[kLength] = 0
64
+
65
+ callback(null, full)
66
+ })
67
+ }
68
+ }
69
+
70
+ module.exports = { PerMessageDeflate }
@@ -17,6 +17,7 @@ const {
17
17
  } = require('./util')
18
18
  const { WebsocketFrameSend } = require('./frame')
19
19
  const { closeWebSocketConnection } = require('./connection')
20
+ const { PerMessageDeflate } = require('./permessage-deflate')
20
21
 
21
22
  // This code was influenced by ws released under the MIT license.
22
23
  // Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
@@ -33,10 +34,18 @@ class ByteParser extends Writable {
33
34
  #info = {}
34
35
  #fragments = []
35
36
 
36
- constructor (ws) {
37
+ /** @type {Map<string, PerMessageDeflate>} */
38
+ #extensions
39
+
40
+ constructor (ws, extensions) {
37
41
  super()
38
42
 
39
43
  this.ws = ws
44
+ this.#extensions = extensions == null ? new Map() : extensions
45
+
46
+ if (this.#extensions.has('permessage-deflate')) {
47
+ this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
48
+ }
40
49
  }
41
50
 
42
51
  /**
@@ -91,7 +100,16 @@ class ByteParser extends Writable {
91
100
  // the negotiated extensions defines the meaning of such a nonzero
92
101
  // value, the receiving endpoint MUST _Fail the WebSocket
93
102
  // Connection_.
94
- if (rsv1 !== 0 || rsv2 !== 0 || rsv3 !== 0) {
103
+ // This document allocates the RSV1 bit of the WebSocket header for
104
+ // PMCEs and calls the bit the "Per-Message Compressed" bit. On a
105
+ // WebSocket connection where a PMCE is in use, this bit indicates
106
+ // whether a message is compressed or not.
107
+ if (rsv1 !== 0 && !this.#extensions.has('permessage-deflate')) {
108
+ failWebsocketConnection(this.ws, 'Expected RSV1 to be clear.')
109
+ return
110
+ }
111
+
112
+ if (rsv2 !== 0 || rsv3 !== 0) {
95
113
  failWebsocketConnection(this.ws, 'RSV1, RSV2, RSV3 must be clear')
96
114
  return
97
115
  }
@@ -122,7 +140,7 @@ class ByteParser extends Writable {
122
140
  return
123
141
  }
124
142
 
125
- if (isContinuationFrame(opcode) && this.#fragments.length === 0) {
143
+ if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) {
126
144
  failWebsocketConnection(this.ws, 'Unexpected continuation frame')
127
145
  return
128
146
  }
@@ -138,6 +156,7 @@ class ByteParser extends Writable {
138
156
 
139
157
  if (isTextBinaryFrame(opcode)) {
140
158
  this.#info.binaryType = opcode
159
+ this.#info.compressed = rsv1 !== 0
141
160
  }
142
161
 
143
162
  this.#info.opcode = opcode
@@ -185,21 +204,50 @@ class ByteParser extends Writable {
185
204
 
186
205
  if (isControlFrame(this.#info.opcode)) {
187
206
  this.#loop = this.parseControlFrame(body)
207
+ this.#state = parserStates.INFO
188
208
  } else {
189
- this.#fragments.push(body)
190
-
191
- // If the frame is not fragmented, a message has been received.
192
- // If the frame is fragmented, it will terminate with a fin bit set
193
- // and an opcode of 0 (continuation), therefore we handle that when
194
- // parsing continuation frames, not here.
195
- if (!this.#info.fragmented && this.#info.fin) {
196
- const fullMessage = Buffer.concat(this.#fragments)
197
- websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
198
- this.#fragments.length = 0
209
+ if (!this.#info.compressed) {
210
+ this.#fragments.push(body)
211
+
212
+ // If the frame is not fragmented, a message has been received.
213
+ // If the frame is fragmented, it will terminate with a fin bit set
214
+ // and an opcode of 0 (continuation), therefore we handle that when
215
+ // parsing continuation frames, not here.
216
+ if (!this.#info.fragmented && this.#info.fin) {
217
+ const fullMessage = Buffer.concat(this.#fragments)
218
+ websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
219
+ this.#fragments.length = 0
220
+ }
221
+
222
+ this.#state = parserStates.INFO
223
+ } else {
224
+ this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
225
+ if (error) {
226
+ closeWebSocketConnection(this.ws, 1007, error.message, error.message.length)
227
+ return
228
+ }
229
+
230
+ this.#fragments.push(data)
231
+
232
+ if (!this.#info.fin) {
233
+ this.#state = parserStates.INFO
234
+ this.#loop = true
235
+ this.run(callback)
236
+ return
237
+ }
238
+
239
+ websocketMessageReceived(this.ws, this.#info.binaryType, Buffer.concat(this.#fragments))
240
+
241
+ this.#loop = true
242
+ this.#state = parserStates.INFO
243
+ this.#fragments.length = 0
244
+ this.run(callback)
245
+ })
246
+
247
+ this.#loop = false
248
+ break
199
249
  }
200
250
  }
201
-
202
- this.#state = parserStates.INFO
203
251
  }
204
252
  }
205
253
  }
@@ -333,7 +381,6 @@ class ByteParser extends Writable {
333
381
  this.ws[kReadyState] = states.CLOSING
334
382
  this.ws[kReceivedClose] = true
335
383
 
336
- this.end()
337
384
  return false
338
385
  } else if (opcode === opcodes.PING) {
339
386
  // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
@@ -0,0 +1,104 @@
1
+ 'use strict'
2
+
3
+ const { WebsocketFrameSend } = require('./frame')
4
+ const { opcodes, sendHints } = require('./constants')
5
+ const FixedQueue = require('../../dispatcher/fixed-queue')
6
+
7
+ /** @type {typeof Uint8Array} */
8
+ const FastBuffer = Buffer[Symbol.species]
9
+
10
+ /**
11
+ * @typedef {object} SendQueueNode
12
+ * @property {Promise<void> | null} promise
13
+ * @property {((...args: any[]) => any)} callback
14
+ * @property {Buffer | null} frame
15
+ */
16
+
17
+ class SendQueue {
18
+ /**
19
+ * @type {FixedQueue}
20
+ */
21
+ #queue = new FixedQueue()
22
+
23
+ /**
24
+ * @type {boolean}
25
+ */
26
+ #running = false
27
+
28
+ /** @type {import('node:net').Socket} */
29
+ #socket
30
+
31
+ constructor (socket) {
32
+ this.#socket = socket
33
+ }
34
+
35
+ add (item, cb, hint) {
36
+ if (hint !== sendHints.blob) {
37
+ const frame = createFrame(item, hint)
38
+ if (!this.#running) {
39
+ // fast-path
40
+ this.#socket.write(frame, cb)
41
+ } else {
42
+ /** @type {SendQueueNode} */
43
+ const node = {
44
+ promise: null,
45
+ callback: cb,
46
+ frame
47
+ }
48
+ this.#queue.push(node)
49
+ }
50
+ return
51
+ }
52
+
53
+ /** @type {SendQueueNode} */
54
+ const node = {
55
+ promise: item.arrayBuffer().then((ab) => {
56
+ node.promise = null
57
+ node.frame = createFrame(ab, hint)
58
+ }),
59
+ callback: cb,
60
+ frame: null
61
+ }
62
+
63
+ this.#queue.push(node)
64
+
65
+ if (!this.#running) {
66
+ this.#run()
67
+ }
68
+ }
69
+
70
+ async #run () {
71
+ this.#running = true
72
+ const queue = this.#queue
73
+ while (!queue.isEmpty()) {
74
+ const node = queue.shift()
75
+ // wait pending promise
76
+ if (node.promise !== null) {
77
+ await node.promise
78
+ }
79
+ // write
80
+ this.#socket.write(node.frame, node.callback)
81
+ // cleanup
82
+ node.callback = node.frame = null
83
+ }
84
+ this.#running = false
85
+ }
86
+ }
87
+
88
+ function createFrame (data, hint) {
89
+ return new WebsocketFrameSend(toBuffer(data, hint)).createFrame(hint === sendHints.string ? opcodes.TEXT : opcodes.BINARY)
90
+ }
91
+
92
+ function toBuffer (data, hint) {
93
+ switch (hint) {
94
+ case sendHints.string:
95
+ return Buffer.from(data)
96
+ case sendHints.arrayBuffer:
97
+ case sendHints.blob:
98
+ return new FastBuffer(data)
99
+ case sendHints.typedArray:
100
+ return new FastBuffer(data.buffer, data.byteOffset, data.byteLength)
101
+ }
102
+ }
103
+
104
+ module.exports = { SendQueue }
@@ -4,6 +4,7 @@ const { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = requ
4
4
  const { states, opcodes } = require('./constants')
5
5
  const { ErrorEvent, createFastMessageEvent } = require('./events')
6
6
  const { isUtf8 } = require('node:buffer')
7
+ const { collectASequenceOfCodePointsFast, removeHTTPWhitespace } = require('../fetch/data-url')
7
8
 
8
9
  /* globals Blob */
9
10
 
@@ -234,6 +235,48 @@ function isValidOpcode (opcode) {
234
235
  return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
235
236
  }
236
237
 
238
+ /**
239
+ * Parses a Sec-WebSocket-Extensions header value.
240
+ * @param {string} extensions
241
+ * @returns {Map<string, string>}
242
+ */
243
+ // TODO(@Uzlopak, @KhafraDev): make compliant https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
244
+ function parseExtensions (extensions) {
245
+ const position = { position: 0 }
246
+ const extensionList = new Map()
247
+
248
+ while (position.position < extensions.length) {
249
+ const pair = collectASequenceOfCodePointsFast(';', extensions, position)
250
+ const [name, value = ''] = pair.split('=')
251
+
252
+ extensionList.set(
253
+ removeHTTPWhitespace(name, true, false),
254
+ removeHTTPWhitespace(value, false, true)
255
+ )
256
+
257
+ position.position++
258
+ }
259
+
260
+ return extensionList
261
+ }
262
+
263
+ /**
264
+ * @see https://www.rfc-editor.org/rfc/rfc7692#section-7.1.2.2
265
+ * @description "client-max-window-bits = 1*DIGIT"
266
+ * @param {string} value
267
+ */
268
+ function isValidClientWindowBits (value) {
269
+ for (let i = 0; i < value.length; i++) {
270
+ const byte = value.charCodeAt(i)
271
+
272
+ if (byte < 0x30 || byte > 0x39) {
273
+ return false
274
+ }
275
+ }
276
+
277
+ return true
278
+ }
279
+
237
280
  // https://nodejs.org/api/intl.html#detecting-internationalization-support
238
281
  const hasIntl = typeof process.versions.icu === 'string'
239
282
  const fatalDecoder = hasIntl ? new TextDecoder('utf-8', { fatal: true }) : undefined
@@ -265,5 +308,7 @@ module.exports = {
265
308
  isControlFrame,
266
309
  isContinuationFrame,
267
310
  isTextBinaryFrame,
268
- isValidOpcode
311
+ isValidOpcode,
312
+ parseExtensions,
313
+ isValidClientWindowBits
269
314
  }
@@ -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, opcodes } = require('./constants')
6
+ const { staticPropertyDescriptors, states, sentCloseFrameState, sendHints } = require('./constants')
7
7
  const {
8
8
  kWebSocketURL,
9
9
  kReadyState,
@@ -21,17 +21,15 @@ const {
21
21
  fireEvent
22
22
  } = require('./util')
23
23
  const { establishWebSocketConnection, closeWebSocketConnection } = require('./connection')
24
- const { WebsocketFrameSend } = require('./frame')
25
24
  const { ByteParser } = require('./receiver')
26
25
  const { kEnumerableProperty, isBlobLike } = require('../../core/util')
27
26
  const { getGlobalDispatcher } = require('../../global')
28
27
  const { types } = require('node:util')
29
28
  const { ErrorEvent, CloseEvent } = require('./events')
29
+ const { SendQueue } = require('./sender')
30
30
 
31
31
  let experimentalWarned = false
32
32
 
33
- const FastBuffer = Buffer[Symbol.species]
34
-
35
33
  // https://websockets.spec.whatwg.org/#interface-definition
36
34
  class WebSocket extends EventTarget {
37
35
  #events = {
@@ -45,6 +43,9 @@ class WebSocket extends EventTarget {
45
43
  #protocol = ''
46
44
  #extensions = ''
47
45
 
46
+ /** @type {SendQueue} */
47
+ #sendQueue
48
+
48
49
  /**
49
50
  * @param {string} url
50
51
  * @param {string|string[]} protocols
@@ -135,7 +136,7 @@ class WebSocket extends EventTarget {
135
136
  protocols,
136
137
  client,
137
138
  this,
138
- (response) => this.#onConnectionEstablished(response),
139
+ (response, extensions) => this.#onConnectionEstablished(response, extensions),
139
140
  options
140
141
  )
141
142
 
@@ -229,9 +230,6 @@ class WebSocket extends EventTarget {
229
230
  return
230
231
  }
231
232
 
232
- /** @type {import('stream').Duplex} */
233
- const socket = this[kResponse].socket
234
-
235
233
  // If data is a string
236
234
  if (typeof data === 'string') {
237
235
  // If the WebSocket connection is established and the WebSocket
@@ -245,14 +243,12 @@ class WebSocket extends EventTarget {
245
243
  // the bufferedAmount attribute by the number of bytes needed to
246
244
  // express the argument as UTF-8.
247
245
 
248
- const value = Buffer.from(data)
249
- const frame = new WebsocketFrameSend(value)
250
- const buffer = frame.createFrame(opcodes.TEXT)
246
+ const length = Buffer.byteLength(data)
251
247
 
252
- this.#bufferedAmount += value.byteLength
253
- socket.write(buffer, () => {
254
- this.#bufferedAmount -= value.byteLength
255
- })
248
+ this.#bufferedAmount += length
249
+ this.#sendQueue.add(data, () => {
250
+ this.#bufferedAmount -= length
251
+ }, sendHints.string)
256
252
  } else if (types.isArrayBuffer(data)) {
257
253
  // If the WebSocket connection is established, and the WebSocket
258
254
  // closing handshake has not yet started, then the user agent must
@@ -266,14 +262,10 @@ class WebSocket extends EventTarget {
266
262
  // increase the bufferedAmount attribute by the length of the
267
263
  // ArrayBuffer in bytes.
268
264
 
269
- const value = new FastBuffer(data)
270
- const frame = new WebsocketFrameSend(value)
271
- const buffer = frame.createFrame(opcodes.BINARY)
272
-
273
- this.#bufferedAmount += value.byteLength
274
- socket.write(buffer, () => {
275
- this.#bufferedAmount -= value.byteLength
276
- })
265
+ this.#bufferedAmount += data.byteLength
266
+ this.#sendQueue.add(data, () => {
267
+ this.#bufferedAmount -= data.byteLength
268
+ }, sendHints.arrayBuffer)
277
269
  } else if (ArrayBuffer.isView(data)) {
278
270
  // If the WebSocket connection is established, and the WebSocket
279
271
  // closing handshake has not yet started, then the user agent must
@@ -287,15 +279,10 @@ class WebSocket extends EventTarget {
287
279
  // not throw an exception must increase the bufferedAmount attribute
288
280
  // by the length of data’s buffer in bytes.
289
281
 
290
- const ab = new FastBuffer(data.buffer, data.byteOffset, data.byteLength)
291
-
292
- const frame = new WebsocketFrameSend(ab)
293
- const buffer = frame.createFrame(opcodes.BINARY)
294
-
295
- this.#bufferedAmount += ab.byteLength
296
- socket.write(buffer, () => {
297
- this.#bufferedAmount -= ab.byteLength
298
- })
282
+ this.#bufferedAmount += data.byteLength
283
+ this.#sendQueue.add(data, () => {
284
+ this.#bufferedAmount -= data.byteLength
285
+ }, sendHints.typedArray)
299
286
  } else if (isBlobLike(data)) {
300
287
  // If the WebSocket connection is established, and the WebSocket
301
288
  // closing handshake has not yet started, then the user agent must
@@ -308,18 +295,10 @@ class WebSocket extends EventTarget {
308
295
  // an exception must increase the bufferedAmount attribute by the size
309
296
  // of the Blob object’s raw data, in bytes.
310
297
 
311
- const frame = new WebsocketFrameSend()
312
-
313
- data.arrayBuffer().then((ab) => {
314
- const value = new FastBuffer(ab)
315
- frame.frameData = value
316
- const buffer = frame.createFrame(opcodes.BINARY)
317
-
318
- this.#bufferedAmount += value.byteLength
319
- socket.write(buffer, () => {
320
- this.#bufferedAmount -= value.byteLength
321
- })
322
- })
298
+ this.#bufferedAmount += data.size
299
+ this.#sendQueue.add(data, () => {
300
+ this.#bufferedAmount -= data.size
301
+ }, sendHints.blob)
323
302
  }
324
303
  }
325
304
 
@@ -458,18 +437,20 @@ class WebSocket extends EventTarget {
458
437
  /**
459
438
  * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
460
439
  */
461
- #onConnectionEstablished (response) {
440
+ #onConnectionEstablished (response, parsedExtensions) {
462
441
  // processResponse is called when the "response’s header list has been received and initialized."
463
442
  // once this happens, the connection is open
464
443
  this[kResponse] = response
465
444
 
466
- const parser = new ByteParser(this)
445
+ const parser = new ByteParser(this, parsedExtensions)
467
446
  parser.on('drain', onParserDrain)
468
447
  parser.on('error', onParserError.bind(this))
469
448
 
470
449
  response.socket.ws = this
471
450
  this[kByteParser] = parser
472
451
 
452
+ this.#sendQueue = new SendQueue(response.socket)
453
+
473
454
  // 1. Change the ready state to OPEN (1).
474
455
  this[kReadyState] = states.OPEN
475
456
 
@@ -558,7 +539,7 @@ webidl.converters.WebSocketInit = webidl.dictionaryConverter([
558
539
  },
559
540
  {
560
541
  key: 'dispatcher',
561
- converter: (V) => V,
542
+ converter: webidl.converters.any,
562
543
  defaultValue: () => getGlobalDispatcher()
563
544
  },
564
545
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "6.17.0",
3
+ "version": "6.18.1",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {