---
title: bus
description: The Bus wire format and the broker client library, with a supervised connection to noded that survives broker restarts.
---

# bus

The Bus wire format and the broker client.

- `bus::wire` (always built): `BusMessage`, `parse`, `parse_strict`,
  `parse_lenient` and the size caps. A message is `---`-fenced `key: value`
  headers (sorted) plus an optional body. The format is a frozen contract
  with every running peer.
- `bus::client` (the default `client` feature): `Connection`, one registered
  WebSocket to the broker ([noded](../noded/README.md)); `SupervisedClient`,
  the same under a reconnect supervisor whose incoming stream survives
  broker restarts and which replays every recorded topic subscription before
  reporting `Connected`; `IncomingCommand`; and `noded_url()`, which finds
  the broker (`MIXOS_NODED_URL`, then `node.conf.mix` via
  `MIXOS_NODE_CONFIG`, `$MIXOS_ETC`, `~/.config/mixos` or `/etc/mixos`, then
  `ws://127.0.0.1:4200/ws`).

A wire-only consumer depends on `bus` with `default-features = false` and
pulls in nothing but `serde_json`.

The source is [`libs/bus`](https://github.com/markc/mixos/tree/main/libs/bus).

## Starting without a broker

Resident GUI hosts can use `SupervisedClient::connect_options(name, url).start()`
inside their existing Tokio runtime to return immediately while the broker is
unavailable. The same supervisor handles initial dial, registration, topic
replay and later reconnects. Until first success the state is `Connecting`,
generation is zero and outbound work fails fast; first complete
registration and replay publishes generation one. The single incoming
receiver remains usable across outages. `connect().await` retains its
five-attempt initial budget for callers that need bounded startup. Neither
API queues outbound work.

## Bounded incoming lanes

`bounded_incoming(capacity)` drops overflow silently, reported only as an
`Overflow` marker. `bounded_incoming_rpc(capacity)` opts into refusal: an
id-bearing request with no `topic` header that the full lane cannot retain
gets an `rc 10` `BUSY` reply on the same connection it arrived on, so the
caller can retry. Topic deliveries and id-less frames remain lossy. If that
refusal cannot be written within 100 ms, the old connection gets a
best-effort close within a further 100 ms, and the supervisor detaches its
handle before redialling; the physical close may follow later if another
holder still references it. No reply is ever sent through the replacement
connection.

## Registration rejections

With `fatal_on_registration_rejection(true)`, `registration_rejection()`
retains the broker's exact return code and diagnostic before publishing
`Fatal`, for both initial registration and reconnect. Sampling it is
non-consuming; transport failure has no refusal diagnostic. Close,
shutdown, deregister and drop are safe before a first socket exists and
fence late registration publication. These are client lifecycle APIs; the
frozen ABP wire format is unchanged.

A broker rejection is typed: `RegistrationRejected` keeps the public `rc`
and `message` fields. Field reads and the tuple accessors stay
source-compatible, but old two-field struct literals no longer compile
(construct with `RegistrationRejected::new`). It adds `kind()`, which
decodes the structured rejection body
(`{"schema": "noded.registration-rejection.v1", "error_code": "NAME_TAKEN", "message": …}`,
sent alongside the unchanged `rc=10` and `error` header).
`RegistrationRejectionKind::NameTaken` is produced only from that body; an
admission refusal, a legacy text-only broker, or a missing, malformed,
oversized or unrecognised body is `Unknown`: classification never
string-matches the wording. `ClientError::registration_rejection_typed()`
and `SupervisedError::registration_rejection_typed()` expose the typed
value, and the `(rc, message)` tuple accessors remain for compatibility.

## Tests

`cargo test -p bus`. The integration test drives a stub broker in process;
no noded is needed.
