From ae2eb61b6f5b4bc63b75ade7182c80ffc400e5ca Mon Sep 17 00:00:00 2001 From: Jens Krause Date: Mon, 10 Jul 2023 15:55:17 +0200 Subject: [PATCH 1/8] Add use_websocket + example --- Cargo.toml | 2 + examples/Cargo.toml | 1 + examples/use_websocket/Cargo.toml | 16 + examples/use_websocket/README.md | 22 ++ examples/use_websocket/Trunk.toml | 2 + examples/use_websocket/index.html | 7 + examples/use_websocket/input.css | 3 + examples/use_websocket/rust-toolchain.toml | 2 + examples/use_websocket/src/main.rs | 203 +++++++++++ examples/use_websocket/style/output.css | 214 +++++++++++ examples/use_websocket/tailwind.config.js | 13 + src/lib.rs | 2 + src/websocket/mod.rs | 3 + src/websocket/use_websocket.rs | 406 +++++++++++++++++++++ 14 files changed, 896 insertions(+) create mode 100644 examples/use_websocket/Cargo.toml create mode 100644 examples/use_websocket/README.md create mode 100644 examples/use_websocket/Trunk.toml create mode 100644 examples/use_websocket/index.html create mode 100644 examples/use_websocket/input.css create mode 100644 examples/use_websocket/rust-toolchain.toml create mode 100644 examples/use_websocket/src/main.rs create mode 100644 examples/use_websocket/style/output.css create mode 100644 examples/use_websocket/tailwind.config.js create mode 100644 src/websocket/mod.rs create mode 100644 src/websocket/use_websocket.rs 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, + } +} From e0a069ead6ae0a65d8e51da841e708d0efb3ab35 Mon Sep 17 00:00:00 2001 From: Jens Krause Date: Tue, 11 Jul 2023 15:08:45 +0200 Subject: [PATCH 2/8] Use `store_value` for non-reactive references to be more Leptos like - no need to do all these Rc> manually. --- examples/use_websocket/style/output.css | 4 - src/websocket/use_websocket.rs | 164 ++++++++++-------------- 2 files changed, 66 insertions(+), 102 deletions(-) diff --git a/examples/use_websocket/style/output.css b/examples/use_websocket/style/output.css index a9427f9..e97073b 100644 --- a/examples/use_websocket/style/output.css +++ b/examples/use_websocket/style/output.css @@ -164,10 +164,6 @@ gap: 1rem } -.p-0 { - padding: 0px -} - .text-2xl { font-size: 1.5rem; line-height: 2rem diff --git a/src/websocket/use_websocket.rs b/src/websocket/use_websocket.rs index 51715a6..bff3a70 100644 --- a/src/websocket/use_websocket.rs +++ b/src/websocket/use_websocket.rs @@ -2,7 +2,7 @@ use leptos::{leptos_dom::helpers::TimeoutHandle, *}; use core::fmt; use std::rc::Rc; -use std::{cell::RefCell, time::Duration}; +use std::time::Duration; use js_sys::Array; use wasm_bindgen::{prelude::*, JsCast, JsValue}; @@ -89,7 +89,7 @@ where /// Latest binary message received from `WebSocket`. pub message_bytes: ReadSignal>>, /// The `WebSocket` instance. - pub ws: Rc>>, + pub ws: Option, pub open: OpenFn, pub close: CloseFn, @@ -124,70 +124,59 @@ pub fn use_websocket_with_options( 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 ws_ref: StoredValue> = store_value(cx, 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 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: Rc>> = Rc::new(RefCell::new(None)); + let reconnect_timer_ref: StoredValue> = store_value(cx, 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 reconnect_times_ref: StoredValue = store_value(cx, 0); + let unmounted_ref = store_value(cx, false); - let connect: Rc>>> = Rc::new(RefCell::new(None)); + let connect_ref: StoredValue>> = store_value(cx, 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(); + let reconnect_ref: StoredValue>> = store_value(cx, None); + reconnect_ref.set_value({ + let ws = ws_ref.get_value(); Some(Rc::new(move || { - if *reconnect_times.borrow() < reconnect_limit + if reconnect_times_ref.get_value() < reconnect_limit && ws - .borrow() - .as_ref() - .map_or(false, |ws: &WebSocket| ws.ready_state() != WebSocket::OPEN) + .clone() + .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() + 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.borrow_mut() = { - let ws = Rc::clone(&ws); + connect_ref.set_value({ + let ws = ws_ref.get_value(); 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 || { + reconnect_timer_ref.set_value(None); { - let web_socket: &mut Option = &mut ws.borrow_mut(); - if let Some(web_socket) = web_socket { + if let Some(web_socket) = &ws { let _ = web_socket.close(); } } @@ -210,15 +199,12 @@ pub fn use_websocket_with_options( // 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() { + if unmounted_ref.get_value() { return; } - let onopen = &mut *onopen.borrow_mut(); - if let Some(onopen) = onopen { + if let Some(onopen) = onopen_ref.get_value().as_mut() { onopen(e); } set_ready_state.set(UseWebSocketReadyState::Open); @@ -230,11 +216,8 @@ pub fn use_websocket_with_options( // 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() { + if unmounted_ref.get_value() { return; } @@ -246,10 +229,8 @@ pub fn use_websocket_with_options( }, |txt| { let txt = String::from(&txt); - let onmessage = &mut *onmessage.borrow_mut(); - if let Some(onmessage) = onmessage { - let txt = txt.clone(); - onmessage(txt); + if let Some(onmessage) = onmessage_ref.get_value().as_mut() { + onmessage(txt.clone()); } set_message.set(Some(txt.clone())); }, @@ -258,8 +239,8 @@ pub fn use_websocket_with_options( |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 { + if let Some(onmessage_bytes) = onmessage_bytes_ref.get_value().as_mut() + { let array = array.clone(); onmessage_bytes(array); } @@ -273,19 +254,17 @@ pub fn use_websocket_with_options( } // onerror handler { - let unmounted = Rc::clone(&unmounted); - let onerror = Rc::clone(&onerror); - let reconnect = Rc::clone(&reconnect); + // let reconnect = reconnect.clone(); let onerror_closure = Closure::wrap(Box::new(move |e: Event| { - if *unmounted.borrow() { + if unmounted_ref.get_value() { return; } - let reconnect: Rc = { reconnect.borrow().as_ref().unwrap().clone() }; - reconnect(); + if let Some(reconnect) = &reconnect_ref.get_value() { + reconnect(); + } - let onerror = &mut *onerror.borrow_mut(); - if let Some(onerror) = onerror { + if let Some(onerror) = onerror_ref.get_value().as_mut() { onerror(e); } set_ready_state.set(UseWebSocketReadyState::Closed); @@ -295,20 +274,16 @@ pub fn use_websocket_with_options( } // 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() { + if unmounted_ref.get_value() { return; } - let reconnect: Rc = { reconnect.borrow().as_ref().unwrap().clone() }; - reconnect(); + if let Some(reconnect) = &reconnect_ref.get_value() { + reconnect(); + } - let onclose = &mut *onclose.borrow_mut(); - if let Some(onclose) = onclose { + if let Some(onclose) = onclose_ref.get_value().as_mut() { onclose(e); } set_ready_state.set(UseWebSocketReadyState::Closed); @@ -318,16 +293,15 @@ pub fn use_websocket_with_options( onclose_closure.forget(); } - *ws.borrow_mut() = Some(web_socket); + ws_ref.set_value(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() { + if let Some(web_socket) = ws_ref.get_value() { let _ = web_socket.send_with_str(&data); } } @@ -336,11 +310,9 @@ pub fn use_websocket_with_options( // 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 { + if let Some(web_socket) = ws_ref.get_value() { let _ = web_socket.send_with_u8_array(&data); } } @@ -349,24 +321,20 @@ pub fn use_websocket_with_options( // 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(); + reconnect_times_ref.set_value(0); + if let Some(connect) = connect_ref.get_value() { + connect(); + } } }; // Close connection let close = { - let ws = Rc::clone(&ws); - let reconnect_times = Rc::clone(&reconnect_times); + reconnect_timer_ref.set_value(None); 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 { + reconnect_times_ref.set_value(reconnect_limit); + if let Some(web_socket) = ws_ref.get_value() { let _ = web_socket.close(); } } @@ -388,7 +356,7 @@ pub fn use_websocket_with_options( { let close = close.clone(); on_cleanup(cx, move || { - *unmounted.borrow_mut() = true; + unmounted_ref.set_value(true); close(); }); } @@ -397,7 +365,7 @@ pub fn use_websocket_with_options( ready_state, message, message_bytes, - ws, + ws: ws_ref.get_value(), open, close, send, From 325db848707c55934b3640f99eeca57483824c39 Mon Sep 17 00:00:00 2001 From: Jens Krause Date: Tue, 11 Jul 2023 15:15:18 +0200 Subject: [PATCH 3/8] clean up --- examples/use_websocket/Trunk.toml | 2 +- examples/use_websocket/input.css | 2 +- examples/use_websocket/style/output.css | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/use_websocket/Trunk.toml b/examples/use_websocket/Trunk.toml index 3e4be08..962e040 100644 --- a/examples/use_websocket/Trunk.toml +++ b/examples/use_websocket/Trunk.toml @@ -1,2 +1,2 @@ [build] -public_url = "/demo/" \ No newline at end of file +public_url = "/demo/" diff --git a/examples/use_websocket/input.css b/examples/use_websocket/input.css index bd6213e..b5c61c9 100644 --- a/examples/use_websocket/input.css +++ b/examples/use_websocket/input.css @@ -1,3 +1,3 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; diff --git a/examples/use_websocket/style/output.css b/examples/use_websocket/style/output.css index e97073b..c5dc82c 100644 --- a/examples/use_websocket/style/output.css +++ b/examples/use_websocket/style/output.css @@ -207,4 +207,4 @@ font-size: 2.25rem; line-height: 2.5rem } -} \ No newline at end of file +} From 12eb953c70aece14de252ca10674c5cb538b5caa Mon Sep 17 00:00:00 2001 From: Jens Krause Date: Tue, 11 Jul 2023 19:23:43 +0200 Subject: [PATCH 4/8] Address review suggestion, update doc + book --- README.md | 2 +- docs/book/src/SUMMARY.md | 6 +- docs/book/src/websocket/use_websocket.md | 3 + examples/use_websocket/src/main.rs | 26 ++- src/websocket/use_websocket.rs | 239 ++++++++++++++--------- 5 files changed, 173 insertions(+), 103 deletions(-) create mode 100644 docs/book/src/websocket/use_websocket.md diff --git a/README.md b/README.md index 8d97917..43d2f36 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ cargo test --all-features First you need to install ```shell -cargo install mdbook-cmdrun trunk +cargo install mdbook mdbook-cmdrun trunk ``` To build the book go in your terminal into the docs/book folder diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 9941910..8e38c3d 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -11,6 +11,10 @@ - [use_session_storage](storage/use_session_storage.md) - [use_storage](storage/use_storage.md) +# @WebSocket + +- [use_websocket](websocket/use_websocket.md) + # Elements - [use_active_element](elements/use_active_element.md) @@ -68,4 +72,4 @@ - [use_floor](math/use_floor.md) - [use_max](math/use_max.md) - [use_min](math/use_min.md) -- [use_round](math/use_round.md) \ No newline at end of file +- [use_round](math/use_round.md) diff --git a/docs/book/src/websocket/use_websocket.md b/docs/book/src/websocket/use_websocket.md new file mode 100644 index 0000000..389c51c --- /dev/null +++ b/docs/book/src/websocket/use_websocket.md @@ -0,0 +1,3 @@ +# use_websocket + + diff --git a/examples/use_websocket/src/main.rs b/examples/use_websocket/src/main.rs index 54a87f2..8b9ddde 100644 --- a/examples/use_websocket/src/main.rs +++ b/examples/use_websocket/src/main.rs @@ -77,34 +77,32 @@ fn Demo(cx: Scope) -> impl IntoView { } = use_websocket_with_options( cx, "wss://echo.websocket.events/".to_string(), - UseWebSocketOptions { - manual: true, - onopen: Some(Box::new(move |e| { + UseWebSocketOptions::default() + .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| { + }))) + .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| { + }))) + .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| { + }))) + .onmessage(Some(Box::new(move |m| { set_history2 .update(|history: &mut Vec<_>| history.push(format! {"[onmessage]: {:?}", m})); - })), - onmessage_bytes: Some(Box::new(move |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 |_| { diff --git a/src/websocket/use_websocket.rs b/src/websocket/use_websocket.rs index bff3a70..68deea5 100644 --- a/src/websocket/use_websocket.rs +++ b/src/websocket/use_websocket.rs @@ -4,6 +4,7 @@ use core::fmt; use std::rc::Rc; use std::time::Duration; +use default_struct_builder::DefaultBuilder; use js_sys::Array; use wasm_bindgen::{prelude::*, JsCast, JsValue}; use web_sys::{BinaryType, Event, MessageEvent, WebSocket}; @@ -12,91 +13,67 @@ 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: Option, - - pub open: OpenFn, - pub close: CloseFn, - pub send: SendFn, - pub send_bytes: SendBytesFn, -} - +/// Creating and managing a [Websocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) connection. +/// +/// ## Demo +/// +/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_websocket) +/// +/// ## Usage +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::websocket::*; +/// # +/// # #[component] +/// # fn Demo(cx: Scope) -> impl IntoView { +/// 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()); +/// }; +/// +/// let send_byte_message = move |_| { +/// let m = b"Hello, world!\r\n".to_vec(); +/// send_bytes(m.clone()); +/// }; +/// +/// 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(); +/// }; +/// +/// view! { cx, +///
+///

"status: " {status}

+/// button on:click=send_message disabled=move || !connected()>"Send" +/// +/// +/// +///

"Receive message: " {format! {"{:?}", message}}

+///

"Receive byte message: " {format! {"{:?}", message_bytes}}

+///
+/// } +/// # } +/// ``` +// #[doc(cfg(feature = "websocket"))] pub fn use_websocket( cx: Scope, url: String, @@ -110,7 +87,7 @@ pub fn use_websocket( } /// Version of [`use_websocket`] that takes `UseWebSocketOptions`. See [`use_websocket`] for how to use. - +// #[doc(cfg(feature = "websocket"))] pub fn use_websocket_with_options( cx: Scope, url: String, @@ -254,7 +231,6 @@ pub fn use_websocket_with_options( } // onerror handler { - // let reconnect = reconnect.clone(); let onerror_closure = Closure::wrap(Box::new(move |e: Event| { if unmounted_ref.get_value() { return; @@ -372,3 +348,92 @@ pub fn use_websocket_with_options( send_bytes, } } + +/// The current state of the `WebSocket` connection. +// #[doc(cfg(feature = "websocket"))] +#[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 [`use_websocket_with_options`]. +// #[doc(cfg(feature = "websocket"))] +#[derive(DefaultBuilder, Clone)] +pub struct UseWebSocketOptions { + /// `WebSocket` connect callback. + onopen: Option>>, + /// `WebSocket` message callback for text. + onmessage: Option>>, + /// `WebSocket` message callback for binary. + onmessage_bytes: Option>>>, + /// `WebSocket` error callback. + onerror: Option>>, + /// `WebSocket` close callback. + onclose: Option>>, + /// Retry times. + reconnect_limit: Option, + /// Retry interval(ms). + reconnect_interval: Option, + /// Manually starts connection + manual: bool, + /// Sub protocols + 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`]. +// #[doc(cfg(feature = "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: Option, + /// Opens the `WebSocket` connection + pub open: OpenFn, + /// Closes the `WebSocket` connection + pub close: CloseFn, + /// Sends `text` (string) based data + pub send: SendFn, + /// Sends binary data + pub send_bytes: SendBytesFn, +} From 2c62c85801d3b92c975392f0693eae617ed72ddd Mon Sep 17 00:00:00 2001 From: Jens Krause Date: Wed, 12 Jul 2023 09:20:43 +0200 Subject: [PATCH 5/8] WIP: Use `builder(into)` - still failing --- examples/use_websocket/src/main.rs | 22 ++++++----- src/websocket/use_websocket.rs | 63 +++++++++++++++--------------- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/examples/use_websocket/src/main.rs b/examples/use_websocket/src/main.rs index 8b9ddde..42541fe 100644 --- a/examples/use_websocket/src/main.rs +++ b/examples/use_websocket/src/main.rs @@ -2,6 +2,8 @@ use leptos::*; use leptos_use::docs::demo_or_body; use leptos_use::websocket::*; +use web_sys::{CloseEvent, Event}; + #[component] fn Demo(cx: Scope) -> impl IntoView { let (history, set_history) = create_signal(cx, vec![]); @@ -79,30 +81,30 @@ fn Demo(cx: Scope) -> impl IntoView { "wss://echo.websocket.events/".to_string(), UseWebSocketOptions::default() .manual(true) - .onopen(Some(Box::new(move |e| { + .onopen(move |e: Event| { set_history2.update(|history: &mut Vec<_>| { history.push(format! {"[onopen]: event {:?}", e.type_()}) }); - }))) - .onclose(Some(Box::new(move |e| { + }) + .onclose(move |e: CloseEvent| { set_history2.update(|history: &mut Vec<_>| { history.push(format! {"[onclose]: event {:?}", e.type_()}) }); - }))) - .onerror(Some(Box::new(move |e| { + }) + .onerror(move |e: Event| { set_history2.update(|history: &mut Vec<_>| { history.push(format! {"[onerror]: event {:?}", e.type_()}) }); - }))) - .onmessage(Some(Box::new(move |m| { + }) + .onmessage(move |m: String| { set_history2 .update(|history: &mut Vec<_>| history.push(format! {"[onmessage]: {:?}", m})); - }))) - .onmessage_bytes(Some(Box::new(move |m| { + }) + .onmessage_bytes(move |m: Vec| { set_history2.update(|history: &mut Vec<_>| { history.push(format! {"[onmessage_bytes]: {:?}", m}) }); - }))), + }), ); let open_connection2 = move |_| { diff --git a/src/websocket/use_websocket.rs b/src/websocket/use_websocket.rs index 68deea5..c9e878b 100644 --- a/src/websocket/use_websocket.rs +++ b/src/websocket/use_websocket.rs @@ -7,9 +7,7 @@ use std::time::Duration; use default_struct_builder::DefaultBuilder; use js_sys::Array; use wasm_bindgen::{prelude::*, JsCast, JsValue}; -use web_sys::{BinaryType, Event, MessageEvent, WebSocket}; - -pub use web_sys::CloseEvent; +use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket}; use crate::utils::CloneableFnMutWithArg; @@ -181,9 +179,9 @@ pub fn use_websocket_with_options( return; } - if let Some(onopen) = onopen_ref.get_value().as_mut() { - onopen(e); - } + let mut onopen = onopen_ref.get_value(); + onopen(e); + set_ready_state.set(UseWebSocketReadyState::Open); }) as Box); web_socket.set_onopen(Some(onopen_closure.as_ref().unchecked_ref())); @@ -206,9 +204,9 @@ pub fn use_websocket_with_options( }, |txt| { let txt = String::from(&txt); - if let Some(onmessage) = onmessage_ref.get_value().as_mut() { - onmessage(txt.clone()); - } + let mut onmessage = onmessage_ref.get_value(); + onmessage(txt.clone()); + set_message.set(Some(txt.clone())); }, ); @@ -216,11 +214,9 @@ pub fn use_websocket_with_options( |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); - } + let mut onmessage_bytes = onmessage_bytes_ref.get_value(); + onmessage_bytes(array.clone()); + set_message_bytes.set(Some(array)); }, ); @@ -240,9 +236,9 @@ pub fn use_websocket_with_options( reconnect(); } - if let Some(onerror) = onerror_ref.get_value().as_mut() { - onerror(e); - } + let mut onerror = onerror_ref.get_value(); + onerror(e); + set_ready_state.set(UseWebSocketReadyState::Closed); }) as Box); web_socket.set_onerror(Some(onerror_closure.as_ref().unchecked_ref())); @@ -259,9 +255,9 @@ pub fn use_websocket_with_options( reconnect(); } - if let Some(onclose) = onclose_ref.get_value().as_mut() { - onclose(e); - } + let mut onclose = onclose_ref.get_value(); + onclose(e); + set_ready_state.set(UseWebSocketReadyState::Closed); }) as Box); @@ -372,18 +368,23 @@ impl fmt::Display for UseWebSocketReadyState { /// Options for [`use_websocket_with_options`]. // #[doc(cfg(feature = "websocket"))] -#[derive(DefaultBuilder, Clone)] +#[derive(DefaultBuilder)] pub struct UseWebSocketOptions { /// `WebSocket` connect callback. - onopen: Option>>, + #[builder(into)] + onopen: Box>, /// `WebSocket` message callback for text. - onmessage: Option>>, + #[builder(into)] + onmessage: Box>, /// `WebSocket` message callback for binary. - onmessage_bytes: Option>>>, + #[builder(into)] + onmessage_bytes: Box>>, /// `WebSocket` error callback. - onerror: Option>>, + #[builder(into)] + onerror: Box>, /// `WebSocket` close callback. - onclose: Option>>, + #[builder(into)] + onclose: Box>, /// Retry times. reconnect_limit: Option, /// Retry interval(ms). @@ -397,11 +398,11 @@ pub struct UseWebSocketOptions { impl Default for UseWebSocketOptions { fn default() -> Self { Self { - onopen: None, - onmessage: None, - onmessage_bytes: None, - onerror: None, - onclose: None, + onopen: Box::new(|_| {}), + onmessage: Box::new(|_| {}), + onmessage_bytes: Box::new(|_| {}), + onerror: Box::new(|_| {}), + onclose: Box::new(|_| {}), reconnect_limit: Some(3), reconnect_interval: Some(3 * 1000), manual: false, From 7ce480e9ad945df231c2cd42a200a47ec0bcc751 Mon Sep 17 00:00:00 2001 From: Jens Krause Date: Thu, 13 Jul 2023 07:33:26 +0200 Subject: [PATCH 6/8] Bump `default-struct-builder` to fix unboxing issues --- Cargo.toml | 2 +- src/websocket/use_websocket.rs | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8969138..f6db118 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ homepage = "https://leptos-use.rs" leptos = "0.4" wasm-bindgen = "0.2" js-sys = "0.3" -default-struct-builder = "0.3" +default-struct-builder = "0.4" num = { version = "0.4", optional = true } serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } diff --git a/src/websocket/use_websocket.rs b/src/websocket/use_websocket.rs index c9e878b..8f7fccf 100644 --- a/src/websocket/use_websocket.rs +++ b/src/websocket/use_websocket.rs @@ -371,20 +371,15 @@ impl fmt::Display for UseWebSocketReadyState { #[derive(DefaultBuilder)] pub struct UseWebSocketOptions { /// `WebSocket` connect callback. - #[builder(into)] - onopen: Box>, + onopen: Box + 'static>, /// `WebSocket` message callback for text. - #[builder(into)] - onmessage: Box>, + onmessage: Box + 'static>, /// `WebSocket` message callback for binary. - #[builder(into)] - onmessage_bytes: Box>>, + onmessage_bytes: Box> + 'static>, /// `WebSocket` error callback. - #[builder(into)] - onerror: Box>, + onerror: Box + 'static>, /// `WebSocket` close callback. - #[builder(into)] - onclose: Box>, + onclose: Box + 'static>, /// Retry times. reconnect_limit: Option, /// Retry interval(ms). From e4f221f275c2ec535e1cf097a979b7edba87201b Mon Sep 17 00:00:00 2001 From: Jens Krause Date: Thu, 13 Jul 2023 08:32:30 +0200 Subject: [PATCH 7/8] CloneableFnMutWithArg -> CloneableFnWithArg --- examples/use_websocket/src/main.rs | 56 +++++++++++++++++------------- src/websocket/use_websocket.rs | 22 ++++++------ 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/examples/use_websocket/src/main.rs b/examples/use_websocket/src/main.rs index 42541fe..ecb8488 100644 --- a/examples/use_websocket/src/main.rs +++ b/examples/use_websocket/src/main.rs @@ -67,6 +67,33 @@ fn Demo(cx: Scope) -> impl IntoView { let (history2, set_history2) = create_signal(cx, vec![]); + let onopen = move |e: Event| { + set_history2.update(|history: &mut Vec<_>| { + history.push(format! {"[onopen]: event {:?}", e.type_()}) + }); + }; + + let onclose = move |e: CloseEvent| { + set_history2.update(|history: &mut Vec<_>| { + history.push(format! {"[onclose]: event {:?}", e.type_()}) + }); + }; + + let onerror = move |e: Event| { + set_history2.update(|history: &mut Vec<_>| { + history.push(format! {"[onerror]: event {:?}", e.type_()}) + }); + }; + + let onmessage = move |m: String| { + set_history2.update(|history: &mut Vec<_>| history.push(format! {"[onmessage]: {:?}", m})); + }; + + let onmessage_bytes = move |m: Vec| { + set_history2 + .update(|history: &mut Vec<_>| history.push(format! {"[onmessage_bytes]: {:?}", m})); + }; + let UseWebsocketReturn { ready_state: ready_state2, send: send2, @@ -81,30 +108,11 @@ fn Demo(cx: Scope) -> impl IntoView { "wss://echo.websocket.events/".to_string(), UseWebSocketOptions::default() .manual(true) - .onopen(move |e: Event| { - set_history2.update(|history: &mut Vec<_>| { - history.push(format! {"[onopen]: event {:?}", e.type_()}) - }); - }) - .onclose(move |e: CloseEvent| { - set_history2.update(|history: &mut Vec<_>| { - history.push(format! {"[onclose]: event {:?}", e.type_()}) - }); - }) - .onerror(move |e: Event| { - set_history2.update(|history: &mut Vec<_>| { - history.push(format! {"[onerror]: event {:?}", e.type_()}) - }); - }) - .onmessage(move |m: String| { - set_history2 - .update(|history: &mut Vec<_>| history.push(format! {"[onmessage]: {:?}", m})); - }) - .onmessage_bytes(move |m: Vec| { - set_history2.update(|history: &mut Vec<_>| { - history.push(format! {"[onmessage_bytes]: {:?}", m}) - }); - }), + .onopen(onopen.clone()) + .onclose(onclose.clone()) + .onerror(onerror.clone()) + .onmessage(onmessage.clone()) + .onmessage_bytes(onmessage_bytes.clone()), ); let open_connection2 = move |_| { diff --git a/src/websocket/use_websocket.rs b/src/websocket/use_websocket.rs index 8f7fccf..24c57d0 100644 --- a/src/websocket/use_websocket.rs +++ b/src/websocket/use_websocket.rs @@ -9,7 +9,7 @@ use js_sys::Array; use wasm_bindgen::{prelude::*, JsCast, JsValue}; use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket}; -use crate::utils::CloneableFnMutWithArg; +use crate::utils::CloneableFnWithArg; /// Creating and managing a [Websocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) connection. /// @@ -179,7 +179,7 @@ pub fn use_websocket_with_options( return; } - let mut onopen = onopen_ref.get_value(); + let onopen = onopen_ref.get_value(); onopen(e); set_ready_state.set(UseWebSocketReadyState::Open); @@ -204,7 +204,7 @@ pub fn use_websocket_with_options( }, |txt| { let txt = String::from(&txt); - let mut onmessage = onmessage_ref.get_value(); + let onmessage = onmessage_ref.get_value(); onmessage(txt.clone()); set_message.set(Some(txt.clone())); @@ -214,7 +214,7 @@ pub fn use_websocket_with_options( |array_buffer| { let array = js_sys::Uint8Array::new(&array_buffer); let array = array.to_vec(); - let mut onmessage_bytes = onmessage_bytes_ref.get_value(); + let onmessage_bytes = onmessage_bytes_ref.get_value(); onmessage_bytes(array.clone()); set_message_bytes.set(Some(array)); @@ -236,7 +236,7 @@ pub fn use_websocket_with_options( reconnect(); } - let mut onerror = onerror_ref.get_value(); + let onerror = onerror_ref.get_value(); onerror(e); set_ready_state.set(UseWebSocketReadyState::Closed); @@ -255,7 +255,7 @@ pub fn use_websocket_with_options( reconnect(); } - let mut onclose = onclose_ref.get_value(); + let onclose = onclose_ref.get_value(); onclose(e); set_ready_state.set(UseWebSocketReadyState::Closed); @@ -371,15 +371,15 @@ impl fmt::Display for UseWebSocketReadyState { #[derive(DefaultBuilder)] pub struct UseWebSocketOptions { /// `WebSocket` connect callback. - onopen: Box + 'static>, + onopen: Box + 'static>, /// `WebSocket` message callback for text. - onmessage: Box + 'static>, + onmessage: Box + 'static>, /// `WebSocket` message callback for binary. - onmessage_bytes: Box> + 'static>, + onmessage_bytes: Box> + 'static>, /// `WebSocket` error callback. - onerror: Box + 'static>, + onerror: Box + 'static>, /// `WebSocket` close callback. - onclose: Box + 'static>, + onclose: Box + 'static>, /// Retry times. reconnect_limit: Option, /// Retry interval(ms). From bd65a4de9243e81a118fc85c6c101fa14c2a3fd4 Mon Sep 17 00:00:00 2001 From: Jens Krause Date: Thu, 13 Jul 2023 08:38:11 +0200 Subject: [PATCH 8/8] on{name}-> on_{name} for using same naming conventions for callbacks all over the place --- examples/use_websocket/src/main.rs | 20 ++++++------ src/websocket/use_websocket.rs | 50 +++++++++++++++--------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/examples/use_websocket/src/main.rs b/examples/use_websocket/src/main.rs index ecb8488..e5a6eb7 100644 --- a/examples/use_websocket/src/main.rs +++ b/examples/use_websocket/src/main.rs @@ -67,29 +67,29 @@ fn Demo(cx: Scope) -> impl IntoView { let (history2, set_history2) = create_signal(cx, vec![]); - let onopen = move |e: Event| { + let on_open_callback = move |e: Event| { set_history2.update(|history: &mut Vec<_>| { history.push(format! {"[onopen]: event {:?}", e.type_()}) }); }; - let onclose = move |e: CloseEvent| { + let on_close_callback = move |e: CloseEvent| { set_history2.update(|history: &mut Vec<_>| { history.push(format! {"[onclose]: event {:?}", e.type_()}) }); }; - let onerror = move |e: Event| { + let on_error_callback = move |e: Event| { set_history2.update(|history: &mut Vec<_>| { history.push(format! {"[onerror]: event {:?}", e.type_()}) }); }; - let onmessage = move |m: String| { + let on_message_callback = move |m: String| { set_history2.update(|history: &mut Vec<_>| history.push(format! {"[onmessage]: {:?}", m})); }; - let onmessage_bytes = move |m: Vec| { + let on_message_bytes_callback = move |m: Vec| { set_history2 .update(|history: &mut Vec<_>| history.push(format! {"[onmessage_bytes]: {:?}", m})); }; @@ -108,11 +108,11 @@ fn Demo(cx: Scope) -> impl IntoView { "wss://echo.websocket.events/".to_string(), UseWebSocketOptions::default() .manual(true) - .onopen(onopen.clone()) - .onclose(onclose.clone()) - .onerror(onerror.clone()) - .onmessage(onmessage.clone()) - .onmessage_bytes(onmessage_bytes.clone()), + .on_open(on_open_callback.clone()) + .on_close(on_close_callback.clone()) + .on_error(on_error_callback.clone()) + .on_message(on_message_callback.clone()) + .on_message_bytes(on_message_bytes_callback.clone()), ); let open_connection2 = move |_| { diff --git a/src/websocket/use_websocket.rs b/src/websocket/use_websocket.rs index 24c57d0..14b002b 100644 --- a/src/websocket/use_websocket.rs +++ b/src/websocket/use_websocket.rs @@ -101,11 +101,11 @@ pub fn use_websocket_with_options( let (message_bytes, set_message_bytes) = create_signal(cx, None); let ws_ref: StoredValue> = 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 on_open_ref = store_value(cx, options.on_open); + let on_message_ref = store_value(cx, options.on_message); + let on_message_bytes_ref = store_value(cx, options.on_message_bytes); + let on_error_ref = store_value(cx, options.on_error); + let on_close_ref = store_value(cx, options.on_close); let reconnect_limit = options.reconnect_limit.unwrap_or(3); let reconnect_interval = options.reconnect_interval.unwrap_or(3 * 1000); @@ -179,8 +179,8 @@ pub fn use_websocket_with_options( return; } - let onopen = onopen_ref.get_value(); - onopen(e); + let callback = on_open_ref.get_value(); + callback(e); set_ready_state.set(UseWebSocketReadyState::Open); }) as Box); @@ -204,8 +204,8 @@ pub fn use_websocket_with_options( }, |txt| { let txt = String::from(&txt); - let onmessage = onmessage_ref.get_value(); - onmessage(txt.clone()); + let callback = on_message_ref.get_value(); + callback(txt.clone()); set_message.set(Some(txt.clone())); }, @@ -214,8 +214,8 @@ pub fn use_websocket_with_options( |array_buffer| { let array = js_sys::Uint8Array::new(&array_buffer); let array = array.to_vec(); - let onmessage_bytes = onmessage_bytes_ref.get_value(); - onmessage_bytes(array.clone()); + let callback = on_message_bytes_ref.get_value(); + callback(array.clone()); set_message_bytes.set(Some(array)); }, @@ -236,8 +236,8 @@ pub fn use_websocket_with_options( reconnect(); } - let onerror = onerror_ref.get_value(); - onerror(e); + let callback = on_error_ref.get_value(); + callback(e); set_ready_state.set(UseWebSocketReadyState::Closed); }) as Box); @@ -255,8 +255,8 @@ pub fn use_websocket_with_options( reconnect(); } - let onclose = onclose_ref.get_value(); - onclose(e); + let callback = on_close_ref.get_value(); + callback(e); set_ready_state.set(UseWebSocketReadyState::Closed); }) @@ -371,15 +371,15 @@ impl fmt::Display for UseWebSocketReadyState { #[derive(DefaultBuilder)] pub struct UseWebSocketOptions { /// `WebSocket` connect callback. - onopen: Box + 'static>, + on_open: Box + 'static>, /// `WebSocket` message callback for text. - onmessage: Box + 'static>, + on_message: Box + 'static>, /// `WebSocket` message callback for binary. - onmessage_bytes: Box> + 'static>, + on_message_bytes: Box> + 'static>, /// `WebSocket` error callback. - onerror: Box + 'static>, + on_error: Box + 'static>, /// `WebSocket` close callback. - onclose: Box + 'static>, + on_close: Box + 'static>, /// Retry times. reconnect_limit: Option, /// Retry interval(ms). @@ -393,11 +393,11 @@ pub struct UseWebSocketOptions { impl Default for UseWebSocketOptions { fn default() -> Self { Self { - onopen: Box::new(|_| {}), - onmessage: Box::new(|_| {}), - onmessage_bytes: Box::new(|_| {}), - onerror: Box::new(|_| {}), - onclose: Box::new(|_| {}), + on_open: Box::new(|_| {}), + on_message: Box::new(|_| {}), + on_message_bytes: Box::new(|_| {}), + on_error: Box::new(|_| {}), + on_close: Box::new(|_| {}), reconnect_limit: Some(3), reconnect_interval: Some(3 * 1000), manual: false,