Primate LogoPrimate
GuidesResponses

Implement a real-time chat with WebSocket

You can use the ws handler to upgrade a GET route to WebSocket. To use this to implement a real-time chat application, first create a HTML client.

This assumes you have installed and loaded @primate/html in your config file.

The HTML frontend will automatically extract <style> and <script> tags and bundle them into the rest of your application.

HTMLviews/index.html
<script>
  window.addEventListener("load", () => {
    const ws = new WebSocket(new URL("/ws?limit=20", window.location.href));
    ws.addEventListener("open", () => {
      document.querySelector("#chat").addEventListener("keypress", event => {
        if (event.key === "Enter") {
          ws.send(event.target.value);
          event.target.value = "";
        }
      })
    });
    ws.addEventListener("message", message => {
      const chatbox = document.querySelector("#box");
      const div = document.createElement("div");
      div.innerText = message.data;
      chatbox.appendChild(div);
    })
  });
</script>
<style>
  #box {
    background-color: lightgrey;
    width: 300px;
    height: 300px;
    overflow: auto;
  }
</style>
<div id="box"></div>
<input id="chat" placeholder="Type to chat" />

Serve the client from the index route (/).

TypeScriptroutes/index.ts
import response from "primate/response";
import route from "primate/route";

export default route({
  get() {
    return response.view("index.html");
  },
});

At the /ws route, upgrade with the ws handler, maintaining a list of active web socket connections and distributing messages to all active chat participants. The message counter belongs to the sending connection and is incremented once before broadcasting; messages beyond its limit are ignored. The optional limit query parameter defaults to 20.

TypeScriptroutes/ws.ts
import p from "pema";
import response from "primate/response";
import route from "primate/route";

type Socket = { send(data: string | Uint8Array): void };

const sockets = new Set<Socket>();

export default route({
  get(request) {
    // limit the number of messages a client can send
    const limit = p.loose.uint.default(20).parse(request.query.try("limit"));
    let messages = 0;
    return response.ws({
      close(socket) {
        sockets.delete(socket);
      },
      message(_, message) {
        if (messages >= limit) return;
        messages++;
        for (const socket of sockets) socket.send(message);
      },
      open(socket) {
        sockets.add(socket);
      },
    });
  },
});