Protocol Specification
TopGun uses a custom WebSocket-based protocol for real-time synchronization, efficient delta updates, and distributed coordination. This document details the wire format and synchronization algorithms for developers building compatible clients.
Overview
rmp_serde::to_vec_named().#[serde(rename_all = “camelCase”)].Message Format
All messages use an internally-tagged discriminated union. The type field identifies the variant, and remaining fields are at the top level (no separate envelope wrapper).
// All messages are internally tagged with a "type" field.
// Fields are flat (no separate envelope wrapper).
// Wire format: binary MsgPack via rmp_serde::to_vec_named().
{
"type": "MESSAGE_TYPE",
// ... message-specific fields at top level
} The Rust Message enum uses #[serde(tag = "type")] for this representation, matching the TypeScript z.discriminatedUnion('type', [...]) pattern.
Binary Batch Framing
The BATCH message type carries multiple inner messages in a single binary frame. Inner messages are length-prefixed with a 4-byte big-endian u32:
// BATCH wire layout (binary MsgPack frame):
//
// ┌──────────────────────────────────────────────┐
// │ MsgPack map: { "type": "BATCH", "count": N, │
// │ "data": <bin blob> } │
// └──────────────────────────────────────────────┘
//
// The "data" blob contains N length-prefixed inner messages:
//
// ┌────────────┬──────────────────────────────────┐
// │ 4 bytes BE │ MsgPack message #1 │
// │ u32 length │ (complete serialized Message) │
// ├────────────┼──────────────────────────────────┤
// │ 4 bytes BE │ MsgPack message #2 │
// │ u32 length │ (complete serialized Message) │
// ├────────────┼──────────────────────────────────┤
// │ ... │ ...repeated N times │
// └────────────┴──────────────────────────────────┘
//
// Example (hex) for a single 12-byte inner message:
// 00 00 00 0C <12 bytes of MsgPack>
data is a complete MsgPack-serialized Message. The 4-byte length prefix allows the receiver to split the binary blob without parsing each message’s MsgPack structure.Connection Lifecycle
1. WebSocket Connect
Client opens a WebSocket connection to /ws. Server immediately sends AUTH_REQUIRED as a binary MsgPack frame.
2. Authentication
Client sends AUTH with a JWT token. Server verifies the token and responds with AUTH_ACK (success) or AUTH_FAIL (failure). Unauthenticated messages are rejected until AUTH_ACK.
3. Synchronization
Client sends SYNC_INIT with the last sync timestamp. Server responds with SYNC_RESP_ROOT. If roots differ, the client walks the Merkle tree via MERKLE_REQ_BUCKET / SYNC_RESP_BUCKETS / SYNC_RESP_LEAF.
4. Live Updates
Bidirectional exchange of CLIENT_OP and SERVER_EVENT.
Message Types
Authentication
AUTH_REQUIRED
Sent by the server immediately after WebSocket connect, before the socket is split.
{
"type": "AUTH_REQUIRED"
}AUTH
{
"type": "AUTH",
"token": "eyJh...",
"protocolVersion": 1 // optional
}AUTH_ACK
{
"type": "AUTH_ACK",
"connectionId": "uuid...",
"serverNodeId": "node-1",
"roles": ["user"]
}AUTH_FAIL
{
"type": "AUTH_FAIL",
"reason": "invalid token"
}Data Operations
CLIENT_OP
Single client operation wrapped in a payload object. Maps to Rust ClientOpMessage { payload: ClientOp }.
{
"type": "CLIENT_OP",
"payload": {
"mapName": "users",
"key": "u1",
"opType": "PUT",
"record": {
"value": { "name": "Alice" },
"timestamp": {
"millis": 1706000000000,
"counter": 1,
"nodeId": "client-1"
}
},
"writeConcern": "PERSISTED",
"timeout": 5000
}
}OP_BATCH
Batch of client operations. Maps to Rust OpBatchMessage { payload: OpBatchPayload }.
{
"type": "OP_BATCH",
"payload": {
"ops": [ ... ],
"writeConcern": "APPLIED",
"timeout": 5000
}
}OP_ACK
Server acknowledgment with optional Write Concern level achieved.
{
"type": "OP_ACK",
"payload": {
"lastId": "op-123",
"achievedLevel": "PERSISTED",
"results": [
{
"opId": "op-123",
"success": true,
"achievedLevel": "PERSISTED",
"latencyMs": 45
}
]
}
}OP_REJECTED
Server rejects an operation (e.g., permission denied).
{
"type": "OP_REJECTED",
"payload": {
"opId": "op-456",
"reason": "permission denied",
"code": 403
}
}Write Concern Levels
Operations can specify a writeConcern to control acknowledgment:
| Level | Description |
|---|---|
| FIRE_AND_FORGET | No acknowledgment, immediate return |
| MEMORY | Acknowledged when in server memory (default) |
| APPLIED | Acknowledged when CRDT merge complete |
| REPLICATED | Acknowledged when broadcast to peers |
| PERSISTED | Acknowledged when written to storage |
See the Write Concern Guide for detailed usage.
Query Subscriptions
QUERY_SUB
{
"type": "QUERY_SUB",
"payload": {
"queryId": "q1",
"mapName": "users",
"query": {
"where": { "role": "admin" },
"limit": 10
}
}
}QUERY_UNSUB
{
"type": "QUERY_UNSUB",
"payload": {
"queryId": "q1"
}
}Synchronization (Merkle)
SYNC_INIT
Client initiates sync. Flat message (no payload wrapper). Maps to Rust SyncInitMessage.
{
"type": "SYNC_INIT",
"mapName": "users",
"lastSyncTimestamp": 1678000000
}SYNC_RESP_ROOT
Server returns root hash for comparison.
{
"type": "SYNC_RESP_ROOT",
"payload": {
"mapName": "users",
"rootHash": 12345678,
"timestamp": { ... }
}
}MERKLE_REQ_BUCKET
Client requests bucket hashes if root differs.
{
"type": "MERKLE_REQ_BUCKET",
"payload": {
"mapName": "users",
"path": "a1"
}
}SYNC_RESP_BUCKETS
Server returns bucket-level hashes for a tree level.
{
"type": "SYNC_RESP_BUCKETS",
"payload": {
"mapName": "users",
"buckets": {
"a1": 11111111,
"a2": 22222222
}
}
}SYNC_RESP_LEAF
Server returns actual records for a leaf bucket.
{
"type": "SYNC_RESP_LEAF",
"payload": {
"mapName": "users",
"path": "a1",
"records": [
{
"key": "u1",
"record": { "value": {...}, "timestamp": {...} }
}
]
}
}Distributed Locks
LOCK_REQUEST
{
"type": "LOCK_REQUEST",
"payload": {
"requestId": "uuid",
"name": "resource-A",
"ttl": 5000
}
}LOCK_GRANTED
{
"type": "LOCK_GRANTED",
"payload": {
"requestId": "uuid",
"fencingToken": 101
}
}LOCK_RELEASE
{
"type": "LOCK_RELEASE",
"payload": {
"requestId": "uuid",
"name": "resource-A",
"fencingToken": 101
}
}Pub/Sub
TOPIC_SUB
{
"type": "TOPIC_SUB",
"payload": {
"topic": "chat"
}
}TOPIC_UNSUB
{
"type": "TOPIC_UNSUB",
"payload": {
"topic": "chat"
}
}TOPIC_PUB
{
"type": "TOPIC_PUB",
"payload": {
"topic": "chat",
"data": { "msg": "hello" }
}
}TOPIC_MESSAGE
{
"type": "TOPIC_MESSAGE",
"payload": {
"topic": "chat",
"data": { "msg": "hello" },
"publisherId": "client-2",
"timestamp": 1678900000
}
}Full Message Type Reference
The Rust Message enum defines 77 message types across 7 domains. The most common types are documented above. The complete list of type discriminants:
| Domain | Types |
|---|---|
| Auth | AUTH, AUTH_REQUIRED, AUTH_ACK, AUTH_FAIL |
| Sync | CLIENT_OP, OP_BATCH, BATCH, SYNC_INIT, SYNC_RESP_ROOT, SYNC_RESP_BUCKETS, SYNC_RESP_LEAF, MERKLE_REQ_BUCKET, OP_ACK, OP_REJECTED, ORMAP_SYNC_INIT, ORMAP_SYNC_RESP_ROOT, ORMAP_SYNC_RESP_BUCKETS, ORMAP_MERKLE_REQ_BUCKET, ORMAP_SYNC_RESP_LEAF, ORMAP_DIFF_REQUEST, ORMAP_DIFF_RESPONSE, ORMAP_PUSH_DIFF |
| Query | QUERY_SUB, QUERY_UNSUB, QUERY_RESP, QUERY_UPDATE |
| Search | SEARCH, SEARCH_RESP, SEARCH_SUB, SEARCH_UPDATE, SEARCH_UNSUB |
| Messaging | TOPIC_SUB, TOPIC_UNSUB, TOPIC_PUB, TOPIC_MESSAGE, LOCK_REQUEST, LOCK_RELEASE, LOCK_GRANTED, LOCK_RELEASED, COUNTER_REQUEST, COUNTER_SYNC, COUNTER_RESPONSE, COUNTER_UPDATE, PING, PONG, ENTRY_PROCESS, ENTRY_PROCESS_BATCH, ENTRY_PROCESS_RESPONSE, ENTRY_PROCESS_BATCH_RESPONSE, JOURNAL_SUBSCRIBE, JOURNAL_UNSUBSCRIBE, JOURNAL_EVENT, JOURNAL_READ, JOURNAL_READ_RESPONSE, REGISTER_RESOLVER, REGISTER_RESOLVER_RESPONSE, UNREGISTER_RESOLVER, UNREGISTER_RESOLVER_RESPONSE, MERGE_REJECTED, LIST_RESOLVERS, LIST_RESOLVERS_RESPONSE |
| Cluster | PARTITION_MAP_REQUEST, PARTITION_MAP, CLUSTER_SUB_REGISTER, CLUSTER_SUB_ACK, CLUSTER_SUB_UPDATE, CLUSTER_SUB_UNREGISTER, CLUSTER_SEARCH_REQ, CLUSTER_SEARCH_RESP, CLUSTER_SEARCH_SUBSCRIBE, CLUSTER_SEARCH_UNSUBSCRIBE, CLUSTER_SEARCH_UPDATE |
| Events | SERVER_EVENT, SERVER_BATCH_EVENT, GC_PRUNE, ERROR, SYNC_RESET_REQUIRED |