diff --git a/docs/api/WebSocket.md b/docs/api/WebSocket.md index 639a5333a1c..9d374f4046c 100644 --- a/docs/api/WebSocket.md +++ b/docs/api/WebSocket.md @@ -1,17 +1,40 @@ # Class: WebSocket -> ⚠️ Warning: the WebSocket API is experimental and has known bugs. +> ⚠️ Warning: the WebSocket API is experimental. Extends: [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) -The WebSocket object provides a way to manage a WebSocket connection to a server, allowing bidirectional communication. The API follows the [WebSocket spec](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). +The WebSocket object provides a way to manage a WebSocket connection to a server, allowing bidirectional communication. The API follows the [WebSocket spec](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) and [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455). ## `new WebSocket(url[, protocol])` Arguments: * **url** `URL | string` - The url's protocol *must* be `ws` or `wss`. -* **protocol** `string | string[]` (optional) - Subprotocol(s) to request the server use. +* **protocol** `string | string[] | WebSocketInit` (optional) - Subprotocol(s) to request the server use, or a [`Dispatcher`](./Dispatcher.md). + +### Example: + +This example will not work in browsers or other platforms that don't allow passing an object. + +```mjs +import { WebSocket, ProxyAgent } from 'undici' + +const proxyAgent = new ProxyAgent('my.proxy.server') + +const ws = new WebSocket('wss://echo.websocket.events', { + dispatcher: proxyAgent, + protocols: ['echo', 'chat'] +}) +``` + +If you do not need a custom Dispatcher, it's recommended to use the following pattern: + +```mjs +import { WebSocket } from 'undici' + +const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat']) +``` ## Read More diff --git a/lib/websocket/connection.js b/lib/websocket/connection.js index 09770247e3f..581f268e059 100644 --- a/lib/websocket/connection.js +++ b/lib/websocket/connection.js @@ -26,8 +26,9 @@ channels.socketError = diagnosticsChannel.channel('undici:websocket:socket_error * @param {string|string[]} protocols * @param {import('./websocket').WebSocket} ws * @param {(response: any) => void} onEstablish + * @param {Partial} options */ -function establishWebSocketConnection (url, protocols, ws, onEstablish) { +function establishWebSocketConnection (url, protocols, ws, onEstablish, 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 @@ -88,7 +89,7 @@ function establishWebSocketConnection (url, protocols, ws, onEstablish) { const controller = fetching({ request, useParallelQueue: true, - dispatcher: getGlobalDispatcher(), + dispatcher: options.dispatcher ?? getGlobalDispatcher(), processResponse (response) { // 1. If response is a network error or its status is not 101, // fail the WebSocket connection. diff --git a/lib/websocket/websocket.js b/lib/websocket/websocket.js index 164d24c6f8a..762f25684ab 100644 --- a/lib/websocket/websocket.js +++ b/lib/websocket/websocket.js @@ -18,6 +18,7 @@ const { establishWebSocketConnection } = require('./connection') const { WebsocketFrameSend } = require('./frame') const { ByteParser } = require('./receiver') const { kEnumerableProperty, isBlobLike } = require('../core/util') +const { getGlobalDispatcher } = require('../global') const { types } = require('util') let experimentalWarned = false @@ -51,8 +52,10 @@ class WebSocket extends EventTarget { }) } + const options = webidl.converters['DOMString or sequence or WebSocketInit'](protocols) + url = webidl.converters.USVString(url) - protocols = webidl.converters['DOMString or sequence'](protocols) + protocols = options.protocols // 1. Let urlRecord be the result of applying the URL parser to url. let urlRecord @@ -110,7 +113,8 @@ class WebSocket extends EventTarget { urlRecord, protocols, this, - (response) => this.#onConnectionEstablished(response) + (response) => this.#onConnectionEstablished(response), + options ) // Each WebSocket object has an associated ready state, which is a @@ -577,6 +581,32 @@ webidl.converters['DOMString or sequence'] = function (V) { return webidl.converters.DOMString(V) } +// This implements the propsal made in https://github.com/whatwg/websockets/issues/42 +webidl.converters.WebSocketInit = webidl.dictionaryConverter([ + { + key: 'protocols', + converter: webidl.converters['DOMString or sequence'], + get defaultValue () { + return [] + } + }, + { + key: 'dispatcher', + converter: (V) => V, + get defaultValue () { + return getGlobalDispatcher() + } + } +]) + +webidl.converters['DOMString or sequence or WebSocketInit'] = function (V) { + if (webidl.util.Type(V) === 'Object' && !(Symbol.iterator in V)) { + return webidl.converters.WebSocketInit(V) + } + + return { protocols: webidl.converters['DOMString or sequence'](V) } +} + webidl.converters.WebSocketSendData = function (V) { if (webidl.util.Type(V) === 'Object') { if (isBlobLike(V)) { diff --git a/test/websocket/websocketinit.js b/test/websocket/websocketinit.js new file mode 100644 index 00000000000..4dda3b48188 --- /dev/null +++ b/test/websocket/websocketinit.js @@ -0,0 +1,45 @@ +'use strict' + +const { test } = require('tap') +const { WebSocketServer } = require('ws') +const { WebSocket, Dispatcher, Agent } = require('../..') + +test('WebSocketInit', (t) => { + t.plan(2) + + class WsDispatcher extends Dispatcher { + constructor () { + super() + this.agent = new Agent() + } + + dispatch () { + t.pass() + return this.agent.dispatch(...arguments) + } + } + + t.test('WebSocketInit as 2nd param', (t) => { + t.plan(1) + + const server = new WebSocketServer({ port: 0 }) + + server.on('connection', (ws) => { + ws.send(Buffer.from('hello, world')) + }) + + t.teardown(server.close.bind(server)) + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { + dispatcher: new WsDispatcher() + }) + + ws.onerror = t.fail + + ws.addEventListener('message', async (event) => { + t.equal(await event.data.text(), 'hello, world') + server.close() + ws.close() + }) + }) +}) diff --git a/types/websocket.d.ts b/types/websocket.d.ts index 7524cbda6c4..578c7602e64 100644 --- a/types/websocket.d.ts +++ b/types/websocket.d.ts @@ -10,6 +10,7 @@ import { AddEventListenerOptions, EventListenerOrEventListenerObject } from './patch' +import Dispatcher from './dispatcher' export type BinaryType = 'blob' | 'arraybuffer' @@ -67,7 +68,7 @@ interface WebSocket extends EventTarget { export declare const WebSocket: { prototype: WebSocket - new (url: string | URL, protocols?: string | string[]): WebSocket + new (url: string | URL, protocols?: string | string[] | WebSocketInit): WebSocket readonly CLOSED: number readonly CLOSING: number readonly CONNECTING: number @@ -121,3 +122,8 @@ export declare const MessageEvent: { prototype: MessageEvent new(type: string, eventInitDict?: MessageEventInit): MessageEvent } + +interface WebSocketInit { + protocols?: string | string[], + dispatcher?: Dispatcher +}