Real-time Communication with Websockets

Websockets provide a powerful mechanism for establishing a full-duplex communication channel between a client and a server. This enables real-time updates and interactions in your application.

Brisa supports server-side WebSockets, with on-the-fly compression, TLS support, and a Bun-native publish-subscribe API.

Bun's WebSockets are fast, ~700,000 messages sent per second.

This WebSockets documentation only works with the Bun.js runtime. If you want to use Node.js as output you will have to implement it with some library in this same file, look here how to do it.

Start a WebSocket server API

For this purpose it is necessary to create the file src/websocket.(ts|js) (or src/websocket/index.(ts|js)) and you can export these functions:

import { ServerWebSocket } from "bun";

export function attach(request: Request) {
  // attach contextual data to the ws.data
  return { foo: "foo" }; // ws.data.foo
}

export function message(ws: ServerWebSocket, message: string) {
  // a message is received
}

export function open(ws: ServerWebSocket) {
  // a socket is opened
}

export function close(ws: ServerWebSocket) {
  // a socket is closed
}

export function drain(ws: ServerWebSocket) {
  // the socket is ready to receive more data
}

These handlers are declared once per server, instead of per socket. So, instead of using an event-based API it is reused for each connection, this leads to less memory usage and less time spent adding/removing event listeners.

attach

The attach function is responsible for attaching contextual data to the ws.data property of the WebSocket.

Before a WebSocket connection is established, this function is invoked, providing access to the initial request (Request).

You can use this function to associate relevant information or metadata with the WebSocket connection.

export function attach(request: Request) {
  const cookies = parseCookies(req.headers.get("Cookie"));

  return {
    createdAt: Date.now(),
    channelId: new URL(req.url).searchParams.get("channelId"),
    authToken: cookies["X-Token"],
  },
}

This information will be accessible through ws.data.

message

The message function is triggered whenever a message is received on the WebSocket connection.

It accepts two parameters: ws (the ServerWebSocket instance) and message (the received message as a string).

You can implement logic within this function to handle and process incoming messages. The message function provides the means to respond to client messages in real-time and execute corresponding actions based on the content of the received message.

open

The open function is called when a new WebSocket connection is established and opened.

It accepts one parameters: ws (the ServerWebSocket instance).

You can use this function to perform setup tasks or execute actions specific to the initiation of a WebSocket connection.

This could include tasks such as logging, authentication, or broadcasting a welcome message to the connected client.

close

The close function is invoked when a WebSocket connection is closed.

It accepts one parameters: ws (the ServerWebSocket instance).

It allows you to define cleanup procedures or perform actions specific to the closure of a WebSocket connection. For example, scenarios like logging disconnections, updating user statuses, or releasing resources related to the closed connection.

drain

The drain function is called when the WebSocket is ready to receive more data. It signifies that the underlying transport is available for sending additional messages.

You can use this function to implement custom logic related to handling the readiness of the WebSocket to receive more data. This might include managing queues of messages to be sent or coordinating the flow of real-time updates based on the current state of the WebSocket connection.

Sending messages

Each ServerWebSocket instance has a .send() method for sending messages to the client. It supports a range of input types.

ws.send("Hello world"); // string
ws.send(response.arrayBuffer()); // ArrayBuffer
ws.send(new Uint8Array([1, 2, 3])); // TypedArray | DataView

You have access to the ServerWebSocket from any server-component, middleware, API route, etc. Since it is inside the RequestContext.

This is an example of sending a message from an API route:

src/api/hello-world.ts

import { type RequestContext } from "brisa";

export function GET({ ws, i18n }: RequestContext) {
  const message = i18n.t("hello-world");

  // Sending a WebSocket message from an API route
  ws.send(message);

  return new Response(message, {
    headers: { "content-type": "text/plain" },
  });
}

Pub/Sub

Bun's ServerWebSocket implementation implements a native publish-subscribe API for topic-based broadcasting. Individual sockets can .subscribe() to a topic (specified with a string identifier) and .publish() messages to all other subscribers to that topic (excluding itself). This topic-based broadcast API is similar to MQTT and Redis Pub/Sub.

