Skip to content

Events Reference

Decoded messages from the Signal Fish server, plus locally generated lifecycle and diagnostic events, are surfaced as [SignalFishEvent] variants through the event receiver returned by [SignalFishClient::start].

This page documents all 36 variants grouped by category, with field descriptions and usage examples.

Protocol v2 relay + v3 mesh

Delivery-accountability, relay diagnostics, graceful drain, and mesh events only arrive on a v3-negotiated connection. Use SignalFishConfig::enable_v3() for relay/accountability or enable_mesh() when a WebRTC driver is also available. See Protocol Versioning and the Mesh Guide.

Type aliases used throughout

PlayerId is an alias for uuid::Uuid. RoomId is an alias for uuid::Uuid.

Overflow backpressure and delivery boundaries

Events are not dropped merely because the bounded event channel overflows (default 256, via SignalFishConfig::event_channel_capacity), and a consumer that falls behind pauses the transport loop until the channel has room — backpressure propagates to the server instead of losing events. Inbound frames that fail to decode surface as DecodeFailed events rather than being skipped. Delivery can still stop or be preempted: the event receiver can be dropped, the client handle can be dropped without calling shutdown(), which aborts the loop immediately, or shutdown() can abandon the one delivery currently blocked on channel capacity. Shutdown attempts a graceful close and delivers the terminal Disconnected best-effort; its configured deadline may abort that work. The event channel closing is the authoritative end-of-stream signal.


Connection Events

Synthetic events generated by the transport layer rather than the server. Use these to track the raw connection lifecycle.

Variant Fields Description
Connected The driver first observed Transport::is_ready() == true and marked ClientSnapshot::transport_ready true; a later terminal transition resets it. Synthetic — see Connection timing for details.
Disconnected reason: Option<String>, last_server_error: Option<ServerErrorInfo> The transport connection was closed or errored.
DecodeFailed message_type: Option<String>, error: String, raw_prefix: String An inbound frame could not be decoded into a ServerMessage; the connection stays open.
ProtocolViolation kind: ProtocolViolationKind, diagnostic: String A decoded message violated lifecycle, version, session-plan, signaling, or delivery-accountability invariants; configured policy decides quarantine, disconnect, or continued observation. Lifecycle/plan/signaling offenders are suppressed.

Disconnected

Field Type Description
reason Option<String> Human-readable close explanation with structured transport code/reason when available. Servers use semantic codes such as 4000 server_shutdown and 4002 slow_consumer; Server 0.7 also uses 4005 room_inactive.
last_server_error Option<ServerErrorInfo> The most recent Error/AuthenticationError received on this connection — a correlation aid for attributing the disconnect. A server that evicts a slow consumer writes a best-effort Error { error_code: SlowConsumer } farewell before closing; when that frame arrives, it shows up here. See the Delivery Contract.

After outbound send failure, ready server errors can still populate last_server_error; the exact bounded-drain and cause-precedence rules are in the Delivery Contract.

Delivery of the terminal Disconnected

During normal operation Disconnected is delivered with backpressure like every other event — it is not dropped merely because the consumer is briefly behind. When the connection ends while a shutdown() is pending (or the handle is dropped), the terminal Disconnected is delivered best-effort: the transport loop lets shutdown preempt a delivery blocked on channel capacity, and the shutdown path attempts the terminal event with non-blocking admission. If the channel is full, the event may be omitted. The configured shutdown deadline may invoke the transport's abort path and stop the task before it reaches that attempt. The event channel closing (recv() returns None) is the guaranteed end-of-stream signal — rely on it, not on always observing a final Disconnected.

DecodeFailed

Emitted when an inbound frame fails to deserialize — an unknown message type from a newer server, an unknown error_code string inside a known message, a proxy injecting non-protocol frames, or corruption. The connection stays open and later frames are unaffected; each occurrence also increments ClientStats::messages_undecodable.

Field Type Description
message_type Option<String> The wire type tag when the frame was valid JSON. Some("Error") plus a decode failure strongly implies an unknown error_code; None means the frame was not valid JSON at all.
error String The deserialization error text.
raw_prefix String The raw frame, truncated to DECODE_FAILED_RAW_PREFIX_MAX (512) bytes on a UTF-8 boundary.

Steady growth of messages_undecodable means protocol drift (upgrade this SDK) or a corrupting middlebox — log DecodeFailed in production builds.

