aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/node_modules/undici/lib/web/websocket
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-13 21:34:48 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-13 21:34:48 -0800
commit76cb9c2a39d477a64824a985ade40507e3bbade1 (patch)
tree41e997aa9c6f538d3a136af61dae9424db2005a9 /vanilla/node_modules/undici/lib/web/websocket
parent819a39a21ac992b1393244a4c283bbb125208c69 (diff)
downloadneko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.gz
neko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.bz2
neko-76cb9c2a39d477a64824a985ade40507e3bbade1.zip
feat(vanilla): add testing infrastructure and tests (NK-wjnczv)
Diffstat (limited to 'vanilla/node_modules/undici/lib/web/websocket')
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/connection.js329
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/constants.js126
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/events.js331
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/frame.js133
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/permessage-deflate.js70
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/receiver.js444
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/sender.js109
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/stream/websocketerror.js104
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/stream/websocketstream.js497
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/util.js339
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/websocket.js739
11 files changed, 3221 insertions, 0 deletions
diff --git a/vanilla/node_modules/undici/lib/web/websocket/connection.js b/vanilla/node_modules/undici/lib/web/websocket/connection.js
new file mode 100644
index 0000000..4ecc8a1
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/connection.js
@@ -0,0 +1,329 @@
+'use strict'
+
+const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants')
+const { parseExtensions, isClosed, isClosing, isEstablished, isConnecting, validateCloseCodeAndReason } = require('./util')
+const { makeRequest } = require('../fetch/request')
+const { fetching } = require('../fetch/index')
+const { Headers, getHeadersList } = require('../fetch/headers')
+const { getDecodeSplit } = require('../fetch/util')
+const { WebsocketFrameSend } = require('./frame')
+const assert = require('node:assert')
+const { runtimeFeatures } = require('../../util/runtime-features')
+
+const crypto = runtimeFeatures.has('crypto')
+ ? require('node:crypto')
+ : null
+
+let warningEmitted = false
+
+/**
+ * @see https://websockets.spec.whatwg.org/#concept-websocket-establish
+ * @param {URL} url
+ * @param {string|string[]} protocols
+ * @param {import('./websocket').Handler} handler
+ * @param {Partial<import('../../../types/websocket').WebSocketInit>} options
+ */
+function establishWebSocketConnection (url, protocols, client, handler, options) {
+ // 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s
+ // scheme is "ws", and to "https" otherwise.
+ const requestURL = url
+
+ requestURL.protocol = url.protocol === 'ws:' ? 'http:' : 'https:'
+
+ // 2. Let request be a new request, whose URL is requestURL, client is client,
+ // service-workers mode is "none", referrer is "no-referrer", mode is
+ // "websocket", credentials mode is "include", cache mode is "no-store" ,
+ // redirect mode is "error", and use-URL-credentials flag is set.
+ const request = makeRequest({
+ urlList: [requestURL],
+ client,
+ serviceWorkers: 'none',
+ referrer: 'no-referrer',
+ mode: 'websocket',
+ credentials: 'include',
+ cache: 'no-store',
+ redirect: 'error',
+ useURLCredentials: true
+ })
+
+ // Note: undici extension, allow setting custom headers.
+ if (options.headers) {
+ const headersList = getHeadersList(new Headers(options.headers))
+
+ request.headersList = headersList
+ }
+
+ // 3. Append (`Upgrade`, `websocket`) to request’s header list.
+ // 4. Append (`Connection`, `Upgrade`) to request’s header list.
+ // Note: both of these are handled by undici currently.
+ // https://github.com/nodejs/undici/blob/68c269c4144c446f3f1220951338daef4a6b5ec4/lib/client.js#L1397
+
+ // 5. Let keyValue be a nonce consisting of a randomly selected
+ // 16-byte value that has been forgiving-base64-encoded and
+ // isomorphic encoded.
+ const keyValue = crypto.randomBytes(16).toString('base64')
+
+ // 6. Append (`Sec-WebSocket-Key`, keyValue) to request’s
+ // header list.
+ request.headersList.append('sec-websocket-key', keyValue, true)
+
+ // 7. Append (`Sec-WebSocket-Version`, `13`) to request’s
+ // header list.
+ request.headersList.append('sec-websocket-version', '13', true)
+
+ // 8. For each protocol in protocols, combine
+ // (`Sec-WebSocket-Protocol`, protocol) in request’s header
+ // list.
+ for (const protocol of protocols) {
+ request.headersList.append('sec-websocket-protocol', protocol, true)
+ }
+
+ // 9. Let permessageDeflate be a user-agent defined
+ // "permessage-deflate" extension header value.
+ // https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673
+ const permessageDeflate = 'permessage-deflate; client_max_window_bits'
+
+ // 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to
+ // request’s header list.
+ request.headersList.append('sec-websocket-extensions', permessageDeflate, true)
+
+ // 11. Fetch request with useParallelQueue set to true, and
+ // processResponse given response being these steps:
+ const controller = fetching({
+ request,
+ useParallelQueue: true,
+ dispatcher: options.dispatcher,
+ processResponse (response) {
+ // 1. If response is a network error or its status is not 101,
+ // fail the WebSocket connection.
+ // if (response.type === 'error' || ((response.socket?.session != null && response.status !== 200) && response.status !== 101)) {
+ if (response.type === 'error' || response.status !== 101) {
+ // The presence of a session property on the socket indicates HTTP2
+ // HTTP1
+ if (response.socket?.session == null) {
+ failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.', response.error)
+ return
+ }
+
+ // HTTP2
+ if (response.status !== 200) {
+ failWebsocketConnection(handler, 1002, 'Received network error or non-200 status code.', response.error)
+ return
+ }
+ }
+
+ if (warningEmitted === false && response.socket?.session != null) {
+ process.emitWarning('WebSocket over HTTP2 is experimental, and subject to change.', 'ExperimentalWarning')
+ warningEmitted = true
+ }
+
+ // 2. If protocols is not the empty list and extracting header
+ // list values given `Sec-WebSocket-Protocol` and response’s
+ // header list results in null, failure, or the empty byte
+ // sequence, then fail the WebSocket connection.
+ if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) {
+ failWebsocketConnection(handler, 1002, 'Server did not respond with sent protocols.')
+ return
+ }
+
+ // 3. Follow the requirements stated step 2 to step 6, inclusive,
+ // of the last set of steps in section 4.1 of The WebSocket
+ // Protocol to validate response. This either results in fail
+ // the WebSocket connection or the WebSocket connection is
+ // established.
+
+ // 2. If the response lacks an |Upgrade| header field or the |Upgrade|
+ // header field contains a value that is not an ASCII case-
+ // insensitive match for the value "websocket", the client MUST
+ // _Fail the WebSocket Connection_.
+ // For H2, no upgrade header is expected.
+ if (response.socket.session == null && response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') {
+ failWebsocketConnection(handler, 1002, 'Server did not set Upgrade header to "websocket".')
+ return
+ }
+
+ // 3. If the response lacks a |Connection| header field or the
+ // |Connection| header field doesn't contain a token that is an
+ // ASCII case-insensitive match for the value "Upgrade", the client
+ // MUST _Fail the WebSocket Connection_.
+ // For H2, no connection header is expected.
+ if (response.socket.session == null && response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') {
+ failWebsocketConnection(handler, 1002, 'Server did not set Connection header to "upgrade".')
+ return
+ }
+
+ // 4. If the response lacks a |Sec-WebSocket-Accept| header field or
+ // the |Sec-WebSocket-Accept| contains a value other than the
+ // base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket-
+ // Key| (as a string, not base64-decoded) with the string "258EAFA5-
+ // E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and
+ // trailing whitespace, the client MUST _Fail the WebSocket
+ // Connection_.
+ const secWSAccept = response.headersList.get('Sec-WebSocket-Accept')
+ const digest = crypto.hash('sha1', keyValue + uid, 'base64')
+ if (secWSAccept !== digest) {
+ failWebsocketConnection(handler, 1002, 'Incorrect hash received in Sec-WebSocket-Accept header.')
+ return
+ }
+
+ // 5. If the response includes a |Sec-WebSocket-Extensions| header
+ // field and this header field indicates the use of an extension
+ // that was not present in the client's handshake (the server has
+ // indicated an extension not requested by the client), the client
+ // MUST _Fail the WebSocket Connection_. (The parsing of this
+ // header field to determine which extensions are requested is
+ // discussed in Section 9.1.)
+ const secExtension = response.headersList.get('Sec-WebSocket-Extensions')
+ let extensions
+
+ if (secExtension !== null) {
+ extensions = parseExtensions(secExtension)
+
+ if (!extensions.has('permessage-deflate')) {
+ failWebsocketConnection(handler, 1002, 'Sec-WebSocket-Extensions header does not match.')
+ return
+ }
+ }
+
+ // 6. If the response includes a |Sec-WebSocket-Protocol| header field
+ // and this header field indicates the use of a subprotocol that was
+ // not present in the client's handshake (the server has indicated a
+ // subprotocol not requested by the client), the client MUST _Fail
+ // the WebSocket Connection_.
+ const secProtocol = response.headersList.get('Sec-WebSocket-Protocol')
+
+ if (secProtocol !== null) {
+ const requestProtocols = getDecodeSplit('sec-websocket-protocol', request.headersList)
+
+ // The client can request that the server use a specific subprotocol by
+ // including the |Sec-WebSocket-Protocol| field in its handshake. If it
+ // is specified, the server needs to include the same field and one of
+ // the selected subprotocol values in its response for the connection to
+ // be established.
+ if (!requestProtocols.includes(secProtocol)) {
+ failWebsocketConnection(handler, 1002, 'Protocol was not set in the opening handshake.')
+ return
+ }
+ }
+
+ response.socket.on('data', handler.onSocketData)
+ response.socket.on('close', handler.onSocketClose)
+ response.socket.on('error', handler.onSocketError)
+
+ handler.wasEverConnected = true
+ handler.onConnectionEstablished(response, extensions)
+ }
+ })
+
+ return controller
+}
+
+/**
+ * @see https://whatpr.org/websockets/48.html#close-the-websocket
+ * @param {import('./websocket').Handler} object
+ * @param {number} [code=null]
+ * @param {string} [reason='']
+ */
+function closeWebSocketConnection (object, code, reason, validate = false) {
+ // 1. If code was not supplied, let code be null.
+ code ??= null
+
+ // 2. If reason was not supplied, let reason be the empty string.
+ reason ??= ''
+
+ // 3. Validate close code and reason with code and reason.
+ if (validate) validateCloseCodeAndReason(code, reason)
+
+ // 4. Run the first matching steps from the following list:
+ // - If object’s ready state is CLOSING (2) or CLOSED (3)
+ // - If the WebSocket connection is not yet established [WSP]
+ // - If the WebSocket closing handshake has not yet been started [WSP]
+ // - Otherwise
+ if (isClosed(object.readyState) || isClosing(object.readyState)) {
+ // Do nothing.
+ } else if (!isEstablished(object.readyState)) {
+ // Fail the WebSocket connection and set object’s ready state to CLOSING (2). [WSP]
+ failWebsocketConnection(object)
+ object.readyState = states.CLOSING
+ } else if (!object.closeState.has(sentCloseFrameState.SENT) && !object.closeState.has(sentCloseFrameState.RECEIVED)) {
+ // Upon either sending or receiving a Close control frame, it is said
+ // that _The WebSocket Closing Handshake is Started_ and that the
+ // WebSocket connection is in the CLOSING state.
+
+ const frame = new WebsocketFrameSend()
+
+ // If neither code nor reason is present, the WebSocket Close
+ // message must not have a body.
+
+ // If code is present, then the status code to use in the
+ // WebSocket Close message must be the integer given by code.
+ // If code is null and reason is the empty string, the WebSocket Close frame must not have a body.
+ // If reason is non-empty but code is null, then set code to 1000 ("Normal Closure").
+ if (reason.length !== 0 && code === null) {
+ code = 1000
+ }
+
+ // If code is set, then the status code to use in the WebSocket Close frame must be the integer given by code.
+ assert(code === null || Number.isInteger(code))
+
+ if (code === null && reason.length === 0) {
+ frame.frameData = emptyBuffer
+ } else if (code !== null && reason === null) {
+ frame.frameData = Buffer.allocUnsafe(2)
+ frame.frameData.writeUInt16BE(code, 0)
+ } else if (code !== null && reason !== null) {
+ // If reason is also present, then reasonBytes must be
+ // provided in the Close message after the status code.
+ frame.frameData = Buffer.allocUnsafe(2 + Buffer.byteLength(reason))
+ frame.frameData.writeUInt16BE(code, 0)
+ // the body MAY contain UTF-8-encoded data with value /reason/
+ frame.frameData.write(reason, 2, 'utf-8')
+ } else {
+ frame.frameData = emptyBuffer
+ }
+
+ object.socket.write(frame.createFrame(opcodes.CLOSE))
+
+ object.closeState.add(sentCloseFrameState.SENT)
+
+ // Upon either sending or receiving a Close control frame, it is said
+ // that _The WebSocket Closing Handshake is Started_ and that the
+ // WebSocket connection is in the CLOSING state.
+ object.readyState = states.CLOSING
+ } else {
+ // Set object’s ready state to CLOSING (2).
+ object.readyState = states.CLOSING
+ }
+}
+
+/**
+ * @param {import('./websocket').Handler} handler
+ * @param {number} code
+ * @param {string|undefined} reason
+ * @param {unknown} cause
+ * @returns {void}
+ */
+function failWebsocketConnection (handler, code, reason, cause) {
+ // If _The WebSocket Connection is Established_ prior to the point where
+ // the endpoint is required to _Fail the WebSocket Connection_, the
+ // endpoint SHOULD send a Close frame with an appropriate status code
+ // (Section 7.4) before proceeding to _Close the WebSocket Connection_.
+ if (isEstablished(handler.readyState)) {
+ closeWebSocketConnection(handler, code, reason, false)
+ }
+
+ handler.controller.abort()
+
+ if (isConnecting(handler.readyState)) {
+ // If the connection was not established, we must still emit an 'error' and 'close' events
+ handler.onSocketClose()
+ } else if (handler.socket?.destroyed === false) {
+ handler.socket.destroy()
+ }
+}
+
+module.exports = {
+ establishWebSocketConnection,
+ failWebsocketConnection,
+ closeWebSocketConnection
+}
diff --git a/vanilla/node_modules/undici/lib/web/websocket/constants.js b/vanilla/node_modules/undici/lib/web/websocket/constants.js
new file mode 100644
index 0000000..e4e6990
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/constants.js
@@ -0,0 +1,126 @@
+'use strict'
+
+/**
+ * This is a Globally Unique Identifier unique used to validate that the
+ * endpoint accepts websocket connections.
+ * @see https://www.rfc-editor.org/rfc/rfc6455.html#section-1.3
+ * @type {'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'}
+ */
+const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
+
+/**
+ * @type {PropertyDescriptor}
+ */
+const staticPropertyDescriptors = {
+ enumerable: true,
+ writable: false,
+ configurable: false
+}
+
+/**
+ * The states of the WebSocket connection.
+ *
+ * @readonly
+ * @enum
+ * @property {0} CONNECTING
+ * @property {1} OPEN
+ * @property {2} CLOSING
+ * @property {3} CLOSED
+ */
+const states = {
+ CONNECTING: 0,
+ OPEN: 1,
+ CLOSING: 2,
+ CLOSED: 3
+}
+
+/**
+ * @readonly
+ * @enum
+ * @property {0} NOT_SENT
+ * @property {1} PROCESSING
+ * @property {2} SENT
+ */
+const sentCloseFrameState = {
+ SENT: 1,
+ RECEIVED: 2
+}
+
+/**
+ * The WebSocket opcodes.
+ *
+ * @readonly
+ * @enum
+ * @property {0x0} CONTINUATION
+ * @property {0x1} TEXT
+ * @property {0x2} BINARY
+ * @property {0x8} CLOSE
+ * @property {0x9} PING
+ * @property {0xA} PONG
+ * @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
+ */
+const opcodes = {
+ CONTINUATION: 0x0,
+ TEXT: 0x1,
+ BINARY: 0x2,
+ CLOSE: 0x8,
+ PING: 0x9,
+ PONG: 0xA
+}
+
+/**
+ * The maximum value for an unsigned 16-bit integer.
+ *
+ * @type {65535} 2 ** 16 - 1
+ */
+const maxUnsigned16Bit = 65535
+
+/**
+ * The states of the parser.
+ *
+ * @readonly
+ * @enum
+ * @property {0} INFO
+ * @property {2} PAYLOADLENGTH_16
+ * @property {3} PAYLOADLENGTH_64
+ * @property {4} READ_DATA
+ */
+const parserStates = {
+ INFO: 0,
+ PAYLOADLENGTH_16: 2,
+ PAYLOADLENGTH_64: 3,
+ READ_DATA: 4
+}
+
+/**
+ * An empty buffer.
+ *
+ * @type {Buffer}
+ */
+const emptyBuffer = Buffer.allocUnsafe(0)
+
+/**
+ * @readonly
+ * @property {1} text
+ * @property {2} typedArray
+ * @property {3} arrayBuffer
+ * @property {4} blob
+ */
+const sendHints = {
+ text: 1,
+ typedArray: 2,
+ arrayBuffer: 3,
+ blob: 4
+}
+
+module.exports = {
+ uid,
+ sentCloseFrameState,
+ staticPropertyDescriptors,
+ states,
+ opcodes,
+ maxUnsigned16Bit,
+ parserStates,
+ emptyBuffer,
+ sendHints
+}
diff --git a/vanilla/node_modules/undici/lib/web/websocket/events.js b/vanilla/node_modules/undici/lib/web/websocket/events.js
new file mode 100644
index 0000000..7ac9566
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/events.js
@@ -0,0 +1,331 @@
+'use strict'
+
+const { webidl } = require('../webidl')
+const { kEnumerableProperty } = require('../../core/util')
+const { kConstruct } = require('../../core/symbols')
+
+/**
+ * @see https://html.spec.whatwg.org/multipage/comms.html#messageevent
+ */
+class MessageEvent extends Event {
+ #eventInit
+
+ constructor (type, eventInitDict = {}) {
+ if (type === kConstruct) {
+ super(arguments[1], arguments[2])
+ webidl.util.markAsUncloneable(this)
+ return
+ }
+
+ const prefix = 'MessageEvent constructor'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ type = webidl.converters.DOMString(type, prefix, 'type')
+ eventInitDict = webidl.converters.MessageEventInit(eventInitDict, prefix, 'eventInitDict')
+
+ super(type, eventInitDict)
+
+ this.#eventInit = eventInitDict
+ webidl.util.markAsUncloneable(this)
+ }
+
+ get data () {
+ webidl.brandCheck(this, MessageEvent)
+
+ return this.#eventInit.data
+ }
+
+ get origin () {
+ webidl.brandCheck(this, MessageEvent)
+
+ return this.#eventInit.origin
+ }
+
+ get lastEventId () {
+ webidl.brandCheck(this, MessageEvent)
+
+ return this.#eventInit.lastEventId
+ }
+
+ get source () {
+ webidl.brandCheck(this, MessageEvent)
+
+ return this.#eventInit.source
+ }
+
+ get ports () {
+ webidl.brandCheck(this, MessageEvent)
+
+ if (!Object.isFrozen(this.#eventInit.ports)) {
+ Object.freeze(this.#eventInit.ports)
+ }
+
+ return this.#eventInit.ports
+ }
+
+ initMessageEvent (
+ type,
+ bubbles = false,
+ cancelable = false,
+ data = null,
+ origin = '',
+ lastEventId = '',
+ source = null,
+ ports = []
+ ) {
+ webidl.brandCheck(this, MessageEvent)
+
+ webidl.argumentLengthCheck(arguments, 1, 'MessageEvent.initMessageEvent')
+
+ return new MessageEvent(type, {
+ bubbles, cancelable, data, origin, lastEventId, source, ports
+ })
+ }
+
+ static createFastMessageEvent (type, init) {
+ const messageEvent = new MessageEvent(kConstruct, type, init)
+ messageEvent.#eventInit = init
+ messageEvent.#eventInit.data ??= null
+ messageEvent.#eventInit.origin ??= ''
+ messageEvent.#eventInit.lastEventId ??= ''
+ messageEvent.#eventInit.source ??= null
+ messageEvent.#eventInit.ports ??= []
+ return messageEvent
+ }
+}
+
+const { createFastMessageEvent } = MessageEvent
+delete MessageEvent.createFastMessageEvent
+
+/**
+ * @see https://websockets.spec.whatwg.org/#the-closeevent-interface
+ */
+class CloseEvent extends Event {
+ #eventInit
+
+ constructor (type, eventInitDict = {}) {
+ const prefix = 'CloseEvent constructor'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ type = webidl.converters.DOMString(type, prefix, 'type')
+ eventInitDict = webidl.converters.CloseEventInit(eventInitDict)
+
+ super(type, eventInitDict)
+
+ this.#eventInit = eventInitDict
+ webidl.util.markAsUncloneable(this)
+ }
+
+ get wasClean () {
+ webidl.brandCheck(this, CloseEvent)
+
+ return this.#eventInit.wasClean
+ }
+
+ get code () {
+ webidl.brandCheck(this, CloseEvent)
+
+ return this.#eventInit.code
+ }
+
+ get reason () {
+ webidl.brandCheck(this, CloseEvent)
+
+ return this.#eventInit.reason
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/webappapis.html#the-errorevent-interface
+class ErrorEvent extends Event {
+ #eventInit
+
+ constructor (type, eventInitDict) {
+ const prefix = 'ErrorEvent constructor'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ super(type, eventInitDict)
+ webidl.util.markAsUncloneable(this)
+
+ type = webidl.converters.DOMString(type, prefix, 'type')
+ eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {})
+
+ this.#eventInit = eventInitDict
+ }
+
+ get message () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.message
+ }
+
+ get filename () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.filename
+ }
+
+ get lineno () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.lineno
+ }
+
+ get colno () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.colno
+ }
+
+ get error () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.error
+ }
+}
+
+Object.defineProperties(MessageEvent.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'MessageEvent',
+ configurable: true
+ },
+ data: kEnumerableProperty,
+ origin: kEnumerableProperty,
+ lastEventId: kEnumerableProperty,
+ source: kEnumerableProperty,
+ ports: kEnumerableProperty,
+ initMessageEvent: kEnumerableProperty
+})
+
+Object.defineProperties(CloseEvent.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'CloseEvent',
+ configurable: true
+ },
+ reason: kEnumerableProperty,
+ code: kEnumerableProperty,
+ wasClean: kEnumerableProperty
+})
+
+Object.defineProperties(ErrorEvent.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'ErrorEvent',
+ configurable: true
+ },
+ message: kEnumerableProperty,
+ filename: kEnumerableProperty,
+ lineno: kEnumerableProperty,
+ colno: kEnumerableProperty,
+ error: kEnumerableProperty
+})
+
+webidl.converters.MessagePort = webidl.interfaceConverter(
+ webidl.is.MessagePort,
+ 'MessagePort'
+)
+
+webidl.converters['sequence<MessagePort>'] = webidl.sequenceConverter(
+ webidl.converters.MessagePort
+)
+
+const eventInit = [
+ {
+ key: 'bubbles',
+ converter: webidl.converters.boolean,
+ defaultValue: () => false
+ },
+ {
+ key: 'cancelable',
+ converter: webidl.converters.boolean,
+ defaultValue: () => false
+ },
+ {
+ key: 'composed',
+ converter: webidl.converters.boolean,
+ defaultValue: () => false
+ }
+]
+
+webidl.converters.MessageEventInit = webidl.dictionaryConverter([
+ ...eventInit,
+ {
+ key: 'data',
+ converter: webidl.converters.any,
+ defaultValue: () => null
+ },
+ {
+ key: 'origin',
+ converter: webidl.converters.USVString,
+ defaultValue: () => ''
+ },
+ {
+ key: 'lastEventId',
+ converter: webidl.converters.DOMString,
+ defaultValue: () => ''
+ },
+ {
+ key: 'source',
+ // Node doesn't implement WindowProxy or ServiceWorker, so the only
+ // valid value for source is a MessagePort.
+ converter: webidl.nullableConverter(webidl.converters.MessagePort),
+ defaultValue: () => null
+ },
+ {
+ key: 'ports',
+ converter: webidl.converters['sequence<MessagePort>'],
+ defaultValue: () => []
+ }
+])
+
+webidl.converters.CloseEventInit = webidl.dictionaryConverter([
+ ...eventInit,
+ {
+ key: 'wasClean',
+ converter: webidl.converters.boolean,
+ defaultValue: () => false
+ },
+ {
+ key: 'code',
+ converter: webidl.converters['unsigned short'],
+ defaultValue: () => 0
+ },
+ {
+ key: 'reason',
+ converter: webidl.converters.USVString,
+ defaultValue: () => ''
+ }
+])
+
+webidl.converters.ErrorEventInit = webidl.dictionaryConverter([
+ ...eventInit,
+ {
+ key: 'message',
+ converter: webidl.converters.DOMString,
+ defaultValue: () => ''
+ },
+ {
+ key: 'filename',
+ converter: webidl.converters.USVString,
+ defaultValue: () => ''
+ },
+ {
+ key: 'lineno',
+ converter: webidl.converters['unsigned long'],
+ defaultValue: () => 0
+ },
+ {
+ key: 'colno',
+ converter: webidl.converters['unsigned long'],
+ defaultValue: () => 0
+ },
+ {
+ key: 'error',
+ converter: webidl.converters.any
+ }
+])
+
+module.exports = {
+ MessageEvent,
+ CloseEvent,
+ ErrorEvent,
+ createFastMessageEvent
+}
diff --git a/vanilla/node_modules/undici/lib/web/websocket/frame.js b/vanilla/node_modules/undici/lib/web/websocket/frame.js
new file mode 100644
index 0000000..e397c87
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/frame.js
@@ -0,0 +1,133 @@
+'use strict'
+
+const { runtimeFeatures } = require('../../util/runtime-features')
+const { maxUnsigned16Bit, opcodes } = require('./constants')
+
+const BUFFER_SIZE = 8 * 1024
+
+let buffer = null
+let bufIdx = BUFFER_SIZE
+
+const randomFillSync = runtimeFeatures.has('crypto')
+ ? require('node:crypto').randomFillSync
+ // not full compatibility, but minimum.
+ : function randomFillSync (buffer, _offset, _size) {
+ for (let i = 0; i < buffer.length; ++i) {
+ buffer[i] = Math.random() * 255 | 0
+ }
+ return buffer
+ }
+
+function generateMask () {
+ if (bufIdx === BUFFER_SIZE) {
+ bufIdx = 0
+ randomFillSync((buffer ??= Buffer.allocUnsafeSlow(BUFFER_SIZE)), 0, BUFFER_SIZE)
+ }
+ return [buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++]]
+}
+
+class WebsocketFrameSend {
+ /**
+ * @param {Buffer|undefined} data
+ */
+ constructor (data) {
+ this.frameData = data
+ }
+
+ createFrame (opcode) {
+ const frameData = this.frameData
+ const maskKey = generateMask()
+ const bodyLength = frameData?.byteLength ?? 0
+
+ /** @type {number} */
+ let payloadLength = bodyLength // 0-125
+ let offset = 6
+
+ if (bodyLength > maxUnsigned16Bit) {
+ offset += 8 // payload length is next 8 bytes
+ payloadLength = 127
+ } else if (bodyLength > 125) {
+ offset += 2 // payload length is next 2 bytes
+ payloadLength = 126
+ }
+
+ const buffer = Buffer.allocUnsafe(bodyLength + offset)
+
+ // Clear first 2 bytes, everything else is overwritten
+ buffer[0] = buffer[1] = 0
+ buffer[0] |= 0x80 // FIN
+ buffer[0] = (buffer[0] & 0xF0) + opcode // opcode
+
+ /*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */
+ buffer[offset - 4] = maskKey[0]
+ buffer[offset - 3] = maskKey[1]
+ buffer[offset - 2] = maskKey[2]
+ buffer[offset - 1] = maskKey[3]
+
+ buffer[1] = payloadLength
+
+ if (payloadLength === 126) {
+ buffer.writeUInt16BE(bodyLength, 2)
+ } else if (payloadLength === 127) {
+ // Clear extended payload length
+ buffer[2] = buffer[3] = 0
+ buffer.writeUIntBE(bodyLength, 4, 6)
+ }
+
+ buffer[1] |= 0x80 // MASK
+
+ // mask body
+ for (let i = 0; i < bodyLength; ++i) {
+ buffer[offset + i] = frameData[i] ^ maskKey[i & 3]
+ }
+
+ return buffer
+ }
+
+ /**
+ * @param {Uint8Array} buffer
+ */
+ static createFastTextFrame (buffer) {
+ const maskKey = generateMask()
+
+ const bodyLength = buffer.length
+
+ // mask body
+ for (let i = 0; i < bodyLength; ++i) {
+ buffer[i] ^= maskKey[i & 3]
+ }
+
+ let payloadLength = bodyLength
+ let offset = 6
+
+ if (bodyLength > maxUnsigned16Bit) {
+ offset += 8 // payload length is next 8 bytes
+ payloadLength = 127
+ } else if (bodyLength > 125) {
+ offset += 2 // payload length is next 2 bytes
+ payloadLength = 126
+ }
+ const head = Buffer.allocUnsafeSlow(offset)
+
+ head[0] = 0x80 /* FIN */ | opcodes.TEXT /* opcode TEXT */
+ head[1] = payloadLength | 0x80 /* MASK */
+ head[offset - 4] = maskKey[0]
+ head[offset - 3] = maskKey[1]
+ head[offset - 2] = maskKey[2]
+ head[offset - 1] = maskKey[3]
+
+ if (payloadLength === 126) {
+ head.writeUInt16BE(bodyLength, 2)
+ } else if (payloadLength === 127) {
+ head[2] = head[3] = 0
+ head.writeUIntBE(bodyLength, 4, 6)
+ }
+
+ return [head, buffer]
+ }
+}
+
+module.exports = {
+ WebsocketFrameSend,
+ generateMask // for benchmark
+}
diff --git a/vanilla/node_modules/undici/lib/web/websocket/permessage-deflate.js b/vanilla/node_modules/undici/lib/web/websocket/permessage-deflate.js
new file mode 100644
index 0000000..76cb366
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/permessage-deflate.js
@@ -0,0 +1,70 @@
+'use strict'
+
+const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib')
+const { isValidClientWindowBits } = require('./util')
+
+const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
+const kBuffer = Symbol('kBuffer')
+const kLength = Symbol('kLength')
+
+class PerMessageDeflate {
+ /** @type {import('node:zlib').InflateRaw} */
+ #inflate
+
+ #options = {}
+
+ constructor (extensions) {
+ this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
+ this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
+ }
+
+ decompress (chunk, fin, callback) {
+ // An endpoint uses the following algorithm to decompress a message.
+ // 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
+ // payload of the message.
+ // 2. Decompress the resulting data using DEFLATE.
+
+ if (!this.#inflate) {
+ let windowBits = Z_DEFAULT_WINDOWBITS
+
+ if (this.#options.serverMaxWindowBits) { // empty values default to Z_DEFAULT_WINDOWBITS
+ if (!isValidClientWindowBits(this.#options.serverMaxWindowBits)) {
+ callback(new Error('Invalid server_max_window_bits'))
+ return
+ }
+
+ windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
+ }
+
+ this.#inflate = createInflateRaw({ windowBits })
+ this.#inflate[kBuffer] = []
+ this.#inflate[kLength] = 0
+
+ this.#inflate.on('data', (data) => {
+ this.#inflate[kBuffer].push(data)
+ this.#inflate[kLength] += data.length
+ })
+
+ this.#inflate.on('error', (err) => {
+ this.#inflate = null
+ callback(err)
+ })
+ }
+
+ this.#inflate.write(chunk)
+ if (fin) {
+ this.#inflate.write(tail)
+ }
+
+ this.#inflate.flush(() => {
+ const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
+
+ this.#inflate[kBuffer].length = 0
+ this.#inflate[kLength] = 0
+
+ callback(null, full)
+ })
+ }
+}
+
+module.exports = { PerMessageDeflate }
diff --git a/vanilla/node_modules/undici/lib/web/websocket/receiver.js b/vanilla/node_modules/undici/lib/web/websocket/receiver.js
new file mode 100644
index 0000000..ba0a5aa
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/receiver.js
@@ -0,0 +1,444 @@
+'use strict'
+
+const { Writable } = require('node:stream')
+const assert = require('node:assert')
+const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants')
+const {
+ isValidStatusCode,
+ isValidOpcode,
+ websocketMessageReceived,
+ utf8Decode,
+ isControlFrame,
+ isTextBinaryFrame,
+ isContinuationFrame
+} = require('./util')
+const { failWebsocketConnection } = require('./connection')
+const { WebsocketFrameSend } = require('./frame')
+const { PerMessageDeflate } = require('./permessage-deflate')
+
+// This code was influenced by ws released under the MIT license.
+// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+// Copyright (c) 2013 Arnout Kazemier and contributors
+// Copyright (c) 2016 Luigi Pinca and contributors
+
+class ByteParser extends Writable {
+ #buffers = []
+ #fragmentsBytes = 0
+ #byteOffset = 0
+ #loop = false
+
+ #state = parserStates.INFO
+
+ #info = {}
+ #fragments = []
+
+ /** @type {Map<string, PerMessageDeflate>} */
+ #extensions
+
+ /** @type {import('./websocket').Handler} */
+ #handler
+
+ constructor (handler, extensions) {
+ super()
+
+ this.#handler = handler
+ this.#extensions = extensions == null ? new Map() : extensions
+
+ if (this.#extensions.has('permessage-deflate')) {
+ this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
+ }
+ }
+
+ /**
+ * @param {Buffer} chunk
+ * @param {() => void} callback
+ */
+ _write (chunk, _, callback) {
+ this.#buffers.push(chunk)
+ this.#byteOffset += chunk.length
+ this.#loop = true
+
+ this.run(callback)
+ }
+
+ /**
+ * Runs whenever a new chunk is received.
+ * Callback is called whenever there are no more chunks buffering,
+ * or not enough bytes are buffered to parse.
+ */
+ run (callback) {
+ while (this.#loop) {
+ if (this.#state === parserStates.INFO) {
+ // If there aren't enough bytes to parse the payload length, etc.
+ if (this.#byteOffset < 2) {
+ return callback()
+ }
+
+ const buffer = this.consume(2)
+ const fin = (buffer[0] & 0x80) !== 0
+ const opcode = buffer[0] & 0x0F
+ const masked = (buffer[1] & 0x80) === 0x80
+
+ const fragmented = !fin && opcode !== opcodes.CONTINUATION
+ const payloadLength = buffer[1] & 0x7F
+
+ const rsv1 = buffer[0] & 0x40
+ const rsv2 = buffer[0] & 0x20
+ const rsv3 = buffer[0] & 0x10
+
+ if (!isValidOpcode(opcode)) {
+ failWebsocketConnection(this.#handler, 1002, 'Invalid opcode received')
+ return callback()
+ }
+
+ if (masked) {
+ failWebsocketConnection(this.#handler, 1002, 'Frame cannot be masked')
+ return callback()
+ }
+
+ // MUST be 0 unless an extension is negotiated that defines meanings
+ // for non-zero values. If a nonzero value is received and none of
+ // the negotiated extensions defines the meaning of such a nonzero
+ // value, the receiving endpoint MUST _Fail the WebSocket
+ // Connection_.
+ // This document allocates the RSV1 bit of the WebSocket header for
+ // PMCEs and calls the bit the "Per-Message Compressed" bit. On a
+ // WebSocket connection where a PMCE is in use, this bit indicates
+ // whether a message is compressed or not.
+ if (rsv1 !== 0 && !this.#extensions.has('permessage-deflate')) {
+ failWebsocketConnection(this.#handler, 1002, 'Expected RSV1 to be clear.')
+ return
+ }
+
+ if (rsv2 !== 0 || rsv3 !== 0) {
+ failWebsocketConnection(this.#handler, 1002, 'RSV1, RSV2, RSV3 must be clear')
+ return
+ }
+
+ if (fragmented && !isTextBinaryFrame(opcode)) {
+ // Only text and binary frames can be fragmented
+ failWebsocketConnection(this.#handler, 1002, 'Invalid frame type was fragmented.')
+ return
+ }
+
+ // If we are already parsing a text/binary frame and do not receive either
+ // a continuation frame or close frame, fail the connection.
+ if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) {
+ failWebsocketConnection(this.#handler, 1002, 'Expected continuation frame')
+ return
+ }
+
+ if (this.#info.fragmented && fragmented) {
+ // A fragmented frame can't be fragmented itself
+ failWebsocketConnection(this.#handler, 1002, 'Fragmented frame exceeded 125 bytes.')
+ return
+ }
+
+ // "All control frames MUST have a payload length of 125 bytes or less
+ // and MUST NOT be fragmented."
+ if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) {
+ failWebsocketConnection(this.#handler, 1002, 'Control frame either too large or fragmented')
+ return
+ }
+
+ if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) {
+ failWebsocketConnection(this.#handler, 1002, 'Unexpected continuation frame')
+ return
+ }
+
+ if (payloadLength <= 125) {
+ this.#info.payloadLength = payloadLength
+ this.#state = parserStates.READ_DATA
+ } else if (payloadLength === 126) {
+ this.#state = parserStates.PAYLOADLENGTH_16
+ } else if (payloadLength === 127) {
+ this.#state = parserStates.PAYLOADLENGTH_64
+ }
+
+ if (isTextBinaryFrame(opcode)) {
+ this.#info.binaryType = opcode
+ this.#info.compressed = rsv1 !== 0
+ }
+
+ this.#info.opcode = opcode
+ this.#info.masked = masked
+ this.#info.fin = fin
+ this.#info.fragmented = fragmented
+ } else if (this.#state === parserStates.PAYLOADLENGTH_16) {
+ if (this.#byteOffset < 2) {
+ return callback()
+ }
+
+ const buffer = this.consume(2)
+
+ this.#info.payloadLength = buffer.readUInt16BE(0)
+ this.#state = parserStates.READ_DATA
+ } else if (this.#state === parserStates.PAYLOADLENGTH_64) {
+ if (this.#byteOffset < 8) {
+ return callback()
+ }
+
+ const buffer = this.consume(8)
+ const upper = buffer.readUInt32BE(0)
+
+ // 2^31 is the maximum bytes an arraybuffer can contain
+ // on 32-bit systems. Although, on 64-bit systems, this is
+ // 2^53-1 bytes.
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
+ // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
+ // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
+ if (upper > 2 ** 31 - 1) {
+ failWebsocketConnection(this.#handler, 1009, 'Received payload length > 2^31 bytes.')
+ return
+ }
+
+ const lower = buffer.readUInt32BE(4)
+
+ this.#info.payloadLength = (upper << 8) + lower
+ this.#state = parserStates.READ_DATA
+ } else if (this.#state === parserStates.READ_DATA) {
+ if (this.#byteOffset < this.#info.payloadLength) {
+ return callback()
+ }
+
+ const body = this.consume(this.#info.payloadLength)
+
+ if (isControlFrame(this.#info.opcode)) {
+ this.#loop = this.parseControlFrame(body)
+ this.#state = parserStates.INFO
+ } else {
+ if (!this.#info.compressed) {
+ this.writeFragments(body)
+
+ // If the frame is not fragmented, a message has been received.
+ // If the frame is fragmented, it will terminate with a fin bit set
+ // and an opcode of 0 (continuation), therefore we handle that when
+ // parsing continuation frames, not here.
+ if (!this.#info.fragmented && this.#info.fin) {
+ websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
+ }
+
+ this.#state = parserStates.INFO
+ } else {
+ this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
+ if (error) {
+ failWebsocketConnection(this.#handler, 1007, error.message)
+ return
+ }
+
+ this.writeFragments(data)
+
+ if (!this.#info.fin) {
+ this.#state = parserStates.INFO
+ this.#loop = true
+ this.run(callback)
+ return
+ }
+
+ websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
+
+ this.#loop = true
+ this.#state = parserStates.INFO
+ this.run(callback)
+ })
+
+ this.#loop = false
+ break
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Take n bytes from the buffered Buffers
+ * @param {number} n
+ * @returns {Buffer}
+ */
+ consume (n) {
+ if (n > this.#byteOffset) {
+ throw new Error('Called consume() before buffers satiated.')
+ } else if (n === 0) {
+ return emptyBuffer
+ }
+
+ this.#byteOffset -= n
+
+ const first = this.#buffers[0]
+
+ if (first.length > n) {
+ // replace with remaining buffer
+ this.#buffers[0] = first.subarray(n, first.length)
+ return first.subarray(0, n)
+ } else if (first.length === n) {
+ // prefect match
+ return this.#buffers.shift()
+ } else {
+ let offset = 0
+ // If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero.
+ const buffer = Buffer.allocUnsafeSlow(n)
+ while (offset !== n) {
+ const next = this.#buffers[0]
+ const length = next.length
+
+ if (length + offset === n) {
+ buffer.set(this.#buffers.shift(), offset)
+ break
+ } else if (length + offset > n) {
+ buffer.set(next.subarray(0, n - offset), offset)
+ this.#buffers[0] = next.subarray(n - offset)
+ break
+ } else {
+ buffer.set(this.#buffers.shift(), offset)
+ offset += length
+ }
+ }
+
+ return buffer
+ }
+ }
+
+ writeFragments (fragment) {
+ this.#fragmentsBytes += fragment.length
+ this.#fragments.push(fragment)
+ }
+
+ consumeFragments () {
+ const fragments = this.#fragments
+
+ if (fragments.length === 1) {
+ // single fragment
+ this.#fragmentsBytes = 0
+ return fragments.shift()
+ }
+
+ let offset = 0
+ // If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero.
+ const output = Buffer.allocUnsafeSlow(this.#fragmentsBytes)
+
+ for (let i = 0; i < fragments.length; ++i) {
+ const buffer = fragments[i]
+ output.set(buffer, offset)
+ offset += buffer.length
+ }
+
+ this.#fragments = []
+ this.#fragmentsBytes = 0
+
+ return output
+ }
+
+ parseCloseBody (data) {
+ assert(data.length !== 1)
+
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
+ /** @type {number|undefined} */
+ let code
+
+ if (data.length >= 2) {
+ // _The WebSocket Connection Close Code_ is
+ // defined as the status code (Section 7.4) contained in the first Close
+ // control frame received by the application
+ code = data.readUInt16BE(0)
+ }
+
+ if (code !== undefined && !isValidStatusCode(code)) {
+ return { code: 1002, reason: 'Invalid status code', error: true }
+ }
+
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6
+ /** @type {Buffer} */
+ let reason = data.subarray(2)
+
+ // Remove BOM
+ if (reason[0] === 0xEF && reason[1] === 0xBB && reason[2] === 0xBF) {
+ reason = reason.subarray(3)
+ }
+
+ try {
+ reason = utf8Decode(reason)
+ } catch {
+ return { code: 1007, reason: 'Invalid UTF-8', error: true }
+ }
+
+ return { code, reason, error: false }
+ }
+
+ /**
+ * Parses control frames.
+ * @param {Buffer} body
+ */
+ parseControlFrame (body) {
+ const { opcode, payloadLength } = this.#info
+
+ if (opcode === opcodes.CLOSE) {
+ if (payloadLength === 1) {
+ failWebsocketConnection(this.#handler, 1002, 'Received close frame with a 1-byte body.')
+ return false
+ }
+
+ this.#info.closeInfo = this.parseCloseBody(body)
+
+ if (this.#info.closeInfo.error) {
+ const { code, reason } = this.#info.closeInfo
+
+ failWebsocketConnection(this.#handler, code, reason)
+ return false
+ }
+
+ // Upon receiving such a frame, the other peer sends a
+ // Close frame in response, if it hasn't already sent one.
+ if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
+ // If an endpoint receives a Close frame and did not previously send a
+ // Close frame, the endpoint MUST send a Close frame in response. (When
+ // sending a Close frame in response, the endpoint typically echos the
+ // status code it received.)
+ let body = emptyBuffer
+ if (this.#info.closeInfo.code) {
+ body = Buffer.allocUnsafe(2)
+ body.writeUInt16BE(this.#info.closeInfo.code, 0)
+ }
+ const closeFrame = new WebsocketFrameSend(body)
+
+ this.#handler.socket.write(closeFrame.createFrame(opcodes.CLOSE))
+ this.#handler.closeState.add(sentCloseFrameState.SENT)
+ }
+
+ // Upon either sending or receiving a Close control frame, it is said
+ // that _The WebSocket Closing Handshake is Started_ and that the
+ // WebSocket connection is in the CLOSING state.
+ this.#handler.readyState = states.CLOSING
+ this.#handler.closeState.add(sentCloseFrameState.RECEIVED)
+
+ return false
+ } else if (opcode === opcodes.PING) {
+ // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
+ // response, unless it already received a Close frame.
+ // A Pong frame sent in response to a Ping frame must have identical
+ // "Application data"
+
+ if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
+ const frame = new WebsocketFrameSend(body)
+
+ this.#handler.socket.write(frame.createFrame(opcodes.PONG))
+
+ this.#handler.onPing(body)
+ }
+ } else if (opcode === opcodes.PONG) {
+ // A Pong frame MAY be sent unsolicited. This serves as a
+ // unidirectional heartbeat. A response to an unsolicited Pong frame is
+ // not expected.
+ this.#handler.onPong(body)
+ }
+
+ return true
+ }
+
+ get closingInfo () {
+ return this.#info.closeInfo
+ }
+}
+
+module.exports = {
+ ByteParser
+}
diff --git a/vanilla/node_modules/undici/lib/web/websocket/sender.js b/vanilla/node_modules/undici/lib/web/websocket/sender.js
new file mode 100644
index 0000000..c647bf6
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/sender.js
@@ -0,0 +1,109 @@
+'use strict'
+
+const { WebsocketFrameSend } = require('./frame')
+const { opcodes, sendHints } = require('./constants')
+const FixedQueue = require('../../dispatcher/fixed-queue')
+
+/**
+ * @typedef {object} SendQueueNode
+ * @property {Promise<void> | null} promise
+ * @property {((...args: any[]) => any)} callback
+ * @property {Buffer | null} frame
+ */
+
+class SendQueue {
+ /**
+ * @type {FixedQueue}
+ */
+ #queue = new FixedQueue()
+
+ /**
+ * @type {boolean}
+ */
+ #running = false
+
+ /** @type {import('node:net').Socket} */
+ #socket
+
+ constructor (socket) {
+ this.#socket = socket
+ }
+
+ add (item, cb, hint) {
+ if (hint !== sendHints.blob) {
+ if (!this.#running) {
+ // TODO(@tsctx): support fast-path for string on running
+ if (hint === sendHints.text) {
+ // special fast-path for string
+ const { 0: head, 1: body } = WebsocketFrameSend.createFastTextFrame(item)
+ this.#socket.cork()
+ this.#socket.write(head)
+ this.#socket.write(body, cb)
+ this.#socket.uncork()
+ } else {
+ // direct writing
+ this.#socket.write(createFrame(item, hint), cb)
+ }
+ } else {
+ /** @type {SendQueueNode} */
+ const node = {
+ promise: null,
+ callback: cb,
+ frame: createFrame(item, hint)
+ }
+ this.#queue.push(node)
+ }
+ return
+ }
+
+ /** @type {SendQueueNode} */
+ const node = {
+ promise: item.arrayBuffer().then((ab) => {
+ node.promise = null
+ node.frame = createFrame(ab, hint)
+ }),
+ callback: cb,
+ frame: null
+ }
+
+ this.#queue.push(node)
+
+ if (!this.#running) {
+ this.#run()
+ }
+ }
+
+ async #run () {
+ this.#running = true
+ const queue = this.#queue
+ while (!queue.isEmpty()) {
+ const node = queue.shift()
+ // wait pending promise
+ if (node.promise !== null) {
+ await node.promise
+ }
+ // write
+ this.#socket.write(node.frame, node.callback)
+ // cleanup
+ node.callback = node.frame = null
+ }
+ this.#running = false
+ }
+}
+
+function createFrame (data, hint) {
+ return new WebsocketFrameSend(toBuffer(data, hint)).createFrame(hint === sendHints.text ? opcodes.TEXT : opcodes.BINARY)
+}
+
+function toBuffer (data, hint) {
+ switch (hint) {
+ case sendHints.text:
+ case sendHints.typedArray:
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
+ case sendHints.arrayBuffer:
+ case sendHints.blob:
+ return new Uint8Array(data)
+ }
+}
+
+module.exports = { SendQueue }
diff --git a/vanilla/node_modules/undici/lib/web/websocket/stream/websocketerror.js b/vanilla/node_modules/undici/lib/web/websocket/stream/websocketerror.js
new file mode 100644
index 0000000..a34c521
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/stream/websocketerror.js
@@ -0,0 +1,104 @@
+'use strict'
+
+const { webidl } = require('../../webidl')
+const { validateCloseCodeAndReason } = require('../util')
+const { kConstruct } = require('../../../core/symbols')
+const { kEnumerableProperty } = require('../../../core/util')
+
+function createInheritableDOMException () {
+ // https://github.com/nodejs/node/issues/59677
+ class Test extends DOMException {
+ get reason () {
+ return ''
+ }
+ }
+
+ if (new Test().reason !== undefined) {
+ return DOMException
+ }
+
+ return new Proxy(DOMException, {
+ construct (target, args, newTarget) {
+ const instance = Reflect.construct(target, args, target)
+ Object.setPrototypeOf(instance, newTarget.prototype)
+ return instance
+ }
+ })
+}
+
+class WebSocketError extends createInheritableDOMException() {
+ #closeCode
+ #reason
+
+ constructor (message = '', init = undefined) {
+ message = webidl.converters.DOMString(message, 'WebSocketError', 'message')
+
+ // 1. Set this 's name to " WebSocketError ".
+ // 2. Set this 's message to message .
+ super(message, 'WebSocketError')
+
+ if (init === kConstruct) {
+ return
+ } else if (init !== null) {
+ init = webidl.converters.WebSocketCloseInfo(init)
+ }
+
+ // 3. Let code be init [" closeCode "] if it exists , or null otherwise.
+ let code = init.closeCode ?? null
+
+ // 4. Let reason be init [" reason "] if it exists , or the empty string otherwise.
+ const reason = init.reason ?? ''
+
+ // 5. Validate close code and reason with code and reason .
+ validateCloseCodeAndReason(code, reason)
+
+ // 6. If reason is non-empty, but code is not set, then set code to 1000 ("Normal Closure").
+ if (reason.length !== 0 && code === null) {
+ code = 1000
+ }
+
+ // 7. Set this 's closeCode to code .
+ this.#closeCode = code
+
+ // 8. Set this 's reason to reason .
+ this.#reason = reason
+ }
+
+ get closeCode () {
+ return this.#closeCode
+ }
+
+ get reason () {
+ return this.#reason
+ }
+
+ /**
+ * @param {string} message
+ * @param {number|null} code
+ * @param {string} reason
+ */
+ static createUnvalidatedWebSocketError (message, code, reason) {
+ const error = new WebSocketError(message, kConstruct)
+ error.#closeCode = code
+ error.#reason = reason
+ return error
+ }
+}
+
+const { createUnvalidatedWebSocketError } = WebSocketError
+delete WebSocketError.createUnvalidatedWebSocketError
+
+Object.defineProperties(WebSocketError.prototype, {
+ closeCode: kEnumerableProperty,
+ reason: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'WebSocketError',
+ writable: false,
+ enumerable: false,
+ configurable: true
+ }
+})
+
+webidl.is.WebSocketError = webidl.util.MakeTypeAssertion(WebSocketError)
+
+module.exports = { WebSocketError, createUnvalidatedWebSocketError }
diff --git a/vanilla/node_modules/undici/lib/web/websocket/stream/websocketstream.js b/vanilla/node_modules/undici/lib/web/websocket/stream/websocketstream.js
new file mode 100644
index 0000000..ce3be84
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/stream/websocketstream.js
@@ -0,0 +1,497 @@
+'use strict'
+
+const { createDeferredPromise } = require('../../../util/promise')
+const { environmentSettingsObject } = require('../../fetch/util')
+const { states, opcodes, sentCloseFrameState } = require('../constants')
+const { webidl } = require('../../webidl')
+const { getURLRecord, isValidSubprotocol, isEstablished, utf8Decode } = require('../util')
+const { establishWebSocketConnection, failWebsocketConnection, closeWebSocketConnection } = require('../connection')
+const { channels } = require('../../../core/diagnostics')
+const { WebsocketFrameSend } = require('../frame')
+const { ByteParser } = require('../receiver')
+const { WebSocketError, createUnvalidatedWebSocketError } = require('./websocketerror')
+const { kEnumerableProperty } = require('../../../core/util')
+const { utf8DecodeBytes } = require('../../../encoding')
+
+let emittedExperimentalWarning = false
+
+class WebSocketStream {
+ // Each WebSocketStream object has an associated url , which is a URL record .
+ /** @type {URL} */
+ #url
+
+ // Each WebSocketStream object has an associated opened promise , which is a promise.
+ /** @type {import('../../../util/promise').DeferredPromise} */
+ #openedPromise
+
+ // Each WebSocketStream object has an associated closed promise , which is a promise.
+ /** @type {import('../../../util/promise').DeferredPromise} */
+ #closedPromise
+
+ // Each WebSocketStream object has an associated readable stream , which is a ReadableStream .
+ /** @type {ReadableStream} */
+ #readableStream
+ /** @type {ReadableStreamDefaultController} */
+ #readableStreamController
+
+ // Each WebSocketStream object has an associated writable stream , which is a WritableStream .
+ /** @type {WritableStream} */
+ #writableStream
+
+ // Each WebSocketStream object has an associated boolean handshake aborted , which is initially false.
+ #handshakeAborted = false
+
+ /** @type {import('../websocket').Handler} */
+ #handler = {
+ // https://whatpr.org/websockets/48/7b748d3...d5570f3.html#feedback-to-websocket-stream-from-the-protocol
+ onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
+ onMessage: (opcode, data) => this.#onMessage(opcode, data),
+ onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message),
+ onParserDrain: () => this.#handler.socket.resume(),
+ onSocketData: (chunk) => {
+ if (!this.#parser.write(chunk)) {
+ this.#handler.socket.pause()
+ }
+ },
+ onSocketError: (err) => {
+ this.#handler.readyState = states.CLOSING
+
+ if (channels.socketError.hasSubscribers) {
+ channels.socketError.publish(err)
+ }
+
+ this.#handler.socket.destroy()
+ },
+ onSocketClose: () => this.#onSocketClose(),
+ onPing: () => {},
+ onPong: () => {},
+
+ readyState: states.CONNECTING,
+ socket: null,
+ closeState: new Set(),
+ controller: null,
+ wasEverConnected: false
+ }
+
+ /** @type {import('../receiver').ByteParser} */
+ #parser
+
+ constructor (url, options = undefined) {
+ if (!emittedExperimentalWarning) {
+ process.emitWarning('WebSocketStream is experimental! Expect it to change at any time.', {
+ code: 'UNDICI-WSS'
+ })
+ emittedExperimentalWarning = true
+ }
+
+ webidl.argumentLengthCheck(arguments, 1, 'WebSocket')
+
+ url = webidl.converters.USVString(url)
+ if (options !== null) {
+ options = webidl.converters.WebSocketStreamOptions(options)
+ }
+
+ // 1. Let baseURL be this 's relevant settings object 's API base URL .
+ const baseURL = environmentSettingsObject.settingsObject.baseUrl
+
+ // 2. Let urlRecord be the result of getting a URL record given url and baseURL .
+ const urlRecord = getURLRecord(url, baseURL)
+
+ // 3. Let protocols be options [" protocols "] if it exists , otherwise an empty sequence.
+ const protocols = options.protocols
+
+ // 4. If any of the values in protocols occur more than once or otherwise fail to match the requirements for elements that comprise the value of ` Sec-WebSocket-Protocol ` fields as defined by The WebSocket Protocol , then throw a " SyntaxError " DOMException . [WSP]
+ if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) {
+ throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
+ }
+
+ if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) {
+ throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
+ }
+
+ // 5. Set this 's url to urlRecord .
+ this.#url = urlRecord.toString()
+
+ // 6. Set this 's opened promise and closed promise to new promises.
+ this.#openedPromise = createDeferredPromise()
+ this.#closedPromise = createDeferredPromise()
+
+ // 7. Apply backpressure to the WebSocket.
+ // TODO
+
+ // 8. If options [" signal "] exists ,
+ if (options.signal != null) {
+ // 8.1. Let signal be options [" signal "].
+ const signal = options.signal
+
+ // 8.2. If signal is aborted , then reject this 's opened promise and closed promise with signal ’s abort reason
+ // and return.
+ if (signal.aborted) {
+ this.#openedPromise.reject(signal.reason)
+ this.#closedPromise.reject(signal.reason)
+ return
+ }
+
+ // 8.3. Add the following abort steps to signal :
+ signal.addEventListener('abort', () => {
+ // 8.3.1. If the WebSocket connection is not yet established : [WSP]
+ if (!isEstablished(this.#handler.readyState)) {
+ // 8.3.1.1. Fail the WebSocket connection .
+ failWebsocketConnection(this.#handler)
+
+ // Set this 's ready state to CLOSING .
+ this.#handler.readyState = states.CLOSING
+
+ // Reject this 's opened promise and closed promise with signal ’s abort reason .
+ this.#openedPromise.reject(signal.reason)
+ this.#closedPromise.reject(signal.reason)
+
+ // Set this 's handshake aborted to true.
+ this.#handshakeAborted = true
+ }
+ }, { once: true })
+ }
+
+ // 9. Let client be this 's relevant settings object .
+ const client = environmentSettingsObject.settingsObject
+
+ // 10. Run this step in parallel :
+ // 10.1. Establish a WebSocket connection given urlRecord , protocols , and client . [FETCH]
+ this.#handler.controller = establishWebSocketConnection(
+ urlRecord,
+ protocols,
+ client,
+ this.#handler,
+ options
+ )
+ }
+
+ // The url getter steps are to return this 's url , serialized .
+ get url () {
+ return this.#url.toString()
+ }
+
+ // The opened getter steps are to return this 's opened promise .
+ get opened () {
+ return this.#openedPromise.promise
+ }
+
+ // The closed getter steps are to return this 's closed promise .
+ get closed () {
+ return this.#closedPromise.promise
+ }
+
+ // The close( closeInfo ) method steps are:
+ close (closeInfo = undefined) {
+ if (closeInfo !== null) {
+ closeInfo = webidl.converters.WebSocketCloseInfo(closeInfo)
+ }
+
+ // 1. Let code be closeInfo [" closeCode "] if present, or null otherwise.
+ const code = closeInfo.closeCode ?? null
+
+ // 2. Let reason be closeInfo [" reason "].
+ const reason = closeInfo.reason
+
+ // 3. Close the WebSocket with this , code , and reason .
+ closeWebSocketConnection(this.#handler, code, reason, true)
+ }
+
+ #write (chunk) {
+ // See /websockets/stream/tentative/write.any.html
+ chunk = webidl.converters.WebSocketStreamWrite(chunk)
+
+ // 1. Let promise be a new promise created in stream ’s relevant realm .
+ const promise = createDeferredPromise()
+
+ // 2. Let data be null.
+ let data = null
+
+ // 3. Let opcode be null.
+ let opcode = null
+
+ // 4. If chunk is a BufferSource ,
+ if (webidl.is.BufferSource(chunk)) {
+ // 4.1. Set data to a copy of the bytes given chunk .
+ data = new Uint8Array(ArrayBuffer.isView(chunk) ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength) : chunk.slice())
+
+ // 4.2. Set opcode to a binary frame opcode.
+ opcode = opcodes.BINARY
+ } else {
+ // 5. Otherwise,
+
+ // 5.1. Let string be the result of converting chunk to an IDL USVString .
+ // If this throws an exception, return a promise rejected with the exception.
+ let string
+
+ try {
+ string = webidl.converters.DOMString(chunk)
+ } catch (e) {
+ promise.reject(e)
+ return promise.promise
+ }
+
+ // 5.2. Set data to the result of UTF-8 encoding string .
+ data = new TextEncoder().encode(string)
+
+ // 5.3. Set opcode to a text frame opcode.
+ opcode = opcodes.TEXT
+ }
+
+ // 6. In parallel,
+ // 6.1. Wait until there is sufficient buffer space in stream to send the message.
+
+ // 6.2. If the closing handshake has not yet started , Send a WebSocket Message to stream comprised of data using opcode .
+ if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
+ const frame = new WebsocketFrameSend(data)
+
+ this.#handler.socket.write(frame.createFrame(opcode), () => {
+ promise.resolve(undefined)
+ })
+ }
+
+ // 6.3. Queue a global task on the WebSocket task source given stream ’s relevant global object to resolve promise with undefined.
+ return promise.promise
+ }
+
+ /** @type {import('../websocket').Handler['onConnectionEstablished']} */
+ #onConnectionEstablished (response, parsedExtensions) {
+ this.#handler.socket = response.socket
+
+ const parser = new ByteParser(this.#handler, parsedExtensions)
+ parser.on('drain', () => this.#handler.onParserDrain())
+ parser.on('error', (err) => this.#handler.onParserError(err))
+
+ this.#parser = parser
+
+ // 1. Change stream ’s ready state to OPEN (1).
+ this.#handler.readyState = states.OPEN
+
+ // 2. Set stream ’s was ever connected to true.
+ // This is done in the opening handshake.
+
+ // 3. Let extensions be the extensions in use .
+ const extensions = parsedExtensions ?? ''
+
+ // 4. Let protocol be the subprotocol in use .
+ const protocol = response.headersList.get('sec-websocket-protocol') ?? ''
+
+ // 5. Let pullAlgorithm be an action that pulls bytes from stream .
+ // 6. Let cancelAlgorithm be an action that cancels stream with reason , given reason .
+ // 7. Let readable be a new ReadableStream .
+ // 8. Set up readable with pullAlgorithm and cancelAlgorithm .
+ const readable = new ReadableStream({
+ start: (controller) => {
+ this.#readableStreamController = controller
+ },
+ pull (controller) {
+ let chunk
+ while (controller.desiredSize > 0 && (chunk = response.socket.read()) !== null) {
+ controller.enqueue(chunk)
+ }
+ },
+ cancel: (reason) => this.#cancel(reason)
+ })
+
+ // 9. Let writeAlgorithm be an action that writes chunk to stream , given chunk .
+ // 10. Let closeAlgorithm be an action that closes stream .
+ // 11. Let abortAlgorithm be an action that aborts stream with reason , given reason .
+ // 12. Let writable be a new WritableStream .
+ // 13. Set up writable with writeAlgorithm , closeAlgorithm , and abortAlgorithm .
+ const writable = new WritableStream({
+ write: (chunk) => this.#write(chunk),
+ close: () => closeWebSocketConnection(this.#handler, null, null),
+ abort: (reason) => this.#closeUsingReason(reason)
+ })
+
+ // Set stream ’s readable stream to readable .
+ this.#readableStream = readable
+
+ // Set stream ’s writable stream to writable .
+ this.#writableStream = writable
+
+ // Resolve stream ’s opened promise with WebSocketOpenInfo «[ " extensions " → extensions , " protocol " → protocol , " readable " → readable , " writable " → writable ]».
+ this.#openedPromise.resolve({
+ extensions,
+ protocol,
+ readable,
+ writable
+ })
+ }
+
+ /** @type {import('../websocket').Handler['onMessage']} */
+ #onMessage (type, data) {
+ // 1. If stream’s ready state is not OPEN (1), then return.
+ if (this.#handler.readyState !== states.OPEN) {
+ return
+ }
+
+ // 2. Let chunk be determined by switching on type:
+ // - type indicates that the data is Text
+ // a new DOMString containing data
+ // - type indicates that the data is Binary
+ // a new Uint8Array object, created in the relevant Realm of the
+ // WebSocketStream object, whose contents are data
+ let chunk
+
+ if (type === opcodes.TEXT) {
+ try {
+ chunk = utf8Decode(data)
+ } catch {
+ failWebsocketConnection(this.#handler, 'Received invalid UTF-8 in text frame.')
+ return
+ }
+ } else if (type === opcodes.BINARY) {
+ chunk = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
+ }
+
+ // 3. Enqueue chunk into stream’s readable stream.
+ this.#readableStreamController.enqueue(chunk)
+
+ // 4. Apply backpressure to the WebSocket.
+ }
+
+ /** @type {import('../websocket').Handler['onSocketClose']} */
+ #onSocketClose () {
+ const wasClean =
+ this.#handler.closeState.has(sentCloseFrameState.SENT) &&
+ this.#handler.closeState.has(sentCloseFrameState.RECEIVED)
+
+ // 1. Change the ready state to CLOSED (3).
+ this.#handler.readyState = states.CLOSED
+
+ // 2. If stream ’s handshake aborted is true, then return.
+ if (this.#handshakeAborted) {
+ return
+ }
+
+ // 3. If stream ’s was ever connected is false, then reject stream ’s opened promise with a new WebSocketError.
+ if (!this.#handler.wasEverConnected) {
+ this.#openedPromise.reject(new WebSocketError('Socket never opened'))
+ }
+
+ const result = this.#parser?.closingInfo
+
+ // 4. Let code be the WebSocket connection close code .
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
+ // If this Close control frame contains no status code, _The WebSocket
+ // Connection Close Code_ is considered to be 1005. If _The WebSocket
+ // Connection is Closed_ and no Close control frame was received by the
+ // endpoint (such as could occur if the underlying transport connection
+ // is lost), _The WebSocket Connection Close Code_ is considered to be
+ // 1006.
+ let code = result?.code ?? 1005
+
+ if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
+ code = 1006
+ }
+
+ // 5. Let reason be the result of applying UTF-8 decode without BOM to the WebSocket connection close reason .
+ const reason = result?.reason == null ? '' : utf8DecodeBytes(Buffer.from(result.reason))
+
+ // 6. If the connection was closed cleanly ,
+ if (wasClean) {
+ // 6.1. Close stream ’s readable stream .
+ this.#readableStreamController.close()
+
+ // 6.2. Error stream ’s writable stream with an " InvalidStateError " DOMException indicating that a closed WebSocketStream cannot be written to.
+ if (!this.#writableStream.locked) {
+ this.#writableStream.abort(new DOMException('A closed WebSocketStream cannot be written to', 'InvalidStateError'))
+ }
+
+ // 6.3. Resolve stream ’s closed promise with WebSocketCloseInfo «[ " closeCode " → code , " reason " → reason ]».
+ this.#closedPromise.resolve({
+ closeCode: code,
+ reason
+ })
+ } else {
+ // 7. Otherwise,
+
+ // 7.1. Let error be a new WebSocketError whose closeCode is code and reason is reason .
+ const error = createUnvalidatedWebSocketError('unclean close', code, reason)
+
+ // 7.2. Error stream ’s readable stream with error .
+ this.#readableStreamController?.error(error)
+
+ // 7.3. Error stream ’s writable stream with error .
+ this.#writableStream?.abort(error)
+
+ // 7.4. Reject stream ’s closed promise with error .
+ this.#closedPromise.reject(error)
+ }
+ }
+
+ #closeUsingReason (reason) {
+ // 1. Let code be null.
+ let code = null
+
+ // 2. Let reasonString be the empty string.
+ let reasonString = ''
+
+ // 3. If reason implements WebSocketError ,
+ if (webidl.is.WebSocketError(reason)) {
+ // 3.1. Set code to reason ’s closeCode .
+ code = reason.closeCode
+
+ // 3.2. Set reasonString to reason ’s reason .
+ reasonString = reason.reason
+ }
+
+ // 4. Close the WebSocket with stream , code , and reasonString . If this throws an exception,
+ // discard code and reasonString and close the WebSocket with stream .
+ closeWebSocketConnection(this.#handler, code, reasonString)
+ }
+
+ // To cancel a WebSocketStream stream given reason , close using reason giving stream and reason .
+ #cancel (reason) {
+ this.#closeUsingReason(reason)
+ }
+}
+
+Object.defineProperties(WebSocketStream.prototype, {
+ url: kEnumerableProperty,
+ opened: kEnumerableProperty,
+ closed: kEnumerableProperty,
+ close: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'WebSocketStream',
+ writable: false,
+ enumerable: false,
+ configurable: true
+ }
+})
+
+webidl.converters.WebSocketStreamOptions = webidl.dictionaryConverter([
+ {
+ key: 'protocols',
+ converter: webidl.sequenceConverter(webidl.converters.USVString),
+ defaultValue: () => []
+ },
+ {
+ key: 'signal',
+ converter: webidl.nullableConverter(webidl.converters.AbortSignal),
+ defaultValue: () => null
+ }
+])
+
+webidl.converters.WebSocketCloseInfo = webidl.dictionaryConverter([
+ {
+ key: 'closeCode',
+ converter: (V) => webidl.converters['unsigned short'](V, webidl.attributes.EnforceRange)
+ },
+ {
+ key: 'reason',
+ converter: webidl.converters.USVString,
+ defaultValue: () => ''
+ }
+])
+
+webidl.converters.WebSocketStreamWrite = function (V) {
+ if (typeof V === 'string') {
+ return webidl.converters.USVString(V)
+ }
+
+ return webidl.converters.BufferSource(V)
+}
+
+module.exports = { WebSocketStream }
diff --git a/vanilla/node_modules/undici/lib/web/websocket/util.js b/vanilla/node_modules/undici/lib/web/websocket/util.js
new file mode 100644
index 0000000..eaa5e7a
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/util.js
@@ -0,0 +1,339 @@
+'use strict'
+
+const { states, opcodes } = require('./constants')
+const { isUtf8 } = require('node:buffer')
+const { removeHTTPWhitespace } = require('../fetch/data-url')
+const { collectASequenceOfCodePointsFast } = require('../infra')
+
+/**
+ * @param {number} readyState
+ * @returns {boolean}
+ */
+function isConnecting (readyState) {
+ // If the WebSocket connection is not yet established, and the connection
+ // is not yet closed, then the WebSocket connection is in the CONNECTING state.
+ return readyState === states.CONNECTING
+}
+
+/**
+ * @param {number} readyState
+ * @returns {boolean}
+ */
+function isEstablished (readyState) {
+ // If the server's response is validated as provided for above, it is
+ // said that _The WebSocket Connection is Established_ and that the
+ // WebSocket Connection is in the OPEN state.
+ return readyState === states.OPEN
+}
+
+/**
+ * @param {number} readyState
+ * @returns {boolean}
+ */
+function isClosing (readyState) {
+ // Upon either sending or receiving a Close control frame, it is said
+ // that _The WebSocket Closing Handshake is Started_ and that the
+ // WebSocket connection is in the CLOSING state.
+ return readyState === states.CLOSING
+}
+
+/**
+ * @param {number} readyState
+ * @returns {boolean}
+ */
+function isClosed (readyState) {
+ return readyState === states.CLOSED
+}
+
+/**
+ * @see https://dom.spec.whatwg.org/#concept-event-fire
+ * @param {string} e
+ * @param {EventTarget} target
+ * @param {(...args: ConstructorParameters<typeof Event>) => Event} eventFactory
+ * @param {EventInit | undefined} eventInitDict
+ * @returns {void}
+ */
+function fireEvent (e, target, eventFactory = (type, init) => new Event(type, init), eventInitDict = {}) {
+ // 1. If eventConstructor is not given, then let eventConstructor be Event.
+
+ // 2. Let event be the result of creating an event given eventConstructor,
+ // in the relevant realm of target.
+ // 3. Initialize event’s type attribute to e.
+ const event = eventFactory(e, eventInitDict)
+
+ // 4. Initialize any other IDL attributes of event as described in the
+ // invocation of this algorithm.
+
+ // 5. Return the result of dispatching event at target, with legacy target
+ // override flag set if set.
+ target.dispatchEvent(event)
+}
+
+/**
+ * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
+ * @param {import('./websocket').Handler} handler
+ * @param {number} type Opcode
+ * @param {Buffer} data application data
+ * @returns {void}
+ */
+function websocketMessageReceived (handler, type, data) {
+ handler.onMessage(type, data)
+}
+
+/**
+ * @param {Buffer} buffer
+ * @returns {ArrayBuffer}
+ */
+function toArrayBuffer (buffer) {
+ if (buffer.byteLength === buffer.buffer.byteLength) {
+ return buffer.buffer
+ }
+ return new Uint8Array(buffer).buffer
+}
+
+/**
+ * @see https://datatracker.ietf.org/doc/html/rfc6455
+ * @see https://datatracker.ietf.org/doc/html/rfc2616
+ * @see https://bugs.chromium.org/p/chromium/issues/detail?id=398407
+ * @param {string} protocol
+ * @returns {boolean}
+ */
+function isValidSubprotocol (protocol) {
+ // If present, this value indicates one
+ // or more comma-separated subprotocol the client wishes to speak,
+ // ordered by preference. The elements that comprise this value
+ // MUST be non-empty strings with characters in the range U+0021 to
+ // U+007E not including separator characters as defined in
+ // [RFC2616] and MUST all be unique strings.
+ if (protocol.length === 0) {
+ return false
+ }
+
+ for (let i = 0; i < protocol.length; ++i) {
+ const code = protocol.charCodeAt(i)
+
+ if (
+ code < 0x21 || // CTL, contains SP (0x20) and HT (0x09)
+ code > 0x7E ||
+ code === 0x22 || // "
+ code === 0x28 || // (
+ code === 0x29 || // )
+ code === 0x2C || // ,
+ code === 0x2F || // /
+ code === 0x3A || // :
+ code === 0x3B || // ;
+ code === 0x3C || // <
+ code === 0x3D || // =
+ code === 0x3E || // >
+ code === 0x3F || // ?
+ code === 0x40 || // @
+ code === 0x5B || // [
+ code === 0x5C || // \
+ code === 0x5D || // ]
+ code === 0x7B || // {
+ code === 0x7D // }
+ ) {
+ return false
+ }
+ }
+
+ return true
+}
+
+/**
+ * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7-4
+ * @param {number} code
+ * @returns {boolean}
+ */
+function isValidStatusCode (code) {
+ if (code >= 1000 && code < 1015) {
+ return (
+ code !== 1004 && // reserved
+ code !== 1005 && // "MUST NOT be set as a status code"
+ code !== 1006 // "MUST NOT be set as a status code"
+ )
+ }
+
+ return code >= 3000 && code <= 4999
+}
+
+/**
+ * @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5
+ * @param {number} opcode
+ * @returns {boolean}
+ */
+function isControlFrame (opcode) {
+ return (
+ opcode === opcodes.CLOSE ||
+ opcode === opcodes.PING ||
+ opcode === opcodes.PONG
+ )
+}
+
+/**
+ * @param {number} opcode
+ * @returns {boolean}
+ */
+function isContinuationFrame (opcode) {
+ return opcode === opcodes.CONTINUATION
+}
+
+/**
+ * @param {number} opcode
+ * @returns {boolean}
+ */
+function isTextBinaryFrame (opcode) {
+ return opcode === opcodes.TEXT || opcode === opcodes.BINARY
+}
+
+/**
+ *
+ * @param {number} opcode
+ * @returns {boolean}
+ */
+function isValidOpcode (opcode) {
+ return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
+}
+
+/**
+ * Parses a Sec-WebSocket-Extensions header value.
+ * @param {string} extensions
+ * @returns {Map<string, string>}
+ */
+// TODO(@Uzlopak, @KhafraDev): make compliant https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
+function parseExtensions (extensions) {
+ const position = { position: 0 }
+ const extensionList = new Map()
+
+ while (position.position < extensions.length) {
+ const pair = collectASequenceOfCodePointsFast(';', extensions, position)
+ const [name, value = ''] = pair.split('=', 2)
+
+ extensionList.set(
+ removeHTTPWhitespace(name, true, false),
+ removeHTTPWhitespace(value, false, true)
+ )
+
+ position.position++
+ }
+
+ return extensionList
+}
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc7692#section-7.1.2.2
+ * @description "client-max-window-bits = 1*DIGIT"
+ * @param {string} value
+ * @returns {boolean}
+ */
+function isValidClientWindowBits (value) {
+ for (let i = 0; i < value.length; i++) {
+ const byte = value.charCodeAt(i)
+
+ if (byte < 0x30 || byte > 0x39) {
+ return false
+ }
+ }
+
+ return true
+}
+
+/**
+ * @see https://whatpr.org/websockets/48/7b748d3...d5570f3.html#get-a-url-record
+ * @param {string} url
+ * @param {string} [baseURL]
+ */
+function getURLRecord (url, baseURL) {
+ // 1. Let urlRecord be the result of applying the URL parser to url with baseURL .
+ // 2. If urlRecord is failure, then throw a " SyntaxError " DOMException .
+ let urlRecord
+
+ try {
+ urlRecord = new URL(url, baseURL)
+ } catch (e) {
+ throw new DOMException(e, 'SyntaxError')
+ }
+
+ // 3. If urlRecord ’s scheme is " http ", then set urlRecord ’s scheme to " ws ".
+ // 4. Otherwise, if urlRecord ’s scheme is " https ", set urlRecord ’s scheme to " wss ".
+ if (urlRecord.protocol === 'http:') {
+ urlRecord.protocol = 'ws:'
+ } else if (urlRecord.protocol === 'https:') {
+ urlRecord.protocol = 'wss:'
+ }
+
+ // 5. If urlRecord ’s scheme is not " ws " or " wss ", then throw a " SyntaxError " DOMException .
+ if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') {
+ throw new DOMException('expected a ws: or wss: url', 'SyntaxError')
+ }
+
+ // If urlRecord ’s fragment is non-null, then throw a " SyntaxError " DOMException .
+ if (urlRecord.hash.length || urlRecord.href.endsWith('#')) {
+ throw new DOMException('hash', 'SyntaxError')
+ }
+
+ // Return urlRecord .
+ return urlRecord
+}
+
+// https://whatpr.org/websockets/48.html#validate-close-code-and-reason
+function validateCloseCodeAndReason (code, reason) {
+ // 1. If code is not null, but is neither an integer equal to
+ // 1000 nor an integer in the range 3000 to 4999, inclusive,
+ // throw an "InvalidAccessError" DOMException.
+ if (code !== null) {
+ if (code !== 1000 && (code < 3000 || code > 4999)) {
+ throw new DOMException('invalid code', 'InvalidAccessError')
+ }
+ }
+
+ // 2. If reason is not null, then:
+ if (reason !== null) {
+ // 2.1. Let reasonBytes be the result of UTF-8 encoding reason.
+ // 2.2. If reasonBytes is longer than 123 bytes, then throw a
+ // "SyntaxError" DOMException.
+ const reasonBytesLength = Buffer.byteLength(reason)
+
+ if (reasonBytesLength > 123) {
+ throw new DOMException(`Reason must be less than 123 bytes; received ${reasonBytesLength}`, 'SyntaxError')
+ }
+ }
+}
+
+/**
+ * Converts a Buffer to utf-8, even on platforms without icu.
+ * @type {(buffer: Buffer) => string}
+ */
+const utf8Decode = (() => {
+ if (typeof process.versions.icu === 'string') {
+ const fatalDecoder = new TextDecoder('utf-8', { fatal: true })
+ return fatalDecoder.decode.bind(fatalDecoder)
+ }
+ return function (buffer) {
+ if (isUtf8(buffer)) {
+ return buffer.toString('utf-8')
+ }
+ throw new TypeError('Invalid utf-8 received.')
+ }
+})()
+
+module.exports = {
+ isConnecting,
+ isEstablished,
+ isClosing,
+ isClosed,
+ fireEvent,
+ isValidSubprotocol,
+ isValidStatusCode,
+ websocketMessageReceived,
+ utf8Decode,
+ isControlFrame,
+ isContinuationFrame,
+ isTextBinaryFrame,
+ isValidOpcode,
+ parseExtensions,
+ isValidClientWindowBits,
+ toArrayBuffer,
+ getURLRecord,
+ validateCloseCodeAndReason
+}
diff --git a/vanilla/node_modules/undici/lib/web/websocket/websocket.js b/vanilla/node_modules/undici/lib/web/websocket/websocket.js
new file mode 100644
index 0000000..3314976
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/websocket.js
@@ -0,0 +1,739 @@
+'use strict'
+
+const { isArrayBuffer } = require('node:util/types')
+const { webidl } = require('../webidl')
+const { URLSerializer } = require('../fetch/data-url')
+const { environmentSettingsObject } = require('../fetch/util')
+const { staticPropertyDescriptors, states, sentCloseFrameState, sendHints, opcodes } = require('./constants')
+const {
+ isConnecting,
+ isEstablished,
+ isClosing,
+ isClosed,
+ isValidSubprotocol,
+ fireEvent,
+ utf8Decode,
+ toArrayBuffer,
+ getURLRecord
+} = require('./util')
+const { establishWebSocketConnection, closeWebSocketConnection, failWebsocketConnection } = require('./connection')
+const { ByteParser } = require('./receiver')
+const { kEnumerableProperty } = require('../../core/util')
+const { getGlobalDispatcher } = require('../../global')
+const { ErrorEvent, CloseEvent, createFastMessageEvent } = require('./events')
+const { SendQueue } = require('./sender')
+const { WebsocketFrameSend } = require('./frame')
+const { channels } = require('../../core/diagnostics')
+
+/**
+ * @typedef {object} Handler
+ * @property {(response: any, extensions?: string[]) => void} onConnectionEstablished
+ * @property {(opcode: number, data: Buffer) => void} onMessage
+ * @property {(error: Error) => void} onParserError
+ * @property {() => void} onParserDrain
+ * @property {(chunk: Buffer) => void} onSocketData
+ * @property {(err: Error) => void} onSocketError
+ * @property {() => void} onSocketClose
+ * @property {(body: Buffer) => void} onPing
+ * @property {(body: Buffer) => void} onPong
+ *
+ * @property {number} readyState
+ * @property {import('stream').Duplex} socket
+ * @property {Set<number>} closeState
+ * @property {import('../fetch/index').Fetch} controller
+ * @property {boolean} [wasEverConnected=false]
+ */
+
+// https://websockets.spec.whatwg.org/#interface-definition
+class WebSocket extends EventTarget {
+ #events = {
+ open: null,
+ error: null,
+ close: null,
+ message: null
+ }
+
+ #bufferedAmount = 0
+ #protocol = ''
+ #extensions = ''
+
+ /** @type {SendQueue} */
+ #sendQueue
+
+ /** @type {Handler} */
+ #handler = {
+ onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
+ onMessage: (opcode, data) => this.#onMessage(opcode, data),
+ onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message),
+ onParserDrain: () => this.#onParserDrain(),
+ onSocketData: (chunk) => {
+ if (!this.#parser.write(chunk)) {
+ this.#handler.socket.pause()
+ }
+ },
+ onSocketError: (err) => {
+ this.#handler.readyState = states.CLOSING
+
+ if (channels.socketError.hasSubscribers) {
+ channels.socketError.publish(err)
+ }
+
+ this.#handler.socket.destroy()
+ },
+ onSocketClose: () => this.#onSocketClose(),
+ onPing: (body) => {
+ if (channels.ping.hasSubscribers) {
+ channels.ping.publish({
+ payload: body,
+ websocket: this
+ })
+ }
+ },
+ onPong: (body) => {
+ if (channels.pong.hasSubscribers) {
+ channels.pong.publish({
+ payload: body,
+ websocket: this
+ })
+ }
+ },
+
+ readyState: states.CONNECTING,
+ socket: null,
+ closeState: new Set(),
+ controller: null,
+ wasEverConnected: false
+ }
+
+ #url
+ #binaryType
+ /** @type {import('./receiver').ByteParser} */
+ #parser
+
+ /**
+ * @param {string} url
+ * @param {string|string[]} protocols
+ */
+ constructor (url, protocols = []) {
+ super()
+
+ webidl.util.markAsUncloneable(this)
+
+ const prefix = 'WebSocket constructor'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ const options = webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'](protocols, prefix, 'options')
+
+ url = webidl.converters.USVString(url)
+ protocols = options.protocols
+
+ // 1. Let baseURL be this's relevant settings object's API base URL.
+ const baseURL = environmentSettingsObject.settingsObject.baseUrl
+
+ // 2. Let urlRecord be the result of getting a URL record given url and baseURL.
+ const urlRecord = getURLRecord(url, baseURL)
+
+ // 3. If protocols is a string, set protocols to a sequence consisting
+ // of just that string.
+ if (typeof protocols === 'string') {
+ protocols = [protocols]
+ }
+
+ // 4. If any of the values in protocols occur more than once or otherwise
+ // fail to match the requirements for elements that comprise the value
+ // of `Sec-WebSocket-Protocol` fields as defined by The WebSocket
+ // protocol, then throw a "SyntaxError" DOMException.
+ if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) {
+ throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
+ }
+
+ if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) {
+ throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
+ }
+
+ // 5. Set this's url to urlRecord.
+ this.#url = new URL(urlRecord.href)
+
+ // 6. Let client be this's relevant settings object.
+ const client = environmentSettingsObject.settingsObject
+
+ // 7. Run this step in parallel:
+ // 7.1. Establish a WebSocket connection given urlRecord, protocols,
+ // and client.
+ this.#handler.controller = establishWebSocketConnection(
+ urlRecord,
+ protocols,
+ client,
+ this.#handler,
+ options
+ )
+
+ // Each WebSocket object has an associated ready state, which is a
+ // number representing the state of the connection. Initially it must
+ // be CONNECTING (0).
+ this.#handler.readyState = WebSocket.CONNECTING
+
+ // The extensions attribute must initially return the empty string.
+
+ // The protocol attribute must initially return the empty string.
+
+ // Each WebSocket object has an associated binary type, which is a
+ // BinaryType. Initially it must be "blob".
+ this.#binaryType = 'blob'
+ }
+
+ /**
+ * @see https://websockets.spec.whatwg.org/#dom-websocket-close
+ * @param {number|undefined} code
+ * @param {string|undefined} reason
+ */
+ close (code = undefined, reason = undefined) {
+ webidl.brandCheck(this, WebSocket)
+
+ const prefix = 'WebSocket.close'
+
+ if (code !== undefined) {
+ code = webidl.converters['unsigned short'](code, prefix, 'code', webidl.attributes.Clamp)
+ }
+
+ if (reason !== undefined) {
+ reason = webidl.converters.USVString(reason)
+ }
+
+ // 1. If code is the special value "missing", then set code to null.
+ code ??= null
+
+ // 2. If reason is the special value "missing", then set reason to the empty string.
+ reason ??= ''
+
+ // 3. Close the WebSocket with this, code, and reason.
+ closeWebSocketConnection(this.#handler, code, reason, true)
+ }
+
+ /**
+ * @see https://websockets.spec.whatwg.org/#dom-websocket-send
+ * @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data
+ */
+ send (data) {
+ webidl.brandCheck(this, WebSocket)
+
+ const prefix = 'WebSocket.send'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ data = webidl.converters.WebSocketSendData(data, prefix, 'data')
+
+ // 1. If this's ready state is CONNECTING, then throw an
+ // "InvalidStateError" DOMException.
+ if (isConnecting(this.#handler.readyState)) {
+ throw new DOMException('Sent before connected.', 'InvalidStateError')
+ }
+
+ // 2. Run the appropriate set of steps from the following list:
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-6.1
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
+
+ if (!isEstablished(this.#handler.readyState) || isClosing(this.#handler.readyState)) {
+ return
+ }
+
+ // If data is a string
+ if (typeof data === 'string') {
+ // If the WebSocket connection is established and the WebSocket
+ // closing handshake has not yet started, then the user agent
+ // must send a WebSocket Message comprised of the data argument
+ // using a text frame opcode; if the data cannot be sent, e.g.
+ // because it would need to be buffered but the buffer is full,
+ // the user agent must flag the WebSocket as full and then close
+ // the WebSocket connection. Any invocation of this method with a
+ // string argument that does not throw an exception must increase
+ // the bufferedAmount attribute by the number of bytes needed to
+ // express the argument as UTF-8.
+
+ const buffer = Buffer.from(data)
+
+ this.#bufferedAmount += buffer.byteLength
+ this.#sendQueue.add(buffer, () => {
+ this.#bufferedAmount -= buffer.byteLength
+ }, sendHints.text)
+ } else if (isArrayBuffer(data)) {
+ // If the WebSocket connection is established, and the WebSocket
+ // closing handshake has not yet started, then the user agent must
+ // send a WebSocket Message comprised of data using a binary frame
+ // opcode; if the data cannot be sent, e.g. because it would need
+ // to be buffered but the buffer is full, the user agent must flag
+ // the WebSocket as full and then close the WebSocket connection.
+ // The data to be sent is the data stored in the buffer described
+ // by the ArrayBuffer object. Any invocation of this method with an
+ // ArrayBuffer argument that does not throw an exception must
+ // increase the bufferedAmount attribute by the length of the
+ // ArrayBuffer in bytes.
+
+ this.#bufferedAmount += data.byteLength
+ this.#sendQueue.add(data, () => {
+ this.#bufferedAmount -= data.byteLength
+ }, sendHints.arrayBuffer)
+ } else if (ArrayBuffer.isView(data)) {
+ // If the WebSocket connection is established, and the WebSocket
+ // closing handshake has not yet started, then the user agent must
+ // send a WebSocket Message comprised of data using a binary frame
+ // opcode; if the data cannot be sent, e.g. because it would need to
+ // be buffered but the buffer is full, the user agent must flag the
+ // WebSocket as full and then close the WebSocket connection. The
+ // data to be sent is the data stored in the section of the buffer
+ // described by the ArrayBuffer object that data references. Any
+ // invocation of this method with this kind of argument that does
+ // not throw an exception must increase the bufferedAmount attribute
+ // by the length of data’s buffer in bytes.
+
+ this.#bufferedAmount += data.byteLength
+ this.#sendQueue.add(data, () => {
+ this.#bufferedAmount -= data.byteLength
+ }, sendHints.typedArray)
+ } else if (webidl.is.Blob(data)) {
+ // If the WebSocket connection is established, and the WebSocket
+ // closing handshake has not yet started, then the user agent must
+ // send a WebSocket Message comprised of data using a binary frame
+ // opcode; if the data cannot be sent, e.g. because it would need to
+ // be buffered but the buffer is full, the user agent must flag the
+ // WebSocket as full and then close the WebSocket connection. The data
+ // to be sent is the raw data represented by the Blob object. Any
+ // invocation of this method with a Blob argument that does not throw
+ // an exception must increase the bufferedAmount attribute by the size
+ // of the Blob object’s raw data, in bytes.
+
+ this.#bufferedAmount += data.size
+ this.#sendQueue.add(data, () => {
+ this.#bufferedAmount -= data.size
+ }, sendHints.blob)
+ }
+ }
+
+ get readyState () {
+ webidl.brandCheck(this, WebSocket)
+
+ // The readyState getter steps are to return this's ready state.
+ return this.#handler.readyState
+ }
+
+ get bufferedAmount () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#bufferedAmount
+ }
+
+ get url () {
+ webidl.brandCheck(this, WebSocket)
+
+ // The url getter steps are to return this's url, serialized.
+ return URLSerializer(this.#url)
+ }
+
+ get extensions () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#extensions
+ }
+
+ get protocol () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#protocol
+ }
+
+ get onopen () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#events.open
+ }
+
+ set onopen (fn) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (this.#events.open) {
+ this.removeEventListener('open', this.#events.open)
+ }
+
+ const listener = webidl.converters.EventHandlerNonNull(fn)
+
+ if (listener !== null) {
+ this.addEventListener('open', listener)
+ this.#events.open = fn
+ } else {
+ this.#events.open = null
+ }
+ }
+
+ get onerror () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#events.error
+ }
+
+ set onerror (fn) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (this.#events.error) {
+ this.removeEventListener('error', this.#events.error)
+ }
+
+ const listener = webidl.converters.EventHandlerNonNull(fn)
+
+ if (listener !== null) {
+ this.addEventListener('error', listener)
+ this.#events.error = fn
+ } else {
+ this.#events.error = null
+ }
+ }
+
+ get onclose () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#events.close
+ }
+
+ set onclose (fn) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (this.#events.close) {
+ this.removeEventListener('close', this.#events.close)
+ }
+
+ const listener = webidl.converters.EventHandlerNonNull(fn)
+
+ if (listener !== null) {
+ this.addEventListener('close', listener)
+ this.#events.close = fn
+ } else {
+ this.#events.close = null
+ }
+ }
+
+ get onmessage () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#events.message
+ }
+
+ set onmessage (fn) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (this.#events.message) {
+ this.removeEventListener('message', this.#events.message)
+ }
+
+ const listener = webidl.converters.EventHandlerNonNull(fn)
+
+ if (listener !== null) {
+ this.addEventListener('message', listener)
+ this.#events.message = fn
+ } else {
+ this.#events.message = null
+ }
+ }
+
+ get binaryType () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#binaryType
+ }
+
+ set binaryType (type) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (type !== 'blob' && type !== 'arraybuffer') {
+ this.#binaryType = 'blob'
+ } else {
+ this.#binaryType = type
+ }
+ }
+
+ /**
+ * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
+ */
+ #onConnectionEstablished (response, parsedExtensions) {
+ // processResponse is called when the "response’s header list has been received and initialized."
+ // once this happens, the connection is open
+ this.#handler.socket = response.socket
+
+ const parser = new ByteParser(this.#handler, parsedExtensions)
+ parser.on('drain', () => this.#handler.onParserDrain())
+ parser.on('error', (err) => this.#handler.onParserError(err))
+
+ this.#parser = parser
+ this.#sendQueue = new SendQueue(response.socket)
+
+ // 1. Change the ready state to OPEN (1).
+ this.#handler.readyState = states.OPEN
+
+ // 2. Change the extensions attribute’s value to the extensions in use, if
+ // it is not the null value.
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
+ const extensions = response.headersList.get('sec-websocket-extensions')
+
+ if (extensions !== null) {
+ this.#extensions = extensions
+ }
+
+ // 3. Change the protocol attribute’s value to the subprotocol in use, if
+ // it is not the null value.
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9
+ const protocol = response.headersList.get('sec-websocket-protocol')
+
+ if (protocol !== null) {
+ this.#protocol = protocol
+ }
+
+ // 4. Fire an event named open at the WebSocket object.
+ fireEvent('open', this)
+
+ if (channels.open.hasSubscribers) {
+ // Convert headers to a plain object for the event
+ const headers = response.headersList.entries
+ channels.open.publish({
+ address: response.socket.address(),
+ protocol: this.#protocol,
+ extensions: this.#extensions,
+ websocket: this,
+ handshakeResponse: {
+ status: response.status,
+ statusText: response.statusText,
+ headers
+ }
+ })
+ }
+ }
+
+ #onMessage (type, data) {
+ // 1. If ready state is not OPEN (1), then return.
+ if (this.#handler.readyState !== states.OPEN) {
+ return
+ }
+
+ // 2. Let dataForEvent be determined by switching on type and binary type:
+ let dataForEvent
+
+ if (type === opcodes.TEXT) {
+ // -> type indicates that the data is Text
+ // a new DOMString containing data
+ try {
+ dataForEvent = utf8Decode(data)
+ } catch {
+ failWebsocketConnection(this.#handler, 1007, 'Received invalid UTF-8 in text frame.')
+ return
+ }
+ } else if (type === opcodes.BINARY) {
+ if (this.#binaryType === 'blob') {
+ // -> type indicates that the data is Binary and binary type is "blob"
+ // a new Blob object, created in the relevant Realm of the WebSocket
+ // object, that represents data as its raw data
+ dataForEvent = new Blob([data])
+ } else {
+ // -> type indicates that the data is Binary and binary type is "arraybuffer"
+ // a new ArrayBuffer object, created in the relevant Realm of the
+ // WebSocket object, whose contents are data
+ dataForEvent = toArrayBuffer(data)
+ }
+ }
+
+ // 3. Fire an event named message at the WebSocket object, using MessageEvent,
+ // with the origin attribute initialized to the serialization of the WebSocket
+ // object’s url's origin, and the data attribute initialized to dataForEvent.
+ fireEvent('message', this, createFastMessageEvent, {
+ origin: this.#url.origin,
+ data: dataForEvent
+ })
+ }
+
+ #onParserDrain () {
+ this.#handler.socket.resume()
+ }
+
+ /**
+ * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
+ * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4
+ */
+ #onSocketClose () {
+ // If the TCP connection was closed after the
+ // WebSocket closing handshake was completed, the WebSocket connection
+ // is said to have been closed _cleanly_.
+ const wasClean =
+ this.#handler.closeState.has(sentCloseFrameState.SENT) &&
+ this.#handler.closeState.has(sentCloseFrameState.RECEIVED)
+
+ let code = 1005
+ let reason = ''
+
+ const result = this.#parser?.closingInfo
+
+ if (result && !result.error) {
+ code = result.code ?? 1005
+ reason = result.reason
+ }
+
+ // 1. Change the ready state to CLOSED (3).
+ this.#handler.readyState = states.CLOSED
+
+ // 2. If the user agent was required to fail the WebSocket
+ // connection, or if the WebSocket connection was closed
+ // after being flagged as full, fire an event named error
+ // at the WebSocket object.
+ if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
+ // If _The WebSocket
+ // Connection is Closed_ and no Close control frame was received by the
+ // endpoint (such as could occur if the underlying transport connection
+ // is lost), _The WebSocket Connection Close Code_ is considered to be
+ // 1006.
+ code = 1006
+
+ fireEvent('error', this, (type, init) => new ErrorEvent(type, init), {
+ error: new TypeError(reason)
+ })
+ }
+
+ // 3. Fire an event named close at the WebSocket object,
+ // using CloseEvent, with the wasClean attribute
+ // initialized to true if the connection closed cleanly
+ // and false otherwise, the code attribute initialized to
+ // the WebSocket connection close code, and the reason
+ // attribute initialized to the result of applying UTF-8
+ // decode without BOM to the WebSocket connection close
+ // reason.
+ // TODO: process.nextTick
+ fireEvent('close', this, (type, init) => new CloseEvent(type, init), {
+ wasClean, code, reason
+ })
+
+ if (channels.close.hasSubscribers) {
+ channels.close.publish({
+ websocket: this,
+ code,
+ reason
+ })
+ }
+ }
+
+ /**
+ * @param {WebSocket} ws
+ * @param {Buffer|undefined} buffer
+ */
+ static ping (ws, buffer) {
+ if (Buffer.isBuffer(buffer)) {
+ if (buffer.length > 125) {
+ throw new TypeError('A PING frame cannot have a body larger than 125 bytes.')
+ }
+ } else if (buffer !== undefined) {
+ throw new TypeError('Expected buffer payload')
+ }
+
+ // An endpoint MAY send a Ping frame any time after the connection is
+ // established and before the connection is closed.
+ const readyState = ws.#handler.readyState
+
+ if (isEstablished(readyState) && !isClosing(readyState) && !isClosed(readyState)) {
+ const frame = new WebsocketFrameSend(buffer)
+ ws.#handler.socket.write(frame.createFrame(opcodes.PING))
+ }
+ }
+}
+
+const { ping } = WebSocket
+Reflect.deleteProperty(WebSocket, 'ping')
+
+// https://websockets.spec.whatwg.org/#dom-websocket-connecting
+WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING
+// https://websockets.spec.whatwg.org/#dom-websocket-open
+WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN
+// https://websockets.spec.whatwg.org/#dom-websocket-closing
+WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING
+// https://websockets.spec.whatwg.org/#dom-websocket-closed
+WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED
+
+Object.defineProperties(WebSocket.prototype, {
+ CONNECTING: staticPropertyDescriptors,
+ OPEN: staticPropertyDescriptors,
+ CLOSING: staticPropertyDescriptors,
+ CLOSED: staticPropertyDescriptors,
+ url: kEnumerableProperty,
+ readyState: kEnumerableProperty,
+ bufferedAmount: kEnumerableProperty,
+ onopen: kEnumerableProperty,
+ onerror: kEnumerableProperty,
+ onclose: kEnumerableProperty,
+ close: kEnumerableProperty,
+ onmessage: kEnumerableProperty,
+ binaryType: kEnumerableProperty,
+ send: kEnumerableProperty,
+ extensions: kEnumerableProperty,
+ protocol: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'WebSocket',
+ writable: false,
+ enumerable: false,
+ configurable: true
+ }
+})
+
+Object.defineProperties(WebSocket, {
+ CONNECTING: staticPropertyDescriptors,
+ OPEN: staticPropertyDescriptors,
+ CLOSING: staticPropertyDescriptors,
+ CLOSED: staticPropertyDescriptors
+})
+
+webidl.converters['sequence<DOMString>'] = webidl.sequenceConverter(
+ webidl.converters.DOMString
+)
+
+webidl.converters['DOMString or sequence<DOMString>'] = function (V, prefix, argument) {
+ if (webidl.util.Type(V) === webidl.util.Types.OBJECT && Symbol.iterator in V) {
+ return webidl.converters['sequence<DOMString>'](V)
+ }
+
+ return webidl.converters.DOMString(V, prefix, argument)
+}
+
+// This implements the proposal made in https://github.com/whatwg/websockets/issues/42
+webidl.converters.WebSocketInit = webidl.dictionaryConverter([
+ {
+ key: 'protocols',
+ converter: webidl.converters['DOMString or sequence<DOMString>'],
+ defaultValue: () => []
+ },
+ {
+ key: 'dispatcher',
+ converter: webidl.converters.any,
+ defaultValue: () => getGlobalDispatcher()
+ },
+ {
+ key: 'headers',
+ converter: webidl.nullableConverter(webidl.converters.HeadersInit)
+ }
+])
+
+webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'] = function (V) {
+ if (webidl.util.Type(V) === webidl.util.Types.OBJECT && !(Symbol.iterator in V)) {
+ return webidl.converters.WebSocketInit(V)
+ }
+
+ return { protocols: webidl.converters['DOMString or sequence<DOMString>'](V) }
+}
+
+webidl.converters.WebSocketSendData = function (V) {
+ if (webidl.util.Type(V) === webidl.util.Types.OBJECT) {
+ if (webidl.is.Blob(V)) {
+ return V
+ }
+
+ if (webidl.is.BufferSource(V)) {
+ return V
+ }
+ }
+
+ return webidl.converters.USVString(V)
+}
+
+module.exports = {
+ WebSocket,
+ ping
+}