libero/wire

ETF (Erlang Term Format) wire codec for Libero RPC.

Encoding walks any Gleam value through erlang:term_to_binary/1, which preserves the full Erlang type structure (atoms, tuples, maps, lists) natively. Decoding uses erlang:binary_to_term/1 to reconstruct the original terms. No manual walk or rebuild is needed because ETF is the BEAM’s native serialization format.

Wire shape:

Cross-target: encode and decode work on both Erlang and JavaScript targets. The Erlang path uses the BEAM’s native term_to_binary / binary_to_term. The JavaScript path uses libero’s own ETF encoder/decoder in rpc_ffi.mjs, which requires that any custom-type constructors in the value have been registered via the generated rpc_decoders.gleam module (which surfaces ensure_decoders from the FFI). Libero’s generator emits that registration for every type reachable from a handler’s params or return type.

Types

pub type DecodeError {
  DecodeError(message: String)
}

Constructors

  • DecodeError(message: String)

Values

pub fn coerce(value: dynamic.Dynamic) -> a

Cast a Dynamic value to any type. Used by generated server dispatch code to coerce the decoded ClientMsg value to its typed form. Safe when client and server are built from the same source (the generator guarantees the types match).

Warning: unwitnessed cast. Same safety model as decode. Type correctness depends on both sides being built from the same source. A mismatch produces silent data corruption.

This is by design: the generated code is the enforcement point. Making this internal would break the generated dispatch modules which live in consumer packages and need pub access.

pub fn decode(data: BitArray) -> a

Decode an ETF binary into an arbitrary Gleam value.

Works on both Erlang and JavaScript targets. Use this for non-RPC paths - for example, reading server-rendered state from Lustre flags on client boot. For decoding incoming RPC call envelopes specifically, use decode_call instead.

Any custom types in the decoded value must be reachable from a handler’s params or return type so their constructors are registered with the JavaScript codec (via the generated rpc_decoders.gleam module, which calls ensure_decoders on import). On Erlang this is automatic because atoms are pre-registered by the generated rpc_atoms module.

Warning: type safety is the caller’s responsibility. The return type a is unwitnessed: the function returns whatever the ETF binary deserializes to, cast to the caller’s expected type. A version skew between client and server will produce silent data corruption, not a runtime error. This is an intentional tradeoff for ergonomics in controlled deployments where both sides are built from the same source.

This is by design: the generated code is the enforcement point.

Panics on malformed input. In a typical libero deployment both sides are controlled, so this is a sharp-edge check rather than a user-facing error. For untrusted input, use decode_safe which returns a Result.

pub fn decode_call(
  data: BitArray,
) -> Result(#(String, Int, dynamic.Dynamic), DecodeError)

Parse a {<<"module_name">>, request_id, toserver_value} tuple from an ETF binary. Returns the module name, request ID, and the raw Dynamic value to be coerced. Since binary_to_term returns real Erlang terms, no rebuild step is needed - atoms are atoms, tuples are tuples, maps are maps.

This is specifically for RPC call envelopes. For decoding arbitrary values, use decode.

pub fn decode_safe(data: BitArray) -> Result(a, DecodeError)

Decode an ETF binary into an arbitrary Gleam value, returning a Result instead of panicking on malformed input.

Use this for non-RPC paths where the input may be untrusted or user-influenced - for example, reading server-rendered state from Lustre flags on client boot where the binary may have been corrupted in transit.

pub fn encode(value: a) -> BitArray

Encode any Gleam value to an ETF binary.

Works on both Erlang and JavaScript targets. Used internally by libero to serialize RPC responses, and also available for non-RPC paths (e.g. passing server-rendered state into a Lustre SPA via flags, in the Elm “init flags” style).

pub fn encode_call(
  module module: String,
  request_id request_id: Int,
  msg msg: a,
) -> BitArray

Encode a call envelope: {module_name, request_id, msg} as ETF binary. Used by generated client stub functions to pack a ClientMsg value for transport to the server.

pub fn tag_push(module module: String, msg msg: a) -> BitArray

Tag a push frame so the JS client routes it to the push handler. The value is an ETF-encoded {module_name, msg} tuple so the client knows which handler to invoke.

pub fn tag_response(
  request_id request_id: Int,
  data data: BitArray,
) -> BitArray

Tag a response frame so the JS client routes it to the correct callback. Prepends a 0 tag byte and the 32-bit request ID so the client can correlate the response with the originating call.

pub fn variant_tag(value: dynamic.Dynamic) -> Result(String, Nil)

Extract the constructor tag (snake_case atom name) from a Gleam variant value at runtime. Used by generated server dispatch to recognize unknown variants before the unwitnessed coerce + structural pattern match would crash with case_clause.

Returns Ok(name) for atoms (zero-arg variants) and tagged tuples (n-arg variants where the first element is the constructor atom). Returns Error(Nil) for any other shape.

Server-side only. The JS fallback panics because dispatch never runs on the JavaScript target.

Search Document