Getting started
This guide walks through opening a channel against a Phoenix-compatible WebSocket endpoint (Phoenix itself or Beryl), pushing a message, receiving a reply, and shutting down.
Install
Section titled “Install”Aquamarine is pre-1.0 and is not published to Hex yet. Until it is, add it as
a Git dependency in your gleam.toml:
[dependencies]aquamarine = { git = "https://github.com/tylerbutler/aquamarine.git", ref = "main" }Aquamarine targets the Erlang runtime. The socket lives in an OTP actor, and the WebSocket itself is Collie.
Connect
Section titled “Connect”aquamarine.connect opens the WebSocket, joins the given topic, waits for
the join reply, and starts the background heartbeat. It returns a Channel
handle that you keep for the rest of the session.
import aquamarineimport aquamarine/phoeniximport aquamarine/transportimport gleam/ioimport gleam/json
pub fn main() { case aquamarine.connect( scheme: transport.Ws, host: "localhost", port: 4000, path: "/socket/websocket", topic: "room:lobby", payload: json.object([]), codec: phoenix.codec(), ) { Ok(channel) -> { // ... use the channel ... case aquamarine.close(channel) { Ok(Nil) -> Nil Error(error) -> { io.debug(error) Nil } } }
Error(error) -> { io.debug(error) Nil } }}Use transport.Wss for TLS. It applies system CA certificates and HTTPS
hostname verification, with no way to turn either off.
The payload argument is the join payload — it is what the server's
join/3 callback sees. What the server answered with is available
afterwards from aquamarine.join_reply(channel).
This is the one-call path, and it is the right one when you want a single
topic. The channel it returns owns its socket, so close on it closes
the connection. For several topics on one connection, see
One socket, many topics.
Push an event
Section titled “Push an event”push assigns a ref automatically and hands the frame to the socket actor.
It is fire-and-forget: it returns immediately and does not wait for a reply.
aquamarine.push( channel, "new_msg", json.object([#("body", json.string("hello"))]),)When you want the server's answer to a specific push, use
push_and_await_reply, which correlates the reply by ref:
case aquamarine.push_and_await_reply( channel, "new_msg", json.object([#("body", json.string("hello"))]), 5000, ){ Ok(reply) -> handle(reply) Error(error) -> io.debug(error)}Frames that arrive while you are waiting are not dropped — they queue up for
receive as usual.
Receive frames
Section titled “Receive frames”receive blocks until the next frame for this channel's topic arrives, or
the timeout elapses. Only this topic's frames arrive here; heartbeat replies
and binary frames never do.
case aquamarine.receive(channel, 5000) { Ok(incoming) -> { // incoming.event, incoming.topic, incoming.payload, ... Nil } Error(error) -> { io.debug(error) Nil }}Only the process that called connect should call receive — see
Channel lifecycle for the full ownership rules, and
Choosing your model if you would rather
select on the subject alongside your own messages.
close closes the connection and stops the socket actor, taking the
heartbeat with it.
case aquamarine.close(channel) { Ok(Nil) -> Nil Error(error) -> { io.debug(error) Nil }}Use aquamarine.leave(channel) when you mean leave the topic and nothing
more.
Next steps
Section titled “Next steps”- Choosing your model — blocking
receiveor the events subject, and which is which. - Channel lifecycle — how the operations fit together, including process ownership.
- One socket, many topics — sharing one connection.
- Reconnect — what happens when the connection drops.
- Supervision — putting a socket in your tree.
- Error handling — the
AquamarineErrorvariants you should expect to handle.