Skip to main content

signal_fish_client/
lib.rs

1//! # Signal Fish Client
2//!
3//! Transport-agnostic Rust client for the Signal Fish multiplayer signaling protocol.
4//!
5//! This crate provides a high-level async client that communicates with a Signal Fish
6//! signaling server using JSON text messages over any bidirectional transport.
7//!
8//! ## Features
9//!
10//! - **Transport-agnostic** — implement the [`Transport`] trait for any backend
11//! - **Wire-compatible** — all protocol types match the server's v2 format exactly
12//! - **Protocol v2 relay + v3 mesh** — v3 is additive and opt-in; a default client
13//!   stays byte-identical to v2 (see [Protocol versions](#protocol-versions))
14//! - **WebSocket built-in** — default `transport-websocket` feature provides `WebSocketTransport`
15//! - **Event-driven** — receive typed `SignalFishEvent`s via a channel
16//! - **No silent loss** — events are delivered with backpressure and sends are
17//!   bounded with explicit congestion signals (see
18//!   [Delivery guarantees](client#delivery-guarantees))
19//!
20//! ## Choosing a client
21//!
22//! The crate ships two clients with identical protocol behavior; pick by how
23//! your application is driven:
24//!
25//! - [`SignalFishClient`] (async) — spawns a background transport loop with
26//!   [`tokio::spawn`]. Use it when a tokio runtime is *running* (a
27//!   `#[tokio::main]`/`block_on` application, multi-thread or
28//!   `current_thread`). It only makes progress while the runtime is driven —
29//!   manually "ticking" a runtime once per frame starves it (see
30//!   [the driving contract](client#driving-the-client-runtime-contract)).
31//! - [`SignalFishPollingClient`] (sync, feature `polling-client`) — no
32//!   background task, no runtime. You
33//!   call [`poll()`](polling_client::SignalFishPollingClient::poll) once per
34//!   frame from a game loop. This is the right client for frame-driven
35//!   engines (Godot, Bevy without tokio, Unity via FFI) and `wasm32` targets.
36//!
37//! ## Protocol versions
38//!
39//! The SDK speaks two protocol generations, and you choose which by how you
40//! build [`SignalFishConfig`]:
41//!
42//! - **v2 — the relay floor (default).** [`SignalFishConfig::new`] advertises no
43//!   v3 capabilities, the server relays all traffic through itself, and the
44//!   `Authenticate` bytes are byte-identical to the old v2 client. This is the
45//!   *relay-floor guarantee*: opt into nothing and nothing changes.
46//! - **v3 — additive mesh (opt-in).** [`SignalFishConfig::enable_mesh`] advertises
47//!   the WebRTC/relay transports and mesh/host/relay topologies, letting the
48//!   server form a peer-to-peer session. v3 capabilities are additive to the
49//!   v2 relay floor, and the server falls back to relay whenever it cannot form
50//!   a session. On current servers, an eligible client explicitly calls
51//!   [`SignalFishClient::start_game`] after readiness instead of relying on
52//!   automatic start.
53//!
54//! The negotiated version comes back in the server's `ProtocolInfo`; check it via
55//! [`SignalFishClient::negotiated_protocol_version`] /
56//! [`SignalFishClient::supports_mesh`]. v3-only sends fail fast with
57//! [`SignalFishError::ProtocolUnsupported`] until v3 is negotiated. The SDK is
58//! *signaling-only* — it bundles no WebRTC stack; with the `mesh` feature you
59//! implement the [`webrtc::WebRtcDriver`] seam (or use
60//! [`webrtc::MeshController`]) against str0m / webrtc-rs / web-sys. The highest
61//! version this SDK speaks is [`PROTOCOL_VERSION`].
62//!
63//! ## Quick Start
64//!
65//! ```rust,ignore
66//! use signal_fish_client::{
67//!     WebSocketTransport, SignalFishClient, SignalFishConfig,
68//!     JoinRoomParams, SignalFishEvent,
69//! };
70//!
71//! #[tokio::main]
72//! async fn main() -> Result<(), signal_fish_client::SignalFishError> {
73//!     // 1. Connect a WebSocket transport to the signaling server.
74//!     let transport = WebSocketTransport::connect("ws://localhost:3536/ws").await?;
75//!
76//!     // 2. Build a client config with your application ID.
77//!     let config = SignalFishConfig::new("mb_app_abc123");
78//!
79//!     // 3. Start the client — returns a handle and an event receiver.
80//!     //    The client automatically sends Authenticate on start.
81//!     let (mut client, mut event_rx) = SignalFishClient::start(transport, config);
82//!
83//!     // 4. Process events — wait for Authenticated before joining a room.
84//!     while let Some(event) = event_rx.recv().await {
85//!         match event {
86//!             SignalFishEvent::Authenticated { app_name, .. } => {
87//!                 println!("Authenticated as {app_name}");
88//!                 // Now it's safe to join a room.
89//!                 client.join_room(JoinRoomParams::new("my-game", "Alice"))?;
90//!             }
91//!             SignalFishEvent::RoomJoined { room_code, .. } => {
92//!                 println!("Joined room {room_code}");
93//!                 client.set_ready()?;
94//!             }
95//!             // Protocol v2: the game starts explicitly, not on readiness.
96//!             SignalFishEvent::LobbyStateChanged { all_ready: true, .. } => {
97//!                 client.start_game()?;
98//!             }
99//!             SignalFishEvent::Disconnected { .. } => break,
100//!             _ => {}
101//!         }
102//!     }
103//!
104//!     // 5. Shut down gracefully.
105//!     client.shutdown().await;
106//!     Ok(())
107//! }
108//! ```
109
110#[cfg(any(feature = "tokio-runtime", feature = "polling-client"))]
111mod accountability;
112pub mod client;
113pub mod client_api;
114#[cfg(any(feature = "tokio-runtime", feature = "polling-client"))]
115mod client_core;
116pub mod error;
117pub mod error_codes;
118pub mod event;
119pub mod protocol;
120pub mod signal;
121pub mod transport;
122pub mod transports;
123
124/// Highest signaling protocol version this SDK speaks.
125///
126/// Advertised in `Authenticate` when a consumer opts into the mesh via
127/// [`SignalFishConfig::enable_mesh`](crate::SignalFishConfig::enable_mesh).
128pub const PROTOCOL_VERSION: u16 = 3;
129
130// Re-export primary types for ergonomic imports.
131pub use client::{
132    ClientSnapshot, ClientStats, GameDataDelivery, JoinRoomParams, ProtocolViolationPolicy,
133    SignalFishClient, SignalFishConfig,
134};
135pub use client_api::SignalFishClientApi;
136pub use error::SignalFishError;
137pub use error_codes::ErrorCode;
138pub use event::{
139    ProtocolViolationKind, ServerErrorInfo, SignalFishEvent, DECODE_FAILED_RAW_PREFIX_MAX,
140};
141pub use protocol::{
142    decode_v3_binary_game_data, ClientMessage, DeliveryClass, DeliveryCountersByClass, DeliveryGap,
143    DeliveryGapReason, DeliveryReportPayload, IceServer, LatestDeliveryCounters, MessageTransport,
144    ReliableDeliveryCounters, ReplayStatus, SenderWatermark, ServerMessage, SessionPeer,
145    SessionPlanPayload, Topology, TransportKind, V3BinaryGameDataFrame, VolatileDeliveryCounters,
146};
147pub use signal::PeerSignal;
148pub use transport::{Transport, TransportCloseInfo, TransportDiagnostics, TransportFrame};
149
150#[cfg(feature = "transport-websocket")]
151pub use transports::{WebSocketConnectOptions, WebSocketTransport};
152
153#[cfg(feature = "polling-client")]
154pub mod polling_client;
155
156#[cfg(feature = "polling-client")]
157pub use polling_client::{
158    PollingClientOptions, PollingClosePolicy, PollingQueueAgeStats, PollingStats,
159    PollingWorkBudget, SignalFishPollingClient,
160};
161
162#[cfg(feature = "mesh")]
163pub mod mesh;
164
165#[cfg(feature = "mesh")]
166pub use mesh::{MeshPeer, MeshSession};
167
168#[cfg(feature = "mesh")]
169pub mod webrtc;
170
171#[cfg(feature = "mesh")]
172pub use webrtc::{DriverEvent, MeshEvent, WebRtcDriver};
173
174#[cfg(all(feature = "mesh", feature = "tokio-runtime"))]
175pub use webrtc::MeshController;
176
177// Re-export only on the correct target (see transports/mod.rs for rationale).
178#[cfg(all(feature = "transport-websocket-emscripten", target_os = "emscripten"))]
179#[allow(deprecated)]
180pub use transports::EmscriptenWebSocketTransport;