//! WebSocket client for live play. use std::cell::RefCell; use std::rc::Rc; use futures::channel::mpsc; use futures::{SinkExt, StreamExt}; use gloo_net::websocket::{futures::WebSocket, Message, WebSocketError}; use wasm_bindgen_futures::spawn_local; use crate::model::ServerMessage; fn ws_url(game_id: &str) -> String { let location = web_sys::window().unwrap().location(); let protocol = location.protocol().unwrap_or_else(|_| "http:".into()); let scheme = if protocol == "https:" { "wss" } else { "ws" }; let host = location.host().unwrap_or_else(|_| "127.0.0.1:8080".into()); format!("{scheme}://{host}/ws/games/{game_id}") } /// A cloneable handle to the game websocket. Incoming server messages are /// pushed into the receiver returned by [`connect`]. #[derive(Clone)] pub struct GameSocket { sender: Rc>>, } impl GameSocket { /// Play a card, optionally capturing the given table cards. pub fn play(&self, card: &str, capture: Option>) { let mut msg = serde_json::json!({ "action": "play", "card": card }); if let Some(capture) = capture { msg["capture"] = serde_json::json!(capture); } self.send_json(msg); } /// Ask the server for a fresh snapshot. #[allow(dead_code)] pub fn sync(&self) { self.send_json(serde_json::json!({ "action": "state" })); } /// Acknowledge the hand-end scoring summary. pub fn ack(&self) { self.send_json(serde_json::json!({ "action": "ack" })); } fn send_json(&self, value: serde_json::Value) { let _ = self.sender.borrow_mut().unbounded_send(value.to_string()); } } /// 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(Option) + 'static, ) -> Option { let ws = WebSocket::open(&ws_url(game_id)).ok()?; let (mut write, mut read) = ws.split(); // Outgoing channel: GameSocket::send_json queues text frames here. let (out_tx, mut out_rx) = mpsc::unbounded::(); spawn_local(async move { while let Some(text) = out_rx.next().await { if write.send(Message::Text(text)).await.is_err() { break; } } }); spawn_local(async move { let mut close_code = None; while let Some(msg) = read.next().await { match msg { Ok(Message::Text(text)) => { if let Ok(parsed) = serde_json::from_str::(&text) { on_message(parsed); } } Ok(Message::Bytes(_)) => {} Err(WebSocketError::ConnectionClose(e)) => { close_code = Some(e.code); break; } Err(_) => break, } } on_close(close_code); }); Some(GameSocket { sender: Rc::new(RefCell::new(out_tx)), }) }