Specs
spec/01-messaging.md · shown exactly as it appears in the repository
01 — Messaging
Status: Draft, v0.1.
License: This text is CC-BY-4.0; the field names, enum values, and other machine-readable schema elements defined here are also MPL-2.0 — see LICENSE-DOCS and LICENSE.
Overview
Every exchange between two connected parties — establishing a connection,
extending it to a new domain, granting a subscription, sharing or mutating
content — travels as a message: a single envelope addressed to a
specific sendKey. This chapter defines that envelope and the generic
mechanism an implementation uses to route an incoming message to the code
that handles it. It does not define what any particular message means —
that's each other chapter's job for the message types it owns — only the
shape every message shares, regardless of what it's for.
The envelope
A message has two parts: an envelope, sent in the clear, and a
payload, always encrypted before it leaves the sender (see
07-security.md for the cryptography itself). The payload is opaque to
the router — only the two connected parties can read it.
The envelope carries:
| Field | Purpose |
|---|---|
version |
Envelope format version. |
messageId |
Unique identifier for this message, used for deduplication (below). |
sendKey |
The key this message is addressed to. |
domain |
Which of the connection's domains this message belongs to. |
The payload carries type, operation, and messageContent — covered in
the next section.
Message types and operations
type identifies which generic category a message belongs to, and
MUST be one of a small set defined by the protocol itself:
connection, subscription, content, or command. operation
identifies what's being done within that type (e.g. subscribe,
cxnEstablished, create), and is likewise a protocol-defined set, not
something a domain invents its own values for. This is different from a
domain's own content types (see 05-content-model.md), which are
domain-defined — type and operation are not.
Planned extension (non-normative preview): both sets are closed in the
current protocol version; a future version may open operation (and
possibly type) to domain extension.
messageContent carries the actual data for that type/operation pair
— an array of content items, whose shape is defined elsewhere (by the
chapter that owns the type, or by the domain's own schema for content
messages).
Deduplication
messageId MUST be unique per message. An implementation MUST
treat a message whose messageId has already been seen for that
sendKey as already delivered, not reprocess it — this is the generic,
envelope-level guard against at-least-once redelivery, independent of
what the message's type/operation/messageContent actually is. In
the current reference implementation this is checked twice: the router
may reject a duplicate send with a 409 the sender treats as "already
delivered," and the receiving agent independently keeps a bounded,
per-sendKey window of recently-seen messageIds as a second line of
defense.
This is a cheaper, generic safety net, not the only one — content
messages carry a separate, stronger ordering guarantee built on
shareSequence and version (see 06-propagation.md). This section
only covers the transport-level guard that applies uniformly to every
message, regardless of type.
Handler dispatch
An implementation MUST register exactly one handler per type before
it can process incoming messages of that type — attempting to dispatch a
message whose type has no registered handler MUST fail rather than
be silently dropped. A handler has the shape (context, connection, operation, messageContent) => ...; by the time it's invoked, the
connection the message's sendKey resolves to has already been found and
passed in. The handler itself, not the dispatch mechanism, is responsible
for interpreting operation and deciding what to do with
messageContent.
Exception: the very first handshake message (connection /
cxnEstablished, see 02-connections.md) is handled specially, before
the generic type-based dispatch — no connection exists yet to resolve the
message against, so it can't go through the normal (context, connection, operation, messageContent) path.