ProtocolViolation is distinct from DecodeFailed: its frame decoded, but its sequence, epoch, lifecycle, gap, counter, or causal state contradicted the negotiated protocol. Lifecycle-, plan-, and signaling-invalid messages never mutate client state or reach the application, including under Observe. Delivery-accountability violations retain Observe's diagnostic delivery semantics. The default quarantine policy suppresses subsequent room game data until an authoritative snapshot resets the baseline. See the Delivery Contract.

Rust
match event {
    SignalFishEvent::Connected => {
        println!("Transport connected — waiting for authentication…");
    }
    SignalFishEvent::Disconnected { reason, last_server_error } => {
        println!(
            "Disconnected: {} (last server error: {last_server_error:?})",
            reason.as_deref().unwrap_or("unknown"),
        );
    }
    SignalFishEvent::DecodeFailed { message_type, error, .. } => {
        eprintln!("undecodable frame (type {message_type:?}): {error}");
    }
    _ => {}
}

Authentication Events

Received in response to the automatic Authenticate message sent when the client starts. You must wait for Authenticated before sending any other commands (e.g., joining a room).

Variant Key Fields Description
Authenticated app_name: String, organization: Option<String>, rate_limits: RateLimitInfo Authentication succeeded.
ProtocolInfo ProtocolInfoPayload (wrapped) SDK/protocol compatibility details advertised after authentication.
AuthenticationError error: String, error_code: ErrorCode Authentication failed.

Authenticated

Field Type Description
app_name String Application name confirmed by the server.
organization Option<String> Organization the app belongs to, if any.
rate_limits RateLimitInfo Rate limits enforced for this application (per_minute, per_hour, per_day).

ProtocolInfo(ProtocolInfoPayload)

The payload is wrapped as a single struct rather than flattened. Important fields include platform, sdk_version, capabilities, game_data_formats, player_name_rules, the negotiated protocol version/range, and available message transports.

AuthenticationError

Field Type Description
error String Human-readable error description.
error_code ErrorCode Structured error code for programmatic handling.
Rust
match event {
    SignalFishEvent::Authenticated { app_name, rate_limits, .. } => {
        println!("Authenticated as {app_name}");
        println!("Rate limits: {}/min", rate_limits.per_minute);
    }
    SignalFishEvent::ProtocolInfo(info) => {
        println!("Capabilities: {:?}", info.capabilities);
    }
    SignalFishEvent::AuthenticationError { error, error_code } => {
        eprintln!("Auth failed [{error_code}]: {error}");
    }
    _ => {}
}

Room Events

Events related to joining, failing to join, or leaving a room.

Variant Key Fields Description
RoomJoined room_id, room_code, player_id, current_players, … Successfully joined a room.
RoomJoinFailed reason: String, error_code: Option<ErrorCode> Failed to join a room.
RoomLeft Successfully left the current room.
RoomOperationFailed reason: String, error_code: Option<ErrorCode> A UUID-correlated room operation failed without an operation-specific terminal result. The exact pending fence is released.

RoomJoined

Field Type Description
room_id RoomId Unique room identifier.
room_code String Human-readable room code.
player_id PlayerId The local player's identifier.
game_name String Name of the game this room is for.
max_players u8 Maximum number of players allowed.
supports_authority bool Whether the room supports authority delegation.
current_players Vec<PlayerInfo> Players already present in the room.
is_authority bool Whether the local player is the authority.
lobby_state LobbyState Current lobby readiness state (Waiting, Lobby, or Finalized).
ready_players Vec<PlayerId> Players that have signaled readiness.
relay_type String Legacy deployment relay label; Server 0.7 uses it as protocol metadata, not proof of a physical path.
current_spectators Vec<SpectatorInfo> Spectators currently watching.
ice_servers Vec<IceServer> Protocol-v3 STUN/TURN servers for early candidate gathering; empty on the v2 floor.
reconnection_token Option<String> Server-issued v3 secret retained by ClientSnapshot for unexpected-disconnect recovery.

RoomJoinFailed

