Schedule a call

Architecture

Deno WebSocketServer with HTTP API and EventSource Client

Deno is an anagram of Node and is written by the same person, Ryan Dahl, not exactly a successor but a fresh approach. What we'll build.

· Tom Eustace
Deno WebSocketServer with HTTP API and EventSource Client

Deno is an anagram of Node and is written by the same person, Ryan Dahl, not exactly a successor but a fresh approach.

What we’ll build. Essentially it’s a WebSocket proxy, it will take existing WebSocket connections and pass events to the connected client if requested. It consists of three main pieces.

  1. $1
  2. $1
  3. $1

The WebSockerServer will store client connections. The EventSource client connects to the server and receives events from connected WebSockets using Server Sent Events.

The WebSockerServer

Lets start by creating a simple WebSocketServer.

import { WebSocketServer } from "https://deno.land/x/[email protected]/mod.ts";

const WS_PORT = 8080;
const wss = new WebSocketServer(WS_PORT);

wss.on("connection", function (ws: any) {
  console.log("client connected");
  ws.on("message", async function (message: any) {
    const data = JSON.parse(message);
    console.log("message:", data);
  });

  ws.on("close", function () {
    console.log("connection closed");
  })
});

console.log("WebSocket: listening on port", WS_PORT );

To run this we can use deno run --allow-net server.ts . Deno requires explicit permissions to be provided for various operations like network access, file reading and writing etc, hence the --allow-net server.ts.

Now we should see our console message WebSocket: listening on port 8080 .

We’ll use a teeny index.html file to test out connection.


  DenoWebsocketServer

    let ws = new WebSocket("ws://localhost:8080");
    ws.onopen = () => {
      console.log("WebSocket Open");
    }

We’ll serve our index.html via serve ( npm i -g serve ). We just need to run serve in the directory with our index.html and open the browser to http://localhost:3000/index.html, we should see our WebSocket Open message in the console.

The HTTP API

Let’s add a simple HTTP operation.

import { WebSocketServer } from "https://deno.land/x/[email protected]/mod.ts";
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
import { oakCors } from "https://deno.land/x/cors/mod.ts";

const WS_PORT = 8080;
const wss = new WebSocketServer(WS_PORT);

wss.on("connection", function (ws: any) {

  console.log("client connected:", ws);
  ws.on("message", async function (message: any) {
    const data = JSON.parse(message);
    console.log("message:", data);
  });

  ws.on("close", function () {
    console.log("connection closed");
  })
});

console.log("listening on port", WS_PORT );

const HTTP_PORT = 8082;
const app = new Application();
const router = new Router();

const hello = async({request, response}: {request: any, response: any}) => {
  response.body = {response: "world"};
};

router.get("/hello", hello);

app.use(oakCors({origin: "*"}))
app.use(router.routes());
app.listen({ port: HTTP_PORT });

Update client to call our new HTTP operation.


  DenoWebsocketServer

    let ws = new WebSocket("ws://localhost:8080");
    ws.onopen = () => {
      console.log("WebSocket Open");
    }

    fetch("http://localhost:8082/hello")
      .then(response => response.json())
      .then(response =>  {
        console.log("http resonse", response);
      });

Great, now we have a WebSocketServer and a basic HTTP API. Let’s hook up the client to receive WebSocket events from the various connections.

First we need a way to track the WebSocket connections, so we’ll need each connection to provide a unique id (we’ll use a poor mans unique id 😪 ) which we’ll store in a Map. We’ll also update our HTTP endpoint to handle Server Sent Events and remove hello.

import { WebSocketServer } from "https://deno.land/x/[email protected]/mod.ts";
import { Application, Context, Router, ServerSentEventTarget } from "https://deno.land/x/oak/mod.ts";
import { oakCors } from "https://deno.land/x/cors/mod.ts";

const WS_PORT = 8080;
const wss = new WebSocketServer(WS_PORT);

let eventClient: ServerSentEventTarget;

// we want to ensure there is a connectionId also
type WebSocketServerId = WebSocketServer & {connectionId: string};
// map of websocket connections
const connections = new Map();

wss.on("connection", function (ws: WebSocketServerId) {
    
   ws.on("message", async function (event) {
   const data = JSON.parse(event);
   // websocket clients must send a connectionId
   if (!data.connectionId) {
     throw new Error("No connectionId");
   }

   ws.connectionId = data.connectionId;

   // store the connection
   connections.set(ws.connectionId, ws);
   try {
      if (eventClient) {
        // dispatch message to EventSource client if exists
        eventClient?.dispatchMessage(data);
      }
    } catch(err) {
      console.log(err);
    }
    
  });
  
  ws.on("close", function () {
    // remove the connection from map
    connections.delete(ws.connectionId);
    const msg = JSON.stringify({ close: { connectionId: ws.connectionId } }); 
    // update client with close event
    if (eventClient) {
      eventClient.dispatchMessage(msg);
    }
  });
    
});

console.log("listening on port", WS_PORT );

const HTTP_PORT = 8082;
const app = new Application();
const router = new Router();

// provide an http endpoint for our EventSource client to connect on
const events = (context: Context) => {
  eventClient = context.sendEvents();
};
router.get("/events", events);

app.use(oakCors({origin: "*"}))
app.use(router.routes());
app.listen({ port: HTTP_PORT });

Now we need to test this using some WebSocket clients that will register connections with the WebSocketServer and then create an EvenSource client that will get pushed messages on any open WebSocket connections.

The WebSocket Client

Lets update our client to just open a WebSocket connection and send a message after 5 seconds.


  DenoWebsocketServer

# WebSocket Client

    const connectionId = Date.now().toString();
    let ws = new WebSocket("ws://localhost:8080");
    ws.onopen = () => {
      console.log("WebSocket Open");
      ws.send( JSON.stringify( {connectionId} ) );
    }

    // send a message to the server
    setTimeout(() => {
      ws.send( JSON.stringify( {connectionId, message: 'hello from WebSocket client!!'} ) );
    }, 5000);

The EventSource Client

We create a separate file for our EventSource client that will get pushed WebSocket messages from the WebSocketServer.


  DenoWebsocketServer

# Event Source Client

    let eventSource = new EventSource("http://localhost:8082/events");
    eventSource.onmessage = (event) => {
      const {connectionId, message, close} = JSON.parse(event.data);
      if (close) {
        console.log(`connectionId ${close.connectionId}: closed`);
        return;
      }
      if (connectionId && !message) {
        console.log(`connectionId ${connectionId}: connected`);
      } else {
        console.log(`connectionId ${connectionId}: ${message}`);
      }
    }

Now restart the WebSocket server, open the EventSource client http://localhost:3000/es-client . Once our EventSource client is connected open a couple of WebSocket clients http://localhost:3000/ws-client.

You should now be able to see messages from the WebSocket clients coming into the EventSource client.

This has been a simple introduction to Deno that may serve as a foundation for more complex use cases where you need a way to tap into WebSocket data.