diff --git a/Cargo.toml b/Cargo.toml index 0eab610..0f22189 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ lazy_static = "1" version = "0.3" features = [ "CssStyleDeclaration", + "CloseEvent", "CustomEvent", "CustomEventInit", "DomRectReadOnly", @@ -62,6 +63,7 @@ features = [ docs = [] math = ["num"] storage = ["serde", "serde_json", "web-sys/StorageEvent"] +websocket = ["web-sys/BinaryType", "web-sys/WebSocket"] [package.metadata.docs.rs] all-features = true diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 5ec52d0..163c8e4 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -28,6 +28,7 @@ members = [ "use_scroll", "use_storage", "use_throttle_fn", + "use_websocket", "watch_debounced", "watch_pausable", "watch_throttled", diff --git a/examples/use_websocket/Cargo.toml b/examples/use_websocket/Cargo.toml new file mode 100644 index 0000000..0b2c27b --- /dev/null +++ b/examples/use_websocket/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "use_websocket" +version = "0.1.0" +edition = "2021" + +[dependencies] +leptos = { version = "0.4", features = ["nightly", "csr"] } +console_error_panic_hook = "0.1" +console_log = "1" +log = "0.4" +leptos-use = { path = "../..", features = ["docs", "websocket"] } +web-sys = "0.3" + +[dev-dependencies] +wasm-bindgen = "0.2" +wasm-bindgen-test = "0.3.0" diff --git a/examples/use_websocket/README.md b/examples/use_websocket/README.md new file mode 100644 index 0000000..7030fa4 --- /dev/null +++ b/examples/use_websocket/README.md @@ -0,0 +1,22 @@ +A simple example for `use_websocket`. + +If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation) +as well as the nightly toolchain for Rust and the wasm32-unknown-unknown target: + +```bash +cargo install trunk +rustup toolchain install nightly +rustup target add wasm32-unknown-unknown +``` + +Then, open two terminals. In the first one, run: + +``` +npx tailwindcss -i ./input.css -o ./style/output.css --watch +``` + +In the second one, run: + +```bash +trunk serve --open +``` diff --git a/examples/use_websocket/Trunk.toml b/examples/use_websocket/Trunk.toml new file mode 100644 index 0000000..3e4be08 --- /dev/null +++ b/examples/use_websocket/Trunk.toml @@ -0,0 +1,2 @@ +[build] +public_url = "/demo/" \ No newline at end of file diff --git a/examples/use_websocket/index.html b/examples/use_websocket/index.html new file mode 100644 index 0000000..ae249a6 --- /dev/null +++ b/examples/use_websocket/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/use_websocket/input.css b/examples/use_websocket/input.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/examples/use_websocket/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/examples/use_websocket/rust-toolchain.toml b/examples/use_websocket/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/examples/use_websocket/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/examples/use_websocket/src/main.rs b/examples/use_websocket/src/main.rs new file mode 100644 index 0000000..54a87f2 --- /dev/null +++ b/examples/use_websocket/src/main.rs @@ -0,0 +1,203 @@ +use leptos::*; +use leptos_use::docs::demo_or_body; +use leptos_use::websocket::*; + +#[component] +fn Demo(cx: Scope) -> impl IntoView { + let (history, set_history) = create_signal(cx, vec![]); + + fn update_history(&history: &WriteSignal>, message: String) { + let _ = &history.update(|history: &mut Vec<_>| history.push(message)); + } + // ---------------------------- + // use_websocket + // ---------------------------- + + let UseWebsocketReturn { + ready_state, + message, + message_bytes, + send, + send_bytes, + open, + close, + .. + } = use_websocket(cx, "wss://echo.websocket.events/".to_string()); + + let send_message = move |_| { + let m = "Hello, world!".to_string(); + send(m.clone()); + set_history.update(|history: &mut Vec<_>| history.push(format! {"[send]: {:?}", m})); + }; + + let send_byte_message = move |_| { + let m = b"Hello, world!\r\n".to_vec(); + send_bytes(m.clone()); + set_history.update(|history: &mut Vec<_>| history.push(format! {"[send_bytes]: {:?}", m})); + }; + + let status = move || ready_state().to_string(); + + let connected = move || ready_state.get() == UseWebSocketReadyState::Open; + + let open_connection = move |_| { + open(); + }; + let close_connection = move |_| { + close(); + }; + + create_effect(cx, move |_| { + if let Some(m) = message.get() { + update_history(&set_history, format! {"[message]: {:?}", m}); + }; + }); + + create_effect(cx, move |_| { + if let Some(m) = message_bytes.get() { + update_history(&set_history, format! {"[message_bytes]: {:?}", m}); + }; + }); + + // ---------------------------- + // use_websocket_with_options + // ---------------------------- + + let (history2, set_history2) = create_signal(cx, vec![]); + + let UseWebsocketReturn { + ready_state: ready_state2, + send: send2, + send_bytes: send_bytes2, + open: open2, + close: close2, + message: message2, + message_bytes: message_bytes2, + .. + } = use_websocket_with_options( + cx, + "wss://echo.websocket.events/".to_string(), + UseWebSocketOptions { + manual: true, + onopen: Some(Box::new(move |e| { + set_history2.update(|history: &mut Vec<_>| { + history.push(format! {"[onopen]: event {:?}", e.type_()}) + }); + })), + onclose: Some(Box::new(move |e| { + set_history2.update(|history: &mut Vec<_>| { + history.push(format! {"[onclose]: event {:?}", e.type_()}) + }); + })), + onerror: Some(Box::new(move |e| { + set_history2.update(|history: &mut Vec<_>| { + history.push(format! {"[onerror]: event {:?}", e.type_()}) + }); + })), + onmessage: Some(Box::new(move |m| { + set_history2 + .update(|history: &mut Vec<_>| history.push(format! {"[onmessage]: {:?}", m})); + })), + onmessage_bytes: Some(Box::new(move |m| { + set_history2.update(|history: &mut Vec<_>| { + history.push(format! {"[onmessage_bytes]: {:?}", m}) + }); + })), + ..Default::default() + }, + ); + + let open_connection2 = move |_| { + open2(); + }; + let close_connection2 = move |_| { + close2(); + }; + + let send_message2 = move |_| { + let message = "Hello, use_leptos!".to_string(); + send2(message.clone()); + update_history(&set_history2, format! {"[send]: {:?}", message}); + }; + + let send_byte_message2 = move |_| { + let m = b"Hello, world!\r\n".to_vec(); + send_bytes2(m.clone()); + update_history(&set_history2, format! {"[send_bytes]: {:?}", m}); + }; + + let status2 = move || ready_state2.get().to_string(); + + create_effect(cx, move |_| { + if let Some(m) = message2.get() { + update_history(&set_history2, format! {"[message]: {:?}", m}); + }; + }); + + create_effect(cx, move |_| { + if let Some(m) = message_bytes2.get() { + update_history(&set_history2, format! {"[message_bytes]: {:?}", m}); + }; + }); + + let connected2 = move || ready_state2.get() == UseWebSocketReadyState::Open; + + view! { cx, +
+
+
+

"use_websocket"

+

"status: " {status}

+ + + + +
+

"History"

+ +
+ {message}
} + } + /> + +
+
+