Field Type Description
reason String Human-readable failure reason.
error_code Option<ErrorCode> Structured error code, if provided.
Rust
match event {
    SignalFishEvent::RoomJoined { room_code, player_id, current_players, .. } => {
        println!("Joined room {room_code} as {player_id}");
        println!("{} player(s) already here", current_players.len());
    }
    SignalFishEvent::RoomJoinFailed { reason, error_code } => {
        eprintln!("Join failed: {reason} ({error_code:?})");
    }
    SignalFishEvent::RoomLeft => {
        println!("Left the room");
    }
    SignalFishEvent::RoomOperationFailed { reason, error_code } => {
        eprintln!("Room operation failed: {reason} ({error_code:?})");
    }
    _ => {}
}

Player Events

Notifications about other players joining or leaving the room you are in.

Variant Fields Description
PlayerJoined player: PlayerInfo Another player joined the room.
PlayerLeft player_id: PlayerId, epoch: Option<u32>, final_seq: Option<u64> Another player left; v3 fields identify the incarnation and terminal relay watermark.

PlayerInfo contains id, name, is_authority, is_ready, connected_at, optional connection_info, and optional protocol-v3 epoch and seq snapshot metadata.

Rust
match event {
    SignalFishEvent::PlayerJoined { player } => {
        println!("{} joined (id: {})", player.name, player.id);
    }
    SignalFishEvent::PlayerLeft { player_id, .. } => {
        println!("Player {player_id} left");
    }
    _ => {}
}

Game Data Events

Carry arbitrary payloads between players. JSON payloads arrive as GameData; binary-encoded payloads (MessagePack, Rkyv) arrive as GameDataBinary.

Variant Fields Description
GameData from_player, data, seq, epoch, class, key JSON game data plus optional v3 delivery stamp and classification.
GameDataBinary from_player, encoding, payload, seq, epoch Binary data from a strict physical envelope; v2 has no stamps, while v3 requires both.

GameDataEncoding is one of Json, MessagePack, or Rkyv.

Debug formatting for SignalFishEvent intentionally prints only the variant name. Events can contain reconnect credentials and arbitrary application data; pattern-match fields explicitly instead of logging the whole event.

Rust
match event {
    SignalFishEvent::GameData { from_player, data, .. } => {
        println!("JSON data from {from_player}: {data}");
    }
    SignalFishEvent::GameDataBinary { from_player, encoding, payload, .. } => {
        println!(
            "Binary data from {from_player}: {encoding:?}, {} bytes",
            payload.len()
        );
    }
    _ => {}
}

Authority Events

Authority delegation lets one player act as the game host. These events report changes and responses to authority requests.

Variant Key Fields Description
AuthorityChanged authority_player: Option<PlayerId>, you_are_authority: bool The room's authority assignment changed.
AuthorityResponse granted: bool, reason: Option<String>, error_code: Option<ErrorCode> Response to an authority request.
Rust
match event {
    SignalFishEvent::AuthorityChanged { authority_player, you_are_authority } => {
        if you_are_authority {
            println!("You are now the authority");
        } else if let Some(id) = authority_player {
            println!("Authority is now player {id}");
        }
    }
    SignalFishEvent::AuthorityResponse { granted, reason, .. } => {
        if granted {
            println!("Authority request granted");
        } else {
            println!("Authority denied: {}", reason.as_deref().unwrap_or("no reason"));
        }
    }
    _ => {}
}

Lobby Events

Lobby state tracks player readiness. Readiness alone does not finalize the lobby: after all_ready becomes true, an eligible client must call client.start_game(). In an authority-enabled room, only the current authority is eligible. A successful request produces GameStarting with peer connection details.

Variant Key Fields Description
LobbyStateChanged lobby_state: LobbyState, ready_players: Vec<PlayerId>, all_ready: bool The lobby readiness state changed.
GameStarting peer_connections: Vec<PeerConnectionInfo> The game is starting with peer connection info.

LobbyState is one of Waiting, Lobby, or Finalized.

PeerConnectionInfo contains player_id, player_name, is_authority, relay_type, and an optional connection_info.

Rust
match event {
    SignalFishEvent::LobbyStateChanged { lobby_state, ready_players, all_ready } => {
        println!("Lobby: {lobby_state:?}, {}/{} ready",
            ready_players.len(),
            ready_players.len() + if all_ready { 0 } else { 1 }
        );
    }
    SignalFishEvent::GameStarting { peer_connections } => {
        println!("Game starting with {} peers", peer_connections.len());
        for peer in &peer_connections {
            println!("  {} (authority={})", peer.player_name, peer.is_authority);
        }
    }
    _ => {}
}