src/websocket.ts

export function open(ws) {
  const msg = `${ws.data.username} has entered the chat`;
  ws.subscribe("the-group-chat");
  ws.publish("the-group-chat", msg);
}

export function message(ws, message) {
  // this is a group chat
  // so the server re-broadcasts incoming message to everyone
  ws.publish("the-group-chat", `${ws.data.username}: ${message}`);
}

export function close(ws) {
  const msg = `${ws.data.username} has left the chat`;
  ws.unsubscribe("the-group-chat");
  ws.publish("the-group-chat", msg);
}

Calling ws.publish(data) will send the message to all subscribers of a topic except the socket that called ws.publish().

To send a message to all subscribers of a topic, use the server.publish() method on the Server instance. You can get the server instance via globalThis.brisaServer:

const server = globalThis.brisaServer;
server.publish("send-message-to-all-subscribers", msg);

Compression

Compression can be enabled for individual messages by passing a boolean as the second argument to .send() or as the third argument to .publish().

ws.send(message, true);
ws.publish(topic, message, true);

Backpressure

The .send(message) method of ServerWebSocket returns a number indicating the result of the operation.

  • -1 โ€” The message was enqueued but there is backpressure
  • 0 โ€” The message was dropped due to a connection issue
  • 1+ โ€” The number of bytes sent

This gives you better control over backpressure in your server.

Start a WebSocket client API

Starting a WebSocket client in Brisa involves using the WebSocket Web API:

Connect to WebSocket Server

To establish a connection to the WebSocket server, you can use the standard WebSocket API in the browser. The following example demonstrates how to initiate a connection in a browser environment:

const ws = new WebSocket("wss://your-server.com");

Handle WebSocket Events inside Web-components

WebSocket connections trigger various events during their lifecycle. To react to these events, you can attach event listeners to the WebSocket instance. Common events include open, message, close, and error. The following snippet illustrates how to handle these events:

// Event listener for when the connection is established
ws.addEventListener("open", (event) => {
  console.log("WebSocket connection opened:", event);
});

// Event listener for incoming messages
ws.addEventListener("message", (event) => {
  console.log("Received message:", event.data);
});

// Event listener for when the connection is closed
ws.addEventListener("close", (event) => {
  console.log("WebSocket connection closed:", event);
});

// Event listener for errors
ws.addEventListener("error", (event) => {
  console.error("WebSocket error:", event);
});

WebSocket connections may not persist indefinitely. Various factors, such as network issues, server restarts, or client-side disruptions, can lead to the termination of WebSocket connections. It is crucial to implement appropriate error handling and reconnection strategies on the client side to gracefully handle unexpected disconnections.

Send Messages to Server

You can send messages to the WebSocket server using the send method. The following example demonstrates how to send a simple text message:

ws.send("Hello, server!");

Close WebSocket Connection

When you want to close the WebSocket connection, you can call the close method. Optionally, you can provide a reason and code for the closure:

ws.close();
// or
ws.close(1000, "Closing connection gracefully");

WebSockets in Node.js

If you want to use WebSockets in a Node.js environment, you can use the ws library. Here is an example of how to create a WebSocket server using the ws library in the src/websocket.ts file:

src/websocket.ts

const WebSocket = require('ws');

// Create a WebSocket server
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
  // Log a message when a new client connects
  console.log('A new client connected!');
  
  // Receive messages from the client
  ws.on('message', function incoming(message) {
    console.log('Received:', message);
    
    // Send a response back to the client
    ws.send(`You said: ${message}`);
  });

  // Log a message when the client disconnects
  ws.on('close', () => {
    console.log('Client has disconnected');
  });
});

This src/websocket.ts file is loaded once when the server is created, if the module exports the attach, message, open, close, and drain functions, they will only be used in Bun.js, but when the file is executed in Node.js, the ws functions can be used to create a WebSocket server in Node.js.