use std::{collections::VecDeque, rc::Rc}; use letterbox_shared::WebsocketMessage; use log::{error, info}; use seed::prelude::*; use serde::{Deserialize, Serialize}; #[cfg(not(target_arch = "wasm32"))] #[allow(dead_code)] mod wasm_sockets { use std::{cell::RefCell, rc::Rc}; use thiserror::Error; use web_sys::{CloseEvent, ErrorEvent}; #[derive(Debug)] pub struct JsValue; #[derive(Debug)] pub enum ConnectionStatus { /// Connecting to a server Connecting, /// Connected to a server Connected, /// Disconnected from a server due to an error Error, /// Disconnected from a server without an error Disconnected, } #[derive(Debug)] pub struct EventClient { pub status: Rc>, } impl EventClient { pub fn new(_: &str) -> Result { todo!("this is a mock") } pub fn send_string(&self, _essage: &str) -> Result<(), JsValue> { todo!("this is a mock") } pub fn set_on_error(&mut self, _: Option>) { todo!("this is a mock") } pub fn set_on_connection(&mut self, _: Option>) { todo!("this is a mock") } pub fn set_on_close(&mut self, _: Option>) { todo!("this is a mock") } pub fn set_on_message(&mut self, _: Option>) { todo!("this is a mock") } } #[derive(Debug, Clone)] pub enum Message { Text(String), Binary(Vec), } #[derive(Debug, Clone, Error)] pub enum WebSocketError {} } #[cfg(not(target_arch = "wasm32"))] use wasm_sockets::{ConnectionStatus, EventClient, Message, WebSocketError}; #[cfg(target_arch = "wasm32")] use wasm_sockets::{ConnectionStatus, EventClient, Message, WebSocketError}; use web_sys::CloseEvent; /// Message from the server to the client. #[derive(Serialize, Deserialize)] pub struct ServerMessage { pub id: usize, pub text: String, } /// Message from the client to the server. #[derive(Serialize, Deserialize)] pub struct ClientMessage { pub text: String, } //const WS_URL: &str = "wss://9000.z.xinu.tv/api/ws"; //const WS_URL: &str = "wss://9345.z.xinu.tv/api/graphql/ws"; //const WS_URL: &str = "wss://6758.z.xinu.tv/api/ws"; // ------ ------ // Model // ------ ------ pub struct Model { ws_url: String, web_socket: EventClient, web_socket_reconnector: Option, pub updates: VecDeque, } // ------ ------ // Init // ------ ------ pub fn init(ws_url: &str, orders: &mut impl Orders) -> Model { Model { ws_url: ws_url.to_string(), web_socket: create_websocket(ws_url, orders).unwrap(), web_socket_reconnector: None, updates: VecDeque::new(), } } // ------ ------ // Update // ------ ------ pub enum Msg { WebSocketOpened, TextMessageReceived(WebsocketMessage), WebSocketClosed(CloseEvent), WebSocketFailed, ReconnectWebSocket(usize), #[allow(dead_code)] SendMessage(ClientMessage), } pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { match msg { Msg::WebSocketOpened => { model.web_socket_reconnector = None; info!("WebSocket connection is open now"); } Msg::TextMessageReceived(msg) => { model.updates.push_back(msg); } Msg::WebSocketClosed(close_event) => { info!( r#"================== WebSocket connection was closed: Clean: {0} Code: {1} Reason: {2} =================="#, close_event.was_clean(), close_event.code(), close_event.reason() ); // Chrome doesn't invoke `on_error` when the connection is lost. if !close_event.was_clean() && model.web_socket_reconnector.is_none() { model.web_socket_reconnector = Some( orders.stream_with_handle(streams::backoff(None, Msg::ReconnectWebSocket)), ); } } Msg::WebSocketFailed => { info!("WebSocket failed"); if model.web_socket_reconnector.is_none() { model.web_socket_reconnector = Some( orders.stream_with_handle(streams::backoff(None, Msg::ReconnectWebSocket)), ); } } Msg::ReconnectWebSocket(retries) => { info!("Reconnect attempt: {}", retries); model.web_socket = create_websocket(&model.ws_url, orders).unwrap(); } Msg::SendMessage(msg) => { let txt = serde_json::to_string(&msg).unwrap(); model.web_socket.send_string(&txt).unwrap(); } } } fn create_websocket(url: &str, orders: &impl Orders) -> Result { let msg_sender = orders.msg_sender(); let mut client = EventClient::new(url)?; client.set_on_error(Some(Box::new(|error| { gloo_console::error!("WS: ", error); }))); let send = msg_sender.clone(); client.set_on_connection(Some(Box::new(move |client: &EventClient| { info!("{:#?}", client.status); let msg = match *client.status.borrow() { ConnectionStatus::Connecting => { info!("Connecting..."); None } ConnectionStatus::Connected => Some(Msg::WebSocketOpened), ConnectionStatus::Error => Some(Msg::WebSocketFailed), ConnectionStatus::Disconnected => { info!("Disconnected"); None } }; send(msg); }))); let send = msg_sender.clone(); client.set_on_close(Some(Box::new(move |ev| { info!("WS: Connection closed"); send(Some(Msg::WebSocketClosed(ev))); }))); let send = msg_sender.clone(); client.set_on_message(Some(Box::new(move |_: &EventClient, msg: Message| { decode_message(msg, Rc::clone(&send)) }))); Ok(client) } fn decode_message(message: Message, msg_sender: Rc)>) { match message { Message::Text(txt) => { let msg: WebsocketMessage = serde_json::from_str(&txt).unwrap_or_else(|e| { panic!("failed to parse json into WebsocketMessage: {e}\n'{txt}'") }); msg_sender(Some(Msg::TextMessageReceived(msg))); } m => error!("unexpected message type received of {m:?}"), } }