Do not call start_game() on every repeated ready-state update. Keep a one-shot request latch, and in authority-enabled rooms re-evaluate eligibility when AuthorityChanged arrives. The compiling basic_lobby example shows that complete pattern.


Mesh Events (protocol v3)

v3-negotiated connections only

These four events arrive only when the connection has negotiated protocol v3 and the server emits the corresponding plan/signaling state. A v2 relay-floor connection never emits them. See the Mesh Guide for the full peer-to-peer flow and Protocol Versioning for how negotiation works.

These events carry the server's WebRTC mesh signaling. The server is authoritative: it chooses the topology and assigns the deterministic WebRTC offerer via the initiate / you_initiate flags, which you must obey verbatim (never compute who offers — that avoids WebRTC glare).

Variant Key Fields Description
SessionPlan generation, topology, transport, host, direct_endpoint, peers, ice_servers, fallback The server's per-recipient authoritative session plan.
NewPeer peer_id: PlayerId, you_initiate: bool A late-joining peer to connect to after the session was finalized.
SignalReceived from: PlayerId, generation, signal: serde_json::Value A generation-validated opaque WebRTC signal relayed from a peer.
PeerTransportStatus peer_id: PlayerId, transport: TransportKind, connected: bool A peer's data-path transport state changed (informational).

SessionPlan

May arrive multiple times (host re-election, late-join re-plan); each one fully replaces the previous plan — replace the peer set, never merge.

Field Type Description
generation Option<SessionGeneration> Handshake generation. Some on server 0.7; None only for legacy generation-less plans.
topology Topology Chosen session topology (Relay, Host, or Mesh).
transport TransportKind Chosen data-path transport (Relay, Direct, or WebRtc).
host Option<PlayerId> The elected host (present for Host topology).
direct_endpoint Option<DirectEndpoint> Validated endpoint for Host + Direct; opening the socket remains the application's responsibility.
peers Vec<SessionPeer> Peers this client should connect to, each with its server-assigned initiate flag.
ice_servers Vec<IceServer> ICE (STUN/TURN) servers for WebRTC.
fallback TransportKind The universal fallback transport (always Relay).

SignalReceived

Convert the opaque signal value with PeerSignal::try_from(&signal) for the common Offer / Answer / IceCandidate shapes; the raw Value is preserved for any other shape.

Rust
use std::collections::HashSet;
use signal_fish_client::{PeerSignal, PlayerId, TransportKind};

let mut current_generation = None;
let mut webrtc_plan_active = false;
let mut connected_peers = HashSet::<PlayerId>::new();
match event {
    SignalFishEvent::SessionPlan {
        generation, topology, transport, peers, ice_servers, ..
    } => {
        // Every plan replaces the previous physical graph, including Direct
        // and Relay reset plans.
        for peer in connected_peers.drain() {
            my_driver.disconnect(peer);
        }
        webrtc_plan_active = transport == TransportKind::WebRtc;
        current_generation = webrtc_plan_active.then_some(generation).flatten();
        println!("Session plan: {topology:?} with {} peer(s)", peers.len());
        if webrtc_plan_active {
            my_driver.set_ice_servers(&ice_servers);
            for peer in &peers {
                connected_peers.insert(peer.player_id);
                my_driver.connect(peer.player_id, generation, peer.initiate);
            }
        }
    }
    SignalFishEvent::NewPeer { peer_id, you_initiate } if webrtc_plan_active => {
        connected_peers.insert(peer_id);
        my_driver.connect(peer_id, current_generation, you_initiate);
    }
    SignalFishEvent::SignalReceived { from, generation, signal } => {
        if webrtc_plan_active
            && generation == current_generation
            && connected_peers.contains(&from)
        {
            if let Ok(peer_signal) = PeerSignal::try_from(&signal) {
                my_driver.on_signal(from, generation, peer_signal);
            }
        }
    }
    SignalFishEvent::PeerTransportStatus { peer_id, connected, .. } => {
        println!("Peer {peer_id} transport connected={connected}");
    }
    _ => {}
}

Let the SDK do the choreography

With the mesh feature, MeshController handles all four of these events for you — calling your WebRtcDriver, relaying signals, and reporting transport status — so you rarely match them by hand. See the Mesh Guide.


Delivery and Drain Events (protocol v3)

