Skip to main content

top_level: true description: The GAR WebSocket protocol in full: handshake, subscribing, filtering, binary and JSON encodings, batching, heartbeats.

Generic Active Records Protocol Documentation

The wire protocol every GAR client and server speaks: the handshake, subscriptions and their filters, the binary and JSON encodings, batching, ownership, failed writes and termination.

Overview

The GAR protocol is a WebSocket-based messaging system built on top of TCP. It is used for streaming and snapshot-style data delivery and operates over a single bi-directional socket connection. When establishing a connection, clients must specify the subprotocol "gar-protocol":

Sec-WebSocket-Protocol: gar-protocol

Encoding

The server supports two formats for message payloads: binary and JSON.

  • JSON (opcode 0x01) is recommended for human-readable debugging and platform-agnostic integration.
  • Binary (opcode 0x02) is compact and efficient but platform-dependent due to endianness, sizing, and alignment.

If the first byte of the initial Introduction message is {, then JSON mode is assumed regardless of the WebSocket opcode.

Binary Format

  • Each message begins with a message_type enumeration value.
  • Immediately followed by a binary-encoded struct corresponding to that type.
  • Must conform to platform-specific alignment and byte order.
  • Should only be used when both ends are aware of the exact schema and platform.

JSON Format

JSON messages are structured with a "message_type" string and a corresponding "value" object.

Example:

{
  "message_type": "Subscribe",
  "value": {
    "subscription_mode": "Snapshot",
    "nagle_interval": 0,
    "name": "S1",
    "key_id": 0,
    "topic_id": 0,
    "class_list": ["Underlier"],
    "key_filter": null,
    "topic_filter": null,
    "exclude_key_filter": null,
    "exclude_topic_filter": null
  }
}

This format is more portable and ideal for most clients.

Framing

Each websocket frame contains a single message - a json {} or binary object.

Session Lifecycle

  1. Introduction must always be the first message. It specifies protocol version, heartbeat expectations, and optional settings such as unique_key_and_record_updates.
  2. Clients may then Subscribe to topics and keys or Publish messages.
  3. Clients must send Heartbeat messages periodically to keep the connection alive. If none is received within the specified timeout, the server will disconnect the client.
  4. The first heartbeat has a 10x grace period to allow for slow client setup.
  5. All topics and keys must be explicitly introduced before publishing record messages.
  6. IDs 0 are considered invalid for both topic_id and key_id.
  7. Enumeration of topics and keys is not globally synchronized between client and server. Each side must track their own ID mappings.

Protocol version

Both sides carry their protocol version in the introduction — BINARY_VERSION on a binary connection, JSON_VERSION on a JSON one (ipc/ipc.ixx) — and the receiver refuses a connection whose version differs from its own, with the error Unsupported version from <peer>: <version>. The two constants are monotonically increasing integers with no relation to the release version; BINARY_VERSION ticks whenever the binary layout or the schema text embedded in the introduction changes, JSON_VERSION only when an old JSON client could no longer interoperate (an added optional field or a new message type does not tick it). JSON clients never parse the schema text, so a schema-text format change leaves them unaffected.

Schema on the wire

Every emitted schema surface is .gar text: the schema in the introduction, the "schema" member of a gardb snapshot, trsgar --print-schema, and the discovery defaults — REST GET /schema and the MCP schema_source tool. The .psi rendering is available only behind format=psi on those two discovery surfaces.

Client identity

Every connection materializes on its peer as a g::Client key named "<application> <user> <uuid>", where the uuid is minted per connection — a library that opens one connection to download the schema and a second to subscribe produces two distinct keys with independent lifetimes, and a process holding two simultaneous connections is two clients. Ownership records (active_ownership, client_introduced_key_invalidated) and the ClientTopicStats / ClientClassStats telemetry key off this key, so it is what a disconnect releases and what a stats row names. A proxy reports its own aggregate stats under the identity it presents to its upstream, suffixed control, so they sit beside the per-downstream-client rows it mirrors without colliding.

Introduction Options

The introduction message supports the following optional fields:

  • unique_key_and_record_updates (bool, default false): If true, only one record update will be sent per key/record even if multiple subscriptions apply. This only affects streaming record updates — snapshot data is still sent for each subscription. Note that active_subscription_group will not be reliable when using this option.
  • disconnect_on_write_failure (bool, default false): bail-out-first writers. The server closes the connection on the first client message with a failed write — after reporting it, as a non-recoverable Error carrying failed_writes (delivered even when the active_ownership msg_ref is 0, which is otherwise log-only). Every write that succeeded stays landed; this is not transactional — a client that needs atomicity batches its writes so each batch is safe under partial failure. Meant for critical processes that would rather restart than continue past a refused write, and for integration tests; subscriptions on the same connection go down with it, so a process that both watches and writes should keep them on separate connections. Through a proxy, a failure detected upstream reaches the client as a relayed failed_writes Error and closes the client's connection the same way, but only when the proxy learned of it — i.e. with a nonzero msg_ref, since the proxy's own upstream connection does not carry the flag. pygar: GARClient(..., disconnect_on_write_failure=True); jsgar: the disconnectOnWriteFailure constructor argument. Both libraries already treat a non-recoverable Error as fatal (exit_code 1, stop). The REST analogue is ?fail=true (see Write-failure replies).

Record Updates

Record updates flow both ways through the same messages: a client writes with them, and a server streams post-snapshot updates to a subscriber with them too — only the initial snapshot (and nagle-coalesced ticks) travel as batch updates. Anything that must ride with every streamed record therefore has to live on these messages as well as on batch_update.

The GAR protocol supports record updates via:

Binary encoding

  • FixedLengthRecordUpdate: record_id directly followed by the fixed-size record data.
  • VariableLengthRecordUpdate: serialized binary_record_update struct with embedded length field.

JSON encoding

  • Encoded using json_record_update with clear field names and value.
  • Non-finite floats. JSON has no literal for them, so every JSON emitter (REST, the JSON wire protocol, --print) spells an infinite float/double as 1e999 / -1e999 — a literal that overflows to ±infinity in every reader (Python, JavaScript, jq, the trs reader itself) — and NaN as null (a NaN result is absence at the record boundary). On input the trs reader accepts 1e999, and the words infinity / -Infinity (case-insensitive), as ±infinity. A .gar float literal takes the same overflow spelling — 1e999 in a key initializer or an expression is +∞ — so a schema can say what the wire says (mkt_feed_test.gar's good_long_leg_no_ask_spread pins an infinite initializer through the g::schema introspection round trip).

Example:

{
  "message_type": "JSONRecordUpdate",
  "value": {
    "record_id": {"key_id": 1, "topic_id": 22},
    "value": 0.3
  }
}

Augment bypass (assume_cached_subscriptions_cover)

A subscription with assume_cached_subscriptions_cover: true asserts that the proxy serving it has cached subscriptions covering every record it asks for. That proxy then skips this subscription's augment differencing entirely and serves it — derived topics included — from its cache, removing the poller-thread set-algebra the snapshot blocks on. The assertion is per subscription: an operator turns it on for the few heavy subscriptions the cache was provisioned around, and every other subscription on the same proxy keeps augmenting normally. No proxy config gates it — the assertion is the caller's own, and a wrong one under-serves only the subscription that made it; a plain (non-proxy) server has no cache to bypass and ignores the flag. Fail-open: anything the cache does not cover is silently absent or null on that subscription. REST equivalent ?assume-cached=true, trsgar --assume-cached. See augment_bypass_spec.

Record tick_time

A subscription with include_tick_time: true receives each record's tick_time: the owning server's wall-clock nanoseconds at the record's last write. It advances on every write, including one whose value did not change, so it answers "is the writer alive", not "did the value change". A value-unchanged write is throttled (no update); a flagged subscription instead receives a RecordTick (record_tick {record_id, tick_time}) — stamp only, no value — which clients that don't track stamps can ignore. Proxies forward the flag upstream and relay the origin's stamp unchanged, never their own receipt time; a relayed tick never triggers recomputation.

  • json_record_update / binary_record_update carry it as tick_time (JSON omits the member, binary sends 0, when the subscription didn't opt in).
  • batch_update.key_major_update.tick_times is an array parallel to topics; JSON carries a "tick_times" object beside "topics" with the same <topic_id> member names. Empty/absent otherwise.

When a RecordTick is sent. Only once the subscription is streaming, only for keys the connection has already introduced, and never held in a nagle window. It fires only on a real advance of the stamp: a relay re-writing an identical record with the same stamp is silent, so a redundant snapshot on a proxy ticks nothing. A subscription without the flag keeps the throttle and receives nothing for an unchanged write.

How the stamp is set. A write that originates on a server — a client's write, a derived recompute, a local topic — is stamped with that server's own clock. A relay that carries an upstream stamp stores it verbatim; a relay that carries no stamp (an unflagged upstream subscription) preserves the stamp the record already holds, so two upstream subscriptions of which only one is flagged converge on the right stamp whatever their delivery order. Derived topics computed on a proxy are origin writes there and carry the proxy's clock.

Off by default — payloads keep their size and shape. pygar keeps the stamps in client.tick_time_map[(key_id, topic_id)] and jsgar in client.tickTimeMap; both offer a record-tick callback (register_record_tick_handler / registerRecordTickHandler).

Record diagnostics

A derived-topic record that has no value carries a structured diagnostic saying why — a derived_diagnostic {reason, missing_inputs[], text}. reason is one of InputMissing, KeyCastMissed, Suppressed (the null class: plain absence with a cause) or ComputeError / InputError (the error class: the function raised or failed — text is the stripped exception message — or one of its inputs is in error). Errors are poison: an InputError consumer stays in error whatever its optional/default argument policy, and ?? coalesces nulls only. missing_inputs names each absent or erroring input — its argument index and declared parameter name, the input topic's path and the key holding it — so a client can chase a null or an error upstream, record by record, to its root cause.

  • Every subscription receives error-class diagnostics with no opt-in; the null-class reasons only reach subscriptions that set include_diagnostics: true.
  • The wire carries them as RecordDiagnostic (record_diagnostic {record_id, diagnostic}), sent after the batches that introduce the record's key during a snapshot, and streamed on change. A diagnostic replaces the record's value: a value→diagnostic transition arrives as the ordinary DeleteRecord followed by the RecordDiagnostic; a value arriving supersedes the diagnostic.
  • A proxy computes derived topics itself, so its diagnostics are its own computation's; a client that takes derived records from a server (trsgar, pygar, jsgar) applies the relayed diagnostic verbatim.

See null_reasons_spec.

Density

The density field controls how records are grouped in JSON output. The default density is KeyMajor.

  • KeyMajor (default): { "key": "key1", "topics": { "topic1": <record>, "topic2": ... } }
  • TopicMajor: { "topic": "topic1", "keys": { "key1": <record>, "key2": ... } }
  • RecordMajor: { "key": "key1", "topic": "topic1", "value": <record> } (one record per object)

Batch updates

Multiple updates can be sent together in a batch. Initial snapshots after subscribing are always sent in one or more batch.

Records are sent in key-major format. Keys are introduced along with their class or class_list. Key names and classes may be omitted when keys have already been introduced.

Binary format uses the batch_update struct.

JSON format is similar, except the records within each "topics" object are JSON objects with topic_id numbers as member names: { "<topic_id>": record_value, ... }

Example:

{
  "message_type": "BatchUpdate",
  "value": {
    "default_class": "A",
    "keys": [
      {
        "key_id": 1,
        "name": "key1",
        "topics": {
          "20": 10
        }
      },
      {
        "key_id": 2,
        "name": "key2",
        "class": "B",
        "topics": {
          "21": 20
        }
      },
      {
        "key_id": 3,
        "name": "key3",
        "classes": ["A", "B", "C"],
        "topics": {
          "20": 30,
          "21": 31,
          "22": 32
        }
      }
    ]
  }
}

Subscriptions

A subscription names the classes, keys and topics a client wants and whether it is a one-shot snapshot or streaming; filters, the referencing-class list, exclusions, the class join and the nagle interval are all fields of it. The full reference is Subscriptions.

Echo

Clients may send an EchoRequest message containing a msg_ref (message reference number). The server will respond with an EchoResponse containing the same msg_ref. This can be used to confirm that all prior writes have been processed before continuing.

In proxy chains, the echo propagates end-to-end: the proxy forwards the echo to its upstream server and only responds to the client once the upstream echo response arrives. This guarantees that all prior writes — including record updates and deletes — have been fully processed through the entire chain by the time the client receives the echo response. The REST API uses this mechanism internally, so HTTP POST and DELETE responses also carry this guarantee.

Compare-exchange

A CompareExchange message applies a write only if the record's current value is what the client expects, so two writers racing on one record cannot silently overwrite each other. It carries a record_id, an inline msg_ref correlation id, a test (the expected current value) and a value (what to write on a match). The server replies with a CompareExchangeResult echoing that msg_ref and carrying success, which is true only when the value was applied. On a failure it also returns the record's current value and a reason: CompareMismatch when that value did not match test, so a client can retry against fresh state without a separate read, or WriteDenied / DeleteDenied when the key is owned by another client, which no retry clears.

Null means absent, on both sides. A missing record reads as null, so:

  • test: null matches a record that is missing — this is create-if-absent. (For a topic whose type declares a default, a missing record instead reads as that default, so the matching test is the default value.)
  • value: null is an empty value, so a successful compare deletes the record. This holds for every topic type, including fixed-length numeric ones — a null value is never stored as a zero.

Binary clients express the same two things with a zero-length test / value byte array.

Together those give compare-and-delete: test = the value you saw, value = null. That makes a record a lock-free "consume exactly once" flag — several readers that all observed X each attempt the delete against X, exactly one succeeds, and the losers are told so. Nothing is claimed, so a reader that dies mid-operation strands nothing. The same operation is available over REST via atomic-compare-exchange=true.

Compare-exchange propagates through proxy chains: the proxy forwards it upstream and relays the result back to the originating client. The REST equivalent forwards the same way, so both surfaces are atomic through a proxy.

The edges of the null rule, and two conventions:

  • A missing record on a plain topic (bool, int, a struct of zeros — no annotated default) is matched only by a null or empty test, never by test: false / 0: a missing x is not equal to false, the same way x == false is false in an expression when x is missing.
  • An annotated default is what the missing record reads as, so a bool default true lock is acquired with test: true against a key that has never been written — no seed record needed.
  • A value written by a successful compare is stored verbatim even when it equals the type default on a trim_default_values topic, so the result is always observable as a record echo.
  • On a fixed-length topic value: null deletes; to write a zero, send value: 0.
  • The inline msg_ref is the correlation id for the result and for any error the compare raises; the active_ownership marker is sent before a compare only when it conveys ownership (client_key_id, skip_ownership_checks, ownership_action) — a plain compare sends none.

pygar/test_gar_client.py (test_compare_exchange_*) pins each of these; ipc/cas_proxy_test.py the proxy path.

Subscription Status

A SubscriptionStatus message will be sent to delineate the lifetime of the subscription.

  • "status": "ProcessingSnapshot" is sent at the start of processing. Topic, Key, and record updates will follow.

  • "status": "NeedsContinue" is sent if snapshot_size_limit is hit; see Subscriptions above.

  • "status": "Streaming" is sent for streaming subscriptions once the snapshot has been fully processed. Additional messages are live updates.

  • "status": "Finished" is sent once the snapshot has been fully processed and the subscription mode is non-streaming. It is also sent after an unsubscribe has been processed.

Heartbeats

Clients specify a heartbeat timeout interval within their introduction. If the server does not receive a Heartbeat message within this interval, it sends the client an Error message ("Heartbeat timeout") and closes the connection gracefully — a client that still reads (e.g. one whose sending stalled) receives the error before the close; one that doesn't is dropped after one further timeout interval.

The server also specifies its timeout interval, and sends regular heartbeats. Clients may choose to respect missed heartbeats and disconnect.

Heartbeats are expected to be sent at twice the frequency of the timeout interval. e.g. with timeout of 4 seconds, heartbeats are sent every 2 seconds.

The send cadence is fixed-rate (anchored to the previous scheduled instant, not to when the handler last ran), but a missed-tick backlog never replays. The send loop is single-threaded, so a slow client that backs up the send buffer can stall it for many intervals; rather than firing a burst of catch-up heartbeats onto the already-slow client when it resumes, the sender snaps the next beat forward past the current time, collapsing the backlog to a single heartbeat one interval out.

Synchronizing without sleep

Client code (and test harnesses) frequently needs to wait until some condition holds — the server is accepting connections, prior writes have been applied, a subscription is live, a derived value has settled — before taking the next step. Do not approximate these waits with a fixed sleep. A blind delay is simultaneously too short (it races under load) and too long (it wastes time on a fast path), and it converts a missed event into a silent, intermittent failure. The protocol exposes an explicit signal for each case; synchronize on the signal and the timing becomes deterministic.

  • Server is ready to accept connections. Launch the server with --server-ready-out FILE; it writes the bound {protocol, url} endpoints to FILE once it is listening. Poll until the file is non-empty, then read the URL — never sleep waiting for a port to bind.

  • All prior writes have been processed. Send an EchoRequest and wait for the matching EchoResponse (see Echo). In a proxy chain the echo is end-to-end, so its response guarantees every prior record update/delete has propagated through the whole chain. The REST API uses this internally, so an HTTP POST/DELETE response already carries the guarantee — no follow-up wait is needed.

  • A streaming subscription is live. After subscribing, wait for the "status": "Streaming" SubscriptionStatus message (see Subscription Status); it is sent once the initial snapshot has been fully processed and means subsequent messages are live updates. This matters most for live-only topics (history: none): such a record is pushed only to subscribers that are already streaming when it is produced and is never retained, so a subscriber still in ProcessingSnapshot when the event fires misses it permanently. Trigger the event only after the Streaming status arrives. The same transition is the signal that a publisher has delivered: tooling that waits for a publishing process to exit is waiting on the wrong thing, because a publisher's exit rides the receiver's echo barrier and still — correctly — waits for a stalled server to come back. Synchronize on the publisher's Streaming transition (trsgar --trs-log FILE records it), not on its exit.

  • A derived value has settled before you act on it. Poll the value (over REST, or via your subscription stream) until it holds the expected value, rather than sleeping a guessed interval.

  • A non-streaming snapshot subscription has completed — most importantly a DeleteKeys / DeleteRecords delete. Wait for the "status": "Finished" SubscriptionStatus before treating the result as done or sending Unsubscribe: the snapshot (and any delete it performs) is processed incrementally, so an Unsubscribe issued synchronously after the subscribe cancels the in-flight work. The REST API waits for Finished for you — an HTTP DELETE returns only after the delete has fully processed — so this caveat applies to direct WebSocket clients (e.g. browser/Node subscribe callers), not REST callers.

For ad-hoc polling, use a bounded retry with a small interval and a hard timeout (e.g. check every 100 ms up to a few seconds) so a stuck condition surfaces as a clear timeout rather than a hang. The only place a fixed delay is defensible is a negative check — confirming that something does not happen within a window.

Termination

To gracefully close a session, clients should send a Logoff message. However, clean TCP connection closure is also acceptable.

A WebSocket client that sends a close frame (code 1000 or 1001, or no code — a browser's tab teardown) gets one back and the server then closes the connection, so the client's socket reaches CLOSED rather than waiting in CLOSING. A close carrying any other code is an error: it is reported and the connection dropped without an answer.

A close frame with an empty body is close code 1005 ("no status received") — the normal signature of a browser page dying without a clean close. The server logs it at Info (Websocket close frame received (1005 no status)) and answers it exactly like 1000. A close frame with a 1-byte body is malformed (a carried close code is two bytes): the server warns Malformed websocket close frame (1-byte payload) and disconnects. ipc/websocket_close_frame_test.bash sends each shape over a raw socket.

If the server encounters an issue, it may respond with an Error message prior to disconnection. The frames a peer sent before resetting the connection are still delivered ahead of the reset, so an Error a server wrote and then closed on — a rejected subscription, a refused introduction — reaches the client as that error rather than as a bare "connection closed".

Connection diagnostics are written from the emitting process's point of view, and the connection code is the same at both ends of a link: a Disconnecting client on write error line in a publisher's log is that publisher dropping its own upstream link (its outgoing write queue overflowed), not evidence that the process is a server.

The server sends Logoff when it is shutting down (trsgar --send-shutdown), tearing every subscription down at once. That is an orderly end, but at the socket it is indistinguishable from the connection breaking, and both stop the client the same way. GARClient.server_logoff records which it was, so a long-running client can exit 0 through a planned restart instead of reporting a failure on every one — the alternative is a supervisor alert per restart, which buries the real failures. Read it after the client stops; register_logoff_handler is the callback form. jsgar carries the same flag as serverLogoff.

Both libraries write their log lines as YYYY-MM-DD HH:MM:SS.mmm LEVEL:<name>:<message> in local time — the shape trsexec and the C++ core use — so a service log carrying several of them reads as one stream rather than as interleaved timezones and formats.

The client never reconnects; the application may start it again

A GARClient (pygar and jsgar alike) holds one connection per start() and never reconnects on its own: a lost connection, a failed connection attempt, a heartbeat timeout, a server Logoff or a non-recoverable Error all stop the client, and the stopped callback (register_stopped_handler / registerStoppedHandler) fires once per session with the cause in stop_reason (stopReason):

stop_reason what ended the session start again?
stop the application's own stop() / logoff() its call
server_logoff the server's Logoff — a planned restart once the server is back
disconnect the socket closed or broke yes
heartbeat_timeout no server heartbeat within the interval; exit_code is 1 yes
error a non-recoverable server Error (pygar also: an undecodable message); exit_code is 1 after fixing what the Error reported
connect_failed the connection attempt was refused, timed out, or the handshake failed when the server is expected back
unauthorized the handshake was rejected with 401 no — the credentials are wrong

Retrying is the application's decision, made by calling start() again on the same client, from the stopped callback included. Each start() is a fresh session that begins from the state of a newly constructed client: every server-assigned and client-assigned id, cached record, tick time, diagnostic and ownership marker is cleared, so the application re-issues its subscriptions and key introductions (both are per connection — see Session Lifecycle); registered handlers and callbacks carry over. Nothing survives from the previous session's teardown: a pygar heartbeat thread or a jsgar send loop and socket still winding down cannot touch the session that replaced them. One start() attempt makes one connection; an application that wants to wait for a server pacing its own retries with a backoff of its choosing (connect_failed comes back at once from a refused port). pygar's start() blocks for the life of the session and returns after the stopped callback, so each session runs on its own thread; jsgar's returns at once. The pygar suite's test_start_again_after_disconnect and the client_restart_test (jsgar) run a client through a killed server, a refused port, a restarted server, and a start() from inside the stopped callback.

Writer ownership (active_ownership)

Ownership of writer-owned keys is carried by the active_ownership message, sent before the key introductions, record updates or batches it governs; it sets connection state that stays in force until the next one, so many writes share one declaration. Its fields:

Field Type Description
msg_ref message_reference Where recoverable errors from subsequent messages are routed. If 0, errors are logged server-side and not returned.
ownership_action enum NONE (default) / Acquire / Release.
skip_ownership_checks bool Ignore the current owner, so the key may be acquired, released, or deleted regardless of who holds it.
require_existing bool Only write to existing keys — do not create new keys. If msg_ref is non-zero, sends recoverable errors for missing keys/classes.
client_key_id key_id The remote g::Client key id the sender will use to refer to this owner — how a proxy attributes ownership to a downstream client. If zero, the connecting client's own connection key is used.
client_key_name string optional Trailing variable field; when present, atomically introduces the g::Client key named here and binds it to client_key_id.

The schema attributes, the ownership lifecycle, the REST and CLI surfaces and the error texts are on writer_owned_keys.md.

Failed writes

Recoverable write failures do not disconnect. Every failed write from processing one client message batches into ONE recoverable Error message whose failed_writes array identifies each failure — a shape-scoped reason enum (record_write/key_write union), the addressed ids and names, malformed (server-classified: a request defect vs a server-state condition), per-failure detail, and origin (the server or proxy that detected the failure; proxies relay upstream failures verbatim). Delivery requires a nonzero msg_ref on the active_ownership marker — with msg_ref 0 the failure is logged server-side only. pygar (register_write_errors_handler) and jsgar (registerWriteErrorsHandler) deliver the array to a callback as (failed_writes, message, msg_ref). A connection introduced with disconnect_on_write_failure instead receives the batch as a non-recoverable Error (msg_ref 0 included) and is then closed — see Introduction Options. Pinned end-to-end by ipc/write_errors_test.bash.

malformed is true for the reasons a retry could never fix: InvalidValue, UnknownTopic and TopicNotWritable on a record write, UnknownClass on a key write. TopicNotInClass is deliberately state, not malformed — a class downgrade running concurrently can legitimately race a correct writer. key_id is 0 when the write addressed the key by name. Value-level failures — an unknown class or topic name, a value that does not parse — are per-write reports; an unmapped binary id, a name-vs-id mismatch and a framing error stay fatal, because ids are protocol state and a desynchronized id map poisons everything after it. The Error's text is composed from the structured fields, in the shape KeyNotFound key 'x' topic 'y' from 'backend': <detail>. A topic-typed value naming a topic the schema lacks is refused on every surface (a client write reports malformed InvalidValue; a subscribe naming it fails naming the topic) — it is never quietly stored as the null topic.

The plain error callback cannot tell the two apart. pygar's register_error_handler and jsgar's registerErrorHandler are text-only conveniences: their wrappers pass message and drop the rest of the Error, including the recoverable flag that says whether the connection survives. Both clients read that flag internally to decide whether to keep going, so the information reaches the library and stops at the wrapper. A client that must distinguish "this write was rejected" from "this connection is finished" registers for the raw message instead — register_handler("Error", …) / registerHandler('Error', …), whose callback receives the whole Error — or, when the interest is specifically which writes failed, uses the write-errors callback above.

Key teardown is per class

All key teardown is delivered as KeyUpdate messages carrying deleted_class — one message per class. When a server deletes a key entirely, it sends one deleted_class KeyUpdate per class the receiver was shown (each with an empty remaining class_list), followed by KeyIdRetired — a connection-scoped bookkeeping message meaning only "the sender's introduction of this key id is gone; drop your mapping for it", never a data teardown. There is no whole-key teardown broadcast: a subscriber's view of a key ends when its last subscribed class is deleted. (Distributed servers can each hold a subset of a key's classes, so "all classes gone" on one server is not global truth — per-class messages are the only view-independent form.)

DeleteKeyCommand is the client→server command requesting whole-key deletion at the receiving server; servers never broadcast it.

A key can also be downgraded: its derived class removed while named base classes — and the records on topics those bases still cover — are kept. On the wire this is the same message, key_update{deleted_class: Derived, class_list: [bases]}, with the retained bases as the surviving list; no protocol version ticks for it. A receiver that predates downgrades applies it as a full delete of the class, dropping the retained bases. pygar's register_deleted_class_handler and jsgar's registerDeletedClassHandler are the client surface for every deleted_class update, whole-key deletes and downgrades alike, and receive the remaining class list with each.

Example Session (Client Perspective)

Sent: {"message_type": "Introduction", "value": { "version": 650269, "heartbeat_timeout_interval": 3000, "user": "jonh" }}
Received: {"message_type": "Introduction", "value": { "version": 650269, "heartbeat_timeout_interval": 3000, "user": "jserver" }}
Sent: {"message_type": "Subscribe", "value": { "subscription_mode": "Streaming", ... }}
Received: {"message_type": "ProcessingSnapshot", "value": { "name": "S1" }}
Received: {"message_type": "TopicIntroduction", "value": { "topic_id": 18, "name": "top_bid_price" }}
...
Received: {"message_type": "SnapshotComplete", "value": { "name": "S1" }}
Sent: {"message_type": "Heartbeat", "value": { "u_milliseconds":, 1745425692890 }
Received: {"message_type": "Heartbeat", "value": { "u_milliseconds":, 1745425693895 }
...
Sent: {"message_type": "Logoff"}

Auditing Topics (Control Port)

When running in audit mode (--ws-control-port <port>), GAR will emit internal topic traffic for monitoring

g Schema

Protocol Schema

  • af_unix_transport.md — local connections silently use AF_UNIX sockets rather than TCP loopback, and why a URL may resolve to unix:.
  • gar_pipelining.md — streaming records between endpoints in a shell pipeline, and the failure modes to watch for.

Deployment Schema

Features in this area

  • client libraries — Connecting to GAR from Python (pygar) and JavaScript (jsgar) over WebSocket.
  • subscription class join — Joining a subscription's key filter against another class's key set.
  • key name list subscription — Making a subscription with a finite list of key names snapshot by direct lookup instead of scanning the class.
  • rpc — Calling a named GAR function and getting a structured reply.

Internals

The implementation record — every private section this page used to carry — is protocol_internals.md.

Release files for pygar-client 4.19.10

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distribution (wheel)

Table of built distributions (wheels) for pygar-client 4.19.10
File Interpreter ABI Platform
pygar_client-4.19.10-py3-none-any.whl Python 3 none any Details

Release files / pygar_client-4.19.10-py3-none-any.whl

Download URL pygar_client-4.19.10-py3-none-any.whl
Size 44.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2b6b3a4b654205a2fe730028f5468d2f5d7efe34390bdd64289df18529a83e69
BLAKE2b-256 checksum
How to use checksums
d62cccc1ece63418e799227189852be271adfe75c936fd10388c3820e0f5b705
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.12.3

Release history Release notifications | RSS feed

4.19.12

1 release file

4.19.11

1 release file

This release

4.19.10 This release

1 release file

4.19.9

1 release file

4.19.6

1 release file

4.19.5

1 release file

4.19.4

1 release file

4.19.2

1 release file

4.19.0

1 release file

4.18.1

1 release file

4.18.0

1 release file

4.17.0

1 release file

4.16.0

1 release file

4.15.0

1 release file

4.14.0

1 release file

4.13.8

1 release file

4.13.6

1 release file

4.13.4

1 release file

4.13.3

1 release file

4.13.0

1 release file

4.11.4

1 release file

4.11.3

1 release file

4.11.1

1 release file

4.11.0

1 release file

4.10.1

1 release file

4.10.0

1 release file

4.9.2

1 release file

4.9.1

1 release file

4.9.0

1 release file

4.8.0

1 release file

4.6.5

1 release file

4.6.4

1 release file

4.6.3

1 release file

4.6.2

1 release file

4.6.1

1 release file

4.6.0

1 release file

4.5.6

1 release file

4.5.3

1 release file

4.5.2

1 release file

4.5.1

1 release file

4.5.0

1 release file

4.4.0

1 release file

4.3.2

1 release file

4.3.1

1 release file

4.3.0

1 release file

4.1.0

1 release file

4.0.5

1 release file

4.0.4

1 release file

4.0.1

1 release file

3.8.8

1 release file

3.8.2

1 release file

3.8.1

1 release file

3.8.0

1 release file

3.7.9

1 release file

3.7.8

1 release file

3.7.7

1 release file

3.7.6

1 release file

3.7.5

1 release file

3.7.4

1 release file

3.7.3

1 release file

3.7.2

1 release file

3.7.1

1 release file

3.7.0

1 release file

3.5.0

1 release file

3.4.2

1 release file

3.4.1

1 release file

3.4.0

1 release file

3.3.0

1 release file

3.2.0

1 release file

3.1.4

1 release file

3.1.3

1 release file

3.1.2

1 release file

3.1.1

1 release file

3.1.0

1 release file

3.0.2

1 release file

3.0.1

1 release file

3.0.0

1 release file

2.2.2

1 release file

2.1.2

1 release file

2.1.1

1 release file

2.1.0

1 release file

2.0.0

1 release file

1.7.8

1 release file

1.7.7

1 release file

1.7.6

1 release file

1.7.5

1 release file

1.7.4

1 release file

1.7.3

1 release file

1.7.2

1 release file

1.7.1

1 release file

1.6.4

1 release file

1.6.3

1 release file

1.6.2

1 release file

1.6.1

1 release file

1.5.4

1 release file

1.5.3

1 release file

1.5.2

1 release file

1.5.1

1 release file

1.4.6

1 release file

1.4.5

1 release file

1.4.4

1 release file

1.4.3

1 release file

1.4.2

1 release file

1.4.1

1 release file

1.3.1

1 release file

0.4.5

1 release file

0.4.4

1 release file

0.4.3

1 release file

0.4.2

1 release file

0.4.1

1 release file

0.3.1

1 release file

0.2.4

1 release file

0.2.3

1 release file

0.2.2

1 release file

0.2.1

1 release file

0.1.1

1 release file

0.1.0

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page