brioche/websocket

Bun implements native WebSocket management. At the creation of a server, you can provide a websocket configuration with a websocket.Config data to start managing WebSockets. Bun takes care of everything for you, from connection to pinging clients to know if they’re still online.

brioche/websockets provides little abstraction layer on the Bun’s API, but focuses on bringing type-safety and gleamish way to do things.

Bun Documentation

Types

Config used to setup Bun’s WebSockets. Config can be created by using websocket.init. It is recommended for each WebSocket Config to have a text handler and a bytes handler.

Take note of the context type. That type is used to pass contextual data to every WebSocket upon initialisation with server.upgrade.

pub type Config(context) {
  Config(
    on_text_message: Option(
      fn(bun.WebSocket(context), String) -> Promise(Nil),
    ),
    on_bytes_message: Option(
      fn(bun.WebSocket(context), BitArray) -> Promise(Nil),
    ),
    on_open: Option(fn(bun.WebSocket(context)) -> Promise(Nil)),
    on_close: Option(
      fn(bun.WebSocket(context), Int, String) -> Promise(Nil),
    ),
    on_drain: Option(fn(bun.WebSocket(context)) -> Promise(Nil)),
    max_payload_length: Option(Int),
    backpressure_limit: Option(Int),
    close_on_backpressure_limit: Option(Int),
    idle_timeout: Option(Int),
    publish_to_self: Option(Bool),
    send_pings: Option(Bool),
  )
}

Constructors

  • Config(
      on_text_message: Option(
        fn(bun.WebSocket(context), String) -> Promise(Nil),
      ),
      on_bytes_message: Option(
        fn(bun.WebSocket(context), BitArray) -> Promise(Nil),
      ),
      on_open: Option(fn(bun.WebSocket(context)) -> Promise(Nil)),
      on_close: Option(
        fn(bun.WebSocket(context), Int, String) -> Promise(Nil),
      ),
      on_drain: Option(fn(bun.WebSocket(context)) -> Promise(Nil)),
      max_payload_length: Option(Int),
      backpressure_limit: Option(Int),
      close_on_backpressure_limit: Option(Int),
      idle_timeout: Option(Int),
      publish_to_self: Option(Bool),
      send_pings: Option(Bool),
    )

    Arguments

    on_text_message

    Set the message event handler, when a text message has been sent.

    on_bytes_message

    Set the message event handler, when a bytes message has been set.

    on_open

    Set the open event handler.

    on_close

    Set the close event handler.

    on_drain

    Set the drain event handler.

    max_payload_length

    Define the maximum size of messages in bytes. Defaults to 16 MB, or 1024 * 1024 * 16 in bytes.

    backpressure_limit

    Define the maximum number of bytes that can be buffered on a single connection. Defaults to 16 MB, or 1024 * 1024 * 16 in bytes.

    close_on_backpressure_limit

    Define if the connection should be closed if backpressure_limit is reached. Defaults to False.

    idle_timeout

    Define the number of seconds to wait before timing out a connection due to no messages or pings. Defaults to 2 minutes, or 120 in seconds.

    publish_to_self

    Define if websocket.publish also sends a message to the websocket, if it is subscribed. Defaults to False.

    send_pings

    Define if the server should automatically send and respond to pings to clients. Defaults to True.

Status representing the outcome of a sent message.

pub type WebSocketSendStatus {
  MessageDropped
  MessageBackpressured
  MessageSent(bytes_sent: Int)
}

Constructors

  • MessageDropped

    Received when message is dropped.

  • MessageBackpressured

    Received when there is backpressure of messages.

  • MessageSent(bytes_sent: Int)

    Received when message has been sent successfully. bytes_sent represents the number of bytes sent.

Functions

pub fn backpressure_limit(
  config: Config(a),
  backpressure_limit: Int,
) -> Config(a)

Defines the maximum number of bytes that can be buffered on a single connection.
Defaults to 16 MB, or 1024 * 1024 * 16 in bytes.

import brioche/websocket

pub fn ws_config() {
  websocket.init()
  // Sets maximum buffer size to 1024 MB, or 1 GB.
  |> websocket.backpressure_limit(1024 * 1024 * 1024)
}
pub fn close(
  websocket: WebSocket(a),
  code: Int,
  reason: String,
) -> Int

Closes the connection. Non exhaustive list of close codes.

  • 1000 means “normal closure” (default).
  • 1009 means a message was too big and was rejected.
  • 1011 means the server encountered an error.
  • 1012 means the server is restarting.
  • 1013 means the server is too busy or the client is rate-limited.
  • 4000 through 4999 are reserved for applications (usable by developers). To close the connection abruptly, use terminate.
