Skip to content

beryl/wire/codec

Pluggable wire codec for beryl.

A Codec plugs the runtime 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 runtime decodes inbound frames and produces outbound frames using the configured callbacks. Codec authors can exercise those callbacks directly with the public apply_* functions.

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

pub type Codec

A wire codec.

Codec is opaque. Build one with new. For binary support, add with_binary_decoder.

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

Errors a codec may emit when decoding inbound bytes.

InvalidJson(reason: String)

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

InvalidFormat(reason: String)

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

MissingField(name: String)

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

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

Encoded WebSocket frame returned by a codec.

TextFrame(String)

A UTF-8 text frame.

BinaryFrame(BitArray)

A binary frame.

pub type Inbound

A normalized inbound message.

Inbound is opaque: construct it with inbound and read it with the inbound_* accessors. The hidden record lets beryl add fields with defaults without breaking custom codecs.

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

Structural inbound message kind used for protocol dispatch.

Join

A client joining a topic.

Leave

A client leaving a topic.

Heartbeat

A heartbeat/keep-alive message.

Event(String)

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

pub type ReplyStatus {
StatusOk
StatusError
}

Status of a reply produced by the app.

StatusOk

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

StatusError

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

pub fn apply_decode_binary(
Codec,
data: BitArray
) -> option.Option(Result(Inbound, DecodeError))

Decode a binary frame with a codec, if it has a binary decoder.

Returns None when the codec has no decoder configured with with_binary_decoder.

pub fn apply_decode_text(
Codec,
text: String
) -> Result(Inbound, DecodeError)

Decode a text frame with a codec.

Codec authors can use this to test the decoder supplied to new.

pub fn apply_encode_close(
Codec,
join_ref: option.Option(String),
topic: String
) -> option.Option(Frame)

Encode a graceful topic close with a codec, if it has a close encoder.

Returns None when the codec has no encoder configured with with_close_encoder.

pub fn apply_encode_error(
Codec,
join_ref: option.Option(String),
topic: String
) -> option.Option(Frame)

Encode an abnormal topic termination with a codec, if it has an error encoder.

Returns None when the codec has no encoder configured with with_error_encoder.

pub fn apply_encode_heartbeat_reply(
Codec,
ref: option.Option(String)
) -> Frame

Encode a heartbeat reply with a codec.

pub fn apply_encode_push(
Codec,
topic: String,
event: String,
payload: json.Json
) -> Frame

Encode a server-initiated push with a codec.

pub fn apply_encode_reply(
Codec,
join_ref: option.Option(String),
ref: option.Option(String),
topic: String,
status: ReplyStatus,
response: json.Json
) -> Frame

Encode a reply with a codec.

pub fn format_decode_error(DecodeError) -> String

Format a DecodeError as human-readable text.

The runtime log messages and wire.format_decode_error use this text.

pub fn inbound(
join_ref: option.Option(String),
ref: option.Option(String),
topic: String,
kind: InboundKind,
payload: dynamic.Dynamic
) -> Inbound

Construct a normalized 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 app to decode
pub fn inbound_join_ref(Inbound) -> option.Option(String)

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

pub fn inbound_kind(Inbound) -> InboundKind

Return the inbound message's structural kind.

pub fn inbound_payload(Inbound) -> dynamic.Dynamic

Return the inbound message body for the app to decode.

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

Return the inbound message's per-message reply reference, if any.

pub fn inbound_topic(Inbound) -> String

Return the inbound message's subscription topic.

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

Build a codec without an inbound binary decoder.

  • decode_text: decode raw inbound text into a normalized 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 delivered to the app's update as a raw Binary event. Add a binary decoder with with_binary_decoder.

pub fn uses_topicless_events(Codec) -> Bool

Whether the codec routes events without an explicit topic.

Codec authors can use this to test with_topicless_events.

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

Add a binary decoder to a codec.

When set, the decoder converts binary WebSocket frames to a normalized Inbound via decode_binary. The app's update function does not receive a raw Binary event.

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

Add a topic-close encoder to a codec.

When set, the runtime sends this frame when one of the client's topics 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_error_encoder(
Codec,
fn(option.Option(String), String) -> Frame
) -> Codec

Add a topic-error encoder to a codec.

When set, the runtime sends this frame when one of the client's topics 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_topicless_events(Codec) -> Codec

Mark a codec's events as topicless.

Some framings, such as 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. The runtime drops it when the socket has zero or multiple joins. Topic-carrying codecs (like the Phoenix codec) must leave this off so the runtime rejects empty-topic frames instead of inferring a topic.