letterbox/web/src/websocket.rs

221 lines
6.5 KiB
Rust

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<RefCell<ConnectionStatus>>,
}
impl EventClient {
pub fn new(_: &str) -> Result<Self, WebSocketError> {
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<Box<dyn Fn(ErrorEvent)>>) {
todo!("this is a mock")
}
pub fn set_on_connection(&mut self, _: Option<Box<dyn Fn(&EventClient)>>) {
todo!("this is a mock")
}
pub fn set_on_close(&mut self, _: Option<Box<dyn Fn(CloseEvent)>>) {
todo!("this is a mock")
}
pub fn set_on_message(&mut self, _: Option<Box<dyn Fn(&EventClient, Message)>>) {
todo!("this is a mock")
}
}
#[derive(Debug, Clone)]
pub enum Message {
Text(String),
Binary(Vec<u8>),
}
#[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<StreamHandle>,
pub updates: VecDeque<WebsocketMessage>,
}
// ------ ------
// Init
// ------ ------
pub fn init(ws_url: &str, orders: &mut impl Orders<Msg>) -> 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<Msg>) {
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<Msg>) -> Result<EventClient, WebSocketError> {
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<dyn Fn(Option<Msg>)>) {
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:?}"),
}
}