221 lines
6.5 KiB
Rust
221 lines
6.5 KiB
Rust
use std::{collections::VecDeque, rc::Rc};
|
|
|
|
use letterbox_shared::WebsocketMessage;
|
|
use log::{debug, error};
|
|
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;
|
|
debug!("WebSocket connection is open now");
|
|
}
|
|
Msg::TextMessageReceived(msg) => {
|
|
model.updates.push_back(msg);
|
|
}
|
|
Msg::WebSocketClosed(close_event) => {
|
|
debug!(
|
|
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 => {
|
|
debug!("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) => {
|
|
debug!("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| {
|
|
debug!("{:#?}", client.status);
|
|
let msg = match *client.status.borrow() {
|
|
ConnectionStatus::Connecting => {
|
|
debug!("Connecting...");
|
|
None
|
|
}
|
|
ConnectionStatus::Connected => Some(Msg::WebSocketOpened),
|
|
ConnectionStatus::Error => Some(Msg::WebSocketFailed),
|
|
ConnectionStatus::Disconnected => {
|
|
debug!("Disconnected");
|
|
None
|
|
}
|
|
};
|
|
send(msg);
|
|
})));
|
|
|
|
let send = msg_sender.clone();
|
|
client.set_on_close(Some(Box::new(move |ev| {
|
|
debug!("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:?}"),
|
|
}
|
|
}
|