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 38 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.
Reconnecting attempt: u32, next_backoff: Duration A configured ReconnectPolicy scheduled a fresh connection after a retryable disconnect.
ReconnectAbandoned attempts: u32, last_reason: Option<String> The configured reconnect policy exhausted its attempts; the event stream ends after this event.
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.8 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.

Reconnecting / ReconnectAbandoned

Emitted only when a ReconnectPolicy is configured on the async client. Reconnecting { attempt, next_backoff } precedes each fresh-transport attempt (1-based attempt, deterministic exponential next_backoff); a successful attempt continues with a fresh Connected → Authenticated sequence and, when the client was a player in a room when the previous connection ended, an automatic directed reconnect. ReconnectAbandoned { attempts, last_reason } is the terminal event when the budget runs out; shutdown, a dropped handle, a ProtocolViolationPolicy::Disconnect teardown, a peer close whose code the policy classifies terminal (with_terminal_close_codes), and a farewell the consumer never drained end the client without attempting reconnection and never produce these events.

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 and the failure position allowed tag recovery (frames larger than the 512-byte raw_prefix window report None even when valid JSON). Some("Error") plus a decode failure strongly implies an unknown error_code; None usually means the frame was not valid JSON at all.
error String The deserialization error text, capped at 512 bytes (serde quotes hostile wire tokens verbatim, so the text is bounded).
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.

Handle raw_prefix as untrusted text

raw_prefix is verbatim frame content: a hostile server can plant credential-looking strings in it for exactly the application that logs decode failures verbatim. Debug formatting redacts it entirely; when you need the frame's shape for diagnostics, use redacted_raw_prefix(), which masks every string literal's content while preserving the JSON skeleton. Never log raw_prefix verbatim.

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. u8 ceilings the model at 255: a server advertising a larger capacity fails the frame as DecodeFailed rather than truncating the value.
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.8 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.

Scripted-peer contract for the join snapshot

The client validates the authoritative snapshot before surfacing it. A mock server driving this event must satisfy all of the following, or the frame is quarantined (or rejected under Disconnect policy):

  • the local player_id appears in current_players exactly once;
  • on a negotiated v3 connection, every PlayerInfo carries paired epoch and seq values (both present or both absent — a mixed pair is a violation, despite the fields being optional in isolation);
  • on a v2 connection, no PlayerInfo may carry epoch/seq at all (a "v2 snapshot exposed delivery baseline" violation).

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. connected_at reads back as an empty string on protocol-v3 snapshots, which trim the server-internal join timestamp for privacy (signal-fish-server #539).

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:?}, {} player(s) ready{}", ready_players.len(),
            if all_ready { " (all ready)" } else { "" });
    }
    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. Counters are cumulative since this connection registered with the relay (they survive reconnection); the SDK validates the authority's invariants (positive constant interval, monotonic counters) and reports a violating frame as ProtocolViolation { Counters }. A rising backpressure_events predicts a 4002 slow_consumer eviction.
GoingAway deadline_ms, retry_after_secs Best-effort graceful-drain advisory. deadline_ms is an absolute Unix epoch timestamp in milliseconds; retry_after_secs is an operator hint in seconds. 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> Completeness of the replayed control-event suffix: Complete (nothing was lost), Truncated (some history was dropped — reconcile from sender_watermarks and your own game state), or Unavailable (the server could not replay — do not rely on missed_events). None means the connected server predates replay reporting (pre-v3 negotiation).
sender_watermarks Vec<SenderWatermark> Authoritative per-sender epoch/sequence baselines for resumed delivery. The SDK validates them against its accountability state; they also let you judge which remote inputs your simulation already applied before the gap.
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 The spectator left the room: either the answer to this client's admitted voluntary leave, or an authoritative exit (server removal, spectator disconnect, or room close).
NewSpectatorJoined spectator: SpectatorInfo, current_spectators, reason, spectator_count: Option<u32> Another spectator joined the room. spectator_count carries the room total on servers with the v3 spectator fan-out slimming tier (where current_spectators is an empty delta) and is None on the full-roster face.
SpectatorDisconnected spectator_id: PlayerId, reason, current_spectators, spectator_count: Option<u32> Another spectator disconnected. spectator_count carries the room total on servers with the v3 spectator fan-out slimming tier and is None on the full-roster face.

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.