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

Conversation

chrisdavidmills
Copy link
Contributor

Description

Chrome 124 supports the WebSocketStream API (see the associated ChromeStatus entry).

Note that WebSocketStream is currently supported in Chromium-based browsers, and is not yet part of a standard so I will mark it as non-standard for now. See whatwg/websockets#48 for progress on that.

Motivation

Additional details

Related issues and pull requests

@chrisdavidmills chrisdavidmills requested review from a team as code owners August 22, 2024 09:00
@chrisdavidmills chrisdavidmills requested review from wbamberg and pepelsbey and removed request for a team August 22, 2024 09:00
@chrisdavidmills chrisdavidmills marked this pull request as draft August 22, 2024 09:01
@github-actions github-actions bot added Content:WebAPI Web API docs size/m [PR only] 51-500 LoC changed labels Aug 22, 2024
Copy link
Contributor

github-actions bot commented Aug 22, 2024

Preview URLs (11 pages)
External URLs (17)

URL: /en-US/docs/Web/API/WebSocketStream
Title: WebSocketStream


URL: /en-US/docs/Web/API/WebSocketStream/WebSocketStream
Title: WebSocketStream: WebSocketStream() constructor


URL: /en-US/docs/Web/API/WebSocketStream/closed
Title: WebSocketStream: closed property


URL: /en-US/docs/Web/API/WebSocketStream/close
Title: WebSocketStream: close() method


URL: /en-US/docs/Web/API/WebSocketStream/url
Title: WebSocketStream: url property


URL: /en-US/docs/Web/API/WebSocketStream/opened
Title: WebSocketStream: opened property


URL: /en-US/docs/Web/API/WebSockets_API
Title: The WebSocket API (WebSockets)


URL: /en-US/docs/Web/API/CloseEvent/code
Title: CloseEvent: code property

(comment last updated: 2024-09-12 18:12:30)

@github-actions github-actions bot added size/l [PR only] 501-1000 LoC changed and removed size/m [PR only] 51-500 LoC changed labels Aug 23, 2024
@chrisdavidmills chrisdavidmills marked this pull request as ready for review August 23, 2024 07:19
Copy link
Contributor

@tomayac tomayac left a comment

Choose a reason for hiding this comment

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

This is in a really good shape. Just some nits.

Maybe ping @ricea for a spec editor review.

@chrisdavidmills
Copy link
Contributor Author

Thanks, @tomayac! I accepted all of your suggestions. I also checked and updated a few example snippets to use wss:// and self for feature detection to keep things consistent.

Also, because we need to specify ws:// in the code example for it to work as written, I've added a note into the WebSocketStream guide to draw the reader's attention to this, just in case they use wss:// and get stuck as a result.

I'm going to put this forward for editorial review now, but @ricea, feel free to have a look and make comments if you wish. It should take a little while for someone to pick it up, so there is time.

@chrisdavidmills chrisdavidmills changed the title Document WebSocketStream Editorial review: Document WebSocketStream Aug 28, 2024
Copy link
Collaborator

@wbamberg wbamberg left a comment

Choose a reason for hiding this comment

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

Here are comments on the non-reference pages.

Comment on lines 17 to 19
### WebSocketStream

