rust_wasm_websocket 0.5.5

crate for wasm websocket
Documentation
// websocket_mod
//! module that cares about WebSocket communication

#![allow(clippy::panic)]

// region: use
use rust_wasm_websys_utils::*;

use dodrio::{RootRender, VdomWeak};
use gloo_timers::future::TimeoutFuture;
use js_sys::Reflect;
use serde_derive::{Deserialize, Serialize};
use unwrap::unwrap;
use wasm_bindgen::{prelude::*, JsCast};
use wasm_bindgen_futures::spawn_local;
use web_sys::{ErrorEvent, WebSocket};
// endregion

/// WsMessageToServer enum for WebSocket
/// The ws server will perform an action according to this type.
#[derive(Serialize, Deserialize, Clone)]
pub enum WsMessageToServer {
    /// Request WebSocket Uid - first message to WebSocket server
    MsgRequestWsUid {
        /// ws client instance unique id. To not listen the echo to yourself.
        msg_sender_ws_uid: usize,
    },
    /// MsgPing
    MsgPing {
        /// random msg_id
        msg_id: u32,
    },
}

/// WsMessageFromServer enum for WebSocket
/// The ws server will send this kind of msgs.
#[derive(Serialize, Deserialize, Clone)]
pub enum WsMessageFromServer {
    /// response from WebSocket server for first message
    MsgResponseWsUid {
        /// WebSocket Uid
        msg_receiver_ws_uid: usize,
        /// server version
        server_version: String,
    },
    /// MsgPong
    MsgPong {
        /// random msg_id
        msg_id: u32,
    },
}

pub trait WebSocketTrait {
    // region: getter setter
    fn get_ws_clone(&self) -> WebSocket;
    fn set_ws(&mut self, ws: WebSocket);
    // endregion getter setter
    fn on_response_ws_uid(root: &mut dyn RootRender, msg_receiver_ws_uid: usize);
    fn on_msg_recv_for_ws_message_for_receivers(vdom: VdomWeak, data: String);
    fn update_on_error(root: &mut dyn RootRender, err_text: String);
    fn update_on_close(root: &mut dyn RootRender);
    // the location_href is not consumed in this function and Clippy wants a reference instead a value
    // but I don't want references, because they have the lifetime problem.
    #[allow(clippy::needless_pass_by_value)]
    /// setup WebSocket connection
    fn setup_ws_connection(
        &mut self,
        location_href: String,
        client_ws_id: usize,
        subdirectory_for_ws: &str,
    ) -> WebSocket {
        // web-sys has WebSocket for Rust exactly like JavaScript has¸
        // location_href comes in this format  http:// localhost:4000/
        let mut loc_href = location_href
            .replace("http://", "ws://")
            .replace("https://", "wss://");
        // Only for debugging in the development environment
        // let mut loc_href = String::from("ws://192.168.1.57:80/");
        websysmod::debug_write(&loc_href);
        // remove the hash at the end
        if let Some(pos) = loc_href.find('#') {
            loc_href = unwrap!(loc_href.get(..pos)).to_string();
        }
        websysmod::debug_write(&loc_href);
        loc_href.push_str(subdirectory_for_ws);
        loc_href.push_str("/");

        // send the client ws id as url_param for the first connect
        // and reconnect on lost connection
        loc_href.push_str(client_ws_id.to_string().as_str());
        /*
            websysmod::debug_write(&format!(
                "location_href {}  loc_href {} client_ws_id {}",
                location_href, loc_href, client_ws_id
            ));
        */

        // same server address and port as http server
        // for reconnect the old ws id will be an url param
        let ws = unwrap!(WebSocket::new(&loc_href), "WebSocket failed to connect.");
        self.set_ws(ws);
        let ws0 = self.get_ws_clone();
        // I don't know why is clone needed
        // It looks that the first send is in some way a handshake and is part of the connection
        // it will be execute on open as a closure
        let ws1 = self.get_ws_clone();
        let open_handler = Box::new(move || {
            // websysmod::debug_write("Connection opened, sending MsgRequestWsUid to server");
            Self::send_to_server_msg_request_ws_uid(ws1.clone(), client_ws_id);
            // region heartbeat ping pong keepalive
            let ws2 = ws1.clone();
            let timer_interval = gloo_timers::callback::Interval::new(10_000, move || {
                // Do something after the one second timer_interval is up!
                Self::send_to_server_msg_ping(ws2.clone(), time_now_in_minutes());
                // websysmod::console_log(format!("gloo timer: {}", time_now_in_minutes).as_str());
            });
            // Since we don't plan on cancelling the timer_interval, call `forget`.
            timer_interval.forget();
            // endregion
        });

        let cb_oh: Closure<dyn Fn()> = Closure::wrap(open_handler);
        ws0.set_onopen(Some(cb_oh.as_ref().unchecked_ref()));

        // don't drop the open_handler memory
        cb_oh.forget();

        ws0
    }
    /// setup all ws events
    fn setup_all_ws_events(ws: &WebSocket, vdom: VdomWeak) {
        // WebSocket on receive message callback
        Self::setup_ws_msg_recv(ws, vdom.clone());

        // WebSocket on error message callback
        Self::setup_ws_onerror(ws, vdom.clone());

        // WebSocket on close message callback
        Self::setup_ws_onclose(ws, vdom.clone());
    }
    /// receive WebSocket msg callback.
    #[allow(clippy::unneeded_field_pattern)]
    #[allow(clippy::too_many_lines)] // I know is long
    fn setup_ws_msg_recv(ws: &WebSocket, vdom: VdomWeak) {
        let msg_recv_handler = Box::new(move |msg: JsValue| {
            let data: JsValue = unwrap!(Reflect::get(&msg, &"data".into()));
            let data = unwrap!(data.as_string());

            // don't log ping pong there are too much
            //if !data.to_string().contains("MsgPong") {
            // websysmod::debug_write(&data);
            //}

            // we can receive 2 types of msgs:
            // 1. from the server WsMessageFromServer
            // 2. from other players WsMessage
            if let Ok(msg) = serde_json::from_str::<WsMessageFromServer>(&data) {
                //msg from ws server
                spawn_local({
                    let vdom_on_next_tick = vdom.clone();
                    async move {
                        let _result = vdom_on_next_tick
                            .with_component({
                                //let vdom = vdom_on_next_tick.clone();
                                move |root| {
                                    // msgs from server
                                    match msg {
                                        WsMessageFromServer::MsgPong { msg_id: _ } => {
                                            // websysmod::debug_write(format!("MsgPong {}", msg_id).as_str())
                                        }
                                        WsMessageFromServer::MsgResponseWsUid {
                                            msg_receiver_ws_uid,
                                            server_version: _,
                                        } => {
                                            // websysmod::debug_write(&format!("MsgResponseWsUid: {}  ", msg_receiver_ws_uid));
                                            Self::on_response_ws_uid(root, msg_receiver_ws_uid);
                                        }
                                    }
                                }
                            })
                            .await;
                    }
                });
            } else {
                Self::on_msg_recv_for_ws_message_for_receivers(vdom.clone(), data);
            }
        });

        // magic ??
        let cb_mrh: Closure<dyn Fn(JsValue)> = Closure::wrap(msg_recv_handler);
        ws.set_onmessage(Some(cb_mrh.as_ref().unchecked_ref()));

        // don't drop the event listener from memory
        cb_mrh.forget();
    }

