Reconnect the game websocket after connectivity loss

The socket had no recovery path: a mid-game drop left the table showing
stale state with no indication, and plays were silently swallowed by the
dead channel. Surface the server close code from ws::connect and add a
reconnect driver in the game page: transient losses retry with
exponential backoff (capped, then a manual Retry), while deliberate
closes (session expired, game gone) stop retrying. Reconnects are free
resyncs because the server pushes a full state snapshot on connect.

Also gate card clicks while disconnected, show a connection banner, and
drop the ticking interval and pending retries on unmount.
This commit is contained in:
2026-09-18 19:16:39 +08:00
parent 8fea4fac74
commit 61f3539c4e
3 changed files with 248 additions and 29 deletions
+12 -3
View File
@@ -4,7 +4,7 @@ use std::rc::Rc;
use futures::channel::mpsc;
use futures::{SinkExt, StreamExt};
use gloo_net::websocket::{futures::WebSocket, Message};
use gloo_net::websocket::{futures::WebSocket, Message, WebSocketError};
use wasm_bindgen_futures::spawn_local;
use crate::model::ServerMessage;
@@ -53,10 +53,14 @@ impl GameSocket {
/// Open the websocket for `game_id` and forward parsed server messages to
/// `on_message`. Returns the socket handle, or `None` if the connection
/// could not be created.
///
/// `on_close` fires exactly once when the connection ends; it receives the
/// server close code when one was sent (e.g. 4401 unauthenticated, 4403 not
/// seated, 4404 unknown game) or `None` for an abnormal network loss.
pub fn connect(
game_id: &str,
on_message: impl Fn(ServerMessage) + 'static,
on_close: impl Fn() + 'static,
on_close: impl Fn(Option<u16>) + 'static,
) -> Option<GameSocket> {
let ws = WebSocket::open(&ws_url(game_id)).ok()?;
let (mut write, mut read) = ws.split();
@@ -72,6 +76,7 @@ pub fn connect(
});
spawn_local(async move {
let mut close_code = None;
while let Some(msg) = read.next().await {
match msg {
Ok(Message::Text(text)) => {
@@ -80,10 +85,14 @@ pub fn connect(
}
}
Ok(Message::Bytes(_)) => {}
Err(WebSocketError::ConnectionClose(e)) => {
close_code = Some(e.code);
break;
}
Err(_) => break,
}
}
on_close();
on_close(close_code);
});
Some(GameSocket {