diff --git a/Cargo.toml b/Cargo.toml index 80cc532..f6db118 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ lazy_static = "1" version = "0.3" features = [ "CssStyleDeclaration", + "CloseEvent", "CustomEvent", "CustomEventInit", "DomRectReadOnly", @@ -63,6 +64,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/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/Cargo.toml b/examples/Cargo.toml index 3e8bfcd..3374a1a 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -29,6 +29,7 @@ members = [ "use_scroll", "use_storage", "use_throttle_fn", + "use_websocket", "use_window_focus", "use_window_scroll", "watch_debounced", 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..962e040 --- /dev/null +++ b/examples/use_websocket/Trunk.toml @@ -0,0 +1,2 @@ +[build] +public_url = "/demo/" 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..b5c61c9 --- /dev/null +++ b/examples/use_websocket/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; 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..e5a6eb7 --- /dev/null +++ b/examples/use_websocket/src/main.rs @@ -0,0 +1,211 @@ +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![]); + + 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 on_open_callback = move |e: Event| { + set_history2.update(|history: &mut Vec<_>| { + history.push(format! {"[onopen]: event {:?}", e.type_()}) + }); + }; + + let on_close_callback = move |e: CloseEvent| { + set_history2.update(|history: &mut Vec<_>| { + history.push(format! {"[onclose]: event {:?}", e.type_()}) + }); + }; + + let on_error_callback = move |e: Event| { + set_history2.update(|history: &mut Vec<_>| { + history.push(format! {"[onerror]: event {:?}", e.type_()}) + }); + }; + + let on_message_callback = move |m: String| { + set_history2.update(|history: &mut Vec<_>| history.push(format! {"[onmessage]: {:?}", m})); + }; + + let on_message_bytes_callback = move |m: Vec| { + set_history2 + .update(|history: &mut Vec<_>| history.push(format! {"[onmessage_bytes]: {:?}", m})); + }; + + 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::default() + .manual(true) + .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 |_| { + 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..c5dc82c --- /dev/null +++ b/examples/use_websocket/style/output.css @@ -0,0 +1,210 @@ +*, ::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 +} + +.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 + } +} 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 9134db4..23e7d23 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..14b002b --- /dev/null +++ b/src/websocket/use_websocket.rs @@ -0,0 +1,435 @@ +use leptos::{leptos_dom::helpers::TimeoutHandle, *}; + +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, CloseEvent, Event, MessageEvent, WebSocket}; + +use crate::utils::CloneableFnWithArg; + +/// 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, +) -> 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. +// #[doc(cfg(feature = "websocket"))] +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_ref: StoredValue> = store_value(cx, None); + + 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); + + let reconnect_timer_ref: StoredValue> = store_value(cx, None); + let manual = options.manual; + let protocols = options.protocols; + + let reconnect_times_ref: StoredValue = store_value(cx, 0); + let unmounted_ref = store_value(cx, false); + + let connect_ref: StoredValue>> = store_value(cx, None); + + 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_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::(); + 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; + } + + let callback = on_open_ref.get_value(); + callback(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 onmessage_closure = Closure::wrap(Box::new(move |e: MessageEvent| { + if unmounted_ref.get_value() { + 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 callback = on_message_ref.get_value(); + callback(txt.clone()); + + set_message.set(Some(txt.clone())); + }, + ); + }, + |array_buffer| { + let array = js_sys::Uint8Array::new(&array_buffer); + let array = array.to_vec(); + let callback = on_message_bytes_ref.get_value(); + callback(array.clone()); + + 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 onerror_closure = Closure::wrap(Box::new(move |e: Event| { + if unmounted_ref.get_value() { + return; + } + + if let Some(reconnect) = &reconnect_ref.get_value() { + reconnect(); + } + + let callback = on_error_ref.get_value(); + callback(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 onclose_closure = Closure::wrap(Box::new(move |e: CloseEvent| { + if unmounted_ref.get_value() { + return; + } + + if let Some(reconnect) = &reconnect_ref.get_value() { + reconnect(); + } + + let callback = on_close_ref.get_value(); + callback(e); + + set_ready_state.set(UseWebSocketReadyState::Closed); + }) + as Box); + 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| { + 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, + } +} + +/// 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)] +pub struct UseWebSocketOptions { + /// `WebSocket` connect callback. + on_open: Box + 'static>, + /// `WebSocket` message callback for text. + on_message: Box + 'static>, + /// `WebSocket` message callback for binary. + on_message_bytes: Box> + 'static>, + /// `WebSocket` error callback. + on_error: Box + 'static>, + /// `WebSocket` close callback. + on_close: Box + 'static>, + /// 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 { + 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, + 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, +}