til: deno_annotated-deno-deploy-demo.md
This data as json
| path | topic | title | url | body | html | shot | created | created_utc | updated | updated_utc | shot_hash | slug |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| deno_annotated-deno-deploy-demo.md | deno | Annotated code for a demo of WebSocket chat in Deno Deploy | https://github.com/simonw/til/blob/main/deno/annotated-deno-deploy-demo.md | Deno Deploy is a hosted Deno service that promises [a multi-tenant JavaScript engine running in 25 data centers across the world](https://deno.com/blog/deploy-beta1/). Today [this demo](https://dash.deno.com/playground/mini-ws-chat) by [Ondřej Žára](https://twitter.com/0ndras/status/1457027832404713479) showed up [on Hacker News](https://news.ycombinator.com/item?id=29131751), which implements "a multi-datacenter chat, client+server in 23 lines of TS". Here's my annotated copy of the code, which I wrote while figuring out how it works. ```typescript // listenAndServe is the Deno standard mechanism for creating an HTTP server // https://deno.land/manual/examples/http_server#using-the-codestdhttpcode-library import { listenAndServe } from "https://deno.land/std/http/server.ts" // Set of all of the currently open WebSocket connections from browsers const sockets = new Set<WebSocket>(), /* BroadcastChannel is a concept that is unique to the Deno Deploy environment. https://deno.com/deploy/docs/runtime-broadcast-channel/ It is modelled after the browser API of the same name. It sets up a channel between ALL instances of the server-side script running in every one of the Deno Deploy global network of data centers. The argument is the name of the channel, which apparently can be an empty string. */ channel = new BroadcastChannel(""), headers = {"Content-type": "text/html"}, /* This is the bare-bones HTML for the browser side of the application It creates a WebSocket connection back to the host, and sets it up so any message that arrives via that WebSocket will be appended to the textContent of the pre element on the page. The input element has an onkeyup that checks for the Enter key and sends the value of that element over the WebSocket channel to the server. */ html = `<script>let ws = new WebSocket("wss://"+location.host) ws.onmessage = e => pre.textContent += e.data+"\\n"</script> <input onkeyup="event.key=='Enter'&&ws.send(this.value)"><pre id=pre>` /* This bit does the broadcast work: any time a message is received from the BroadcastChannel it is forwarded on to every single one of the currently attached WebSocket connections, using the data in that "sockets" set. Additionally, this covers the case of messages coming from a client connected to THIS instance - these are also sent to the channel (see code below), but here it spots that the message event's e.target is NOT the current instance and sends the message to that channel so it broadcast to the other data centers. */ channel.onmessage = e => { (e.target != channel) && channel.postMessage(e.data) sockets.forEach(s => s.send(e.data)) } /* I tried removing the await here and the demo still worked. But https://deno.land/std@0.113.0/http/server.ts#L224 shows that this function is indeed an async that returns a Promise. */ await listenAndServe(":8080", (r: Request) => { try { /* Deno.upgradeWebSocket is a relatively new feature, added in Deno v1.12 in July 2021: https://deno.com/blog/v1.12#server-side-websocket-support-in-native-http It gives you back a response that you should return to the client in order to finish establishing the WebSocket connection, and a socket object which you can then use for further WebSocket communication. */ const { socket, response } = Deno.upgradeWebSocket(r) // Add it to the set so we can send to all of them later sockets.add(socket) /* This is a sneaky hack: when a message arrives from the WebSocket we pass it directly to the BroadcastChannel - then use the e.target != channel check above to broadcast it on to every other global instance. */ socket.onmessage = channel.onmessage // When browser disconnects, remove the socket from the set of sockets socket.onclose = _ => sockets.delete(socket) return response } catch { /* I added code here to catch(e) and display e.toString() which showed me that the exception caught here is: exception: TypeError: Invalid Header: 'upgrade' header must be 'websocket' This is an exception thrown by Deno.upgradeWebSocket(r) if the incoming request does not include the "upgrade: websocket" HTTP header, which is added by browsers when using new WebSocket("wss://...") So here we return the HTML and headers for the application itself. */ return new Response(html, {headers}) } }) ``` Relevant links: - [Deno listenAndServe documentation](https://deno.land/manual/examples/http_server#using-the-codestdhttpcode-library) - [Deno Deploy BroadcastChannel documentation](https://deno.com/deploy/docs/runtime-broadcast-channel/) - [MDN documentation of the related BroadcastChannel browser API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API) | <p>Deno Deploy is a hosted Deno service that promises <a href="https://deno.com/blog/deploy-beta1/" rel="nofollow">a multi-tenant JavaScript engine running in 25 data centers across the world</a>.</p> <p>Today <a href="https://dash.deno.com/playground/mini-ws-chat" rel="nofollow">this demo</a> by <a href="https://twitter.com/0ndras/status/1457027832404713479" rel="nofollow">Ondřej Žára</a> showed up <a href="https://news.ycombinator.com/item?id=29131751" rel="nofollow">on Hacker News</a>, which implements "a multi-datacenter chat, client+server in 23 lines of TS".</p> <p>Here's my annotated copy of the code, which I wrote while figuring out how it works.</p> <div class="highlight highlight-source-ts"><pre><span class="pl-c">// listenAndServe is the Deno standard mechanism for creating an HTTP server</span> <span class="pl-c">// https://deno.land/manual/examples/http_server#using-the-codestdhttpcode-library</span> <span class="pl-k">import</span> <span class="pl-kos">{</span> <span class="pl-s1">listenAndServe</span> <span class="pl-kos">}</span> <span class="pl-k">from</span> <span class="pl-s">"https://deno.land/std/http/server.ts"</span> <span class="pl-c">// Set of all of the currently open WebSocket connections from browsers</span> <span class="pl-k">const</span> <span class="pl-s1">sockets</span> <span class="pl-c1">=</span> <span class="pl-k">new</span> <span class="pl-smi">Set</span><span class="pl-kos"><</span><span class="pl-smi">WebSocket</span><span class="pl-kos">></span><span class="pl-kos">(</span><span class="pl-kos">)</span><span class="pl-kos">,</span> <span class="pl-c">/*</span> <span class="pl-c">BroadcastChannel is a concept that is unique to the Deno Deploy environment.</span> <span class="pl-c"></span> <span class="pl-c">https://deno.com/deploy/docs/runtime-broadcast-channel/</span> <span class="pl-c"></span> <span class="pl-c">It is modelled after the browser API of the same name.</span> <span class="pl-c"></span> <span class="pl-c">It sets up a channel between ALL instances of the server-side script running</span> <span class="pl-c">in every one of the Deno Deploy global network of data centers.</span> <span class="pl-c"></span> <span class="pl-c">The argument is the name of the channel, which apparently can be an empty string.</span> <span class="pl-c">*/</span> <span class="pl-s1">channel</span> <span class="pl-c1">=</span> <span class="pl-k">new</span> <span class="pl-smi">BroadcastChannel</span><span class="pl-kos">(</span><span class="pl-s">""</span><span class="pl-kos">)</span><span class="pl-kos">,</span> <span class="pl-s1">headers</span> <span class="pl-c1">=</span> <span class="pl-kos">{</span><span class="pl-s">"Content-type"</span>: <span class="pl-s">"text/html"</span><span class="pl-kos">}</span><span class="pl-kos">,</span> <span class="pl-c">/*</span> <span class="pl-c">This is the bare-bones HTML for the browser side of the application</span> <span class="pl-c"></span> <span class="pl-c">It creates a WebSocket connection back to the host, and sets it up so any</span> <span class="pl-c">message that arrives via that WebSocket will be appended to the textContent</span> <span class="pl-c">of the pre element on the page.</span> <span class="pl-c"></span> <span class="pl-c">The input element has an onkeyup that checks for the Enter key and sends</span> <span class="pl-c">the value of that element over the WebSocket channel to the server.</span> <span class="pl-c">*/</span> <span class="pl-s1">html</span> <span class="pl-c1">=</span> <span class="pl-s">`<script>let ws = new WebSocket("wss://"+location.host)</span> <span class="pl-s">ws.onmessage = e => pre.textContent += e.data+"\\n"</script></span> <span class="pl-s"><input onkeyup="event.key=='Enter'&&ws.send(this.value)"><pre id=pre>`</span> <span class="pl-c">/*</span> <span class="pl-c">This bit does the broadcast work: any time a message is received from the</span> <span class="pl-c">BroadcastChannel it is forwarded on to every single one of the currently</span> <span class="pl-c">attached WebSocket connections, using the data in that "sockets" set.</span> <span class="pl-c"></span> <span class="pl-c">Additionally, this covers the case of messages coming from a client connected</span> <span class="pl-c">to THIS instance - these are also sent to the channel (see code below), but</span> <span class="pl-c">here it spots that the message event's e.target is NOT the current instance</span> <span class="pl-c">and sends the message to that channel so it broadcast to the other data centers.</span> <span class="pl-c">*/</span> <span class="pl-s1">channel</span><span class="pl-kos">.</span><span class="pl-en">onmessage</span> <span class="pl-c1">=</span> <span class="pl-s1">e</span> <span class="pl-c1">=></span> <span class="pl-kos">{</span> <span class="pl-kos">(</span><span class="pl-s1">e</span><span class="pl-kos">.</span><span class="pl-c1">target</span> <span class="pl-c1">!=</span> <span class="pl-s1">channel</span><span class="pl-kos">)</span> <span class="pl-c1">&&</span> <span class="pl-s1">channel</span><span class="pl-kos">.</span><span class="pl-en">postMessage</span><span class="pl-kos">(</span><span class="pl-s1">e</span><span class="pl-kos">.</span><span class="pl-c1">data</span><span class="pl-kos">)</span> <span class="pl-s1">sockets</span><span class="pl-kos">.</span><span class="pl-en">forEach</span><span class="pl-kos">(</span><span class="pl-s1">s</span> <span class="pl-c1">=></span> <span class="pl-s1">s</span><span class="pl-kos">.</span><span class="pl-en">send</span><span class="pl-kos">(</span><span class="pl-s1">e</span><span class="pl-kos">.</span><span class="pl-c1">data</span><span class="pl-kos">)</span><span class="pl-kos">)</span> <span class="pl-kos">}</span> <span class="pl-c">/*</span> <span class="pl-c">I tried removing the await here and the demo still worked.</span> <span class="pl-c"></span> <span class="pl-c">But https://deno.land/std@0.113.0/http/server.ts#L224 shows that this function</span> <span class="pl-c">is indeed an async that returns a Promise.</span> <span class="pl-c">*/</span> <span class="pl-k">await</span> <span class="pl-en">listenAndServe</span><span class="pl-kos">(</span><span class="pl-s">":8080"</span><span class="pl-kos">,</span> <span class="pl-kos">(</span><span class="pl-s1">r</span>: <span class="pl-smi">Request</span><span class="pl-kos">)</span> <span class="pl-c1">=></span> <span class="pl-kos">{</span> <span class="pl-k">try</span> <span class="pl-kos">{</span> <span class="pl-c">/*</span> <span class="pl-c"> Deno.upgradeWebSocket is a relatively new feature, added in Deno v1.12</span> <span class="pl-c"> in July 2021:</span> <span class="pl-c"> https://deno.com/blog/v1.12#server-side-websocket-support-in-native-http</span> <span class="pl-c"> </span> <span class="pl-c"> It gives you back a response that you should return to the client in order</span> <span class="pl-c"> to finish establishing the WebSocket connection, and a socket object which</span> <span class="pl-c"> you can then use for further WebSocket communication.</span> <span class="pl-c"> */</span> <span class="pl-k">const</span> <span class="pl-kos">{</span> socket<span class="pl-kos">,</span> response <span class="pl-kos">}</span> <span class="pl-c1">=</span> <span class="pl-smi">Deno</span><span class="pl-kos">.</span><span class="pl-en">upgradeWebSocket</span><span class="pl-kos">(</span><span class="pl-s1">r</span><span class="pl-kos">)</span> <span class="pl-c">// Add it to the set so we can send to all of them later</span> <span class="pl-s1">sockets</span><span class="pl-kos">.</span><span class="pl-en">add</span><span class="pl-kos">(</span><span class="pl-s1">socket</span><span class="pl-kos">)</span> <span class="pl-c">/*</span> <span class="pl-c"> This is a sneaky hack: when a message arrives from the WebSocket we pass it</span> <span class="pl-c"> directly to the BroadcastChannel - then use the e.target != channel check</span> <span class="pl-c"> above to broadcast it on to every other global instance.</span> <span class="pl-c"> */</span> <span class="pl-s1">socket</span><span class="pl-kos">.</span><span class="pl-c1">onmessage</span> <span class="pl-c1">=</span> <span class="pl-s1">channel</span><span class="pl-kos">.</span><span class="pl-c1">onmessage</span> <span class="pl-c">// When browser disconnects, remove the socket from the set of sockets</span> <span class="pl-s1">socket</span><span class="pl-kos">.</span><span class="pl-en">onclose</span> <span class="pl-c1">=</span> <span class="pl-s1">_</span> <span class="pl-c1">=></span> <span class="pl-s1">sockets</span><span class="pl-kos">.</span><span class="pl-en">delete</span><span class="pl-kos">(</span><span class="pl-s1">socket</span><span class="pl-kos">)</span> <span class="pl-k">return</span> <span class="pl-s1">response</span> <span class="pl-kos">}</span> <span class="pl-k">catch</span> <span class="pl-kos">{</span> <span class="pl-c">/*</span> <span class="pl-c"> I added code here to catch(e) and display e.toString() which showed me</span> <span class="pl-c"> that the exception caught here is:</span> <span class="pl-c"></span> <span class="pl-c"> exception: TypeError: Invalid Header: 'upgrade' header must be 'websocket'</span> <span class="pl-c"></span> <span class="pl-c"> This is an exception thrown by Deno.upgradeWebSocket(r) if the incoming</span> <span class="pl-c"> request does not include the "upgrade: websocket" HTTP header, which</span> <span class="pl-c"> is added by browsers when using new WebSocket("wss://...")</span> <span class="pl-c"> </span> <span class="pl-c"> So here we return the HTML and headers for the application itself.</span> <span class="pl-c"> */</span> <span class="pl-k">return</span> <span class="pl-k">new</span> <span class="pl-smi">Response</span><span class="pl-kos">(</span><span class="pl-s1">html</span><span class="pl-kos">,</span> <span class="pl-kos">{</span>headers<span class="pl-kos">}</span><span class="pl-kos">)</span> <span class="pl-kos">}</span> <span class="pl-kos">}</span><span class="pl-kos">)</span></pre></div> <p>Relevant links:</p> <ul> <li><a href="https://deno.land/manual/examples/http_server#using-the-codestdhttpcode-library" rel="nofollow">Deno listenAndServe documentation</a></li> <li><a href="https://deno.com/deploy/docs/runtime-broadcast-channel/" rel="nofollow">Deno Deploy BroadcastChannel documentation</a></li> <li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API" rel="nofollow">MDN documentation of the related BroadcastChannel browser API</a></li> </ul> | <Binary: 75,043 bytes> | 2021-11-06T18:34:17-07:00 | 2021-11-07T01:34:17+00:00 | 2021-11-07T09:01:47-08:00 | 2021-11-07T17:01:47+00:00 | cd72f542e30595301089ca728b6be770 | annotated-deno-deploy-demo |