4.7 KiB
0011. A dedicated authority stream for server-pushed state
- Status: Accepted
- Date: 2026-07-31
Context
Because our simulation is strictly server-authoritative (see ADR-0004), there is an entire category of network traffic that the client never actually asks for: state the server just decides to push on its own cadence. Simulation snapshots will eventually make up the bulk of this, but periodic server diagnostics were the first concrete instance we hit.
We could have crammed this traffic into two existing streams, but both were a terrible fit:
- The control stream (stream 0) handles handshakes and disconnects. It is strictly request/response and effectively one-shot per connection. Shoving a recurring push onto it mixes one-time lifecycle negotiation with steady-state spam, meaning a burst of pushed state could delay a time-sensitive disconnect notice sitting in the exact same ordered stream.
- The chunk stream (stream 3) is bidirectional and handles massive frames (up to a 1 MiB cap). QUIC handles head-of-line blocking on a per-stream basis. If a tiny, highly time-sensitive state push gets queued directly behind a 500 KB chunk delivery, it completely inherits that chunk's awful latency. That is precisely the coupling that separate streams are designed to avoid.
Stream assignment acts as a hard wire contract shared by both peers (StreamLayout rigidly fixes the IDs), and changing one is a protocol break. Because of this, we need to make this decision exactly once, ahead of the massive snapshot work that will rely on it, rather than discovering we need it later and breaking the protocol.
Decision
Server-pushed authoritative state travels on its very own, unidirectional-in-practice stream: StreamLayout::authority (stream 2). It carries shared::protocol::authority::AuthorityMessage.
- The stream is strictly server => client only. Absolutely nothing the client sends belongs on it. When client input arrives, it will get its own dedicated stream.
AuthorityMessageis an enum. Because of this, any new pushed payloads are simply added as variants rather than requiring brand new streams.ServerStatsis the first variant, and simulation snapshots will eventually join it.- Frames use our existing length-prefixed
postcardcodec, capped safely atMAX_AUTHORITY_FRAME_LEN(64 KiB). This is well above what a fixed-shape diagnostics record needs, and it safely limits how much memory a malformed length prefix can trick a peer into allocating. - The async/sync bridge strictly follows the pattern we established for chunk delivery in ADR-0010. The simulation loop holds an
AuthoritySink, which is a synchronous, non-blocking handle wrapping atokioMPSC sender. This ensures neither theservernorclientcrate ever has to explicitly name atokiotype. - If we attempt a send on a departed connection, it simply logs at debug level and drops. The simulation loop cannot act on a disconnected client anyway, and since pushed state is by definition unsolicited, failing to deliver it is never an actual error condition for the sender.
Consequences
- The latency of pushed state is now completely independent of chunk delivery volume. Even if a client is pulling its initial region at maximum bandwidth, it still receives its state snapshots perfectly on time.
- Adding a brand new pushed payload is as simple as adding an enum variant. It requires zero new streams to negotiate on either peer and zero
StreamLayoutchanges. - The stream layout now explicitly commits four IDs (control 0, reserved 1, authority 2, chunk LOD0 3). Trying to reassign any of these will demand a full
PROTOCOL_VERSIONbump. - Loss and ordering semantics are strictly per-stream. Authority messages are perfectly ordered relative to each other, but completely unordered relative to chunk deliveries. If something requires a snapshot to be interpreted against a specific delivered chunk, it must carry its own correlation data (like a tick number or chunk version) rather than lazily relying on arrival order across streams.
- The current sink is fire-and-forget and unbounded. While this is perfectly fine for a low-rate diagnostics push, sending massive snapshots at tick rate will absolutely require a bound and a drop policy. We cannot allow a slow client to grow the server's queue without limit. This is a known follow-up requirement before snapshots officially ship.
- Because diagnostics are on the authority stream rather than beside it, they are subject to the exact same server-authoritative framing: the client strictly reports what the server measured, never what it inferred locally. See
docs/diagnostics.mdfor more details.