    /// on error write it on the screen for debugging
    #[allow(clippy::as_conversions)]
    fn setup_ws_onerror(ws: &WebSocket, vdom: VdomWeak) {
        let onerror_callback = Closure::wrap(Box::new(move |e: ErrorEvent| {
            let err_text = format!("error event {:?}", e);
            // websysmod::debug_write(&err_text);
            {
                spawn_local({
                    let vdom_on_next_tick = vdom.clone();
                    async move {
                        let _result = vdom_on_next_tick
                            .with_component({
                                let vdom = vdom_on_next_tick.clone();
                                move |root| {
                                    Self::update_on_error(root, err_text);
                                    vdom.schedule_render();
                                }
                            })
                            .await;
                    }
                });
            }
        }) as Box<dyn FnMut(ErrorEvent)>);
        ws.set_onerror(Some(onerror_callback.as_ref().unchecked_ref()));
        onerror_callback.forget();
    }

    /// on close WebSocket connection
    #[allow(clippy::as_conversions)]
    fn setup_ws_onclose(ws: &WebSocket, vdom: VdomWeak) {
        let onclose_callback = Closure::wrap(Box::new(move |e: ErrorEvent| {
            let err_text = format!("ws_onclose {:?}", e);
            websysmod::debug_write(&format!("onclose_callback {}", &err_text));
            {
                spawn_local({
                    let vdom_on_next_tick = vdom.clone();
                    async move {
                        let _result = vdom_on_next_tick
                            .with_component({
                                let vdom = vdom_on_next_tick.clone();
                                move |root| {
                                    Self::update_on_close(root);
                                    vdom.schedule_render();
                                }
                            })
                            .await;
                    }
                });
            }
        }) as Box<dyn FnMut(ErrorEvent)>);
        ws.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
        onclose_callback.forget();
    }

    fn send_to_server_msg_ping(ws: WebSocket, msg_id: u32) {
        let ws_message = WsMessageToServer::MsgPing { msg_id };
        let json_message = unwrap!(serde_json::to_string(&ws_message));
        Self::ws_send_json_msg_with_retry(&ws, json_message);
    }

    fn send_to_server_msg_request_ws_uid(ws: WebSocket, client_ws_id: usize) {
        unwrap!(ws.send_with_str(&unwrap!(serde_json::to_string(
            &WsMessageToServer::MsgRequestWsUid {
                msg_sender_ws_uid: client_ws_id
            }
        ))));
    }

    /// send ws message to other players
    fn ws_send_json_msg_with_retry(ws: &WebSocket, json_message: String) {
        let x = ws.send_with_str(&json_message);
        // retry send a 10 times before panicking
        if let Err(_err) = x {
            let ws = ws.clone();
            spawn_local({
                async move {
                    let mut retries: usize = 1;
                    while retries <= 10 {
                        websysmod::debug_write(&format!("send retries: {}", retries));
                        // Wait 100 ms
                        TimeoutFuture::new(100).await;
                        let x = ws.send_with_str(&json_message);
                        if let Ok(_y) = x {
                            break;
                        }
                        // this will go until 10 and cannot overflow
                        #[allow(clippy::integer_arithmetic)]
                        {
                            retries += 1;
                        }
                    }
                    if retries == 0 {
                        panic!("error 10 times retry ws_send_json_msg_with_retry");
                    }
                }
            });
        }
    }
}

/// usize of time
#[allow(clippy::integer_arithmetic)]
// usize will not overflow, the minutes are max 60, so 6 mio
pub fn time_now_in_minutes() -> u32 {
    let now = js_sys::Date::new_0();
    now.get_minutes() * 100_000 + now.get_seconds() * 1_000 + now.get_milliseconds()
}