Skip to content

beryl/wire/codec

Pluggable wire codec for beryl.

A Codec plugs the coordinator into any over-the-wire framing. The canonical implementation is beryl/wire.phoenix_codec(), which ships the Phoenix array format ([join_ref, ref, topic, event, payload]).

To run beryl over your own framing, build a Codec value and pass it to beryl.config(codec). The coordinator decodes inbound text via codec.decode_text, optionally decodes inbound binary via codec.decode_binary, dispatches based on the structural InboundKind, and produces outbound text or binary frames via codec.encode_* helpers.

All codecs must normalise inbound traffic to the Inbound shape so the coordinator can stay framing-agnostic.

A wire codec.

Codec is opaque; build one with new (and, for binary support, with_binary_decoder). The coordinator reads the codec's behaviour through the @internal accessors below.

pub type Codec

Errors a codec may emit when decoding inbound bytes.

pub type DecodeError {
InvalidJson(reason: String)
InvalidFormat(reason: String)
MissingField(name: String)
}

The bytes were not valid JSON; reason describes the parse error.

The message was valid JSON but did not match the expected framing; reason describes the mismatch.

A required field was absent; name is the missing field.

Encoded WebSocket frame returned by a codec.

pub type Frame {
TextFrame(String)
BinaryFrame(BitArray)
}

A UTF-8 text frame.

A binary frame.

Normalised inbound message shape.

Inbound is opaque: construct it with inbound and read it with the inbound_* accessors. Keeping the record hidden lets beryl add fields (which default sensibly) without breaking every custom codec.

pub type Inbound

Structural inbound message kind used for protocol dispatch.

pub type InboundKind {
Join
Leave
Heartbeat
Event(String)
}

A client joining a topic.

A client leaving a topic.

A heartbeat/keep-alive message.

A user-defined event; the wrapped String is the event name.

Status of a reply produced by a channel callback.

pub type ReplyStatus {
StatusOk
StatusError
}

The callback succeeded ("ok" in Phoenix framing).

The callback failed ("error" in Phoenix framing).

Format a DecodeError as a human-readable string. Used by the coordinator's log messages and by wire.format_decode_error.

pub fn format_decode_error(DecodeError) -> String

Construct a normalised inbound message.

  • join_ref: optional client-side reference assigned at join time (used by some Phoenix replies; codecs without this concept should pass None)
  • ref: optional per-message reference for reply correlation
  • topic: subscription topic (e.g. "room:lobby", "doc:abc")
  • kind: structural protocol event or user event
  • payload: message body as a Dynamic for the channel callback to decode
pub fn inbound(
join_ref: option.Option(String),
ref: option.Option(String),
topic: String,
kind: InboundKind,
payload: dynamic.Dynamic
) -> Inbound

The inbound message's join-time client reference, if any.

pub fn inbound_join_ref(Inbound) -> option.Option(String)

The inbound message's structural kind.

pub fn inbound_kind(Inbound) -> InboundKind

The inbound message's body, for the channel callback to decode.

pub fn inbound_payload(Inbound) -> dynamic.Dynamic

The inbound message's per-message reference for reply correlation, if any.

pub fn inbound_ref(Inbound) -> option.Option(String)

The inbound message's subscription topic.

pub fn inbound_topic(Inbound) -> String

Build a text-only wire codec.

  • decode_text: decode raw inbound text into a normalised Inbound.
  • encode_reply: encode a reply to a client message: (join_ref, ref, topic, status, response_payload).
  • encode_push: encode a server-initiated push: (topic, event, payload).
  • encode_heartbeat_reply: encode a heartbeat reply for a given client ref.

The resulting codec has no binary decoder; binary WebSocket frames are routed to channel.handle_binary as raw data. Add a binary decoder with with_binary_decoder.

pub fn new(
decode_text: fn(String) -> Result(Inbound, DecodeError),
encode_reply: fn(option.Option(String), option.Option(String), String, ReplyStatus, json.Json) -> Frame,
encode_push: fn(String, String, json.Json) -> Frame,
encode_heartbeat_reply: fn(option.Option(String)) -> Frame
) -> Codec

Attach a binary decoder to a codec.

When set, binary WebSocket frames are decoded into a normalised Inbound via decode_binary instead of being routed to channel.handle_binary as raw data.

pub fn with_binary_decoder(
Codec,
fn(BitArray) -> Result(Inbound, DecodeError)
) -> Codec

Attach a channel-close encoder to a codec.

When set, the coordinator emits this frame to a client whenever one of its channels terminates gracefully (leave, server shutdown, heartbeat eviction): (join_ref, topic). Phoenix clients rely on phx_close to leave the joined state instead of waiting out push timeouts.

pub fn with_close_encoder(
Codec,
fn(option.Option(String), String) -> Frame
) -> Codec

Attach a channel-error encoder to a codec.

When set, the coordinator emits this frame to a client whenever one of its channels terminates abnormally (crashed or stopped with an error): (join_ref, topic). Phoenix clients rely on phx_error to schedule an automatic rejoin.

pub fn with_error_encoder(
Codec,
fn(option.Option(String), String) -> Frame
) -> Codec

Mark a codec's events as topicless.

Some framings (e.g. Socket.IO-style protocols) do not carry a per-frame topic. When set, an inbound event whose topic is empty is routed to the socket's single joined topic; with zero or multiple joins it is dropped. Topic-carrying codecs (like the Phoenix codec) must leave this off so empty-topic frames are rejected instead of guessed at.

pub fn with_topicless_events(Codec) -> Codec