๐ Collie
A WebSocket client for Gleam.
Installation
gleam add collie@1
Autobahn
Collie passes all Autobahn WebSocket test suite cases, ensuring full compliance with the WebSocket protocol specification. You can view detailed test results and compare them with other Gleam WebSocket implementations like Stratus at:
https://vshakitskiy.github.io/collie
Usage
Basic Client
This example connects to an echo server, sends random 4 bytes as binary data, and waits to receive them back before closing the connection:
import collie
import gleam/crypto
import gleam/erlang/process
import gleam/http/request
import gleam/io
import gleam/otp/actor
import logging
pub fn main() {
logging.configure()
logging.set_level(logging.Info)
// Create a WebSocket request
let assert Ok(req) = request.to("https://echo.websocket.org")
// Build and start the client
let client =
collie.new(req, crypto.strong_random_bytes(4))
|> collie.on_message(handle_message)
|> collie.start()
case client {
Ok(actor.Started(pid:, ..)) -> {
// Keep the main process alive while the WebSocket client is alive
let monitor = process.monitor(pid)
let selector =
process.new_selector()
|> process.select_specific_monitor(monitor, fn(_down) { Nil })
process.selector_receive_forever(selector)
}
Error(_) -> io.println_error("Server is busy, try again later")
}
}
fn handle_message(
conn: collie.Connection,
key: BitArray,
message: collie.Message(BitArray),
) -> collie.Next(BitArray, BitArray) {
case message {
collie.Text(text) -> {
io.println(text)
case collie.send_binary_frame(conn, key) {
Ok(Nil) -> collie.continue(key)
Error(reason) -> {
let reason = collie.socket_reason_to_string(reason)
io.println_error(reason)
collie.stop_abnormal(reason)
}
}
}
collie.Binary(bin) if bin == key -> collie.stop()
_ -> collie.stop_abnormal("Got unexpected binary payload")
}
}
Named Clients and Sending Messages
This example shows how to create a named WebSocket client that can receive messages from other processes. By using collie.named, you can register the client with a name and then send it custom messages using collie.to_user_message from anywhere in your application:
import collie
import gleam/erlang/process
import gleam/http/request
import gleam/io
import gleam/otp/actor
import logging
pub type Message {
Send(String)
Shutdown
}
pub fn main() {
logging.configure()
logging.set_level(logging.Info)
let assert Ok(req) = request.to("https://echo.websocket.org")
// Create a name for the client
let name = process.new_name("collie")
// Start a named client
let client =
collie.new(req, Nil)
|> collie.named(name)
|> collie.on_message(handle_message)
|> collie.start()
case client {
Ok(actor.Started(pid:, ..)) -> {
let monitor = process.monitor(pid)
let selector =
process.new_selector()
|> process.select_specific_monitor(monitor, fn(_down) { Nil })
// Send messages to the client from other processes
let subject = process.named_subject(name)
process.send(subject, collie.to_user_message(Send("Hello!")))
process.send(subject, collie.to_user_message(Shutdown))
process.selector_receive_forever(selector)
}
Error(_) -> io.println_error("Server is busy, try again later")
}
}
fn handle_message(conn, state, message) {
case message {
collie.Text(text) -> {
io.println(text)
collie.continue(state)
}
collie.User(Send(text)) -> {
io.println("Sending `" <> text <> "`")
// Send the message through WebSocket
let _ = collie.send_text_frame(conn, text)
collie.continue(state)
}
collie.User(Shutdown) ->
collie.send_close_frame(conn, collie.NormalClosure(<<>>))
collie.Binary(_) -> collie.continue(state)
}
}
Using an Initialiser and Custom Selectors
This example demonstrates usage with collie.new_with_initialiser to perform setup before the client starts handling messages. It creates a custom timer process and uses a selector to receive timer messages, sending a ping to the server every second:
import collie
import gleam/erlang/process
import gleam/http/request
import gleam/io
pub type Message {
Tick
}
pub type State {
State(timer: process.Subject(Message))
}
pub fn main() {
let assert Ok(req) = request.to("https://echo.websocket.org")
let assert Ok(_client) =
collie.new_with_initialiser(req, fn(_self) {
// Spawn a timer process
let timer = process.new_subject()
process.spawn(fn() { timer_loop(timer) })
// Create a selector for timer messages
let selector =
process.new_selector()
|> process.select(timer)
// Return state with the selector
let state = State(timer:)
collie.initialised(state)
|> collie.selecting(selector)
|> Ok
})
|> collie.on_message(handle_message)
|> collie.start
process.sleep_forever()
}
fn timer_loop(subject) {
process.sleep(1000)
process.send(subject, Tick)
timer_loop(subject)
}
fn handle_message(conn, state, message) {
case message {
collie.Text(text) -> {
io.println(text)
collie.continue(state)
}
collie.User(Tick) -> {
io.println("tick")
let _ = collie.send_ping(conn, <<"ping">>)
collie.continue(state)
}
collie.Binary(_) -> collie.continue(state)
}
}
API Documentation
For full API documentation, see hexdocs.pm/collie.