The {{domxref("WebSocketStream")}} API is a modern reimagining of WebSocket client-side JavaScript functionality. It is promise-based, and therefore simpler to work with than the traditional {{domxref("WebSocket")}} API in the modern JavaScript scosystem. In addition, 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](/en-US/docs/Web/API/Streams_API/Concepts#backpressure) automatically, regulating the speed of reading or writing to avoid bottlenecks in the application. See [Using WebSocketStream to write a client](/en-US/docs/Web/API/WebSockets_API/Using_WebSocketStream) for more information.
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 this needs more context. We have an intro about what websockets are, then go right into:

The WebSocketStream API is a modern reimagining of WebSocket client-side JavaScript functionality. It is promise-based, and therefore simpler to work with than the traditional WebSocket API ...

...but we haven't even talked about "traditional" WebSocket yet.

Also, "a modern reimagining of WebSocket client-side JavaScript functionality" reads like marketing to me. As a web developer who doesn't know yet about WebSockets, let alone Web Transport, what am I to make of all this?

IIUC there's a major drawback to WebSocketStream: it's nonstandard and only supported in one browser engine. Given that, I'm not sure MDN should recommend it above standard and cross-platform alternatives.

It would be very helpful here to lay out all the considerations, including Web Transport, in a neutral and factual way, to help developers make good choices.

It seems like the main motivation for WebSocketStream over WebSocket is to support backpressure, and I would put that first, before the "promise-based API" motivation (I doubt that would be a strong enough motivation alone for web developers to want to adopt this API). The explainer does a nice job of that actually.

So we might say something roughly like:

In the WebSocket API, there are two alternative ways to create and use web sockets: the WebSocket interface and the WebSocketStream interface.

  • The WebSocket interface is stable and has good browser and server support. However it doesn't support backpressure (explain what this is and why it is a problem)
  • The WebSocketStream is an alternative to WebSocket, that's based on the Streams API and therefore does support backpressure. However, it's nonstandard and has poor cross-browser support.

Additionally, the WebTransport API is expected to replace the WebSocket API for many applications, and supports backpressure along with many other features not supported by either WebSocket or WebSocketStream. something something about when and in which circs people should choose WebSocket over WebTransport or vice versa

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. In my next commit, I have used your suggested structure as a basis, and filled in the missing details.


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

The {{domxref("WebSocketStream")}} API is a modern reimagining of WebSocket client-side JavaScript functionality. It is promise-based, and therefore simpler to work with than the traditional {{domxref("WebSocket")}} API in the modern JavaScript scosystem. In addition, 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](/en-US/docs/Web/API/Streams_API/Concepts#backpressure) automatically, regulating the speed of reading or writing to avoid bottlenecks in the application.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a copy/paste from the overview page. I'm not sure it's needed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The overview page no longer contains this paragraph. I have kept it on this page, but updated it to make it more concise and less marketing-y.


The {{domxref("WebSocketStream")}} API is a modern reimagining of WebSocket client-side JavaScript functionality. It is promise-based, and therefore simpler to work with than the traditional {{domxref("WebSocket")}} API in the modern JavaScript scosystem. In addition, 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](/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 simple WebSocket client.
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
This article explains how to use the {{domxref("WebSocketStream")}} API to create a simple WebSocket client.
This article explains how to use the {{domxref("WebSocketStream")}} API to create a WebSocket client.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Argh, the curse of "simple"! Removed in next commit ;-)

Comment on lines 15 to 16
> [!NOTE]
> Backpressure is an issue with the traditional `WebSocket` API — it has no way to apply 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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, this note seems out of place, especially if you remove or cut down the first para. What would be helpful, IMO, would be something about what (if anything) the developer has to do to take advantage of backpressure support. I think nothing? i.e. if they just don't read, then backpressure is exerted. Is that right though?

The page at https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Concepts#backpressure says:

To use backpressure in a ReadableStream, we can ask the controller for the chunk size desired by the consumer by querying the ReadableStreamDefaultController.desiredSize property on the controller. If it is too low, our ReadableStream can tell its underlying source to stop sending data, and we backpressure along the stream chain.

...which I don't really understand - who is "we" here?

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 have removed the note from here, and put a shortened version on the WebSocket page.

In terms of how to use it, it is just automatic for WebSocketStream. I do say "automatically" in the opening paragraph. Do you think I need to be a bit more explicit?

It is somewhat out of scope for this PR to start updating the Streams API docs. We could do that in a separate PR. Maybe file an issue for now?

Copy link
Collaborator

Choose a reason for hiding this comment

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

In terms of how to use it, it is just automatic for WebSocketStream. I do say "automatically" in the opening paragraph. Do you think I need to be a bit more explicit?

Yes, I do. The doc links to https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Concepts#backpressure, which as I say is hard to understand, but does not make it obvious that the developer has to do nothing for backpressure to work.

I think it would be best to spell this out in the bit about "To read data from the socket, you can continuously call ReadableStreamDefaultReader.read() ...", and will leave a suggestion there.

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, cool. I've also added a little qualifier here in parens - "(no additional action required by the developer)" - which will hopefully help too.

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

