Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editorial review: Document WebSocketStream #35548

Merged
merged 19 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0440e77
Document WebSocketStream
chrisdavidmills Aug 22, 2024
fb8a621
Add WebSocket interface documentation
chrisdavidmills Aug 23, 2024
54ab5af
Merge branch 'main' into websocketstream
chrisdavidmills Aug 23, 2024
3c8f355
Update files/en-us/web/api/websockets_api/using_websocketstream/index.md
chrisdavidmills Aug 28, 2024
6ca71c9
Update files/en-us/web/api/websockets_api/using_websocketstream/index.md
chrisdavidmills Aug 28, 2024
7a55d73
Update files/en-us/web/api/websockets_api/using_websocketstream/index.md
chrisdavidmills Aug 28, 2024
8f60ee6
Update files/en-us/web/api/websockets_api/using_websocketstream/index.md
chrisdavidmills Aug 28, 2024
a979ad4
Update files/en-us/web/api/websockets_api/using_websocketstream/index.md
chrisdavidmills Aug 28, 2024
1016e40
Update files/en-us/web/api/websockets_api/using_websocketstream/index.md
chrisdavidmills Aug 28, 2024
f2e04e4
Update files/en-us/web/api/websocketstream/closed/index.md
chrisdavidmills Aug 28, 2024
2c01603
Update files/en-us/web/api/websocketstream/opened/index.md
chrisdavidmills Aug 28, 2024
76923cc
Update files/en-us/web/api/websocketstream/url/index.md
chrisdavidmills Aug 28, 2024
71defc9
Update files/en-us/web/api/websocketstream/websocketstream/index.md
chrisdavidmills Aug 28, 2024
2a82373
make protocol usage consistent ascross example snippets
chrisdavidmills Aug 28, 2024
190b3da
Merge branch 'main' into websocketstream
chrisdavidmills Sep 6, 2024
bbb9b37
Fixes for wbamberg review comments
chrisdavidmills Sep 6, 2024
f72d08d
Merge branch 'main' into websocketstream
chrisdavidmills Sep 6, 2024
2c2ceed
Fixes for 2nd round of wbamberg review
chrisdavidmills Sep 9, 2024
7bc83c3
typo
wbamberg Sep 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions files/en-us/web/api/websocket/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ The `WebSocket` object provides the API for creating and managing a [WebSocket](

To construct a `WebSocket`, use the [`WebSocket()`](/en-US/docs/Web/API/WebSocket/WebSocket) constructor.

> [!NOTE]
> The `WebSocket` API has no way to apply [backpressure](/en-US/docs/Web/API/Streams_API/Concepts#backpressure), therefore when messages arrive faster than the application can process them, the application will either fill up the device's memory by buffering those messages, become unresponsive due to 100% CPU usage, or both. For an alterative that provides backpressure automatically, see {{domxref("WebSocketStream")}}.
wbamberg marked this conversation as resolved.
Show resolved Hide resolved

{{InheritanceDiagram}}

## Constructor
Expand Down
23 changes: 16 additions & 7 deletions files/en-us/web/api/websockets_api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@
title: The WebSocket API (WebSockets)
slug: Web/API/WebSockets_API
page-type: web-api-overview
browser-compat: api.WebSocket
browser-compat:
- api.WebSocket
- api.WebSocketStream
---

{{DefaultAPISidebar("WebSockets API")}}

The **WebSocket API** is an advanced technology that makes it possible to open a two-way interactive communication session between the user's browser and a server. With this API, you can send messages to a server and receive event-driven responses without having to poll the server for a reply.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The **WebSocket API** is an advanced technology that makes it possible to open a two-way interactive communication session between the user's browser and a server. With this API, you can send messages to a server and receive event-driven responses without having to poll the server for a reply.
The **WebSocket API** makes it possible to open a two-way interactive communication session between the user's browser and a server. With this API, you can send messages to a server and receive responses without having to poll the server for a reply.

(not necessarily event-driven)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable. Updated to use your wording.


The WebSocket API provides two alternative mechanisms for creating and using web socket connections: the {{domxref("WebSocket")}} interface and the {{domxref("WebSocketStream")}} interface.

- The `WebSocket` interface is stable and has good browser and server support. However it doesn't support [backpressure](/en-US/docs/Web/API/Streams_API/Concepts#backpressure). As a result, when messages arrive faster than the application can process them it will either fill up the device's memory by buffering those messages, become unresponsive due to 100% CPU usage, or both.
- The `WebSocketStream` is a {{jsxref("Promise")}}-based alternative to `WebSocket`. It uses the [Streams API](/en-US/docs/Web/API/Streams_API) to handle receiving and sending messages, meaning that socket connections can take advantage of stream backpressure automatically, regulating the speed of reading or writing to avoid bottlenecks in the application. However, `WebSocketSteam` is non-standard and currently only supported in one rendering engine.

Additionally, the [WebTransport API](/en-US/docs/Web/API/WebTransport_API) is expected to replace the WebSocket API for many applications. WebTransport is a versatile, low-level API that provides backpressure and many other features not supported by either `WebSocket` or `WebSocketStream`, such as unidirectional streams, out-of-order delivery, and unreliable data transmission via datagrams. WebTransport is more complex to use than WebSockets and its cross-browser support is not as wide, but it enables the implementation of sophisticated solutions. If standard WebSocket connections are a good fit for your use case and you need wide browser compatibility, you should employ the WebSockets API to get up and running quickly. However, if your application requires a non-standard custom solution, then you should use the WebTransport API.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, I like this a lot better.

> [!NOTE]
> While a WebSocket connection is functionally somewhat similar to standard Unix-style sockets, they are not related.

## Interfaces

- [`WebSocket`](/en-US/docs/Web/API/WebSocket)
- : The primary interface for connecting to a WebSocket server and then sending and receiving data on the connection.
- [`WebSocketStream`](/en-US/docs/Web/API/WebSocketStream) {{non-standard_inline}}
- : Promise-based interface for connecting to a WebSocket server; uses [streams](/en-US/docs/Web/API/Streams_API) to send and receive data on the connection.
- [`CloseEvent`](/en-US/docs/Web/API/CloseEvent)
- : The event sent by the WebSocket object when the connection closes.
- [`MessageEvent`](/en-US/docs/Web/API/MessageEvent)
Expand All @@ -28,28 +39,26 @@ The **WebSocket API** is an advanced technology that makes it possible to open a
- [Writing a WebSocket server in C#](/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_server)
- [Writing a WebSocket server in Java](/en-US/docs/Web/API/WebSockets_API/Writing_a_WebSocket_server_in_Java)
- [Writing a WebSocket server in JavaScript (Deno)](/en-US/docs/Web/API/WebSockets_API/Writing_a_WebSocket_server_in_JavaScript_Deno)
- [Using WebSocketStream to write a client](/en-US/docs/Web/API/WebSockets_API/Using_WebSocketStream)

## Tools

- [AsyncAPI](https://www.asyncapi.com/): A specification for describing event-driven architectures based on protocols like WebSocket. You can use it to describe WebSocket-based APIs just as you would describe REST APIs with the OpenAPI specification. Learn [why you should consider using AsyncAPI with WebSocket](https://www.asyncapi.com/blog/websocket-part1) and [how to do so](https://www.asyncapi.com/blog/websocket-part2).
- [HumbleNet](https://hacks.mozilla.org/2017/06/introducing-humblenet-a-cross-platform-networking-library-that-works-in-the-browser/): A cross-platform networking library that works in the browser. It consists of a C wrapper around WebSockets and WebRTC that abstracts away cross-browser differences, facilitating the creation of multi-user networking functionality for games and other apps.
- [µWebSockets](https://github.com/uNetworking/uWebSockets): Highly scalable WebSocket server and client implementation for [C++11](https://isocpp.org/) and [Node.js](https://nodejs.org/).
- [Socket.IO](https://socket.io/): A long polling/WebSocket based third party transfer protocol for [Node.js](https://nodejs.org/).
- [SocketCluster](https://socketcluster.io/): A pub/sub WebSocket framework for [Node.js](https://nodejs.org/) with a focus on scalability.
- [WebSocket-Node](https://github.com/theturtle32/WebSocket-Node): A WebSocket server API implementation for [Node.js](https://nodejs.org/).
- [Total.js](https://www.totaljs.com/): Web application framework for [Node.js](https://nodejs.org/en) (Example: [WebSocket chat](https://github.com/totaljs/examples/tree/master/websocket))
- [Faye](https://www.npmjs.com/package/faye-websocket): A {{DOMxRef("WebSocket")}} (two-ways connections) and [EventSource](/en-US/docs/Web/API/EventSource) (one-way connections) for [Node.js](https://nodejs.org/) Server and Client.
- [SignalR](https://dotnet.microsoft.com/en-us/apps/aspnet/signalr): SignalR will use WebSockets under the covers when it's available, and gracefully fallback to other techniques and technologies when it isn't, while your application code stays the same.
- [Caddy](https://caddyserver.com/): A web server capable of proxying arbitrary commands (stdin/stdout) as a websocket.
- [ws](https://github.com/websockets/ws): a popular WebSocket client & server library for [Node.js](https://nodejs.org/en).
- [jsonrpc-bidirectional](https://github.com/bigstepinc/jsonrpc-bidirectional): Asynchronous RPC which, on a single connection, may have functions exported on the server and, and the same time, on the client (client may call server, server may also call client).
- [cowboy](https://github.com/ninenines/cowboy): Cowboy is a small, fast and modern HTTP server for Erlang/OTP with WebSocket support.
- [ZeroMQ](https://zeromq.org/): ZeroMQ is embeddable networking library that carries messages across in-process, IPC, TCP, UDP, TIPC, multicast and WebSocket.
- [WebSocket King](https://websocketking.com/): A client tool to help develop, test and work with WebSocket servers.
- [PHP WebSocket Server](https://github.com/napengam/phpWebSocketServer): Server written in PHP to handle connections via websockets `wss://` or `ws://` and normal sockets over `ssl://`, `tcp://`
- [Channels](https://channels.readthedocs.io/en/stable/index.html): Django library that adds support for WebSockets (and other protocols that require long running asynchronous connections).
- [Channels](https://hexdocs.pm/phoenix/channels.html): Scalable real-time communication using WebSocket in Elixir Phoenix framework.
- [LiveView](https://github.com/phoenixframework/phoenix_live_view): Real-time interactive web experiences through WebSocket in Elixir Phoenix framework.
- [Django Channels](https://channels.readthedocs.io/en/stable/index.html): Django library that adds support for WebSockets (and other protocols that require long running asynchronous connections).
- [(Phoenix) Channels](https://hexdocs.pm/phoenix/channels.html): Scalable real-time communication using WebSocket in Elixir Phoenix framework.
- [Phoenix LiveView](https://github.com/phoenixframework/phoenix_live_view): Real-time interactive web experiences through WebSocket in Elixir Phoenix framework.
- [Flask-SocketIO](https://flask-socketio.readthedocs.io/en/latest/): gives Flask applications access to low latency bi-directional communications between the clients and the server.
- [Gorilla WebSocket](https://pkg.go.dev/github.com/gorilla/websocket): Gorilla WebSocket is a [Go](https://go.dev/) implementation of the WebSocket protocol.

Expand Down
290 changes: 290 additions & 0 deletions files/en-us/web/api/websockets_api/using_websocketstream/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
---
title: Using WebSocketStream to write a client
slug: Web/API/WebSockets_API/Using_WebSocketStream
page-type: guide
status:
- experimental
- non-standard
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also experimental?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, makes sense. I've also added it to the WebSocketStream interface pages.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW this should get added automatically from the BCD. But only in reference pages, not in guide pages.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, OK. I keep forgetting what is automatically added and what isn't.

---

{{DefaultAPISidebar("WebSockets API")}}{{non-standard_header}}

The {{domxref("WebSocketStream")}} API is a {{jsxref("Promise")}}-based alternative to {{domxref("WebSocket")}} for creating and using client-side WebSocket connections. `WebSocketStream` uses the [Streams API](/en-US/docs/Web/API/Streams_API) to handle receiving and sending messages, meaning that socket connections can take advantage of stream [backpressure](/en-US/docs/Web/API/Streams_API/Concepts#backpressure) automatically, regulating the speed of reading or writing to avoid bottlenecks in the application.

This article explains how to use the {{domxref("WebSocketStream")}} API to create a WebSocket client.

## Feature detection

To check whether the `WebSocketStream` API is supported, you can use the following:

```js
if ("WebSocketStream" in self) {
// WebSocketStream is supported
}
```

## Creating a WebSocketStream object

To create a WebSocket client, you first need to create a new `WebSocketStream` instance using the {{domxref("WebSocketStream.WebSocketStream", "WebSocketStream()")}} constructor. In its simplest form, it takes the URL of the WebSocket server as an argument:

```js
const wss = new WebSocketStream("wss://example.com/wss");
```

It can also take an options object containing custom protocols and/or an {{domxref("AbortSignal")}} (see [Closing the connection](#closing_the_connection)):

```js
const controller = new AbortController();
const chatWSS = new WebSocketStream("wss://example.com/chat", {
protocols: ["chat", "chatv2"],
signal: controller.signal,
});
```

## Sending and receiving data

The `WebSocketStream` instance has an {{domxref("WebSocketStream.opened", "opened")}} property — this returns a promise that fulfills with an object containing a {{domxref("ReadableStream")}} and a {{domxref("WritableStream")}} instance once the WebSocket connection is opened successfully:

```js
const { readable, writable } = await wss.opened;
```

Calling {{domxref("ReadableStream.getReader", "getReader()")}} and {{domxref("WritableStream.getWriter", "getWriter()")}} on these objects provides us with a {{domxref("ReadableStreamDefaultReader")}} and a {{domxref("WritableStreamDefaultWriter")}} respectively, which can be used to read from and write to the socket connection:

```js
const reader = readable.getReader();
const writer = writable.getWriter();
```

To write data to the socket, you can use {{domxref("WritableStreamDefaultWriter.write()")}}:

```js
writer.write("My message");
```

To read data from the socket, you can continuously call {{domxref("ReadableStreamDefaultReader.read()")}} until the stream has finished, which is indicated by `done` being `true`:

```js
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}

// Process value in some way
}
```

Comment on lines +65 to +77
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I think is where we should spell out the backpressure thing. Perhaps something like:

Suggested change
To read data from the socket, you can continuously call {{domxref("ReadableStreamDefaultReader.read()")}} until the stream has finished, which is indicated by `done` being `true`:
```js
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
// Process value in some way
}
```
To read data from the socket, you can continuously call {{domxref("ReadableStreamDefaultReader.read()")}} until the stream has finished, which is indicated by `done` being `true`:
```js
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
// Process value in some way
}
```
Note that the client can control the rate at which it receives data by calling `read()` only when it is ready: if data is arriving faster than the client can read it, the underlying Streams API automatically exerts backpressure on the sender.

(assuming that is true!)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the point when read() is called has anything to do with backpressure. This is not automatic, but a choice made by the developer in the code?

I've read around this subject a bit, and added the following paragraph after the code snippet:

The browser automatically controls the rate at which the client receives and sends data by applying backpressure when needed. If data is arriving faster than the client can read() it, the underlying Streams API exerts backpressure on the server. In addition, write() operations will only proceed if it is safe to do so.

## Closing the connection

With `WebSocketStream`, the information previously available via the `WebSocket` {{domxref("WebSocket.close_event", "close")}} and {{domxref("WebSocket.error_event", "error")}} events is now available via the {{domxref("WebSocketStream.closed", "closed")}} property — this returns a promise that fulfills with an object containing the closing code (see the full list of [`CloseEvent` status codes](/en-US/docs/Web/API/CloseEvent/code#value)) and reason sent by the server:

```js
const { code, reason } = await wss.closed;
```

As mentioned earlier, the WebSocket connection can be closed using an {{domxref("AbortController")}}. The necessary {{domxref("AbortSignal")}} is passed to the `WebSocketStream` constructor during creation, and {{domxref("AbortController.abort()")}} can then be called when required:

```js
const controller = new AbortController();
const wss = new WebSocketStream("wss://example.com/wss", {
signal: controller.signal,
});

// some time later

controller.abort();
```

Alternatively you can use the {{domxref("WebSocketStream.close()")}} method to close a connection. This is mainly used if you wish to specify a custom code and reason for the server to report:

```js
wss.close({
code: 4000,
reason: "Night draws to a close",
});
```

> [!NOTE]
> Depending on the server setup and status code you use, the server may choose to ignore a custom code in favor of a valid code that is correct for the closing reason.

## A complete sample client

To demonstrate basic usage of `WebSocketStream`, we've created a sample client. You can see the [full listing](#full_listing) at the bottom of the article, and follow along with the explanation below.

> [!NOTE]
> To get the example working, you'll also need a server component. We wrote our client to work along with the Deno server explained in [Writing a WebSocket server in JavaScript (Deno)](/en-US/docs/Web/API/WebSockets_API/Writing_a_WebSocket_server_in_JavaScript_Deno), but any compatible server will do.

The HTML for the demo is as follows. It includes informational {{htmlelment("h1")}} and {{htmlelment("h1")}} elements, a {{htmlelment("button")}} to close the WebSocket connection that is initially disabled, and a {{htmlelment("div")}} for us to write output messages into.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The HTML for the demo is as follows. It includes informational {{htmlelment("h1")}} and {{htmlelment("h1")}} elements, a {{htmlelment("button")}} to close the WebSocket connection that is initially disabled, and a {{htmlelment("div")}} for us to write output messages into.
The HTML for the demo is as follows. It includes informational {{htmlelement("Heading_Elements", "h2")}} and {{htmlelement("p")}} elements, a {{htmlelement("button")}} to close the WebSocket connection that is initially disabled, and a {{htmlelement("div")}} for us to write output messages into.

(please double check the syntax for the H2)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh darn, thanks for the fixes there.

The syntax worked, but I ended up replacing the h2 macro call with a regular link, as for some reason the link was not showing the angle brackets or in code font.


```html
<h2>WebSocketStream Test</h2>
<p>Sends a ping every five seconds</p>
<button id="close" disabled>Close socket connection</button>
<div id="output"></div>
```

Now on to the JavaScript. First we grab references to the output `<div>` and the close `<button>`, and define a utility function that writes messages to the `<div>`:

```js
const output = document.querySelector("#output");
const closeBtn = document.querySelector("#close");

function writeToScreen(message) {
output.insertAdjacentHTML("beforeend", `<p>${message}</p>`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the server sends "<script src="https://evil.org/steal-your-stuff.js"></script>"? Is this a concern?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be, yes, but this is a general security concern, not something specific to WebSockets. It would be the same if you somehow fetched a code snippet with something malicious in it? It is of course up to the developer to make sure they connect to a trusted server, have CSP or other mechanisms in place to prevent unwanted scripts running, etc.

I'm not convinced I need to include such details on this page. What do you think should be included?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking, use textContent or something else safe, instead of insertAdjacentHTML.

It is of course up to the developer to make sure they connect to a trusted server, have CSP or other mechanisms in place to prevent unwanted scripts running,

The issue about a trusted server is, how do you know you can trust it, and it hasn't been compromised? As for CSP, guidance I've read is that it is not a substitute for not removing XSS vectors.

It seems inherently unsafe to connect to a server expecting it to send you messages, then immediately (effectively) executing them without sanitizing them. And by putting this in an example we're telling devs that this is an OK thing to do.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be the same if you somehow fetched a code snippet with something malicious in it?

Yup, and I would make the same comment about a fetch() example that did this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use textContent or something else safe, instead of insertAdjacentHTML.

Right, this makes sense — I am not sure why I used insertAdjacentHTML(). I will update it to use textContent, which is what I normally use for such examples ;-\

}
```

Next, we create an `if ... else` structure to feature detect `WebSocketStream` and output an informative message on non-supporting browsers:

```js
if (!("WebSocketStream" in self)) {
writeToScreen("Your browser does not support WebSocketStream");
} else {
// supporting code path
}
```

In the supporting code path, we begin by defining a variable containing the WebSocket server URL, and constructing a new `WebSocketServer` instance:

```js
const wsURL = "ws://127.0.0.1/";
const wss = new WebSocketStream(wsURL);
```

> [!NOTE]
> Best practice is to use secure WebSockets (`wss://`) in production apps. However, in this demo we are connecting to localhost, therefore we need to use the non-secure WebSocket protocol (`ws://`) for the example to work.

The main bulk of our code is contained within the `start()` function, which we define and then immediately invoke. We await the {{domxref("WebSocketStream.opened", "opened")}} promise, then after it fulfills write a message to let the reader know the connection is successful and create {{domxref("ReadableStreamDefaultReader")}} and {{domxref("WritableStreamDefaultWriter")}} instances from the returned `readable` and `writable` properties.

Next, we create a `start()` function that sends "ping" messages to the server and receives "pong" messages back, and invoke it. In the function body we await the `wss.opened` promise and create a reader and writer from its fulfillment values. Once the socket is open, we communicate that to the user and enable the close button. Next, we `write()` a `"ping"` value to the socket and communicate that to the user. At this point, the server will respond with a `"pong"` message. We await the `read()` of the response, communicate it to the user, then write another `"ping"` to the server after a timeout of 5 seconds. This continues the `"ping"`/`"pong"` loop indefinitely.

```js
async function start() {
const { readable, writable } = await wss.opened;
writeToScreen("CONNECTED");
closeBtn.disabled = false;
const reader = readable.getReader();
const writer = writable.getWriter();

await writer.write("ping");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why await? What happens if we don't? What difference does it make if we do? One thing is that error cases throw, but we don't have a try/catch handler for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's promise-based, so it won't work without it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't have to await to make it work: await just ensures that the promise fulfills before it executes the next line.

I mean, usually I see await being used if you want to make your code behave like synchronous code, for instance if it's returning something you need in the next line:

const response = await fetch(url);
const data = await response.json();

(or as we are doing with read(), where we need the read value in the next line).

But in this case we're not, so what's functionally the difference between awaiting and not awaiting, except that we're imposing an ordering when we maybe don't need to.

I suppose awaiting means we can report writeToScreen("SENT: ping");, because we know write() didn't reject, although it's not clear whether not rejecting actually means we sent it, as https://developer.mozilla.org/en-US/docs/Web/API/WritableStreamDefaultWriter/write says:

Note that what "success" means is up to the underlying sink; it might indicate that the chunk has been accepted, and not necessarily that it is safely saved to its ultimate destination.

But then if that is the reason, isn't it better to use try/catch so we can report an error?

So it could say:

    try {
          await writer.write("ping");
          writeToScreen("SENT: ping");
    } catch(e) {
          writeToScreen(`Error writing to socket: ${e.message}`);
    }

...and then there's a use for await.

Ah, maybe it's not worth worrying about.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't have to await to make it work: await just ensures that the promise fulfills before it executes the next line.

Ah, this is really interesting. I've been using this stuff for years and not considered that before. I've learnt something today.

I thought about it a bit and decided to just remove the unneeded await keywords (and the async keyword on the anonymous function inside the setTimeout() call). I don't want to add in a bunch of error handling code just cos. It doesn't add much to what I am trying to demonstrate, and increases complexity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: I've actually updated the write() call inside the setTimeout() function to use the try/catch structure you suggested because sometimes you close the connection and then the app still tries to write to the stream afterwards, causing an error.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be clear: I'm not certain what the right thing to do here is, which is why I'm asking this as questions. I think async can be complex, especially when there's error-handling going on, and I don't have the time/bandwidth to say what this code ought to be doing. I just want us to think carefully about why we're making the choices that we are here.

writeToScreen("SENT: ping");

while (true) {
const { value, done } = await reader.read();
writeToScreen(`RECEIVED: ${value}`);
if (done) {
break;
}

setTimeout(async () => {
await writer.write("ping");
writeToScreen("SENT: ping");
}, 5000);
}
}

start();
```

We now include a promise-style code section to inform the user of the code and reason if the WebSocket connection is closed, as signalled by the {{domxref("WebSocketStream.closed", "closed")}} promise fulfilling:

```js
wss.closed.then((result) => {
writeToScreen(
`DISCONNECTED: code ${result.closeCode}, message "${result.reason}"`,
);
console.log("Socket closed", result.closeCode, result.reason);
});
```

Finally, we add an event listener to the close button that closes the conenction using the `close()` method, with a code and custom reason. The function also disables the close button — we don't want users to press it once the connection is already closed.

```js
closeBtn.addEventListener("click", () => {
wss.close({
code: 1000,
reason: "That's all folks",
Comment on lines +216 to +218
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We say a few times that it's preferable to use abort(), maybe better to show that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also say that close() is used when providing a custom code and/or reason. In this case we are providing a custom reason, so I think this is OK.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be pedantic, which I always am, it says "custom code and reason".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, all relevant instances now changed to "and/or".

});

closeBtn.disabled = false;
});
```

### Full listing

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>WebSocketStream Test</title>
</head>

<body>
<h2>WebSocketStream Test</h2>
<p>Sends a ping every five seconds</p>
<button id="close" disabled>Close socket connection</button>
<div id="output"></div>
<script>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be easier to read this if the JS were in an external script.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly so, but this would also make it slightly more annoying for a user to copy and paste and chuck into a file for testing purposes. I think it is better to leave as-is, by this token.

const output = document.querySelector("#output");
const closeBtn = document.querySelector("#close");

function writeToScreen(message) {
output.insertAdjacentHTML("beforeend", `<p>${message}</p>`);
}

if (!("WebSocketStream" in self)) {
writeToScreen("Your browser does not support WebSocketStream");
} else {
const wsURL = "ws://127.0.0.1/";
const wss = new WebSocketStream(wsURL);

console.log(wss.url);

async function start() {
const { readable, writable, extensions, protocol } = await wss.opened;
writeToScreen("CONNECTED");
closeBtn.disabled = false;
const reader = readable.getReader();
const writer = writable.getWriter();

await writer.write("ping");
writeToScreen("SENT: ping");

while (true) {
const { value, done } = await reader.read();
writeToScreen(`RECEIVED: ${value}`);
if (done) {
break;
}

setTimeout(async () => {
await writer.write("ping");
writeToScreen("SENT: ping");
}, 5000);
}
}

start();

wss.closed.then((result) => {
writeToScreen(
`DISCONNECTED: code ${result.closeCode}, message "${result.reason}"`,
);
console.log("Socket closed", result.closeCode, result.reason);
});

closeBtn.addEventListener("click", () => {
wss.close({
code: 1000,
reason: "That's all folks",
});

closeBtn.disabled = false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call; updated.

});
}
</script>
</body>
</html>
```
Loading