From 4d42f7234af46f1bfc253393313d69c533bbab41 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Mon, 1 Jul 2024 02:29:27 +0100 Subject: [PATCH] use_websocket now uses codecs --- .github/workflows/ci.yml | 6 +- .github/workflows/tests.yml | 6 +- .idea/leptos-use.iml | 1 + CHANGELOG.md | 51 ++++- docs/book/src/SUMMARY.md | 3 + examples/use_storage/Cargo.toml | 2 +- examples/use_websocket/Cargo.toml | 3 +- examples/use_websocket/src/main.rs | 104 ++++------ src/storage/use_storage.rs | 21 +- src/use_cookie.rs | 15 +- src/use_device_pixel_ratio.rs | 5 +- src/use_event_source.rs | 10 +- src/use_websocket.rs | 273 ++++++++++++++++++++------ src/use_webtransport.rs | 2 +- src/utils/codecs/bin/bincode_serde.rs | 1 - src/utils/codecs/bin/mod.rs | 4 +- src/utils/codecs/bin/msgpack_serde.rs | 1 - src/utils/codecs/hybrid.rs | 108 ++++++++++ src/utils/codecs/mod.rs | 18 +- src/utils/codecs/string/base64.rs | 2 +- 20 files changed, 442 insertions(+), 194 deletions(-) create mode 100644 src/utils/codecs/hybrid.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a12fb58..2cc0813 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,11 +32,11 @@ jobs: - name: Clippy run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings - name: Run tests (general) - run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde + run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64 - name: Run tests (axum) - run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,axum --doc use_cookie::use_cookie + run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64,axum --doc use_cookie::use_cookie - name: Run tests (actix) - run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,actix --doc use_cookie::use_cookie + run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64,actix --doc use_cookie::use_cookie #### mdbook - name: Install mdbook I diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a04226a..559a729 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,8 +25,8 @@ jobs: - name: Clippy run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings - name: Run tests (general) - run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde + run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64 - name: Run tests (axum) - run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,axum --doc use_cookie::use_cookie + run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64,axum --doc use_cookie::use_cookie - name: Run tests (actix) - run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,actix --doc use_cookie::use_cookie + run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64,actix --doc use_cookie::use_cookie diff --git a/.idea/leptos-use.iml b/.idea/leptos-use.iml index 66b8cda..7d56fcb 100644 --- a/.idea/leptos-use.iml +++ b/.idea/leptos-use.iml @@ -76,6 +76,7 @@ + diff --git a/CHANGELOG.md b/CHANGELOG.md index c4a94f4..a9d18f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,22 +5,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Changes 🔥 +### New Features 🚀 +- There are now binary codecs in addition to string codecs. + - `FromToBytesCodec` + - `WebpackSerdeCodec` (requires feature `webpack_serde`) + - `BincodeSerdeCodec` (requires feature `bincode_serde`) + - `ProstCodec` (requires feature `prost`) (see also the section "Breaking Changes 🛠" below) +- Every binary codec can be used as a string codec with the `Base64` wrapper which encodes the binary data as a base64 + string. + - This required feature `base64` + - It can be wrapped for example like this: `Base64`. +- There is now an `OptionCodec` wrapper that allows to wrap any string codec that encodes `T` to encode `Option`. + - Use it like this: `OptionCodec>`. - `ElementMaybeSignal` is now implemented for `websys::HtmlElement` (thanks to @blorbb). - `UseStorageOptions` now has `delay_during_hydration` which has to be used when you conditionally show parts of the DOM controlled by a value from storage. This leads to hydration errors which can be fixed by setting this new option to `true`. - `cookie::SameSite` is now re-exported -- Fixed typo in compiler error messages in `use_cookie` (thanks to @SleeplessOne1917). +- New book chapter about codecs ### Breaking Changes 🛠 -- `UseStorageOptions` no longer accepts a `codec` value because this is already provided as a generic parameter to - the respective function calls. -- `UseWebsocketOptions::reconnect_limit` is now `ReconnectLimit` instead of `u64`. Use `ReconnectLimit::Infinite` for - infinite retries or `ReconnectLimit::Limited(...)` for limited retries. -- `StringCodec::decode` now takes a `&str` instead of a `String`. +- `UseStorageOptions` and `UseEventSourceOptions` no longer accept a `codec` value because this is already provided as a + generic parameter to the respective function calls. +- Codecs have been refactored. There are now two traits that codecs implement: `Encoder` and `Decoder`. The + trait `StringCodec` is gone. The methods are now associated methods and their params now always take references. + - `JsonCodec` has been renamed to `JsonSerdeCodec`. + - The feature to enable this codec is now called `json_serde` instead of just `serde`. + - `ProstCodec` now encodes as binary data. If you want to keep using it with string data you can wrap it like + this: `Base64`. You have to enable both features `prost` and `base64` for this. +- `use_websocket`: + - `UseWebsocketOptions` has been renamed to `UseWebSocketOptions` (uppercase S) to be consistent with the return + type. + - `UseWebSocketOptions::reconnect_limit` and `UseEventSourceOptions::reconnect_limit` is now `ReconnectLimit` + instead + of `u64`. Use `ReconnectLimit::Infinite` for infinite retries or `ReconnectLimit::Limited(...)` for limited + retries. + - `use_websocket` now uses codecs to send typed messages over the network. + - When calling you have give type parameters for the message type and the + codec: `use_websocket::` + - You can use binary or string codecs. + - The `UseWebSocketReturn::send` closure now takes a `&T` which is encoded using the codec. + - The `UseWebSocketReturn::message` signal now returns an `Option` which is decoded using the codec. + - `UseWebSocketReturn::send_bytes` and `UseWebSocketReturn::message_bytes` are gone. + - `UseWebSocketOptions::on_message` and `UseWebSocketOptions::on_message_bytes` have been renamed + to `on_message_raw` and `on_message_raw_bytes`. + - The new `UseWebSocketOptions::on_message` takes a `&T`. + - `UseWebSocketOptions::on_error` now takes a `UseWebSocketError` instead of a `web_sys::Event`. + +### Fixes 🍕 + +- Fixed auto-reconnect in `use_websocket` +- Fixed typo in compiler error messages in `use_cookie` (thanks to @SleeplessOne1917). ## [0.10.10] - 2024-05-10 diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 51ca13f..d43fb8a 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -4,6 +4,7 @@ [Get Started](get_started.md) [Element Parameters](element_parameters.md) [Server-Side Rendering](server_side_rendering.md) +[Encoding and Decoding Data](codecs.md) [Changelog](changelog.md) [Functions](functions.md) @@ -65,6 +66,7 @@ - [use_event_source](network/use_event_source.md) - [use_websocket](network/use_websocket.md) + # Animation @@ -94,6 +96,7 @@ - [use_sorted](iterable/use_sorted.md) # Utilities + - [is_err](utilities/is_err.md) - [is_none](utilities/is_none.md) - [is_ok](utilities/is_ok.md) diff --git a/examples/use_storage/Cargo.toml b/examples/use_storage/Cargo.toml index a4ba692..98ebbf3 100644 --- a/examples/use_storage/Cargo.toml +++ b/examples/use_storage/Cargo.toml @@ -8,7 +8,7 @@ leptos = { version = "0.6", features = ["nightly", "csr"] } console_error_panic_hook = "0.1" console_log = "1" log = "0.4" -leptos-use = { path = "../..", features = ["docs", "prost", "serde"] } +leptos-use = { path = "../..", features = ["docs", "json_serde"] } web-sys = "0.3" serde = "1.0.163" diff --git a/examples/use_websocket/Cargo.toml b/examples/use_websocket/Cargo.toml index 2f1f0a5..76baa77 100644 --- a/examples/use_websocket/Cargo.toml +++ b/examples/use_websocket/Cargo.toml @@ -8,7 +8,8 @@ leptos = { version = "0.6", features = ["nightly", "csr"] } console_error_panic_hook = "0.1" console_log = "1" log = "0.4" -leptos-use = { path = "../..", features = ["docs"] } +leptos-use = { path = "../..", features = ["docs", "msgpack_serde"] } +serde = { version = "1", features = ["derive"] } web-sys = "0.3" [dev-dependencies] diff --git a/examples/use_websocket/src/main.rs b/examples/use_websocket/src/main.rs index d7fef69..7a5e370 100644 --- a/examples/use_websocket/src/main.rs +++ b/examples/use_websocket/src/main.rs @@ -1,12 +1,20 @@ use leptos::*; use leptos_use::docs::demo_or_body; use leptos_use::{ - core::ConnectionReadyState, use_websocket, use_websocket_with_options, UseWebSocketOptions, - UseWebsocketReturn, + core::ConnectionReadyState, use_websocket, use_websocket_with_options, UseWebSocketError, + UseWebSocketOptions, UseWebSocketReturn, }; +use serde::{Deserialize, Serialize}; +use leptos_use::utils::{FromToStringCodec, MsgpackSerdeCodec}; use web_sys::{CloseEvent, Event}; +#[derive(Serialize, Deserialize, Debug)] +struct Apple { + name: String, + worm_count: u32, +} + #[component] fn Demo() -> impl IntoView { let (history, set_history) = create_signal(vec![]); @@ -18,27 +26,22 @@ fn Demo() -> impl IntoView { // use_websocket // ---------------------------- - let UseWebsocketReturn { + let UseWebSocketReturn { ready_state, message, - message_bytes, send, - send_bytes, open, close, .. - } = use_websocket("wss://echo.websocket.events/"); + } = use_websocket::("wss://echo.websocket.events/"); let send_message = move |_| { - let m = "Hello, world!"; - send(m); - 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 m = Apple { + name: "More worm than apple".to_string(), + worm_count: 10, + }; + send(&m); + set_history.update(|history: &mut Vec<_>| history.push(format!("[send]: {:?}", m))); }; let status = move || ready_state().to_string(); @@ -53,15 +56,11 @@ fn Demo() -> impl IntoView { }; create_effect(move |_| { - if let Some(m) = message.get() { - update_history(&set_history, format! {"[message]: {:?}", m}); - }; - }); - - create_effect(move |_| { - if let Some(m) = message_bytes.get() { - update_history(&set_history, format! {"[message_bytes]: {:?}", m}); - }; + message.with(move |message| { + if let Some(m) = message { + update_history(&set_history, format!("[message]: {:?}", m)); + } + }) }); // ---------------------------- @@ -72,49 +71,44 @@ fn Demo() -> impl IntoView { let on_open_callback = move |e: Event| { set_history2.update(|history: &mut Vec<_>| { - history.push(format! {"[onopen]: event {:?}", e.type_()}) + 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_()}) + history.push(format!("[onclose]: event {:?}", e.type_())) }); }; - let on_error_callback = move |e: Event| { + let on_error_callback = move |e: UseWebSocketError<_, _>| { set_history2.update(|history: &mut Vec<_>| { - history.push(format! {"[onerror]: event {:?}", e.type_()}) + history.push(match e { + UseWebSocketError::Event(e) => format!("[onerror]: event {:?}", e.type_()), + _ => format!("[onerror]: {:?}", e), + }) }); }; - let on_message_callback = move |m: String| { - set_history2.update(|history: &mut Vec<_>| history.push(format! {"[onmessage]: {:?}", m})); + 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 { + 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( + } = use_websocket_with_options::( "wss://echo.websocket.events/", UseWebSocketOptions::default() .immediate(false) .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()), + .on_message(on_message_callback.clone()), ); let open_connection2 = move |_| { @@ -125,28 +119,16 @@ fn Demo() -> impl IntoView { }; let send_message2 = move |_| { - let message = "Hello, use_leptos!"; - send2(message); - 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 message = "Hello, use_leptos!".to_string(); + send2(&message); + update_history(&set_history2, format!("[send]: {:?}", message)); }; let status2 = move || ready_state2.get().to_string(); create_effect(move |_| { if let Some(m) = message2.get() { - update_history(&set_history2, format! {"[message]: {:?}", m}); - }; - }); - - create_effect(move |_| { - if let Some(m) = message_bytes2.get() { - update_history(&set_history2, format! {"[message_bytes]: {:?}", m}); + update_history(&set_history2, format!("[message]: {:?}", m)); }; }); @@ -161,9 +143,7 @@ fn Demo() -> impl IntoView { - + @@ -200,9 +180,7 @@ fn Demo() -> impl IntoView { - +

