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;