mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Implements the dependency-free half of §4 (huddle lifecycle): the non-owner side reacts to the owner's teardown signal read off the `HuddleControl` stream and tears the local client down so it can rejoin. This is the piece Wren's gate names — it reacts to wire signals only and needs no huddle-lane renewer (the owner-side proactive-loss `Goodbye` producer is the separate next increment). Wire mapping (no `Lost` variant exists; the enum is SessionEnded/Draining/ StaleGeneration): owner-loss surfaces as `Goodbye(StaleGeneration)` -> OwnerLost; `Goodbye(Draining)` -> OwnerDraining; `Goodbye(SessionEnded)` -> ordinary disconnect; a bare stream close/reset (owner pod died mid-flight) -> StreamClosed. Every cause is recoverable by a rejoin, so behaviour is uniform (cancel + forget); the cause is observability only. join.rs: - New `HuddleTeardownCause` + `read_teardown_cause(&mut MeshStream)`: a pure, testable reader that returns on the first terminal signal and skips non-terminal owner->client frames (the forward-compat seam for future roster deltas, which must not be mistaken for teardown). - `RemoteHuddleSession` no longer owns the control stream — it holds only the outbound (client->owner) half. `dial_remote_owner` now returns `(RemoteHuddleSession, MeshStream)`: the session forwards media, the stream is handed to the reader task. `close()` is replaced by a free `send_clean_close(stream, fenced, pubkey)` so the reader task owns the stream end-to-end (one owner, one closer). handler.rs: - Spawn a reader task that races `read_teardown_cause` against the connection's cancel token. Owner speaks first -> `teardown_remote_huddle`: cancel the connection (drives the client's WS Close = rejoin) + forget the local generation floor for this session. We cancel first (client left / heartbeat death) -> `send_clean_close` so the owner drops us. The task is joined during cleanup so its close completes before the connection returns. mesh_boot.rs: - Share one `Arc<GenerationFloor>` on `MeshHandle` (`audio_fence`) so the datagram receive path (`MeshAudioRouter`, wired via the pending `register_*`) and this teardown path consult and clear ONE floor. A private floor per consumer would let a torn-down session keep suppressing a rejoin. The floor stays a local stale-frame guard only: `forget` clears local suppression so a rejoin's fresh generation is accepted; Redis fenced CAS remains the ownership arbiter and `forget` never authorizes ownership (per Perci's invariant). Adds six tests over the existing in-memory MeshStream pair: each `GoodbyeReason` maps to its cause, a bare stream close maps to StreamClosed, a non-terminal Data frame is skipped before a later Goodbye, and the clean close emits UnregisterPeer then Goodbye(SessionEnded) in order. fmt + clippy clean; 519 buzz-relay tests pass. Scoped out (next increment, Wren-gated): the huddle-lane renewer + owner-side proactive-loss `Goodbye(StaleGeneration)` producer. This commit reacts to that signal; it does not yet produce it. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>