pub fn close_on_backpressure_limit(
  config: Config(a),
  close_on_backpressure_limit: Int,
) -> Config(a)

Defines if the connection should be closed if backpressure_limit is reached.
Defaults to False.

import brioche/websocket

pub fn ws_config() {
  websocket.init()
  // Sets connection to close if backpressure limit is reached.
  |> websocket.close_on_backpressure_limit(True)
}
pub fn cork(
  websocket: WebSocket(a),
  callback: fn(WebSocket(a)) -> Nil,
) -> Nil

Batches send and publish operations, which makes it faster to send data.

The message, open, and drain callbacks are automatically corked, so you only need to call this if you are sending messages outside of those callbacks or in async functions.

fn on_text(ws: brioche.WebSocket(context), message: String) {
  use _ <- promise.map(promise.wait(1000))
  use ws <- websocket.cork()
  websocket.send(ws, "My message")
  websocket.send(ws, "My other message")
}
pub fn data(websocket: WebSocket(a)) -> a

Read the data stored on the WebSocket upon initialisation.

import brioche
import brioche/server
import brioche/websocket
import gleam/bit_array
import gleam/io

type Context = String

pub fn main() -> brioche.Server(Context) {
  server.handler(handler)
  |> server.websocket(websocket())
  |> server.serve
}

fn handler(request: server.Request, server: brioche.Server(Context)) {
  let headers = []
  let session_id = brioche.random_uuid_v7()
  use <- server.upgrade(server, request, headers, session_id)
  server.internal_error()
}

fn websocket() -> websocket.Config(Context) {
  websocket.init()
  |> websocket.on_text(on_text)
}

fn on_text(ws: brioche.WebSocket(Context), text: String) {
  let session_id = websocket.data(ws)
  websocket.send("Your session_id is: " <> session_id)
  promise.resolve(Nil)
}
pub fn idle_timeout(
  config: Config(a),
  idle_timeout: Int,
) -> Config(a)

Defines the number of seconds to wait before timing out a connection due to no messages or pings.
Defaults to 2 minutes, or 120 in seconds.

import brioche/websocket

pub fn ws_config() {
  websocket.init()
  // Sets the idle timeout to 1 minute.
  |> websocket.idle_timeout(60)
}
pub fn init() -> Config(a)

Accepting WebSockets in a Bun application is done by providing a websocket configuration to serve.

Use init to create an empty Config option.

import brioche
import brioche/server
import brioche/websocket

type Context = Nil

pub fn main() -> brioche.Server(Context) {
  server.handler(handler)
  |> server.websocket(websocket())
  |> server.serve
}

fn websocket() -> websocket.Config(Context) {
  websocket.init()
  |> websocket.on_open(on_open)
  |> websocket.on_close(on_close)
  |> websocket.on_bytes(on_bytes)
  |> websocket.on_text(on_text)
}
pub fn is_subscribed(
  websocket: WebSocket(a),
  topic: String,
) -> Bool

Indicates if a WebSocket is connected to a topic or not. To get more information on Pub-Sub, topics and subscriptions, take a look at subscribe.

pub fn max_payload_length(
  config: Config(a),
  max_payload_length: Int,
) -> Config(a)

Define the maximum size of messages in bytes.
Defaults to 16 MB, or 1024 * 1024 * 16 in bytes.

import brioche/websocket

pub fn ws_config() {
  websocket.init()
  // Sets payload size to 1024 MB, or 1 GB.
  |> websocket.max_payload_length(1024 * 1024 * 1024)
}
pub fn on_bytes(
  config: Config(a),
  handler: fn(WebSocket(a), BitArray) -> Promise(Nil),
) -> Config(a)

WebSocket emits a message message after after receiving a message. A message can be binary or textual. In brioche, the hard routing & decoding task is already done for you. You can subscribe to messages emitted as text or binary simply by using on_text or on_bytes.

First argument of the handler is always the current WebSocket. It’s possible to use it to communicate with clients, send message, close the connection, etc.

Second argument is the binary message received.

import brioche
import brioche/server
import brioche/websocket
import gleam/bit_array
import gleam/io

type Context = Nil

pub fn main() -> brioche.Server(Context) {
  server.handler(handler)
  |> server.websocket(websocket())
  |> server.serve
}

fn websocket() -> websocket.Config(Context) {
  websocket.init()
  |> websocket.on_bytes(on_bytes)
}

fn on_bytes(ws: brioche.WebSocket(Context), message: BitArray) {
  // WebSocket received a binary message!
  io.println("WebSocket received a message!")
  let message = bit_array.from_string(message)
  let message = bit_array.append(<<"Echoed message: ">>, message)
  websocket.send_bytes(ws, message)
  promise.resolve(Nil)
}
pub fn on_close(
  config: Config(a),
  handler: fn(WebSocket(a), Int, String) -> Promise(Nil),
) -> Config(a)