```js
await writer.write("My message");
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 here?

Copy link
Contributor Author

@chrisdavidmills chrisdavidmills Sep 6, 2024

Choose a reason for hiding this comment

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

I was just including it because write() returns a promise. I guess I don't need it for this trivial line. Removed.

Comment on lines +198 to +200
wss.close({
code: 1000,
reason: "That's all folks",
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".

<h2>WebSocketStream Test</h2>
<p>Sends a ping every five seconds</p>
<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.

slug: Web/API/WebSockets_API/Using_WebSocketStream
page-type: guide
status:
- 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.


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 `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 read message, 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.
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
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 read message, 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.
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 reasponse, 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

> [!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.

First we grab a reference to a DOM element into which we will write output messages, and define a utility function that writes a message to it:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm a bit worried that this duplication of the code will make it hard to maintain. I understand though that just quoting the whole block then talking through it all might be indigestible, so idk what is for the best.

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 appreciate that, but I've not got a better solution. Leaving as-is.

Copy link
Collaborator

@wbamberg wbamberg left a comment

Choose a reason for hiding this comment

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

Comments for the other pages.

- `options` {{optional_inline}}
- : An object than can contain the following properties:
- `protocols` {{optional_inline}}
- : An array of strings representing custom protocols to use for the connection.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we explain what this option is used for? Also, are there syntactic restrictions on the strings?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added. Info on allowable names was hard to track down, and I didn't really find anything concrete about syntactic restrictions. I have added some more useful info.

Comment on lines 62 to 63
> [!NOTE]
> Alternatively, you can use the {{domxref("WebSocketStream.close()")}} method to close a connection, however this is mainly needed if you wish to specify a custom code and reason for the server to report.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't think this should be a note. It works fine as a normal paragraph.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. I've de-noted it.

- `url`
- : A string representing the URL of the WebSocket server you want to connect to with this `WebSocketStream` instance. Allowed URL schemes are `"ws"`, `"wss"`, `"http"`, and `"https"`.
- `options` {{optional_inline}}
- : An object than can contain the following properties:
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
- : An object than can contain the following properties:
- : An object that can contain the following properties:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed - ta!

## Syntax

```js-nolint
new WebSocketStream(url, options)
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
new WebSocketStream(url, options)
new WebSocketStream(url)
new WebSocketStream(url, options)

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.

Comment on lines 16 to 17
> [!NOTE]
> An alternative mechanism for closing a `WebSocketStream` is to specify an {{domxref("AbortSignal")}} in the [`signal`](/en-US/docs/Web/API/WebSocketStream/WebSocketStream#signal) option of the constructor upon creation. The associated {{domxref("AbortController")}} can then be used to close the WebSocket connection. This is generally the preferred mechanism, however `close()` can be used if you wish to specify a custom code and reason for the server to report.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again I don't think this is a note.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Again, de-noted.

- {{domxref("WebSocketStream.url", "url")}} {{ReadOnlyInline}}
- : Returns the URL of the WebSocket server that the `WebSocketStream` instance was created with.
- {{domxref("WebSocketStream.closed", "closed")}} {{ReadOnlyInline}}
- : Returns a {{jsxref("Promise")}} that fulfills with an object once the socket connection is closed. The object contains the closing code and reason sent by the server.
Copy link
Collaborator

Choose a reason for hiding this comment

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

re "sent by the server", same comment as in the closed page, what if the client closes it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same answer as above. I'm not sure if this needs an update. I have modified it slightly, to "as sent by the server"

- {{domxref("WebSocketStream.closed", "closed")}} {{ReadOnlyInline}}
- : Returns a {{jsxref("Promise")}} that fulfills with an object once the socket connection is closed. The object contains the closing code and reason sent by the server.
- {{domxref("WebSocketStream.opened", "opened")}} {{ReadOnlyInline}}
- : Returns a {{jsxref("Promise")}} that fulfills with an object once the socket connection is successfully opened. The object contains several useful features, including a {{domxref("ReadableStream")}} and a {{domxref("WritableStream")}} instance for receiving and sending data on the connection.
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
- : Returns a {{jsxref("Promise")}} that fulfills with an object once the socket connection is successfully opened. The object contains several useful features, including a {{domxref("ReadableStream")}} and a {{domxref("WritableStream")}} instance for receiving and sending data on the connection.
- : Returns a {{jsxref("Promise")}} that fulfills with an object once the socket connection is successfully opened. Among other properties, the object includes a {{domxref("ReadableStream")}} and a {{domxref("WritableStream")}} instance for receiving and sending data on the connection.

Very picky. "several useful features" seems a bit odd. Should it be taken for granted that the properties are useful?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, very picky. You are right too, which is even more annoying ;-)

I have updated the wording to "Among other features, this object contains a...", as the items listed aren't the actual property names, but the values they contain.

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

The **`opened`** read-only property of the
{{domxref("WebSocketStream")}} interface returns a {{jsxref("Promise")}} that fulfills with an object once the socket connection is successfully opened. The object contains several useful features, including a {{domxref("ReadableStream")}} and a {{domxref("WritableStream")}} instance for receiving and sending data on the connection.
Copy link
Collaborator

Choose a reason for hiding this comment

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

"several useful features" again.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same change made here.

- `extensions`
- : A string representing any extensions applied to the `WebSocketStream`. Such extensions are not currently defined, but may be in the future. Currently returns an empty string.
- `protocol`
- : A string representing any custom protocol used to open the current WebSocket connection, as specified in the [`protocols`](/en-US/docs/Web/API/WebSocketStream/WebSocketStream#protocols) option of the `WebSocketStream()` constructor. Returns an empty string if no custom protocol was used.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess this must be one of the ones given in the constructor?

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 really understand the question. The text clearly says "as specified in the protocols option of the WebSocketStream() constructor".

I've updated the end of the description to "if no custom protocol was set during instantiation", in case it is useful.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think "as specified in..." is very clear. protocols is an array, this is a single item. I'm assuming what happens is that the client passes, in protocols, an array of custom protocols it is prepared to work with, and the server responds with a single protocol from the array. But the docs don't say this, so I have to guess.

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 getcha now. I've done a bit of reading around this, and have updated this description to:

A string representing the sub-protocol used to open the current WebSocket connection (chosen from the options specified in the protocols option of the WebSocketStream() constructor). Returns an empty string if no sub-protocol has been used to open the connection (i.e. no sub-protocol options were included in the constructor call).

## Examples

```js
const output = document.querySelector("#output");
Copy link
Collaborator

Choose a reason for hiding this comment

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

As for closed, I think a smaller more focused example would be better.

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've cut it down quite a bit, but not as much as the closed example.

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 `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]
> 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.

Comment on lines 15 to 16
> [!NOTE]
> Backpressure is an issue with the traditional `WebSocket` API — it has no way to apply 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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

In terms of how to use it, it is just automatic for WebSocketStream. I do say "automatically" in the opening paragraph. Do you think I need to be a bit more explicit?

Yes, I do. The doc links to https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Concepts#backpressure, which as I say is hard to understand, but does not make it obvious that the developer has to do nothing for backpressure to work.

I think it would be best to spell this out in the bit about "To read data from the socket, you can continuously call ReadableStreamDefaultReader.read() ...", and will leave a suggestion there.

Comment on lines +65 to +77
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
}
```

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.

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.

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.

slug: Web/API/WebSockets_API/Using_WebSocketStream
page-type: guide
status:
- 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.

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

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.

Comment on lines +198 to +200
wss.close({
code: 1000,
reason: "That's all folks",
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
Collaborator

@wbamberg wbamberg left a comment

Choose a reason for hiding this comment

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

👍 thanks Chris!

@wbamberg wbamberg merged commit f56df7c into mdn:main Sep 12, 2024
9 checks passed
@chrisdavidmills chrisdavidmills deleted the websocketstream branch September 13, 2024 07:07
@chrisdavidmills
Copy link
Contributor Author

👍 thanks Chris!

Likewise, thanks so much for the review Will!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Content:WebAPI Web API docs size/l [PR only] 501-1000 LoC changed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants