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.
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 toFalse
. - 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 toFalse
. - 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
through4999
are reserved for applications (usable by developers). To close the connection abruptly, useterminate
.
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
through4999
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
.