WebSocket emits a close message after the WebSocket has been closed, whether from the server or the client.

First argument of the handler is always the current WebSocket. It’s possible to use it to communicate with clients, send message, close the connection, etc.

Second argument is the error code, as a number. Non exhaustive list of close codes.

  • 1000 means “normal closure” (default).
  • 1009 means a message was too big and was rejected.
  • 1011 means the server encountered an error.
  • 1012 means the server is restarting.
  • 1013 means the server is too busy or the client is rate-limited.
  • 4000 through 4999 are reserved for applications.

Third argument is the reason, as string.

import brioche
import brioche/server
import brioche/websocket

type Context = Nil

pub fn main() -> brioche.Server(Context) {
  server.handler(handler)
  |> server.websocket(websocket())
  |> server.serve
}

fn websocket() -> websocket.Config(Context) {
  websocket.init()
  |> websocket.on_close(on_close)
}

fn on_close(ws: brioche.WebSocket(Context)) {
  // WebSocket is closing!
  websocket.send(ws, "Closing!")
  websocket.send_bytes(ws, <<"Closing!">>)
  promise.resolve(Nil)
}
pub fn on_drain(
  config: Config(a),
  handler: fn(WebSocket(a)) -> Promise(Nil),
) -> Config(a)

WebSocket emits a drain message after a backpressure has been applied, and that WebSocket is now able to handle new data.

First argument of the handler is always the current WebSocket. It’s possible to use it to communicate with clients, send message, close the connection, etc.

import brioche
import brioche/server
import brioche/websocket

type Context = Nil

pub fn main() -> brioche.Server(Context) {
  server.handler(handler)
  |> server.websocket(websocket())
  |> server.serve
}

fn websocket() -> websocket.Config(Context) {
  websocket.init()
  |> websocket.on_drain(on_drain)
}

fn on_drain(ws: brioche.WebSocket(Context)) {
  // WebSocket is now usable again!
  websocket.send(ws, "Drained!")
  websocket.send_bytes(ws, <<"Drained!">>)
  promise.resolve(Nil)
}
pub fn on_open(
  config: Config(a),
  handler: fn(WebSocket(a)) -> Promise(Nil),
) -> Config(a)

WebSocket emits an open message after a connection has been upgraded, and once the connection has been established.

First argument of the handler is always the current WebSocket. It’s possible to use it to communicate with clients, send message, close the connection, etc.

import brioche
import brioche/server
import brioche/websocket

type Context = Nil

pub fn main() -> brioche.Server(Context) {
  server.handler(handler)
  |> server.websocket(websocket())
  |> server.serve
}

fn websocket() -> websocket.Config(Context) {
  websocket.init()
  |> websocket.on_open(on_open)
}

fn on_open(ws: brioche.WebSocket(Context)) {
  // Connection has been established!
  websocket.send(ws, "Connected!")
  websocket.send_bytes(ws, <<"Connected!">>)
  promise.resolve(Nil)
}
pub fn on_text(
  config: Config(a),
  handler: fn(WebSocket(a), String) -> Promise(Nil),
) -> Config(a)

WebSocket emits a message message after after receiving a message. A message can be binary or textual. In brioche, the hard routing & decoding task is already done for you. You can subscribe to messages emitted as text or binary simply by using on_text or on_bytes.

First argument of the handler is always the current WebSocket. It’s possible to use it to communicate with clients, send message, close the connection, etc.

Second argument is the text message received.

import brioche
import brioche/server
import brioche/websocket
import gleam/bit_array
import gleam/io

type Context = Nil

pub fn main() -> brioche.Server(Context) {
  server.handler(handler)
  |> server.websocket(websocket())
  |> server.serve
}

fn websocket() -> websocket.Config(Context) {
  websocket.init()
  |> websocket.on_text(on_text)
}

fn on_text(ws: brioche.WebSocket(Context), message: String) {
  // WebSocket received a textual message!
  io.println("WebSocket received a message: " <> message)
  websocket.send(ws, "Echoed message: " <> message)
  let message = bit_array.from_string(message)
  let message = bit_array.append(<<"Echoed message: ">>, message)
  websocket.send_bytes(ws, message)
  promise.resolve(Nil)
}
pub fn ping(websocket: WebSocket(a)) -> Nil

Send a ping message. Ping and pong messages are part of the WebSockets Heartbeat. More information can be found on MDN for that subject.

pub fn pong(websocket: WebSocket(a)) -> Nil

Send a pong message. Ping and pong messages are part of the WebSockets Heartbeat. More information can be found on MDN for that subject.