"History"

-/// /// /// /// ///

"Receive message: " {move || format!("{:?}", message.get())}

-///

"Receive byte message: " {move || format!("{:?}", message_bytes.get())}

///
/// } /// # } /// ``` /// +/// Here is another example using `msgpack` for encoding and decoding. This means that only binary +/// messages can be sent or received. For this to work you have to enable the **`msgpack_serde` feature** flag. +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::utils::MsgpackSerdeCodec; +/// # use leptos_use::{use_websocket, UseWebSocketReturn}; +/// # use serde::{Deserialize, Serialize}; +/// # +/// # #[component] +/// # fn Demo() -> impl IntoView { +/// #[derive(Serialize, Deserialize)] +/// struct SomeData { +/// name: String, +/// count: i32, +/// } +/// +/// let UseWebSocketReturn { +/// message, +/// send, +/// .. +/// } = use_websocket::("wss://some.websocket.server/"); +/// +/// let send_data = move || { +/// send(&SomeData { +/// name: "John Doe".to_string(), +/// count: 42, +/// }); +/// }; +/// # +/// # view! {} +/// } +/// ``` +/// /// ## Relative Paths /// /// If the provided `url` is relative, it will be resolved relative to the current page. @@ -105,11 +141,11 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket}; /// #[derive(Clone)] /// pub struct WebsocketContext { /// pub message: Signal>, -/// send: Rc, // use Rc to make it easily cloneable +/// send: Rc, // use Rc to make it easily cloneable /// } /// /// impl WebsocketContext { -/// pub fn new(message: Signal>, send: Rc) -> Self { +/// pub fn new(message: Signal>, send: Rc) -> Self { /// Self { /// message, /// send, @@ -119,7 +155,7 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket}; /// // create a method to avoid having to use parantheses around the field /// #[inline(always)] /// pub fn send(&self, message: &str) { -/// (self.send)(message) +/// (self.send)(&message.to_string()) /// } /// } /// ``` @@ -128,16 +164,17 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket}; /// /// ``` /// # use leptos::*; -/// # use leptos_use::{use_websocket, UseWebsocketReturn}; +/// # use leptos_use::utils::FromToStringCodec; +/// # use leptos_use::{use_websocket, UseWebSocketReturn}; /// # use std::rc::Rc; /// # #[derive(Clone)] /// # pub struct WebsocketContext { /// # pub message: Signal>, -/// # send: Rc, +/// # send: Rc, /// # } /// # /// # impl WebsocketContext { -/// # pub fn new(message: Signal>, send: Rc) -> Self { +/// # pub fn new(message: Signal>, send: Rc) -> Self { /// # Self { /// # message, /// # send, @@ -147,11 +184,11 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket}; /// /// # #[component] /// # fn Demo() -> impl IntoView { -/// let UseWebsocketReturn { +/// let UseWebSocketReturn { /// message, /// send, /// .. -/// } = use_websocket("ws:://some.websocket.io"); +/// } = use_websocket::("ws:://some.websocket.io"); /// /// provide_context(WebsocketContext::new(message, Rc::new(send.clone()))); /// # @@ -163,18 +200,18 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket}; /// /// ``` /// # use leptos::*; -/// # use leptos_use::{use_websocket, UseWebsocketReturn}; +/// # use leptos_use::{use_websocket, UseWebSocketReturn}; /// # use std::rc::Rc; /// # #[derive(Clone)] /// # pub struct WebsocketContext { /// # pub message: Signal>, -/// # send: Rc, +/// # send: Rc, /// # } /// # /// # impl WebsocketContext { /// # #[inline(always)] /// # pub fn send(&self, message: &str) { -/// # (self.send)(message) +/// # (self.send)(&message.to_string()) /// # } /// # } /// @@ -191,32 +228,52 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket}; /// ## Server-Side Rendering /// /// On the server the returned functions amount to no-ops. -pub fn use_websocket( +pub fn use_websocket( url: &str, -) -> UseWebsocketReturn< +) -> UseWebSocketReturn< + T, impl Fn() + Clone + 'static, impl Fn() + Clone + 'static, - impl Fn(&str) + Clone + 'static, - impl Fn(Vec) + Clone + 'static, -> { - use_websocket_with_options(url, UseWebSocketOptions::default()) + impl Fn(&T) + Clone + 'static, +> +where + T: 'static, + C: Encoder + Decoder, + C: IsBinary>::Encoded>, + C: HybridDecoder>::Encoded, Error = >::Error>, + C: HybridEncoder>::Encoded, Error = >::Error>, +{ + use_websocket_with_options::(url, UseWebSocketOptions::default()) } /// Version of [`use_websocket`] that takes `UseWebSocketOptions`. See [`use_websocket`] for how to use. -pub fn use_websocket_with_options( +pub fn use_websocket_with_options( url: &str, - options: UseWebSocketOptions, -) -> UseWebsocketReturn< + options: UseWebSocketOptions< + T, + HybridCoderError<>::Error>, + HybridCoderError<>::Error>, + >, +) -> UseWebSocketReturn< + T, impl Fn() + Clone + 'static, impl Fn() + Clone + 'static, - impl Fn(&str) + Clone + 'static, - impl Fn(Vec) + Clone, -> { + impl Fn(&T) + Clone + 'static, +> +where + T: 'static, + C: Encoder + Decoder, + C: IsBinary>::Encoded>, + C: HybridDecoder>::Encoded, Error = >::Error>, + C: HybridEncoder>::Encoded, Error = >::Error>, +{ let url = normalize_url(url); + let UseWebSocketOptions { on_open, on_message, - on_message_bytes, + on_message_raw, + on_message_raw_bytes, on_error, on_close, reconnect_limit, @@ -227,7 +284,6 @@ pub fn use_websocket_with_options( let (ready_state, set_ready_state) = create_signal(ConnectionReadyState::Closed); let (message, set_message) = create_signal(None); - let (message_bytes, set_message_bytes) = create_signal(None); let ws_ref: StoredValue> = store_value(None); let reconnect_timer_ref: StoredValue> = store_value(None); @@ -268,6 +324,7 @@ pub fn use_websocket_with_options( connect_ref.set_value({ let unmounted = Rc::clone(&unmounted); + let on_error = Rc::clone(&on_error); Some(Rc::new(move || { reconnect_timer_ref.set_value(None); @@ -322,7 +379,9 @@ pub fn use_websocket_with_options( { let unmounted = Rc::clone(&unmounted); let on_message = Rc::clone(&on_message); - let on_message_bytes = Rc::clone(&on_message_bytes); + let on_message_raw = Rc::clone(&on_message_raw); + let on_message_raw_bytes = Rc::clone(&on_message_raw_bytes); + let on_error = Rc::clone(&on_error); let onmessage_closure = Closure::wrap(Box::new(move |e: MessageEvent| { if unmounted.get() { @@ -344,12 +403,27 @@ pub fn use_websocket_with_options( #[cfg(debug_assertions)] let prev = SpecialNonReactiveZone::enter(); - on_message(txt.clone()); + on_message_raw(&txt); #[cfg(debug_assertions)] SpecialNonReactiveZone::exit(prev); - set_message.set(Some(txt)); + match C::decode_str(&txt) { + Ok(val) => { + #[cfg(debug_assertions)] + let prev = SpecialNonReactiveZone::enter(); + + on_message(&val); + + #[cfg(debug_assertions)] + SpecialNonReactiveZone::exit(prev); + + set_message.set(Some(val)); + } + Err(err) => { + on_error(CodecError::Decode(err).into()); + } + } }, ); }, @@ -360,12 +434,27 @@ pub fn use_websocket_with_options( #[cfg(debug_assertions)] let prev = SpecialNonReactiveZone::enter(); - on_message_bytes(array.clone()); + on_message_raw_bytes(&array); #[cfg(debug_assertions)] SpecialNonReactiveZone::exit(prev); - set_message_bytes.set(Some(array)); + match C::decode_bin(array.as_slice()) { + Ok(val) => { + #[cfg(debug_assertions)] + let prev = SpecialNonReactiveZone::enter(); + + on_message(&val); + + #[cfg(debug_assertions)] + SpecialNonReactiveZone::exit(prev); + + set_message.set(Some(val)); + } + Err(err) => { + on_error(CodecError::Decode(err).into()); + } + } }, ); }) @@ -391,7 +480,7 @@ pub fn use_websocket_with_options( #[cfg(debug_assertions)] let prev = SpecialNonReactiveZone::enter(); - on_error(e); + on_error(UseWebSocketError::Event(e)); #[cfg(debug_assertions)] SpecialNonReactiveZone::exit(prev); @@ -438,7 +527,7 @@ pub fn use_websocket_with_options( } // Send text (String) - let send = { + let send_str = { Box::new(move |data: &str| { if ready_state.get_untracked() == ConnectionReadyState::Open { if let Some(web_socket) = ws_ref.get_value() { @@ -449,10 +538,28 @@ pub fn use_websocket_with_options( }; // Send bytes - let send_bytes = move |data: Vec| { + let send_bytes = move |data: &[u8]| { if ready_state.get_untracked() == ConnectionReadyState::Open { if let Some(web_socket) = ws_ref.get_value() { - let _ = web_socket.send_with_u8_array(&data); + let _ = web_socket.send_with_u8_array(data); + } + } + }; + + let send = { + let on_error = Rc::clone(&on_error); + + move |value: &T| { + if C::is_binary() { + match C::encode_bin(value) { + Ok(val) => send_bytes(&val), + Err(err) => on_error(CodecError::Encode(err).into()), + } + } else { + match C::encode_str(value) { + Ok(val) => send_str(&val), + Err(err) => on_error(CodecError::Encode(err).into()), + } } } }; @@ -490,15 +597,13 @@ pub fn use_websocket_with_options( close(); }); - UseWebsocketReturn { + UseWebSocketReturn { ready_state: ready_state.into(), message: message.into(), - message_bytes: message_bytes.into(), ws: ws_ref.get_value(), open, close, send, - send_bytes, } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -522,17 +627,26 @@ impl ReconnectLimit { } } +type RcFnBytes = Rc; + /// Options for [`use_websocket_with_options`]. #[derive(DefaultBuilder)] -pub struct UseWebSocketOptions { +pub struct UseWebSocketOptions +where + T: ?Sized, +{ /// `WebSocket` connect callback. on_open: Rc, + /// `WebSocket` message callback for typed message decoded by codec. + #[builder(skip)] + on_message: Rc, /// `WebSocket` message callback for text. - on_message: Rc, + on_message_raw: Rc, /// `WebSocket` message callback for binary. - on_message_bytes: Rc)>, + on_message_raw_bytes: RcFnBytes, /// `WebSocket` error callback. - on_error: Rc, + #[builder(skip)] + on_error: Rc)>, /// `WebSocket` close callback. on_close: Rc, /// Retry times. Defaults to `ReconnectLimit::Limited(3)`. Use `ReconnectLimit::Infinite` for @@ -548,12 +662,37 @@ pub struct UseWebSocketOptions { protocols: Option>, } -impl Default for UseWebSocketOptions { +impl UseWebSocketOptions { + /// `WebSocket` error callback. + pub fn on_error(self, handler: F) -> Self + where + F: Fn(UseWebSocketError) + 'static, + { + Self { + on_error: Rc::new(handler), + ..self + } + } + + /// `WebSocket` message callback for typed message decoded by codec. + pub fn on_message(self, handler: F) -> Self + where + F: Fn(&T) + 'static, + { + Self { + on_message: Rc::new(handler), + ..self + } + } +} + +impl Default for UseWebSocketOptions { fn default() -> Self { Self { on_open: Rc::new(|_| {}), on_message: Rc::new(|_| {}), - on_message_bytes: Rc::new(|_| {}), + on_message_raw: Rc::new(|_| {}), + on_message_raw_bytes: Rc::new(|_| {}), on_error: Rc::new(|_| {}), on_close: Rc::new(|_| {}), reconnect_limit: ReconnectLimit::default(), @@ -566,29 +705,33 @@ impl Default for UseWebSocketOptions { /// Return type of [`use_websocket`]. #[derive(Clone)] -pub struct UseWebsocketReturn +pub struct UseWebSocketReturn where + T: 'static, OpenFn: Fn() + Clone + 'static, CloseFn: Fn() + Clone + 'static, - SendFn: Fn(&str) + Clone + 'static, - SendBytesFn: Fn(Vec) + Clone + 'static, + SendFn: Fn(&T) + Clone + 'static, { /// The current state of the `WebSocket` connection. pub ready_state: Signal, - /// Latest text message received from `WebSocket`. - pub message: Signal>, - /// Latest binary message received from `WebSocket`. - pub message_bytes: Signal>>, + /// Latest message received from `WebSocket`. + pub message: Signal>, /// 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 + /// Sends data through the socket pub send: SendFn, - /// Sends binary data - pub send_bytes: SendBytesFn, +} + +#[derive(Error, Debug)] +pub enum UseWebSocketError { + #[error("WebSocket error event")] + Event(Event), + #[error("WebSocket codec error: {0}")] + Codec(#[from] CodecError), } fn normalize_url(url: &str) -> String { diff --git a/src/use_webtransport.rs b/src/use_webtransport.rs index b6b6c09..b58057b 100644 --- a/src/use_webtransport.rs +++ b/src/use_webtransport.rs @@ -21,7 +21,7 @@ use web_sys::WebTransportBidirectionalStream; #[cfg(feature = "bincode")] use bincode::serde::{decode_from_slice as from_slice, encode_to_vec as to_vec}; -/// +/// This still under development and will not arrive before Leptos 0.7. /// /// ## Demo /// diff --git a/src/utils/codecs/bin/bincode_serde.rs b/src/utils/codecs/bin/bincode_serde.rs index 0442fc7..db1e309 100644 --- a/src/utils/codecs/bin/bincode_serde.rs +++ b/src/utils/codecs/bin/bincode_serde.rs @@ -1,5 +1,4 @@ use crate::utils::{Decoder, Encoder}; -use serde::{Deserialize, Serialize}; /// A codec that relies on `bincode` adn `serde` to encode data in the bincode format. /// diff --git a/src/utils/codecs/bin/mod.rs b/src/utils/codecs/bin/mod.rs index dfcbc72..8698906 100644 --- a/src/utils/codecs/bin/mod.rs +++ b/src/utils/codecs/bin/mod.rs @@ -6,11 +6,11 @@ mod msgpack_serde; #[cfg(feature = "prost")] mod prost; -#[cfg(feature = "bincode")] +#[cfg(feature = "bincode_serde")] pub use bincode_serde::*; #[allow(unused_imports)] pub use from_to_bytes::*; -#[cfg(feature = "msgpack")] +#[cfg(feature = "msgpack_serde")] pub use msgpack_serde::*; #[cfg(feature = "prost")] pub use prost::*; diff --git a/src/utils/codecs/bin/msgpack_serde.rs b/src/utils/codecs/bin/msgpack_serde.rs index 719a72f..77778c8 100644 --- a/src/utils/codecs/bin/msgpack_serde.rs +++ b/src/utils/codecs/bin/msgpack_serde.rs @@ -1,5 +1,4 @@ use crate::utils::{Decoder, Encoder}; -use serde::{Deserialize, Serialize}; /// A codec that relies on `rmp-serde` to encode data in the msgpack format. /// diff --git a/src/utils/codecs/hybrid.rs b/src/utils/codecs/hybrid.rs new file mode 100644 index 0000000..42ccdc8 --- /dev/null +++ b/src/utils/codecs/hybrid.rs @@ -0,0 +1,108 @@ +use crate::utils::{Decoder, Encoder}; +use thiserror::Error; + +pub trait IsBinary { + fn is_binary() -> bool; +} + +impl IsBinary for D +where + D: Decoder, +{ + fn is_binary() -> bool { + true + } +} + +impl IsBinary for D +where + D: Decoder, +{ + fn is_binary() -> bool { + false + } +} + +#[derive(Debug, Error)] +pub enum HybridCoderError { + #[error("Not implemented: {0}")] + NotImplemented(&'static str), + #[error("Decoding error")] + Coder(#[from] E), +} + +pub trait HybridDecoder { + type Error; + + fn decode_str(_val: &str) -> Result> { + Err(HybridCoderError::NotImplemented( + "You're trying to decode from a string. This codec is binary.", + )) + } + + fn decode_bin(_val: &[u8]) -> Result> { + Err(HybridCoderError::NotImplemented( + "You're trying to decode from a byte slice. This codec is a string codec.", + )) + } +} + +impl HybridDecoder for D +where + D: Decoder, +{ + type Error = D::Error; + + fn decode_bin(val: &[u8]) -> Result> { + Ok(D::decode(val)?) + } +} + +impl HybridDecoder for D +where + D: Decoder, +{ + type Error = D::Error; + + fn decode_str(val: &str) -> Result> { + Ok(D::decode(val)?) + } +} + +pub trait HybridEncoder { + type Error; + + fn encode_str(_val: &T) -> Result> { + Err(HybridCoderError::NotImplemented( + "You're trying to encode into a string. This codec is binary.", + )) + } + + fn encode_bin(_val: &T) -> Result, HybridCoderError> { + Err(HybridCoderError::NotImplemented( + "You're trying to encode into a byte vec. This codec is a string codec.", + )) + } +} + +impl HybridEncoder> for E +where + E: Encoder>, +{ + type Error = E::Error; + + fn encode_bin(val: &T) -> Result, HybridCoderError> { + Ok(E::encode(val)?) + } +} + +impl HybridEncoder for E +where + E: Encoder, +{ + type Error = E::Error; + + fn encode_str(val: &T) -> Result> { + Ok(E::encode(val)?) + } +} diff --git a/src/utils/codecs/mod.rs b/src/utils/codecs/mod.rs index 2f266ad..708ec1d 100644 --- a/src/utils/codecs/mod.rs +++ b/src/utils/codecs/mod.rs @@ -1,7 +1,9 @@ mod bin; +mod hybrid; mod string; pub use bin::*; +pub use hybrid::*; pub use string::*; use thiserror::Error; @@ -21,22 +23,6 @@ pub trait Decoder: 'static { fn decode(val: &Self::Encoded) -> Result; } -/// Trait to check if a type is binary or encodes data in a string. -pub trait IsBinary { - fn is_binary() -> bool { - true - } -} - -impl IsBinary for Enc -where - Enc: Encoder, -{ - fn is_binary() -> bool { - false - } -} - #[derive(Error, Debug)] pub enum CodecError { #[error("failed to encode: {0}")] diff --git a/src/utils/codecs/string/base64.rs b/src/utils/codecs/string/base64.rs index baa0587..b9f0265 100644 --- a/src/utils/codecs/string/base64.rs +++ b/src/utils/codecs/string/base64.rs @@ -62,6 +62,6 @@ where fn decode(val: &Self::Encoded) -> Result { let buf = base64::engine::general_purpose::STANDARD.decode(val)?; - D::decode(&buf).map_err(|err| Base64DecodeError::Decoder(err)) + D::decode(&buf).map_err(Base64DecodeError::Decoder) } }