CRUMB a card from devarno-cloud

ProtocolMessage Envelope

chronicle intermediate 4 min read

ELI5

Every message between app and relay is a sealed envelope with a type: label on the outside. The label tells the relay which drawer to open. There is one master enum that lists every legal label, and changing it without updating both Rust and TypeScript is how you summon INVALID_MESSAGE.

Technical Deep Dive

Wire Shape

ProtocolMessage (relay/src/protocol/schema.rs:427) is a tagged enum:

#[serde(tag = "type", rename_all = "snake_case")]
pub enum ProtocolMessage { Authenticate(...), AuthResult(...), ... }

JSON example: {"type":"authenticate","token":"...","protocol_version":1}. The TypeScript mirror in app/src/services/protocol.ts produces and consumes the same shape.

Variant Categories

classDiagram
class ProtocolMessage {
<<enum, tag=type>>
}
class Authentication {
authenticate
auth_result
}
class DocumentRoom {
join_document
joined_document
leave_document
left_document
}
class Presence {
presence
presence_sync
user_joined
user_left
heartbeat
}
class Operations {
operation
operation_ack
operation_rejected
operation_batch
}
class Sync {
sync_request
sync_response
snapshot_request
snapshot
}
class Timeline {
timeline_query
timeline_snapshot
timeline_metadata_request
timeline_metadata
}
class System {
ping
pong
error
system_notification
}
ProtocolMessage <|-- Authentication
ProtocolMessage <|-- DocumentRoom
ProtocolMessage <|-- Presence
ProtocolMessage <|-- Operations
ProtocolMessage <|-- Sync
ProtocolMessage <|-- Timeline
ProtocolMessage <|-- System

Versioning

PROTOCOL_VERSION and MIN_PROTOCOL_VERSION live in relay/src/protocol/mod.rs. The client embeds protocol_version inside Authenticate (defaulting via default_protocol_version() to PROTOCOL_VERSION). Validation rejects values below the minimum with UNSUPPORTED_PROTOCOL_VERSION.

Validation Pass

flowchart LR
raw["raw bytes"] --> parse[serde_json::from_str]
parse -- ok --> val[validate_message]
parse -- err --> e1[INVALID_MESSAGE]
val -- ok --> route[handler dispatch]
val -- err --> e2[ValidationError → ErrorMsg]

validate_message (relay/src/protocol/validation.rs) enforces non-empty tokens, non-nil UUIDs on JoinDocument / Operation, and the protocol-version floor.

Server-Augmented Fields

Some fields are server-added when broadcasting and MUST NOT be sent by clients:

  • OperationMsg.author_id — filled in by the relay from the verified identity.
  • OperationAckMsg.server_timestamp, audit_ref — set on the relay side.

Key Terms

  • tag = “type” → serde attribute making the variant discriminator a JSON string field; the canonical name comes from rename_all = "snake_case".
  • mirror fileapp/src/services/protocol.ts; any Rust change here lands there.
  • system messagesPing/Pong/Error/SystemNotification; Ping and Pong are unit variants (no payload object).

Q&A

Q: How is ProtocolMessage::Ping serialised? A: As {"type":"ping"} — unit variants have no extra fields with this serde config.

Q: What error fires when type is unknown? A: serde fails to deserialise and the relay emits Error { code: "INVALID_MESSAGE" }.

Q: Are operation IDs required to be non-nil? A: Yes — validate_message rejects nil UUIDs on Operation with INVALID_OPERATION (validation.rs:91).

Q: Why are some fields wrapped in Option with skip_serializing_if? A: To keep the on-wire JSON compact and to clearly distinguish “absent” from “null”.

Examples

Adding a comment_thread message means: (1) add a struct + variant in schema.rs, (2) add validation in validation.rs, (3) add the same variant and TS interface in app/src/services/protocol.ts, (4) bump PROTOCOL_VERSION only if the change is breaking. Forgetting (3) is the canonical way to ship a “works on my machine” bug.

neighbors on the map