Call systems & serialization
The call system (sometimes CaSy) is the part of the runtime that turns an invocation of a protocol function into bytes on a transport and back again.
It is pluggable along two axes:
- Call format (framing) — how a request/response is framed. Three ship
today:
datagram@2(the default), a compact binary framing for connections; its lockstep first version,datagram; and JSON-RPC 2.0. Framing is chosen when code is generated (a protocol's@framingannotation, else the package'swire.framing), not swapped at the call site. - Message serialization (wire format) — how the
structures carried by a call are encoded:
packed@1(the default: fixed-width fields, no tags or names), JSON, and MessagePack (Rust only, for now). The package declares it aswire.format, and generated code names itFormat; a connection made by hand can pass another.
Both are declared in config.idp's wire key
(Wire defaults).
The runtime handles routing between a caller and the protocol implementation; you pick the formats that fit your deployment.
Handshake
Before either side makes a real call, connect / serve run the
protocol's handshake. Comline's own, comline@1 (the default), exchanges a
small fixed-size frame declaring the
schema fingerprint both ends were generated from, plus the wire format
and framing in use. A mismatch on any of those — different schema
versions, one end MessagePack and the other JSON, incompatible framing —
is refused immediately, rather than surfacing later as a confusing decode
error partway through a real call. Capability bits are exchanged too, but
a difference there is advisory, not a refusal.
A protocol talking to a peer that can't take part — a legacy service, say —
declares @handshake = "none" and says why with @unverified (see Giving up
a guarantee); its generated
connect / serve then skip the exchange. In the runtimes the handshake is
a trait (Handshake; an interface in TypeScript), so a custom one can be
passed to Client::connect_with / Server::serve_with by hand. Hand-wired
code can also skip it at the call site with Client::new_unverified /
Server::serve_unverified (Client.unverified / serveUnverified in
TypeScript) — named so that skipping is as visible there as in a schema.
Connections
A client and a server (the datagram framing) work in lockstep: the client
sends a request and reads the next frame as its reply. A connection, which
every protocol uses by default, lifts that limit:
- Either side can call, since
datagram@2marks each frame as a request or a reply. - Several calls can be in flight. A reply is matched to its call by request id.
- Several protocols share it. Each frame carries its protocol's number.
- A side waiting for a reply keeps serving the peer's requests, so a handler can call back the peer that called it.
The runtime keeps the protocol logic in a core that does no I/O: bytes go in,
events come out ("serve this request", "this reply ends call 7"). The
Connection around it owns the transport. The handshake is the same as a
client's and a server's; a connection checks one hash over every schema in
the package, which also fixes the protocols' numbers.
Which protocols run on a connection is the schema's choice: see Calls in both directions.
Concrete API
This page stays the language-agnostic concept; Language Guides has the concrete, per-language API — constructing a wire format, picking a framing, and what the handshake looks like in code.