Skip to content

Latest commit

 

History

History
464 lines (356 loc) · 13.3 KB

websockets.md

File metadata and controls

464 lines (356 loc) · 13.3 KB

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

{% callout %}

⚡️ 7x more throughput — Bun's WebSockets are fast. For a simple chatroom on Linux x64, Bun can handle 7x more requests per second than Node.js + "ws".

Messages sent per second Runtime Clients
~700,000 (Bun.serve) Bun v0.2.1 (x64) 16
~100,000 (ws) Node v18.10.0 (x64) 16

Internally Bun's WebSocket implementation is built on uWebSockets. {% /callout %}

Connect to a WebSocket server

To connect to an external socket server, create an instance of WebSocket with the constructor.

const socket = new WebSocket("ws://localhost:3000");

Bun supports setting custom headers. This is a Bun-specific extension of the WebSocket standard.

const socket = new WebSocket("ws://localhost:3000", {
  headers: {
    // custom headers
  },
});

To add event listeners to the socket:

// message is received
socket.addEventListener("message", event => {});

// socket opened
socket.addEventListener("open", event => {});

// socket closed
socket.addEventListener("close", event => {});

// error handler
socket.addEventListener("error", event => {});

Create a WebSocket server

Below is a simple WebSocket server built with Bun.serve, in which all incoming requests are upgraded to WebSocket connections in the fetch handler. The socket handlers are declared in the websocket parameter.

Bun.serve({
  fetch(req, server) {
    // upgrade the request to a WebSocket
    if (server.upgrade(req)) {
      return; // do not return a Response
    }
    return new Response("Upgrade failed :(", { status: 500 });
  },
  websocket: {}, // handlers
});

The following WebSocket event handlers are supported:

Bun.serve({
  fetch(req, server) {}, // upgrade logic
  websocket: {
    message(ws, message) {}, // a message is received
    open(ws) {}, // a socket is opened
    close(ws, code, message) {}, // a socket is closed
    drain(ws) {}, // the socket is ready to receive more data
  },
});

{% details summary="An API designed for speed" %}

In Bun, handlers are declared once per server, instead of per socket.

ServerWebSocket expects you to pass a WebSocketHandler object to the Bun.serve() method which has methods for open, message, close, drain, and error. This is different than the client-side WebSocket class which extends EventTarget (onmessage, onopen, onclose),

Clients tend to not have many socket connections open so an event-based API makes sense.

But servers tend to have many socket connections open, which means:

  • Time spent adding/removing event listeners for each connection adds up
  • Extra memory spent on storing references to callbacks function for each connection
  • Usually, people create new functions for each connection, which also means more memory

So, instead of using an event-based API, ServerWebSocket expects you to pass a single object with methods for each event in Bun.serve() and it is reused for each connection.

This leads to less memory usage and less time spent adding/removing event listeners. {% /details %}

The first argument to each handler is the instance of ServerWebSocket handling the event. The ServerWebSocket class is a fast, Bun-native implementation of WebSocket with some additional features.

Bun.serve({
  fetch(req, server) {}, // upgrade logic
  websocket: {
    message(ws, message) {
      ws.send(message); // echo back the message
    },
  },
});

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

Headers

Once the upgrade succeeds, Bun will send a 101 Switching Protocols response per the spec. Additional headers can be attched to this Response in the call to server.upgrade().

Bun.serve({
  fetch(req, server) {
    const sessionId = await generateSessionId();
    server.upgrade(req, {
      headers: {
        "Set-Cookie": `SessionId=${sessionId}`,
      },
    });
  },
  websocket: {}, // handlers
});

Contextual data

Contextual data can be attached to a new WebSocket in the .upgrade() call. This data is made available on the ws.data property inside the WebSocket handlers.

type WebSocketData = {
  createdAt: number;
  channelId: string;
};

// TypeScript: specify the type of `data`
Bun.serve<WebSocketData>({
  fetch(req, server) {
    server.upgrade(req, {
      // TS: this object must conform to WebSocketData
      data: {
        createdAt: Date.now(),
        channelId: new URL(req.url).searchParams.get("channelId"),
      },
    });

    return undefined;
  },
  websocket: {
    // handler called when a message is received
    async message(ws, message) {
      ws.data; // WebSocketData
      await saveMessageToDatabase({
        channel: ws.data.channelId,
        message: String(message),
      });
    },
  },
});

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. This topic-based broadcast API is similar to MQTT and Redis Pub/Sub.

const pubsubserver = Bun.serve<{username: string}>({
  fetch(req, server) {
    if (req.url === '/chat') {
      const cookies = getCookieFromRequest(req);
      const success = server.upgrade(req, {
        data: {username: cookies.username},
      });
      return success
        ? undefined
        : new Response('WebSocket upgrade error', {status: 400});
    }

    return new Response('Hello world');
  },
  websocket: {
    open(ws) {
      ws.subscribe('the-group-chat');
      ws.publish('the-group-chat', `${ws.data.username} has entered the chat`);
    },
    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}`);
    },
    close(ws) {
      ws.unsubscribe('the-group-chat');
      ws.publish('the-group-chat', `${ws.data.username} has left the chat`);
    },
});

Compression

Per-message compression can be enabled with the perMessageDeflate parameter.

Bun.serve({
  fetch(req, server) {}, // upgrade logic
  websocket: {
    // enable compression and decompression
    perMessageDeflate: true,
  },
});

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

ws.send("Hello world", true);

For fine-grained control over compression characteristics, refer to the Reference.

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.

Reference

namespace Bun {
  export function serve(params: {
    fetch: (req: Request, server: Server) => Response | Promise<Response>;
    websocket?: {
      message: (ws: ServerWebSocket, message: string | ArrayBuffer | Uint8Array) => void;
      open?: (ws: ServerWebSocket) => void;
      close?: (ws: ServerWebSocket) => void;
      error?: (ws: ServerWebSocket, error: Error) => void;
      drain?: (ws: ServerWebSocket) => void;
      perMessageDeflate?:
        | boolean
        | {
            compress?: boolean | Compressor;
            decompress?: boolean | Compressor;
          };
    };
  }): Server;
}

type Compressor =
  | `"disable"`
  | `"shared"`
  | `"dedicated"`
  | `"3KB"`
  | `"4KB"`
  | `"8KB"`
  | `"16KB"`
  | `"32KB"`
  | `"64KB"`
  | `"128KB"`
  | `"256KB"`;

interface Server {
  pendingWebsockets: number;
  publish(topic: string, data: string | ArrayBufferView | ArrayBuffer, compress?: boolean): number;
  upgrade(
    req: Request,
    options?: {
      headers?: HeadersInit;
      data?: any;
    },
  ): boolean;
}

interface ServerWebSocket {
  readonly data: any;
  readonly readyState: number;
  readonly remoteAddress: string;
  send(message: string | ArrayBuffer | Uint8Array, compress?: boolean): number;
  close(code?: number, reason?: string): void;
  subscribe(topic: string): void;
  unsubscribe(topic: string): void;
  publish(topic: string, message: string | ArrayBuffer | Uint8Array): void;
  isSubscribed(topic: string): boolean;
  cork(cb: (ws: ServerWebSocket) => void): void;
}