"use_websocket_with_options"

+

"status: " {status2}

+ + + + +
+

"History"

+ +
+
    + {message} } + } + /> +
+
+
+ + + + } +} + +fn main() { + _ = console_log::init_with_level(log::Level::Info); + console_error_panic_hook::set_once(); + + mount_to(demo_or_body(), |cx| { + view! { cx, } + }) +} diff --git a/examples/use_websocket/style/output.css b/examples/use_websocket/style/output.css new file mode 100644 index 0000000..a9427f9 --- /dev/null +++ b/examples/use_websocket/style/output.css @@ -0,0 +1,214 @@ +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: +} + +.container { + width: 100% +} + +@media (min-width: 640px) { + .container { + max-width: 640px + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px + } +} + +.static { + position: static +} + +.mb-2 { + margin-bottom: 0.5rem +} + +.mr-2 { + margin-right: 0.5rem +} + +.flex { + display: flex +} + +.w-full { + width: 100% +} + +.flex-col { + flex-direction: column +} + +.items-center { + align-items: center +} + +.gap-4 { + gap: 1rem +} + +.p-0 { + padding: 0px +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem +} + +.text-\[--brand-color\] { + color: var(--brand-color) +} + +.text-green-600 { + --tw-text-opacity: 1; + color: rgb(22 163 74 / var(--tw-text-opacity)) +} + +.opacity-75 { + opacity: 0.75 +} + +@media (prefers-color-scheme: dark) { + .dark\:text-green-500 { + --tw-text-opacity: 1; + color: rgb(34 197 94 / var(--tw-text-opacity)) + } +} + +@media (min-width: 1024px) { + .lg\:w-1\/2 { + width: 50% + } + + .lg\:flex-row { + flex-direction: row + } + + .lg\:text-4xl { + font-size: 2.25rem; + line-height: 2.5rem + } +} \ No newline at end of file diff --git a/examples/use_websocket/tailwind.config.js b/examples/use_websocket/tailwind.config.js new file mode 100644 index 0000000..c5f4159 --- /dev/null +++ b/examples/use_websocket/tailwind.config.js @@ -0,0 +1,13 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: { + files: ["*.html", "./src/**/*.rs", "../../src/docs/**/*.rs"], + }, + theme: { + extend: {}, + }, + corePlugins: { + preflight: false, + }, + plugins: [], +} diff --git a/src/lib.rs b/src/lib.rs index 9d2d5d0..987a01d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ pub mod math; #[cfg(feature = "storage")] pub mod storage; pub mod utils; +#[cfg(feature = "websocket")] +pub mod websocket; #[cfg(web_sys_unstable_apis)] mod use_element_size; diff --git a/src/websocket/mod.rs b/src/websocket/mod.rs new file mode 100644 index 0000000..ef6e33b --- /dev/null +++ b/src/websocket/mod.rs @@ -0,0 +1,3 @@ +mod use_websocket; + +pub use use_websocket::*; diff --git a/src/websocket/use_websocket.rs b/src/websocket/use_websocket.rs new file mode 100644 index 0000000..51715a6 --- /dev/null +++ b/src/websocket/use_websocket.rs @@ -0,0 +1,406 @@ +use leptos::{leptos_dom::helpers::TimeoutHandle, *}; + +use core::fmt; +use std::rc::Rc; +use std::{cell::RefCell, 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>>, + /// `WebSocket` message callback for text. + pub onmessage: Option>>, + /// `WebSocket` message callback for binary. + pub onmessage_bytes: Option>>>, + /// `WebSocket` error callback. + pub onerror: Option>>, + /// `WebSocket` close callback. + pub onclose: Option>>, + + /// Retry times. + pub reconnect_limit: Option, + /// Retry interval(ms). + pub reconnect_interval: Option, + /// Manually starts connection + pub manual: bool, + /// Sub protocols + pub protocols: Option>, +} + +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 +where + OpenFn: Fn() + Clone + 'static, + CloseFn: Fn() + Clone + 'static, + SendFn: Fn(String) + Clone + 'static, + SendBytesFn: Fn(Vec) + Clone + 'static, +{ + /// The current state of the `WebSocket` connection. + pub ready_state: ReadSignal, + /// Latest text message received from `WebSocket`. + pub message: ReadSignal>, + /// Latest binary message received from `WebSocket`. + pub message_bytes: ReadSignal>>, + /// The `WebSocket` instance. + pub ws: Rc>>, + + 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) + 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) + 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: Rc>> = Rc::new(RefCell::new(None)); + + let onopen = Rc::new(RefCell::new(options.onopen)); + let onmessage = Rc::new(RefCell::new(options.onmessage)); + let onmessage_bytes = Rc::new(RefCell::new(options.onmessage_bytes)); + let onerror = Rc::new(RefCell::new(options.onerror)); + let onclose = Rc::new(RefCell::new(options.onclose)); + + let reconnect_limit = options.reconnect_limit.unwrap_or(3); + let reconnect_interval = options.reconnect_interval.unwrap_or(3 * 1000); + + let reconnect_timer: Rc>> = Rc::new(RefCell::new(None)); + let manual = options.manual; + let protocols = options.protocols; + + let reconnect_times: Rc> = Rc::new(RefCell::new(0)); + let unmounted = Rc::new(RefCell::new(false)); + + let connect: Rc>>> = Rc::new(RefCell::new(None)); + + let reconnect: Rc>>> = Rc::new(RefCell::new(None)); + *reconnect.borrow_mut() = { + let ws = Rc::clone(&ws); + let reconnect_times = Rc::clone(&reconnect_times); + let connect = connect.clone(); + Some(Rc::new(move || { + if *reconnect_times.borrow() < reconnect_limit + && ws + .borrow() + .as_ref() + .map_or(false, |ws: &WebSocket| ws.ready_state() != WebSocket::OPEN) + { + let reconnect_times = Rc::clone(&reconnect_times); + let connect = Rc::clone(&connect); + + *reconnect_timer.borrow_mut() = set_timeout_with_handle( + move || { + let connect = &mut *connect.borrow_mut(); + if let Some(connect) = connect { + connect(); + *reconnect_times.borrow_mut() += 1; + } + }, + Duration::from_millis(reconnect_interval), + ) + .ok() + } + })) + }; + + *connect.borrow_mut() = { + let ws = Rc::clone(&ws); + let url = url.clone(); + let unmounted = Rc::clone(&unmounted); + let onopen = Rc::clone(&onopen); + let onmessage = Rc::clone(&onmessage); + let onerror = Rc::clone(&onerror); + let onclose = Rc::clone(&onclose); + let reconnect = Rc::clone(&reconnect); + + Some(Rc::new(move || { + { + let web_socket: &mut Option = &mut ws.borrow_mut(); + if let Some(web_socket) = web_socket { + 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::(); + 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 unmounted = Rc::clone(&unmounted); + let onopen = Rc::clone(&onopen); + let onopen_closure = Closure::wrap(Box::new(move |e: Event| { + if *unmounted.borrow() { + return; + } + + let onopen = &mut *onopen.borrow_mut(); + if let Some(onopen) = onopen { + onopen(e); + } + set_ready_state.set(UseWebSocketReadyState::Open); + }) as Box); + web_socket.set_onopen(Some(onopen_closure.as_ref().unchecked_ref())); + // Forget the closure to keep it alive + onopen_closure.forget(); + } + + // onmessage handler + { + let unmounted = Rc::clone(&unmounted); + let onmessage = Rc::clone(&onmessage); + let onmessage_bytes = onmessage_bytes.clone(); + let onmessage_closure = Closure::wrap(Box::new(move |e: MessageEvent| { + if *unmounted.borrow() { + return; + } + + e.data().dyn_into::().map_or_else( + |_| { + e.data().dyn_into::().map_or_else( + |_| { + unreachable!("message event, received Unknown: {:?}", e.data()); + }, + |txt| { + let txt = String::from(&txt); + let onmessage = &mut *onmessage.borrow_mut(); + if let Some(onmessage) = onmessage { + let txt = txt.clone(); + onmessage(txt); + } + set_message.set(Some(txt.clone())); + }, + ); + }, + |array_buffer| { + let array = js_sys::Uint8Array::new(&array_buffer); + let array = array.to_vec(); + let onmessage_bytes = &mut *onmessage_bytes.borrow_mut(); + if let Some(onmessage_bytes) = onmessage_bytes { + let array = array.clone(); + onmessage_bytes(array); + } + set_message_bytes.set(Some(array)); + }, + ); + }) + as Box); + web_socket.set_onmessage(Some(onmessage_closure.as_ref().unchecked_ref())); + onmessage_closure.forget(); + } + // onerror handler + { + let unmounted = Rc::clone(&unmounted); + let onerror = Rc::clone(&onerror); + let reconnect = Rc::clone(&reconnect); + let onerror_closure = Closure::wrap(Box::new(move |e: Event| { + if *unmounted.borrow() { + return; + } + + let reconnect: Rc = { reconnect.borrow().as_ref().unwrap().clone() }; + reconnect(); + + let onerror = &mut *onerror.borrow_mut(); + if let Some(onerror) = onerror { + onerror(e); + } + set_ready_state.set(UseWebSocketReadyState::Closed); + }) as Box); + web_socket.set_onerror(Some(onerror_closure.as_ref().unchecked_ref())); + onerror_closure.forget(); + } + // onclose handler + { + let unmounted = Rc::clone(&unmounted); + let onclose = Rc::clone(&onclose); + + let reconnect = Rc::clone(&reconnect); + let onclose_closure = Closure::wrap(Box::new(move |e: CloseEvent| { + if *unmounted.borrow() { + return; + } + + let reconnect: Rc = { reconnect.borrow().as_ref().unwrap().clone() }; + reconnect(); + + let onclose = &mut *onclose.borrow_mut(); + if let Some(onclose) = onclose { + onclose(e); + } + set_ready_state.set(UseWebSocketReadyState::Closed); + }) + as Box); + web_socket.set_onclose(Some(onclose_closure.as_ref().unchecked_ref())); + onclose_closure.forget(); + } + + *ws.borrow_mut() = Some(web_socket); + })) + }; + + // Send text (String) + let send = { + let ws = Rc::clone(&ws); + Box::new(move |data: String| { + if ready_state.get() == UseWebSocketReadyState::Open { + if let Some(web_socket) = ws.borrow_mut().as_ref() { + let _ = web_socket.send_with_str(&data); + } + } + }) + }; + + // Send bytes + let send_bytes = { + let ws = Rc::clone(&ws); + move |data: Vec| { + if ready_state.get() == UseWebSocketReadyState::Open { + let web_socket: &mut Option = &mut ws.borrow_mut(); + if let Some(web_socket) = web_socket { + let _ = web_socket.send_with_u8_array(&data); + } + } + } + }; + + // Open connection + let open = { + let reconnect_times_ref = Rc::clone(&reconnect_times); + // let connect = Rc::clone(&connect); + move || { + let connect = connect.clone(); + *reconnect_times_ref.borrow_mut() = 0; + let connect: Rc = { connect.borrow().as_ref().unwrap().clone() }; + connect(); + } + }; + + // Close connection + let close = { + let ws = Rc::clone(&ws); + let reconnect_times = Rc::clone(&reconnect_times); + move || { + *reconnect_times.as_ref().borrow_mut() = reconnect_limit; + let web_socket: &mut Option = &mut ws.borrow_mut(); + if let Some(web_socket) = web_socket { + 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.borrow_mut() = true; + close(); + }); + } + + UseWebsocketReturn { + ready_state, + message, + message_bytes, + ws, + open, + close, + send, + send_bytes, + } +}