leptos-use/src/websocket/use_websocket.rs
Jens Krause e0a069ead6 Use store_value for non-reactive references
to be more Leptos like - no need to do all these Rc<RefCell<..>> manually.
2023-07-11 15:08:45 +02:00

374 lines
12 KiB
Rust

use leptos::{leptos_dom::helpers::TimeoutHandle, *};
use core::fmt;
use std::rc::Rc;
use std::time::Duration;
use js_sys::Array;
use wasm_bindgen::{prelude::*, JsCast, JsValue};
use web_sys::{BinaryType, Event, MessageEvent, WebSocket};
pub use web_sys::CloseEvent;
use crate::utils::CloneableFnMutWithArg;
/// The current state of the `WebSocket` connection.
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum UseWebSocketReadyState {
Connecting,
Open,
Closing,
Closed,
}
impl fmt::Display for UseWebSocketReadyState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
UseWebSocketReadyState::Connecting => write!(f, "Connecting"),
UseWebSocketReadyState::Open => write!(f, "Open"),
UseWebSocketReadyState::Closing => write!(f, "Closing"),
UseWebSocketReadyState::Closed => write!(f, "Closed"),
}
}
}
/// Options for `WebSocket`.
// #[derive(DefaultBuilder)]
#[derive(Clone)]
pub struct UseWebSocketOptions {
/// `WebSocket` connect callback.
pub onopen: Option<Box<dyn CloneableFnMutWithArg<Event>>>,
/// `WebSocket` message callback for text.
pub onmessage: Option<Box<dyn CloneableFnMutWithArg<String>>>,
/// `WebSocket` message callback for binary.
pub onmessage_bytes: Option<Box<dyn CloneableFnMutWithArg<Vec<u8>>>>,
/// `WebSocket` error callback.
pub onerror: Option<Box<dyn CloneableFnMutWithArg<Event>>>,
/// `WebSocket` close callback.
pub onclose: Option<Box<dyn CloneableFnMutWithArg<CloseEvent>>>,
/// Retry times.
pub reconnect_limit: Option<u64>,
/// Retry interval(ms).
pub reconnect_interval: Option<u64>,
/// Manually starts connection
pub manual: bool,
/// Sub protocols
pub protocols: Option<Vec<String>>,
}
impl Default for UseWebSocketOptions {
fn default() -> Self {
Self {
onopen: None,
onmessage: None,
onmessage_bytes: None,
onerror: None,
onclose: None,
reconnect_limit: Some(3),
reconnect_interval: Some(3 * 1000),
manual: false,
protocols: Default::default(),
}
}
}
/// Return type of [`use_websocket`].
#[derive(Clone)]
pub struct UseWebsocketReturn<OpenFn, CloseFn, SendFn, SendBytesFn>
where
OpenFn: Fn() + Clone + 'static,
CloseFn: Fn() + Clone + 'static,
SendFn: Fn(String) + Clone + 'static,
SendBytesFn: Fn(Vec<u8>) + Clone + 'static,
{
/// The current state of the `WebSocket` connection.
pub ready_state: ReadSignal<UseWebSocketReadyState>,
/// Latest text message received from `WebSocket`.
pub message: ReadSignal<Option<String>>,
/// Latest binary message received from `WebSocket`.
pub message_bytes: ReadSignal<Option<Vec<u8>>>,
/// The `WebSocket` instance.
pub ws: Option<WebSocket>,
pub open: OpenFn,
pub close: CloseFn,
pub send: SendFn,
pub send_bytes: SendBytesFn,
}
pub fn use_websocket(
cx: Scope,
url: String,
) -> UseWebsocketReturn<
impl Fn() + Clone + 'static,
impl Fn() + Clone + 'static,
impl Fn(String) + Clone + 'static,
impl Fn(Vec<u8>) + Clone + 'static,
> {
use_websocket_with_options(cx, url, UseWebSocketOptions::default())
}
/// Version of [`use_websocket`] that takes `UseWebSocketOptions`. See [`use_websocket`] for how to use.
pub fn use_websocket_with_options(
cx: Scope,
url: String,
options: UseWebSocketOptions,
) -> UseWebsocketReturn<
impl Fn() + Clone + 'static,
impl Fn() + Clone + 'static,
impl Fn(String) + Clone + 'static,
impl Fn(Vec<u8>) + Clone,
> {
let (ready_state, set_ready_state) = create_signal(cx, UseWebSocketReadyState::Closed);
let (message, set_message) = create_signal(cx, None);
let (message_bytes, set_message_bytes) = create_signal(cx, None);
let ws_ref: StoredValue<Option<WebSocket>> = store_value(cx, None);
let onopen_ref = store_value(cx, options.onopen);
let onmessage_ref = store_value(cx, options.onmessage);
let onmessage_bytes_ref = store_value(cx, options.onmessage_bytes);
let onerror_ref = store_value(cx, options.onerror);
let onclose_ref = store_value(cx, options.onclose);
let reconnect_limit = options.reconnect_limit.unwrap_or(3);
let reconnect_interval = options.reconnect_interval.unwrap_or(3 * 1000);
let reconnect_timer_ref: StoredValue<Option<TimeoutHandle>> = store_value(cx, None);
let manual = options.manual;
let protocols = options.protocols;
let reconnect_times_ref: StoredValue<u64> = store_value(cx, 0);
let unmounted_ref = store_value(cx, false);
let connect_ref: StoredValue<Option<Rc<dyn Fn()>>> = store_value(cx, None);
let reconnect_ref: StoredValue<Option<Rc<dyn Fn()>>> = store_value(cx, None);
reconnect_ref.set_value({
let ws = ws_ref.get_value();
Some(Rc::new(move || {
if reconnect_times_ref.get_value() < reconnect_limit
&& ws
.clone()
.map_or(false, |ws: WebSocket| ws.ready_state() != WebSocket::OPEN)
{
reconnect_timer_ref.set_value(
set_timeout_with_handle(
move || {
if let Some(connect) = connect_ref.get_value() {
connect();
reconnect_times_ref.update_value(|current| *current += 1);
}
},
Duration::from_millis(reconnect_interval),
)
.ok(),
);
}
}))
});
connect_ref.set_value({
let ws = ws_ref.get_value();
let url = url.clone();
Some(Rc::new(move || {
reconnect_timer_ref.set_value(None);
{
if let Some(web_socket) = &ws {
let _ = web_socket.close();
}
}
let web_socket = {
protocols.as_ref().map_or_else(
|| WebSocket::new(&url).unwrap_throw(),
|protocols| {
let array = protocols
.iter()
.map(|p| JsValue::from(p.clone()))
.collect::<Array>();
WebSocket::new_with_str_sequence(&url, &JsValue::from(&array))
.unwrap_throw()
},
)
};
web_socket.set_binary_type(BinaryType::Arraybuffer);
set_ready_state.set(UseWebSocketReadyState::Connecting);
// onopen handler
{
let onopen_closure = Closure::wrap(Box::new(move |e: Event| {
if unmounted_ref.get_value() {
return;
}
if let Some(onopen) = onopen_ref.get_value().as_mut() {
onopen(e);
}
set_ready_state.set(UseWebSocketReadyState::Open);
}) as Box<dyn FnMut(Event)>);
web_socket.set_onopen(Some(onopen_closure.as_ref().unchecked_ref()));
// Forget the closure to keep it alive
onopen_closure.forget();
}
// onmessage handler
{
let onmessage_closure = Closure::wrap(Box::new(move |e: MessageEvent| {
if unmounted_ref.get_value() {
return;
}
e.data().dyn_into::<js_sys::ArrayBuffer>().map_or_else(
|_| {
e.data().dyn_into::<js_sys::JsString>().map_or_else(
|_| {
unreachable!("message event, received Unknown: {:?}", e.data());
},
|txt| {
let txt = String::from(&txt);
if let Some(onmessage) = onmessage_ref.get_value().as_mut() {
onmessage(txt.clone());
}
set_message.set(Some(txt.clone()));
},
);
},
|array_buffer| {
let array = js_sys::Uint8Array::new(&array_buffer);
let array = array.to_vec();
if let Some(onmessage_bytes) = onmessage_bytes_ref.get_value().as_mut()
{
let array = array.clone();
onmessage_bytes(array);
}
set_message_bytes.set(Some(array));
},
);
})
as Box<dyn FnMut(MessageEvent)>);
web_socket.set_onmessage(Some(onmessage_closure.as_ref().unchecked_ref()));
onmessage_closure.forget();
}
// onerror handler
{
// let reconnect = reconnect.clone();
let onerror_closure = Closure::wrap(Box::new(move |e: Event| {
if unmounted_ref.get_value() {
return;
}
if let Some(reconnect) = &reconnect_ref.get_value() {
reconnect();
}
if let Some(onerror) = onerror_ref.get_value().as_mut() {
onerror(e);
}
set_ready_state.set(UseWebSocketReadyState::Closed);
}) as Box<dyn FnMut(Event)>);
web_socket.set_onerror(Some(onerror_closure.as_ref().unchecked_ref()));
onerror_closure.forget();
}
// onclose handler
{
let onclose_closure = Closure::wrap(Box::new(move |e: CloseEvent| {
if unmounted_ref.get_value() {
return;
}
if let Some(reconnect) = &reconnect_ref.get_value() {
reconnect();
}
if let Some(onclose) = onclose_ref.get_value().as_mut() {
onclose(e);
}
set_ready_state.set(UseWebSocketReadyState::Closed);
})
as Box<dyn FnMut(CloseEvent)>);
web_socket.set_onclose(Some(onclose_closure.as_ref().unchecked_ref()));
onclose_closure.forget();
}
ws_ref.set_value(Some(web_socket));
}))
});
// Send text (String)
let send = {
Box::new(move |data: String| {
if ready_state.get() == UseWebSocketReadyState::Open {
if let Some(web_socket) = ws_ref.get_value() {
let _ = web_socket.send_with_str(&data);
}
}
})
};
// Send bytes
let send_bytes = {
move |data: Vec<u8>| {
if ready_state.get() == UseWebSocketReadyState::Open {
if let Some(web_socket) = ws_ref.get_value() {
let _ = web_socket.send_with_u8_array(&data);
}
}
}
};
// Open connection
let open = {
move || {
reconnect_times_ref.set_value(0);
if let Some(connect) = connect_ref.get_value() {
connect();
}
}
};
// Close connection
let close = {
reconnect_timer_ref.set_value(None);
move || {
reconnect_times_ref.set_value(reconnect_limit);
if let Some(web_socket) = ws_ref.get_value() {
let _ = web_socket.close();
}
}
};
// Open connection (not called if option `manual` is true)
{
let open = open.clone();
create_effect(cx, move |_| {
if !manual {
open();
}
|| ()
});
}
// clean up (unmount)
{
let close = close.clone();
on_cleanup(cx, move || {
unmounted_ref.set_value(true);
close();
});
}
UseWebsocketReturn {
ready_state,
message,
message_bytes,
ws: ws_ref.get_value(),
open,
close,
send,
send_bytes,
}
}