use crate::*;
fn generate_uuid() -> String {
let hex: String = (0..32)
.map(|_: i32| format!("{:x}", (Math::random() * 16.0) as u8 & 0x0f))
.collect();
format!(
"{}-{}-4{}-{}-{}",
&hex[0..8],
&hex[8..12],
&hex[12..15],
&hex[15..19],
&hex[19..32]
)
}
fn format_timestamp(timestamp_ms: f64) -> String {
let date: js_sys::Date = js_sys::Date::new(&JsValue::from_f64(timestamp_ms));
let hours: u32 = date.get_hours();
let minutes: u32 = date.get_minutes();
let seconds: u32 = date.get_seconds();
format!("{hours:02}:{minutes:02}:{seconds:02}")
}
fn parse_ws_message(raw: &str) -> WsMessage {
serde_json::from_str::<WsServerMessage>(raw)
.map(|server_message: WsServerMessage| WsMessage {
data: server_message.data,
time: format_timestamp(server_message.time),
})
.unwrap_or_else(|_| WsMessage {
data: raw.to_string(),
time: String::new(),
})
}
fn build_websocket_url() -> String {
format!("{WEBSOCKET_DEFAULT_URL_PREFIX}{}", generate_uuid())
}
pub(crate) fn use_websocket() -> UseWebSocket {
UseWebSocket {
url: use_signal(build_websocket_url),
connected: use_signal(|| false),
connecting: use_signal(|| false),
message_input: use_signal(String::new),
messages: use_signal(Vec::new),
error: use_signal(String::new),
ping_handle: use_signal(|| None),
}
}
fn start_ping_timer(ping_handle_signal: Signal<Option<IntervalHandle>>) {
let handle: IntervalHandle = use_interval(WEBSOCKET_PING_INTERVAL_MS, move || {
WS_INSTANCE.with(|instance: &RefCell<Option<WebSocket>>| {
if let Some(socket) = instance.borrow().as_ref() {
let _ = socket.send_with_str(WEBSOCKET_PING_MESSAGE);
}
});
});
ping_handle_signal.set(Some(handle));
}
fn stop_ping_timer(ping_handle_signal: Signal<Option<IntervalHandle>>) {
if let Some(handle) = ping_handle_signal.get() {
handle.clear();
ping_handle_signal.set(None);
}
}
pub(crate) fn websocket_on_connect(state: UseWebSocket) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_event: Event| {
let url: String = state.get_url().get();
if url.is_empty() {
state
.get_error()
.set("Please enter a valid WebSocket URL".to_string());
return;
}
ws_close_instance();
stop_ping_timer(state.get_ping_handle());
state.get_connecting().set(true);
state.get_error().set(String::new());
state.get_messages().set(Vec::new());
let socket: WebSocket = match WebSocket::new(&url) {
Ok(ws) => ws,
Err(_) => {
state.get_connecting().set(false);
state
.get_error()
.set("Failed to create WebSocket".to_string());
return;
}
};
WS_INSTANCE.with(|instance: &RefCell<Option<WebSocket>>| {
*instance.borrow_mut() = Some(socket.clone());
});
let on_open: Closure<dyn FnMut(JsValue)> = Closure::wrap(Box::new({
let state: UseWebSocket = state;
move |_event: JsValue| {
state.get_connected().set(true);
state.get_connecting().set(false);
start_ping_timer(state.get_ping_handle());
let mut messages: Vec<WsMessage> = state.get_messages().get();
messages.push(WsMessage {
data: "[System] Connection established".to_string(),
time: String::new(),
});
state.get_messages().set(messages);
}
}));
socket.set_onopen(Some(on_open.as_ref().unchecked_ref()));
on_open.forget();
let on_message: Closure<dyn FnMut(JsValue)> = Closure::wrap(Box::new({
let state: UseWebSocket = state;
move |event_value: JsValue| {
let message_event: MessageEvent = event_value.unchecked_into();
let data: JsValue = message_event.data();
let raw: String = if data.is_string() {
data.as_string().unwrap_or_default()
} else {
format!("{:?}", data)
};
let ws_message: WsMessage = parse_ws_message(&raw);
let mut messages: Vec<WsMessage> = state.get_messages().get();
messages.push(ws_message);
if messages.len() > WEBSOCKET_MAX_MESSAGES {
messages.drain(0..messages.len() - WEBSOCKET_MAX_MESSAGES);
}
state.get_messages().set(messages);
}
}));
socket.set_onmessage(Some(on_message.as_ref().unchecked_ref()));
on_message.forget();
let on_close: Closure<dyn FnMut(JsValue)> = Closure::wrap(Box::new({
let state: UseWebSocket = state;
move |_event: JsValue| {
state.get_connected().set(false);
state.get_connecting().set(false);
stop_ping_timer(state.get_ping_handle());
let mut messages: Vec<WsMessage> = state.get_messages().get();
messages.push(WsMessage {
data: "[System] Connection closed".to_string(),
time: String::new(),
});
state.get_messages().set(messages);
}
}));
socket.set_onclose(Some(on_close.as_ref().unchecked_ref()));
on_close.forget();
let on_error: Closure<dyn FnMut(JsValue)> = Closure::wrap(Box::new({
let state: UseWebSocket = state;
move |_event: JsValue| {
state
.get_error()
.set("WebSocket error occurred".to_string());
}
}));
socket.set_onerror(Some(on_error.as_ref().unchecked_ref()));
on_error.forget();
}))
}
pub(crate) fn websocket_on_disconnect(state: UseWebSocket) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_event: Event| {
stop_ping_timer(state.get_ping_handle());
ws_close_instance();
state.get_connected().set(false);
state.get_connecting().set(false);
state.get_error().set(String::new());
}))
}
pub(crate) fn websocket_on_send(state: UseWebSocket) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_event: Event| {
let text: String = state.get_message_input().get();
if text.is_empty() {
return;
}
let body: String = format!("{}{}\"}}", WEBSOCKET_TEXT_MESSAGE_TEMPLATE, text);
WS_INSTANCE.with(|instance: &RefCell<Option<WebSocket>>| {
if let Some(socket) = instance.borrow().as_ref() {
let _ = socket.send_with_str(&body);
}
});
state.get_message_input().set(String::new());
}))
}
pub(crate) fn ws_cleanup(state: UseWebSocket) {
use_cleanup(move || {
stop_ping_timer(state.get_ping_handle());
ws_close_instance();
state.get_connected().set(false);
state.get_connecting().set(false);
state.get_error().set(String::new());
});
}
fn ws_close_instance() {
WS_INSTANCE.with(|instance: &RefCell<Option<WebSocket>>| {
if let Some(socket) = instance.borrow_mut().take() {
let _ = socket.close();
}
});
}