Skip to content

Commit

Permalink
websocket: add websocketinit (#2088)
Browse files Browse the repository at this point in the history
* websocket: add websocketinit

Refs: #1811 (comment)

* update types

* remove 3rd param

it's not as backwards compatible as I thought...

* update docs
  • Loading branch information
KhafraDev committed Apr 23, 2023
1 parent 286d5ec commit 7fe5a0d
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 8 deletions.
29 changes: 26 additions & 3 deletions docs/api/WebSocket.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
5 changes: 3 additions & 2 deletions lib/websocket/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<import('../../types/websocket').WebSocketInit>} 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
Expand Down Expand Up @@ -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.
Expand Down
34 changes: 32 additions & 2 deletions lib/websocket/websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,8 +52,10 @@ class WebSocket extends EventTarget {
})
}

const options = webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'](protocols)

url = webidl.converters.USVString(url)
protocols = webidl.converters['DOMString or sequence<DOMString>'](protocols)
protocols = options.protocols

// 1. Let urlRecord be the result of applying the URL parser to url.
let urlRecord
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -577,6 +581,32 @@ webidl.converters['DOMString or sequence<DOMString>'] = 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<DOMString>'],
get defaultValue () {
return []
}
},
{
key: 'dispatcher',
converter: (V) => V,
get defaultValue () {
return getGlobalDispatcher()
}
}
])

webidl.converters['DOMString or sequence<DOMString> 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<DOMString>'](V) }
}

webidl.converters.WebSocketSendData = function (V) {
if (webidl.util.Type(V) === 'Object') {
if (isBlobLike(V)) {
Expand Down
45 changes: 45 additions & 0 deletions test/websocket/websocketinit.js
Original file line number Diff line number Diff line change
@@ -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()
})
})
})
8 changes: 7 additions & 1 deletion types/websocket.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
AddEventListenerOptions,
EventListenerOrEventListenerObject
} from './patch'
import Dispatcher from './dispatcher'

export type BinaryType = 'blob' | 'arraybuffer'

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -121,3 +122,8 @@ export declare const MessageEvent: {
prototype: MessageEvent
new<T>(type: string, eventInitDict?: MessageEventInit<T>): MessageEvent<T>
}

interface WebSocketInit {
protocols?: string | string[],
dispatcher?: Dispatcher
}

0 comments on commit 7fe5a0d

Please sign in to comment.