sono.land

The Chatroom

The Chatroom

In this tutorial we will create a basic chat application using a basic Deno webserver and the sono.io library.

Why use WebSockets?

Communcation between multiple parties has traditionally been done through long-polling a server. However, long polling is much more resource intensive on servers whereas WebSockets have an extremely lightweight footprint on servers, delivering low-latency messages between clients in real-time.

Why use Deno?

Deno is a run-time alternative to Node.js that offers first-class Typescript support, security, and a performnant module/package managament system. There is no real-time communcication library in Deno's standard modules as of yet.

The Server

Since the sono.io framework, in most cases, will lay on top of a Deno server, we will first set up a standard Deno server (without Sono yet) in TypeScript in order to serve our chatroom application. You can install the Deno runtime here and learn more about TypeScript here.

src/server.ts
1import { serve } from "https://deno.land/std@0.95.0/http/server.ts";
2
3const server = serve({ port: 8080 });
4
5for await (const req of server) {
6 if (req.method === "GET" && req.url === "/") {
7 req.respond({status: 200, body: 'Hello World!'})
8 }
9}
% deno run --allow-net mytest.ts

Once this code snippet is ran with the above command, you should see a page with 'Hello World!' displayed. The server is up and running with a top-level for await loop. But how do we serve a html file of our choice and use Sono.io to facilitate WebSocket requests?

src/server.ts
1import { serve } from "https://deno.land/std@0.95.0/http/server.ts";
2import { serveFile } from "https://deno.land/std@0.95.0/http/file_server.ts";
3import { Sono } from "https://deno.land/x/sono@v1.1/mod.ts"
4
5const server = serve({ port: 3000 });
6const sono = new Sono();
7
8for await (const req of server) {
9 if (req.method === "GET" && req.url === "/") {
10 const path = `${Deno.cwd()}/static/index.html`
11 const content = await serveFile(req, path);
12 req.respond(content)
13 }
14 else if (req.method === "GET" && req.url === "/ws") {
15 sono.connect(req, () => {
16 sono.emit('new client connected')
17 });
18 }
19 else if (req.method === "GET" && req.url === "/favicon.ico") {
20 // Do nothing in case of favicon request
21 }
22 else {
23 const path = `${Deno.cwd()}/static/${req.url}`;
24 const content = await serveFile(req, path);
25 req.respond(content)
26 }
27}

To serve the file: we have the 'serveFile' module from Deno's standard library to help us serve index.html in the static folder. To connect to the WebSocket request: at the top, we invoke a new instance of the Sono constructor. When the request URL is made to '/ws', we will call the connect method on the Sono object, which will allow us to accept the WebSocket request coming in from the client-side and then emit to any connected clients (including yourself) the message 'new client connected'.

The HTML

Now to initialize the index.html file that we are serving:

src/static/index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta http-equiv="X-UA-Compatible" content="IE=edge">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 <title>Document</title>
8</head>
9<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css">
10<style>
11 body { font-family: "Courier New"; }
12 #messages_container { height: 350px; }
13 .message {
14 border-bottom: 1px solid #000000;
15 padding: 5px;
16 }
17</style>
18<body>
19 <div class="mx-auto border border-black mt-10 p-2" id="container" style="max-width: 500px">
20 <div class="font-bold bg-black text-white text-center p-3" id="banner">Sono.io Chat</div>
21 <div class="overflow-x-auto" id="messages_container">
22 <ul id="messageBoard"></ul>
23 </div>
24 <div class="flex">
25 <input class="w-1/2 border border-black p-2" id="username" type="text" placeholder="username" />
26 <input class="w-1/2 border border-black p-2" id="input" type="text" placeholder="message" />
27 </div>
28 <button class="p-2 text-white bg-black w-full mt-1" id="button">send message</button>
29 </div>
30</body>
31<script type='module' src="main.js"></script>
32</html>

Copy and paste this code snippet into your directory as "index.html" in a folder named "static", where we'll be serving our static files. You should see a basic chatroom if you open the HTML file.

The JavaScript

Now for main.js, which will be our JavaScript file that makes use of the client-side Sono object:

src/static/main.js
1import { SonoClient } from "https://deno.land/x/sono@v1.1/src/sonoClient.js"
2
3const sono = new SonoClient('ws://localhost:8080/ws');
4sono.onconnection(()=>{
5 sono.message('client connected')
6})
7
8sono.on('message', (msg) => {
9 console.log('message received:', msg)
10})

As you can see above, we pass a request to our server, in this case localhost:8080, to the SonoClient constructor. Then we can call the .onconnection method to notify everyone connected to this WebSocket 'client connected'. The only other piece we need is an event listener, which simply prints out any messages received to the console. Now, the server and the front-end come together: if you start the server and then open a new tab pointing to that URL, you should see a message in the console saying 'client connected' once you connect to the WebSocket. Now onto adding messages to the DOM and seeing other clients' messages.

src/static/main.js
1import { SonoClient } from "https://deno.land/x/sono@v1.1/src/sonoClient.js"
2
3const sono = new SonoClient('ws://localhost:8080/ws');
4sono.onconnection(()=>{
5 sono.message('client connected')
6})
7
8sono.on('message', (payload) => {
9 const messageBoard = document.getElementById('messageBoard');
10 const newMessage = document.createElement('li')
11 if(payload.message.username) newMessage.innerHTML = `<strong>${payload.message.username}:</strong> ${payload.message.message}`;
12 else newMessage.innerHTML = payload.message;
13 messageBoard.appendChild(newMessage);
14})
15
16document.getElementById('button').addEventListener('click', () => {
17 const message = document.getElementById('input').value;
18 const username = document.getElementById('username').value;
19 sono.broadcast({message, username});
20 const messageBoard = document.getElementById('messageBoard');
21 const newMessage = document.createElement('li')
22 newMessage.innerHTML = `<strong>${username}:</strong> ${message}`;
23 newMessage.style.cssFloat='right';
24 messageBoard.appendChild(newMessage);
25 messageBoard.appendChild(document.createElement('br'));
26});

And there it is. A simple implementation of a chatroom. Upon a client connecting, the server will message everyone that a client has connected. However, upon a user sending a message, we can use the .broadcast method to send the message (which in this case will be an object with a username and a message) to every connected client, except for ourselves. Take note that broadcast can optionally take a second parameter that allows you to specify the event name. The default event name is 'message', but you customize the event name and simply listen for that specific event using the .on('insertCustomNameHere') method. To distinguish our own texts, we will put them to the right of the chatbox.

Edit this page on GitHub