pub fn publish(
  websocket: WebSocket(a),
  topic: String,
  message: String,
) -> WebSocketSendStatus

Publish textual message to a topic. To get more information on Pub-Sub, topics and subscriptions, take a look at subscribe.

pub fn publish_bytes(
  websocket: WebSocket(a),
  topic: String,
  message: BitArray,
) -> WebSocketSendStatus

Publish binary message to a topic. To get more information on Pub-Sub, topics and subscriptions, take a look at subscribe.

pub fn publish_to_self(
  config: Config(a),
  publish_to_self: Bool,
) -> Config(a)

Defines if websocket.publish also sends a message to the websocket, if it is subscribed.
Defaults to False.

import brioche/websocket

pub fn ws_config() {
  websocket.init()
  // Sets WebSocket to publish to itself when publishing.
  |> websocket.publish_to_self(True)
}
pub fn ready_state(websocket: WebSocket(a)) -> Int

Read the state of the Websocket.

  • If 0, the client is connecting.
  • If 1, the client is connected.
  • If 2, the client is closing.
  • If 3, the client is closed.
pub fn remote_address(websocket: WebSocket(a)) -> String

Read IP address of the client.

fn on_text(ws: brioche.WebSocket(context), message: String) {
  websocket.remote_address(ws)
  // -> "127.0.0.1"
  promise.resolve(Nil)
}
pub fn send(
  websocket: WebSocket(a),
  message: String,
) -> WebSocketSendStatus

Send a textual message to the connected client.

import brioche
import brioche/websocket

fn on_text(ws: brioche.WebSocket(context), message: String) {
  // Echoes back the message.
  websocket.send(ws, "Message received! " <> message)
  promise.resolve(Nil)
}
pub fn send_bytes(
  websocket: WebSocket(a),
  message: BitArray,
) -> WebSocketSendStatus

Send a binary message to the connected client.

import brioche
import brioche/websocket

fn on_bytes(ws: brioche.WebSocket(context), message: BitArray) {
  // Echoes back the message.
  websocket.send_bytes(ws, message)
  promise.resolve(Nil)
}
pub fn send_pings(
  config: Config(a),
  send_pings: Bool,
) -> Config(a)

Defines if the server should automatically send and respond to pings to clients.
Defaults to True.

import brioche/websocket

pub fn ws_config() {
  websocket.init()
  // Sets the connection to not automatically send and respond to pings.
  |> websocket.send_pings(False)
}
pub fn subscribe(websocket: WebSocket(a), topic: String) -> Nil

Bun makes it easy to implement a Pub-Sub mechanism by using topics. Every WebSocket can subscribe to specific topics, and listen on new incoming messages. Every time the server or another WebSocket publishes on that topic, every listening WebSockets will receive the message.

To unsubscribe to a topic, take a look at unsubscribe.

import brioche
import brioche/server
import brioche/websocket

// Context is the session id.
type Context = String

pub fn main() -> brioche.Server(Context) {
  server.handler(handler)
  |> server.websocket(websocket())
  |> server.serve
}

fn websocket() -> websocket.Config(Context) {
  websocket.init()
  |> websocket.on_open(on_open)
  |> websocket.on_text(on_text)
}

fn on_open(ws: brioche.WebSocket(Context)) {
  // Subscribing to new connected users.
  websocket.subscribe(ws, "new-users")
  // Sending the information to other users that we're connecting.
  json.object([
    #("session_id", json.string(websocket.data(ws))),
    #("name", json.string("John Doe")),
  ])
  |> json.to_string
  |> websocket.publish(ws, "new-users", _)
  promise.resolve(Nil)
}

fn on_text(ws: brioche.WebSocket(Context), message: String) {
  // Decode the data.
  let decoder = {
    use session_id <- decode.field("session_id", decode.string)
    use name <- decode.field("name", decode.string)
    #(session_id, name)
  }
  case json.parse(message, decode.at(["session_id"], decode.string)) {
    // Ignore the error, message is not for us.
    Error(_) -> promise.resolve(Nil)
    // Signal a new user connected.
    Ok(#(session_id, name)) -> {
      // Create the new data to send.
      json.object([
        #("name", json.string(name)),
        #("type", json.string("new-connected-user")),
      ])
      |> json.string
      |> websocket.send(ws, _)
      promise.resolve(Nil)
    }
  }
}
pub fn terminate(websocket: WebSocket(a)) -> Int

Abruptly close the connection.
To gracefully close the connection, use close.

pub fn unsubscribe(websocket: WebSocket(a), topic: String) -> Nil

Unsubscribe from a topic. To get more information on Pub-Sub, topics and subscriptions, take a look at subscribe.

Search Document