Variant Fields Description
DeliveryReport(payload) DeliveryReportPayload Cumulative per-class outcomes and exact omitted sequence ranges. Exact ranges are the only authorization for continuing-connection gaps.
RelayStats interval_ms, sent_to_you, dropped_for_you, backpressure_events Optional cumulative connection diagnostics; never gap authorization.
GoingAway deadline_ms, retry_after_secs Best-effort graceful-drain advisory. The subsequent structured transport close remains authoritative.

Applications normally let the SDK consume these for accountability and also record the typed events for telemetry. GoingAway is a cue to preserve the current reconnect snapshot and prepare a retry after the advertised delay.


Heartbeat Events

Call client.ping() to send a heartbeat message that keeps the connection alive. The server replies with a Pong event confirming receipt.

Variant Fields Description
Pong Connection-scoped response to a ping; valid while authentication or protocol negotiation is still in flight.
Rust
match event {
    SignalFishEvent::Pong => {
        // Connection is healthy — usually no action needed.
    }
    _ => {}
}

Reconnection Events

If a player's connection drops, the SDK can attempt to rejoin the same room. On success the server replays any events that were missed.

Variant Key Fields Description
Reconnected room_id, room_code, player_id, missed_events, … Reconnection succeeded; state is restored.
ReconnectionFailed reason: String, error_code: ErrorCode Reconnection failed.
PlayerReconnected player_id: PlayerId, epoch: Option<u32> Another player reconnected; v3 carries the new incarnation epoch.

Reconnected

Carries the same room-state fields as RoomJoined plus:

Field Type Description
missed_events Vec<SignalFishEvent> Events that occurred while the client was disconnected.
replay Option<ReplayStatus> Whether the replayed control-event suffix is complete or truncated.
sender_watermarks Vec<SenderWatermark> Authoritative per-sender epoch/sequence baselines for resumed delivery.
ice_servers Vec<IceServer> Protocol-v3 STUN/TURN servers for early candidate gathering.
reconnection_token Option<String> Fresh secret replacing the consumed token; also stored in ClientSnapshot.

ReconnectionFailed

Field Type Description
reason String Human-readable failure reason.
error_code ErrorCode Structured error code.
Rust
match event {
    SignalFishEvent::Reconnected { room_code, missed_events, .. } => {
        println!("Reconnected to {room_code}");
        println!("Replaying {} missed events", missed_events.len());
        for missed in &missed_events {
            println!("  missed: {missed:?}");
        }
    }
    SignalFishEvent::ReconnectionFailed { reason, error_code } => {
        eprintln!("Reconnection failed [{error_code}]: {reason}");
    }
    SignalFishEvent::PlayerReconnected { player_id, .. } => {
        println!("Player {player_id} reconnected");
    }
    _ => {}
}

Spectator Events

Spectators can watch a room without participating. These events cover the full spectator lifecycle.

Variant Key Fields Description
SpectatorJoined room_id, spectator_id, current_players, current_spectators, … Successfully joined a room as a spectator.
SpectatorJoinFailed reason: String, error_code: Option<ErrorCode> Failed to join as a spectator.
SpectatorLeft room_id: Option<RoomId>, room_code: Option<String>, reason, current_spectators Successfully left spectator mode.
NewSpectatorJoined spectator: SpectatorInfo, current_spectators, reason Another spectator joined the room.
SpectatorDisconnected spectator_id: PlayerId, reason, current_spectators Another spectator disconnected.

SpectatorJoined

Field Type Description
room_id RoomId Unique room identifier.
room_code String Human-readable room code.
spectator_id PlayerId The local spectator's identifier.
game_name String Name of the game this room is for.
current_players Vec<PlayerInfo> Players currently in the room.
current_spectators Vec<SpectatorInfo> Spectators currently watching.
lobby_state LobbyState Current lobby readiness state.
reason Option<SpectatorStateChangeReason> Reason the spectator state changed, if applicable.

SpectatorLeft

Field Type Description
room_id Option<RoomId> Room identifier, if available.
room_code Option<String> Room code, if available.
reason Option<SpectatorStateChangeReason> Reason for leaving, if available.
current_spectators Vec<SpectatorInfo> Remaining spectators in the room.

SpectatorStateChangeReason is one of Joined, VoluntaryLeave, Disconnected, Removed, or RoomClosed.

