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

Transport:WebSocket (binary MsgPack). All frames are binary, serialized with rmp_serde::to_vec_named().
Model:Bidirectional, Event-driven. Client pushes ops, Server pushes updates.
Sync:Hybrid Logical Clocks (HLC) + Merkle Trees for convergence.
Encoding:Named MsgPack maps with camelCase field names. All struct fields use #[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).

Message Format
// 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
// 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>
Each inner message within 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

Two-phase authentication: AUTH_REQUIRED on connect, then AUTH with JWT token.

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

Operations are idempotent and commute based on HLC timestamps.

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:

LevelDescription
FIRE_AND_FORGETNo acknowledgment, immediate return
MEMORYAcknowledged when in server memory (default)
APPLIEDAcknowledged when CRDT merge complete
REPLICATEDAcknowledged when broadcast to peers
PERSISTEDAcknowledged when written to storage

See the Write Concern Guide for detailed usage.

Query Subscriptions

Subscribe to live query results.

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)

TopGun uses a prefix-trie Merkle Tree. The client walks the tree to find differing buckets, then fetches leaf records for those buckets.

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

Distributed locking with fencing tokens.

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

Real-time messaging with topics.

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:

DomainTypes
AuthAUTH, AUTH_REQUIRED, AUTH_ACK, AUTH_FAIL
SyncCLIENT_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
QueryQUERY_SUB, QUERY_UNSUB, QUERY_RESP, QUERY_UPDATE
SearchSEARCH, SEARCH_RESP, SEARCH_SUB, SEARCH_UPDATE, SEARCH_UNSUB
MessagingTOPIC_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
ClusterPARTITION_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
EventsSERVER_EVENT, SERVER_BATCH_EVENT, GC_PRUNE, ERROR, SYNC_RESET_REQUIRED