Rust
match event {
    SignalFishEvent::SpectatorJoined { room_code, spectator_id, current_players, .. } => {
        println!("Spectating room {room_code} as {spectator_id}");
        println!("{} player(s) in game", current_players.len());
    }
    SignalFishEvent::SpectatorJoinFailed { reason, error_code } => {
        eprintln!("Spectator join failed: {reason} ({error_code:?})");
    }
    SignalFishEvent::SpectatorLeft { reason, .. } => {
        println!("Left spectator mode: {reason:?}");
    }
    SignalFishEvent::NewSpectatorJoined { spectator, current_spectators, .. } => {
        println!("{} started spectating ({} total)",
            spectator.name, current_spectators.len());
    }
    SignalFishEvent::SpectatorDisconnected { spectator_id, current_spectators, .. } => {
        println!("Spectator {spectator_id} left ({} remaining)",
            current_spectators.len());
    }
    _ => {}
}

Error Events

Catch-all for server-side errors that don't fit a more specific variant.

Variant Fields Description
Error message: String, error_code: Option<ErrorCode> A generic server error.
Rust
match event {
    SignalFishEvent::Error { message, error_code } => {
        eprintln!("Server error: {message} ({error_code:?})");
    }
    _ => {}
}

Complete Event Loop Pattern

A realistic event loop handling the most important variants. Use this as a starting template and expand as needed for your game.

Rust
use signal_fish_client::{
    JoinRoomParams, SignalFishClient, SignalFishConfig,
    SignalFishEvent, WebSocketTransport,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = std::env::var("SIGNAL_FISH_URL")
        .unwrap_or_else(|_| "ws://localhost:3536/v2/ws".to_string());

    let transport = WebSocketTransport::connect(&url).await?;
    let config = SignalFishConfig::new("your-app-id");
    let (mut client, mut event_rx) = SignalFishClient::start(transport, config);
    let mut start_request_sent = false;

    while let Some(event) = event_rx.recv().await {
        match event {
            // ── Connection ──────────────────────────────────────
            SignalFishEvent::Connected => {
                println!("Connected — authenticating…");
            }

            // ── Authentication ──────────────────────────────────
            SignalFishEvent::Authenticated { app_name, .. } => {
                println!("Authenticated as {app_name}");
                client.join_room(JoinRoomParams::new("my-game", "Alice"))?;
            }
            SignalFishEvent::AuthenticationError { error, error_code } => {
                eprintln!("Auth failed [{error_code}]: {error}");
                break;
            }

            // ── Room lifecycle ──────────────────────────────────
            SignalFishEvent::RoomJoined { room_code, current_players, .. } => {
                println!("Joined room {room_code}");
                println!("{} player(s) in room", current_players.len());
                client.set_ready()?;
            }

            // ── Player presence ─────────────────────────────────
            SignalFishEvent::PlayerJoined { player } => {
                println!("{} joined the room", player.name);
            }
            SignalFishEvent::PlayerLeft { player_id, .. } => {
                println!("Player {player_id} left");
            }

            // ── Game data ───────────────────────────────────────
            SignalFishEvent::GameData { from_player, data, .. } => {
                println!("Data from {from_player}: {data}");
            }

            // ── Lobby ───────────────────────────────────────────
            SignalFishEvent::LobbyStateChanged { all_ready, .. } => {
                if all_ready && !start_request_sent {
                    println!("All players ready!");
                    // This example creates a non-authority room. Authority-enabled
                    // rooms must additionally require that this client is authority.
                    client.start_game()?;
                    start_request_sent = true;
                }
            }
            SignalFishEvent::GameStarting { peer_connections } => {
                println!("Game starting with {} peers!", peer_connections.len());
            }

            // ── Errors ──────────────────────────────────────────
            SignalFishEvent::Error { message, error_code } => {
                eprintln!("Server error: {message} ({error_code:?})");
            }

            // ── Disconnection ───────────────────────────────────
            SignalFishEvent::Disconnected { reason, .. } => {
                println!("Disconnected: {}",
                    reason.as_deref().unwrap_or("unknown"));
                break;
            }

            // ── Everything else ─────────────────────────────────
            _ => {}
        }
    }

    client.shutdown().await;
    Ok(())
}

Tip

All public enums in this crate are exhaustive. This introductory loop uses _ => {} to focus on common events. Production code can enumerate every variant and omit the catch-all when it wants compiler errors to surface new variants during a breaking-version upgrade.