From 62754e0d1d1a433c743f3f7883348e05cbbb5f92 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Fri, 10 May 2024 14:15:59 -0500 Subject: [PATCH 01/24] ssr + wasm compile warning --- CHANGELOG.md | 6 ++++++ Cargo.toml | 1 + build.rs | 17 +++++++++++++++++ docs/book/src/server_side_rendering.md | 7 +++++++ 4 files changed, 31 insertions(+) create mode 100644 build.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ee9c288..de31d32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.10.10] - 2024-05-10 + +### Change 🔥 + +- Added compile-time warning when you use `ssr` feature with `wasm32`. You can enable `wasm_ssr` to remove the warning. + ## [0.10.9] - 2024-04-27 ### Fixes 🍕 diff --git a/Cargo.toml b/Cargo.toml index 2679d07..21308a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,6 +146,7 @@ serde = ["dep:serde", "serde_json"] spin = ["dep:leptos-spin", "dep:http1"] ssr = [] msgpack = ["dep:rmp-serde", "dep:serde"] +wasm_ssr = [] [package.metadata.docs.rs] features = ["math", "docs", "ssr", "prost", "serde"] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..7ef2a97 --- /dev/null +++ b/build.rs @@ -0,0 +1,17 @@ +use std::env; + +fn main() { + let ssr = env::var("CARGO_FEATURE_SSR").is_ok(); + let wasm_ssr = env::var("CARGO_FEATURE_WASM_SSR").is_ok(); + let wasm32 = env::var("CARGO_CFG_TARGET_ARCH").expect("should be present in the build script") + == "wasm32"; + if ssr && wasm32 && !wasm_ssr { + println!( + "cargo::warning=You have enabled the `ssr` feature for a wasm32 target. \ +This is probably not what you want. Please check https://leptos-use.rs/server_side_rendering.html \ +for how to use the `ssr` feature correctly.\n \ +If you're building for wasm32 on the server you can enable the `wasm_ssr` feature to get rid of \ +this warning." + ); + } +} diff --git a/docs/book/src/server_side_rendering.md b/docs/book/src/server_side_rendering.md index 6c4cc37..4165b79 100644 --- a/docs/book/src/server_side_rendering.md +++ b/docs/book/src/server_side_rendering.md @@ -54,6 +54,13 @@ By adding `"leptos-use/ssr"` to the `ssr` feature of your project, it will only be enabled when your project is built with `ssr`, and you will get the server functions server-side, and the client functions client-side. +## WASM on the server + +If you enable `ssr` in your project on a `wasm32` target architecture, you will get +a compile-time warning in the console because it is a common mistake that users enable `ssr` globally. +If you're using `wasm32` on the server however you can safely disable this warning by +enabling the `wasm_ssr` feature together with `ssr`. + ## Functions with Target Elements A lot of functions like `use_resize_observer` and `use_element_size` are only useful when a target HTML/SVG element is From 5dee1c753bbb26c5b3052976ecf9dbf52dd19989 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Fri, 10 May 2024 14:22:51 -0500 Subject: [PATCH 02/24] removed broken link in docs --- src/use_color_mode.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index 7acd1e1..af35e7f 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -120,7 +120,6 @@ use wasm_bindgen::JsCast; /// /// ## See also /// -/// * [`use_dark`] /// * [`use_preferred_dark`] /// * [`use_storage`] /// * [`use_cookie`] From 0e84a512cc552611266938a6a0ee8ccaaee8e1fd Mon Sep 17 00:00:00 2001 From: Maccesch Date: Fri, 10 May 2024 14:33:00 -0500 Subject: [PATCH 03/24] chore: clippy --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 71b98c5..47c50b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(unexpected_cfgs)] // #![feature(doc_cfg)] //! Collection of essential Leptos utilities inspired by SolidJS USE / VueUse From 2523f17d20de735b0e70ed6238edf5be30d42eff Mon Sep 17 00:00:00 2001 From: Maccesch Date: Fri, 10 May 2024 14:46:11 -0500 Subject: [PATCH 04/24] stooopid: forgot to update version in cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 21308a4..e26ee4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos-use" -version = "0.10.9" +version = "0.10.10" edition = "2021" authors = ["Marc-Stefan Cassola"] categories = ["gui", "web-programming"] From 384ca082a2baa6f1457b3305c353c27131a953ad Mon Sep 17 00:00:00 2001 From: blorbb <88137137+blorbb@users.noreply.github.com> Date: Tue, 28 May 2024 11:47:10 +1000 Subject: [PATCH 05/24] add impls into `web_sys::HtmlElement` for `ElementMaybeSignal` --- src/core/element_maybe_signal.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/core/element_maybe_signal.rs b/src/core/element_maybe_signal.rs index 7ef0c98..26c1216 100644 --- a/src/core/element_maybe_signal.rs +++ b/src/core/element_maybe_signal.rs @@ -6,7 +6,7 @@ use std::marker::PhantomData; use std::ops::Deref; /// Used as an argument type to make it easily possible to pass either -/// * a `web_sys` element that implements `E` (for example `EventTarget` or `Element`), +/// * a `web_sys` element that implements `E` (for example `EventTarget`, `Element` or `HtmlElement`), /// * an `Option` where `T` is the web_sys element, /// * a `Signal` where `T` is the web_sys element, /// * a `Signal>` where `T` is the web_sys element, @@ -287,6 +287,7 @@ macro_rules! impl_from_node_ref { impl_from_node_ref!(web_sys::EventTarget); impl_from_node_ref!(web_sys::Element); +impl_from_node_ref!(web_sys::HtmlElement); // From leptos::html::HTMLElement /////////////////////////////////////////////// @@ -306,6 +307,7 @@ macro_rules! impl_from_html_element { impl_from_html_element!(web_sys::EventTarget); impl_from_html_element!(web_sys::Element); +impl_from_html_element!(web_sys::HtmlElement); // From Signal ///////////////////////////////////////// @@ -366,3 +368,11 @@ impl_from_signal_html_element!(Signal>>, web_sys::Ele impl_from_signal_html_element!(ReadSignal>>, web_sys::Element); impl_from_signal_html_element!(RwSignal>>, web_sys::Element); impl_from_signal_html_element!(Memo>>, web_sys::Element); + +impl_from_signal_html_element!(Signal>>, web_sys::HtmlElement); +impl_from_signal_html_element!( + ReadSignal>>, + web_sys::HtmlElement +); +impl_from_signal_html_element!(RwSignal>>, web_sys::HtmlElement); +impl_from_signal_html_element!(Memo>>, web_sys::HtmlElement); From 1771d3ac078fee176e798626f8fd46e7d17607f1 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Wed, 5 Jun 2024 11:21:25 +0200 Subject: [PATCH 06/24] added delay_during_hydration option to UseStorageOptions. Fixes #108 --- CHANGELOG.md | 14 +++ examples/ssr/src/app.rs | 11 ++- src/storage/use_local_storage.rs | 8 +- src/storage/use_session_storage.rs | 8 +- src/storage/use_storage.rs | 135 ++++++++++++++++++++--------- 5 files changed, 126 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de31d32..aa8e844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changes 🔥 + +- `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`. + +### Breaking Changes 🛠 + +- `UseStorageOptions` no longer accepts a `codec` value because this is already provided as a generic parameter to + the respective function calls. + ## [0.10.10] - 2024-05-10 ### Change 🔥 diff --git a/examples/ssr/src/app.rs b/examples/ssr/src/app.rs index 9b4e6f0..80b8695 100644 --- a/examples/ssr/src/app.rs +++ b/examples/ssr/src/app.rs @@ -3,7 +3,7 @@ use leptos::ev::{keypress, KeyboardEvent}; use leptos::*; use leptos_meta::*; use leptos_router::*; -use leptos_use::storage::use_local_storage; +use leptos_use::storage::{use_local_storage, use_local_storage_with_options, UseStorageOptions}; use leptos_use::utils::FromToStringCodec; use leptos_use::{ use_color_mode_with_options, use_cookie_with_options, use_debounce_fn, use_event_listener, @@ -40,7 +40,10 @@ pub fn App() -> impl IntoView { #[component] fn HomePage() -> impl IntoView { // Creates a reactive value to update the button - let (count, set_count, _) = use_local_storage::("count-state"); + let (count, set_count, _) = use_local_storage_with_options::( + "count-state", + UseStorageOptions::default().delay_during_hydration(true), + ); let on_click = move |_| set_count.update(|count| *count += 1); let nf = use_intl_number_format( @@ -96,6 +99,10 @@ fn HomePage() -> impl IntoView {

Dark preferred: {is_dark_preferred}

Test cookie: {move || test_cookie().unwrap_or("".to_string())}

+ + 0 }> +
Greater than 0
+
} } diff --git a/src/storage/use_local_storage.rs b/src/storage/use_local_storage.rs index 2e9d375..a392406 100644 --- a/src/storage/use_local_storage.rs +++ b/src/storage/use_local_storage.rs @@ -17,21 +17,21 @@ where T: Clone + Default + PartialEq, C: StringCodec + Default, { - use_storage_with_options( + use_storage_with_options::( StorageType::Local, key, - UseStorageOptions::::default(), + UseStorageOptions::::default(), ) } /// Accepts [`UseStorageOptions`]. See [`use_local_storage`] for details. pub fn use_local_storage_with_options( key: impl AsRef, - options: UseStorageOptions, + options: UseStorageOptions, ) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + PartialEq, C: StringCodec + Default, { - use_storage_with_options(StorageType::Local, key, options) + use_storage_with_options::(StorageType::Local, key, options) } diff --git a/src/storage/use_session_storage.rs b/src/storage/use_session_storage.rs index 55c963b..3edf36e 100644 --- a/src/storage/use_session_storage.rs +++ b/src/storage/use_session_storage.rs @@ -17,21 +17,21 @@ where T: Clone + Default + PartialEq, C: StringCodec + Default, { - use_storage_with_options( + use_storage_with_options::( StorageType::Session, key, - UseStorageOptions::::default(), + UseStorageOptions::::default(), ) } /// Accepts [`UseStorageOptions`]. See [`use_session_storage`] for details. pub fn use_session_storage_with_options( key: impl AsRef, - options: UseStorageOptions, + options: UseStorageOptions, ) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + PartialEq, C: StringCodec + Default, { - use_storage_with_options(StorageType::Session, key, options) + use_storage_with_options::(StorageType::Session, key, options) } diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 20f14d5..39aa5ea 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -2,7 +2,8 @@ use crate::{ core::{MaybeRwSignal, StorageType}, utils::{FilterOptions, StringCodec}, }; -use cfg_if::cfg_if; +use default_struct_builder::DefaultBuilder; +use leptos::leptos_dom::HydrationCtx; use leptos::*; use std::rc::Rc; use thiserror::Error; @@ -39,6 +40,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// # use serde::{Deserialize, Serialize}; /// # use leptos_use::utils::{FromToStringCodec, JsonCodec, ProstCodec}; /// # +/// # #[component] /// # pub fn Demo() -> impl IntoView { /// // Binds a struct: /// let (state, set_state, _) = use_local_storage::("my-state"); @@ -86,6 +88,64 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// ## Server-Side Rendering /// /// On the server the returned signals will just read/manipulate the `initial_value` without persistence. +/// +/// ### Hydration bugs and `use_cookie` +/// +/// If you use a value from storage to control conditional rendering you might run into issues with +/// hydration. +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::storage::use_local_storage; +/// # use leptos_use::utils::FromToStringCodec; +/// # +/// # #[component] +/// # pub fn Example() -> impl IntoView { +/// let (flag, set_flag, _) = use_session_storage::("my-flag"); +/// +/// view! { +/// +///
Some conditional content
+///
+/// } +/// # } +/// ``` +/// +/// You can see hydration warnings in the browser console and the conditional parts of +/// the app might never show up when rendered on the server and then hydrated in the browser. The +/// reason for this is that the server has no access to storage and therefore will always use +/// `initial_value` as described above. So on the server your app is always rendered as if +/// the value from storage was `initial_value`. Then in the browser the actual stored value is used +/// which might be different, hence during hydration the DOM looks different from the one rendered +/// on the server which produces the hydration warnings. +/// +/// The recommended way to avoid this is to use `use_cookie` instead because values stored in cookies +/// are available on the server as well as in the browser. +/// +/// If you still want to use storage instead of cookies you can use the `delay_during_hydration` +/// option that will use the `initial_value` during hydration just as on the server and delay loading +/// the value from storage by an animation frame. This gets rid of the hydration warnings and makes +/// the app correctly render things. Some flickering might be unavoidable though. +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::storage::{use_local_storage_with_options, UseStorageOptions}; +/// # use leptos_use::utils::FromToStringCodec; +/// # +/// # #[component] +/// # pub fn Example() -> impl IntoView { +/// let (flag, set_flag, _) = use_session_storage_with_options::( +/// "my-flag", +/// UseStorageOptions::default().delay_during_hydration(true), +/// ); +/// +/// view! { +/// +///
Some conditional content
+///
+/// } +/// # } +/// ``` #[inline(always)] pub fn use_storage( storage_type: StorageType, @@ -102,24 +162,27 @@ where pub fn use_storage_with_options( storage_type: StorageType, key: impl AsRef, - options: UseStorageOptions, + options: UseStorageOptions, ) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + PartialEq, C: StringCodec + Default, { let UseStorageOptions { - codec, on_error, listen_to_storage_changes, initial_value, filter, + delay_during_hydration, } = options; + let codec = C::default(); + let (data, set_data) = initial_value.into_signal(); let default = data.get_untracked(); - cfg_if! { if #[cfg(feature = "ssr")] { + #[cfg(feature = "ssr")] + { let _ = codec; let _ = on_error; let _ = listen_to_storage_changes; @@ -128,13 +191,15 @@ where let _ = key; let _ = INTERNAL_STORAGE_EVENT; - let remove = move || { set_data.set(default.clone()); }; (data.into(), set_data, remove) - } else { + } + + #[cfg(not(feature = "ssr"))] + { use crate::{use_event_listener, use_window, watch_with_options, WatchOptions}; // Get storage API @@ -176,6 +241,7 @@ where let codec = codec.to_owned(); let key = key.as_ref().to_owned(); let on_error = on_error.to_owned(); + move || { let fetched = storage .to_owned() @@ -212,7 +278,11 @@ where }; // Fetch initial value - fetch_from_storage(); + if delay_during_hydration && HydrationCtx::is_hydrating() { + request_animation_frame(fetch_from_storage.clone()); + } else { + fetch_from_storage(); + } // Fires when storage needs to be fetched let notify = create_trigger(); @@ -306,7 +376,7 @@ where }; (data, set_data, remove) - }} + } } /// Session handling errors returned by [`use_storage_with_options`]. @@ -329,17 +399,26 @@ pub enum UseStorageError { } /// Options for use with [`use_local_storage_with_options`], [`use_session_storage_with_options`] and [`use_storage_with_options`]. -pub struct UseStorageOptions> { - // Translates to and from UTF-16 strings - codec: C, +#[derive(DefaultBuilder)] +pub struct UseStorageOptions +where + T: 'static, +{ // Callback for when an error occurs - on_error: Rc)>, + #[builder(skip)] + on_error: Rc)>, // Whether to continuously listen to changes from browser storage listen_to_storage_changes: bool, // Initial value to use when the storage key is not set + #[builder(skip)] initial_value: MaybeRwSignal, // Debounce or throttle the writing to storage whenever the value changes + #[builder(into)] filter: FilterOptions, + /// Delays the reading of the value from storage by one animation frame during hydration. + /// This ensures that during hydration the value is the initial value just like it is on the server + /// which helps prevent hydration errors. Defaults to `false`. + delay_during_hydration: bool, } /// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling. @@ -351,43 +430,27 @@ fn handle_error( result.map_err(|err| (on_error)(err)) } -impl + Default> Default for UseStorageOptions { +impl Default for UseStorageOptions { fn default() -> Self { Self { - codec: C::default(), on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, initial_value: MaybeRwSignal::default(), filter: FilterOptions::default(), + delay_during_hydration: false, } } } -impl> UseStorageOptions { - /// Sets the codec to use for encoding and decoding values to and from UTF-16 strings. - pub fn codec(self, codec: impl Into) -> Self { - Self { - codec: codec.into(), - ..self - } - } - +impl UseStorageOptions { /// Optional callback whenever an error occurs. - pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { + pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { Self { on_error: Rc::new(on_error), ..self } } - /// Listen to changes to this storage key from browser and page events. Defaults to true. - pub fn listen_to_storage_changes(self, listen_to_storage_changes: bool) -> Self { - Self { - listen_to_storage_changes, - ..self - } - } - /// Initial value to use when the storage key is not set. Note that this value is read once on creation of the storage hook and not updated again. Accepts a signal and defaults to `T::default()`. pub fn initial_value(self, initial: impl Into>) -> Self { Self { @@ -395,12 +458,4 @@ impl> UseStorageOptions { ..self } } - - /// Debounce or throttle the writing to storage whenever the value changes. - pub fn filter(self, filter: impl Into) -> Self { - Self { - filter: filter.into(), - ..self - } - } } From a1f3ca0486cc23607705ed12007c56cff45b4bbd Mon Sep 17 00:00:00 2001 From: Maccesch Date: Wed, 5 Jun 2024 11:53:59 +0200 Subject: [PATCH 07/24] re-export cookie::SameSite closes #106 --- src/use_cookie.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/use_cookie.rs b/src/use_cookie.rs index bb6cbe1..c02f589 100644 --- a/src/use_cookie.rs +++ b/src/use_cookie.rs @@ -3,7 +3,8 @@ use crate::core::now; use crate::utils::StringCodec; use cookie::time::{Duration, OffsetDateTime}; -use cookie::{Cookie, CookieJar, SameSite}; +pub use cookie::SameSite; +use cookie::{Cookie, CookieJar}; use default_struct_builder::DefaultBuilder; use leptos::*; use std::rc::Rc; From 3c7f2d9d28e36b6f0507bb699aa29756afbdc246 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Wed, 5 Jun 2024 11:54:34 +0200 Subject: [PATCH 08/24] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa8e844..9573dcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `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 no re-exported ### Breaking Changes 🛠 From 2cfb62cd2049e373ba634d970e23da4eb6b44733 Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com> Date: Wed, 5 Jun 2024 20:52:13 -0400 Subject: [PATCH 09/24] Fix typo in use_cookie compile errors. --- src/use_cookie.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/use_cookie.rs b/src/use_cookie.rs index c02f589..4fe20d8 100644 --- a/src/use_cookie.rs +++ b/src/use_cookie.rs @@ -484,13 +484,13 @@ impl Default for UseCookieOptions { #[cfg(feature = "ssr")] { #[cfg(all(feature = "actix", feature = "axum"))] - compile_error!("You cannot enable only one of features \"actix\" and \"axum\" at the same time"); + compile_error!("You can only enable one of features \"actix\" and \"axum\" at the same time"); #[cfg(all(feature = "actix", feature = "spin"))] - compile_error!("You cannot enable only one of features \"actix\" and \"spin\" at the same time"); + compile_error!("You can only enable one of features \"actix\" and \"spin\" at the same time"); #[cfg(all(feature = "axum", feature = "spin"))] - compile_error!("You cannot enable only one of features \"axum\" and \"spin\" at the same time"); + compile_error!("You can only enable one of features \"axum\" and \"spin\" at the same time"); #[cfg(feature = "actix")] const COOKIE: http0_2::HeaderName = http0_2::header::COOKIE; From 2f280b4d636f4520aff50febe8fc6364bcb14d1c Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com> Date: Wed, 5 Jun 2024 21:54:44 -0400 Subject: [PATCH 10/24] Update changelog --- CHANGELOG.md | 101 ++++++++++++++++++++++++++------------------------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9573dcc..37ce592 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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 no re-exported +- Fixed typo in compiler error messages in `use_cookie`. ### Breaking Changes 🛠 @@ -116,8 +117,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `leptos` version is now 0.6 - The trait `Codec` has been renamed to `StringCodec` and has been moved to `util::StringCodec`. - - The struct `StringCodec` has been renamed to `FromToStringCodec` and has been moved to `util::FromToStringCodec`. - - The structs `JsonCodec` and `ProstCodec` have been moved to `util` as well. + - The struct `StringCodec` has been renamed to `FromToStringCodec` and has been moved to `util::FromToStringCodec`. + - The structs `JsonCodec` and `ProstCodec` have been moved to `util` as well. - The function `use_storage` now requires type parameters for the stored type and the codec like all the other `...storage...` functions. @@ -132,7 +133,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - You can now convert `leptos::html::HtmlElement` into `Element(s)MaybeSignal`. This should make functions a lot easier to use in directives. - There's now a chapter in the book especially for `Element(s)MaybeSignal`. -- Throttled or debounced callbacks (in watch_* or *_fn) no longer are called after the containing scope was cleaned up. +- Throttled or debounced callbacks (in watch\__ or _\_fn) no longer are called after the containing scope was cleaned up. - The document returned from `use_document` now supports the methods `query_selector` and `query_selector_all`. ## [0.9.0] - 2023-12-06 @@ -145,15 +146,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - (@feral-dot-io) The use `use__storage` functions have been rewritten to use `Codec`s instead of always requiring `serde`. - - This also removes the feature `storage` - - By default the `StringCodec` is used which relies on types implementing `FromString + ToString` - - If you want to use `JsonCodec` you have to enable the feature `serde` - - If you want to use `ProstCodec` (new!) you have to enable the feature `prost`. + - This also removes the feature `storage` + - By default the `StringCodec` is used which relies on types implementing `FromString + ToString` + - If you want to use `JsonCodec` you have to enable the feature `serde` + - If you want to use `ProstCodec` (new!) you have to enable the feature `prost`. - (@feral-dot-io) The Rust flag `--cfg=web_sys_unstable_apis` is not needed anymore since relevant `web_sys` APIs are now stable. This affects in particular - - `use_element_size` - - `use_resize_observer` + - `use_element_size` + - `use_resize_observer` ### Fixes 🍕 @@ -162,23 +163,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `use_scroll` now uses `try_get_untracked` in the debounced callback to avoid panics if the context has been destroyed while the callback was waiting to be called. - `use_idle` works properly now (no more idles too early). -- `use_web_notification` doesn't panic on the server anymore. +- `use_web_notification` doesn't panic on the server anymore. ## [0.8.2] - 2023-11-09 ### Fixes 🍕 - Fixed SSR for - - use_timestamp - - use_raf_fn - - use_idle + - use_timestamp + - use_raf_fn + - use_idle ## [0.8.1] - 2023-10-28 ### Fixes 🍕 - Using strings for `ElementMaybeSignal` and `ElementsMaybeSignal` is now SSR safe. - - This fixes specifically `use_color_mode` to work on the server. + - This fixes specifically `use_color_mode` to work on the server. ## [0.8.0] - 2023-10-24 @@ -233,17 +234,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `use_mutation_observer_with_options` now takes a `UseMutationObserverOptions` instead of a `web_sys::MutationObserverInit`. - `use_websocket`: - - takes now a `&str` instead of a `String` as its `url` parameter. - - same for the returned `send` method. - - The `ready_state` return type is now renamed to `ConnectionReadyState` instead of `UseWebSocketReadyState`. - - The returned signals `ready_state`, `message`, `message_bytes` have now the type - `Signal<...>` instead of `ReadSignal<...>` to make them more consistent with other functions. - - The options `reconnect_limit` and `reconnect_interval` now take a `u64` instead of `Option` to improve DX. - - The option `manual` has been renamed to `immediate` to make it more consistent with other functions. - To port please note that `immediate` is the inverse of `manual` (`immediate` = `!manual`). - - Added documentation how pass it ergonomically as context. + - takes now a `&str` instead of a `String` as its `url` parameter. + - same for the returned `send` method. + - The `ready_state` return type is now renamed to `ConnectionReadyState` instead of `UseWebSocketReadyState`. + - The returned signals `ready_state`, `message`, `message_bytes` have now the type + `Signal<...>` instead of `ReadSignal<...>` to make them more consistent with other functions. + - The options `reconnect_limit` and `reconnect_interval` now take a `u64` instead of `Option` to improve DX. + - The option `manual` has been renamed to `immediate` to make it more consistent with other functions. + To port please note that `immediate` is the inverse of `manual` (`immediate` = `!manual`). + - Added documentation how pass it ergonomically as context. - `use_color_mode`: - - The optional `on_changed` handler parameters have changed slightly. Please refer to the docs for more details. + - The optional `on_changed` handler parameters have changed slightly. Please refer to the docs for more details. - Throttled or debounced functions cannot be `FnOnce` anymore. - All traits `ClonableFn...` have been removed. @@ -254,16 +255,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Callback in `use_raf_fn` doesn't require to be cloneable anymore - All (!) functions can now be safely called on the server. Specifically this includes the following that before panicked on the server: - - `use_scroll` - - `use_event_listener` - - `use_element_hover` - - `on_click_outside` - - `use_drop_zone` - - `use_element_size` - - `use_element_visibility` - - `use_resize_observer` - - `use_intersection_observer` - - `use_mutation_observer` + - `use_scroll` + - `use_event_listener` + - `use_element_hover` + - `on_click_outside` + - `use_drop_zone` + - `use_element_size` + - `use_element_visibility` + - `use_resize_observer` + - `use_intersection_observer` + - `use_mutation_observer` ### Fixes 🍕 @@ -303,18 +304,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The following functions now accept a `MaybeRwSignal` as their initial/default value which means you can use a synchronized `RwSignal` in those places. - - `use_color_mode` - - `use_cycle_list` - - `use_favicon` - - `use_storage` - - `use_local_storage` - - `use_session_storage` + - `use_color_mode` + - `use_cycle_list` + - `use_favicon` + - `use_storage` + - `use_local_storage` + - `use_session_storage` - Instead of returning `ReadSignal`, the following functions now return `Signal`. - - `use_color_mode` - - `use_favicon` - - `use_storage` - - `use_local_storage` - - `use_session_storage` + - `use_color_mode` + - `use_favicon` + - `use_storage` + - `use_local_storage` + - `use_session_storage` ### Fixes 🍕 @@ -387,11 +388,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - You can now specify a `&str` or `Signal` with CSS selectors wherever a node ref is accepted - Callbacks of the following functions no longer require `Clone` - - `use_resize_observer` - - `use_intersection_observer` + - `use_resize_observer` + - `use_intersection_observer` - These functions now also accept multiple target elements in addition to a single one: - - `use_resize_observer` - - `use_intersection_observer` + - `use_resize_observer` + - `use_intersection_observer` ### New Functions 🚀 @@ -484,4 +485,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Other Changes -- Better and more beautiful demo integration into the guide. \ No newline at end of file +- Better and more beautiful demo integration into the guide. From ae019b4734894df110178052b93994c480efbf59 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Thu, 6 Jun 2024 22:30:49 +0200 Subject: [PATCH 11/24] fixed websocket auto reconnect not working. Also added ReconnetLimit enum to allow for infinite reconnects. Fixes #115 --- src/use_websocket.rs | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/use_websocket.rs b/src/use_websocket.rs index 006c62c..81dc785 100644 --- a/src/use_websocket.rs +++ b/src/use_websocket.rs @@ -233,6 +233,7 @@ pub fn use_websocket_with_options( let reconnect_timer_ref: StoredValue> = store_value(None); let reconnect_times_ref: StoredValue = store_value(0); + let manually_closed_ref: StoredValue = store_value(false); let unmounted = Rc::new(Cell::new(false)); @@ -242,11 +243,11 @@ pub fn use_websocket_with_options( { let reconnect_ref: StoredValue>> = store_value(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() + if !manually_closed_ref.get_value() + && !reconnect_limit.is_exceeded_by(reconnect_times_ref.get_value()) + && ws_ref + .get_value() .map_or(false, |ws: WebSocket| ws.ready_state() != WebSocket::OPEN) { reconnect_timer_ref.set_value( @@ -266,13 +267,12 @@ pub fn use_websocket_with_options( }); connect_ref.set_value({ - let ws = ws_ref.get_value(); let unmounted = Rc::clone(&unmounted); Some(Rc::new(move || { reconnect_timer_ref.set_value(None); - if let Some(web_socket) = &ws { + if let Some(web_socket) = ws_ref.get_value() { let _ = web_socket.close(); } @@ -470,7 +470,7 @@ pub fn use_websocket_with_options( reconnect_timer_ref.set_value(None); move || { - reconnect_times_ref.set_value(reconnect_limit); + manually_closed_ref.set_value(true); if let Some(web_socket) = ws_ref.get_value() { let _ = web_socket.close(); } @@ -501,6 +501,26 @@ pub fn use_websocket_with_options( send_bytes, } } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ReconnectLimit { + Infinite, + Limited(u64), +} + +impl Default for ReconnectLimit { + fn default() -> Self { + ReconnectLimit::Limited(3) + } +} + +impl ReconnectLimit { + pub fn is_exceeded_by(self, times: u64) -> bool { + match self { + ReconnectLimit::Infinite => false, + ReconnectLimit::Limited(limit) => times >= limit, + } + } +} /// Options for [`use_websocket_with_options`]. #[derive(DefaultBuilder)] @@ -515,8 +535,9 @@ pub struct UseWebSocketOptions { on_error: Rc, /// `WebSocket` close callback. on_close: Rc, - /// Retry times. Defaults to 3. - reconnect_limit: u64, + /// Retry times. Defaults to `ReconnectLimit::Limited(3)`. Use `ReconnectLimit::Infinite` for + /// infinite retries. + reconnect_limit: ReconnectLimit, /// Retry interval in ms. Defaults to 3000. reconnect_interval: u64, /// If `true` the `WebSocket` connection will immediately be opened when calling this function. @@ -535,7 +556,7 @@ impl Default for UseWebSocketOptions { on_message_bytes: Rc::new(|_| {}), on_error: Rc::new(|_| {}), on_close: Rc::new(|_| {}), - reconnect_limit: 3, + reconnect_limit: ReconnectLimit::default(), reconnect_interval: 3000, immediate: true, protocols: Default::default(), From a8de6a96dd7dd7a1a6d91985dc677239d88c4543 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Fri, 7 Jun 2024 02:19:07 +0200 Subject: [PATCH 12/24] refactored codecs and implemented msgpack and bincode as binary codecs. prost is now a binary codec as well and base64 is available as an adapter. --- .github/workflows/ci.yml | 6 +- .github/workflows/tests.yml | 6 +- CHANGELOG.md | 104 ++++++++------- Cargo.toml | 10 +- docs/book/src/codecs.md | 156 ++++++++++++++++++++++ src/storage/use_local_storage.rs | 10 +- src/storage/use_session_storage.rs | 10 +- src/storage/use_storage.rs | 59 ++++---- src/use_broadcast_channel.rs | 63 +++++---- src/use_cookie.rs | 64 ++++----- src/use_event_source.rs | 54 ++++---- src/utils/codecs/bin/bincode_serde.rs | 46 +++++++ src/utils/codecs/bin/from_to_bytes.rs | 69 +++++++--- src/utils/codecs/bin/mod.rs | 23 ++-- src/utils/codecs/bin/msgpack_serde.rs | 46 +++++++ src/utils/codecs/bin/prost.rs | 76 +++++++++++ src/utils/codecs/mod.rs | 42 ++++++ src/utils/codecs/string/base64.rs | 67 ++++++++++ src/utils/codecs/string/from_to_string.rs | 30 +++-- src/utils/codecs/string/json.rs | 154 --------------------- src/utils/codecs/string/json_serde.rs | 67 ++++++++++ src/utils/codecs/string/mod.rs | 43 ++---- src/utils/codecs/string/option.rs | 45 +++++++ src/utils/codecs/string/prost.rs | 80 ----------- 24 files changed, 826 insertions(+), 504 deletions(-) create mode 100644 docs/book/src/codecs.md create mode 100644 src/utils/codecs/bin/bincode_serde.rs create mode 100644 src/utils/codecs/bin/msgpack_serde.rs create mode 100644 src/utils/codecs/bin/prost.rs create mode 100644 src/utils/codecs/string/base64.rs delete mode 100644 src/utils/codecs/string/json.rs create mode 100644 src/utils/codecs/string/json_serde.rs create mode 100644 src/utils/codecs/string/option.rs delete mode 100644 src/utils/codecs/string/prost.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d06c55f..a12fb58 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,serde + run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde - name: Run tests (axum) - run: cargo test --features math,docs,ssr,prost,serde,axum --doc use_cookie::use_cookie + run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,axum --doc use_cookie::use_cookie - name: Run tests (actix) - run: cargo test --features math,docs,ssr,prost,serde,actix --doc use_cookie::use_cookie + run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,actix --doc use_cookie::use_cookie #### mdbook - name: Install mdbook I diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d43d0b4..a04226a 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,serde + run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde - name: Run tests (axum) - run: cargo test --features math,docs,ssr,prost,serde,axum --doc use_cookie::use_cookie + run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,axum --doc use_cookie::use_cookie - name: Run tests (actix) - run: cargo test --features math,docs,ssr,prost,serde,actix --doc use_cookie::use_cookie + run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,actix --doc use_cookie::use_cookie diff --git a/CHANGELOG.md b/CHANGELOG.md index 37ce592..c4a94f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,13 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `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 no re-exported -- Fixed typo in compiler error messages in `use_cookie`. +- `cookie::SameSite` is now re-exported +- Fixed typo in compiler error messages in `use_cookie` (thanks to @SleeplessOne1917). ### 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`. ## [0.10.10] - 2024-05-10 @@ -117,8 +120,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `leptos` version is now 0.6 - The trait `Codec` has been renamed to `StringCodec` and has been moved to `util::StringCodec`. - - The struct `StringCodec` has been renamed to `FromToStringCodec` and has been moved to `util::FromToStringCodec`. - - The structs `JsonCodec` and `ProstCodec` have been moved to `util` as well. + - The struct `StringCodec` has been renamed to `FromToStringCodec` and has been moved to `util::FromToStringCodec`. + - The structs `JsonCodec` and `ProstCodec` have been moved to `util` as well. - The function `use_storage` now requires type parameters for the stored type and the codec like all the other `...storage...` functions. @@ -133,7 +136,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - You can now convert `leptos::html::HtmlElement` into `Element(s)MaybeSignal`. This should make functions a lot easier to use in directives. - There's now a chapter in the book especially for `Element(s)MaybeSignal`. -- Throttled or debounced callbacks (in watch\__ or _\_fn) no longer are called after the containing scope was cleaned up. +- Throttled or debounced callbacks (in watch\__ or _\_fn) no longer are called after the containing scope was cleaned + up. - The document returned from `use_document` now supports the methods `query_selector` and `query_selector_all`. ## [0.9.0] - 2023-12-06 @@ -146,15 +150,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - (@feral-dot-io) The use `use__storage` functions have been rewritten to use `Codec`s instead of always requiring `serde`. - - This also removes the feature `storage` - - By default the `StringCodec` is used which relies on types implementing `FromString + ToString` - - If you want to use `JsonCodec` you have to enable the feature `serde` - - If you want to use `ProstCodec` (new!) you have to enable the feature `prost`. + - This also removes the feature `storage` + - By default the `StringCodec` is used which relies on types implementing `FromString + ToString` + - If you want to use `JsonCodec` you have to enable the feature `serde` + - If you want to use `ProstCodec` (new!) you have to enable the feature `prost`. - (@feral-dot-io) The Rust flag `--cfg=web_sys_unstable_apis` is not needed anymore since relevant `web_sys` APIs are now stable. This affects in particular - - `use_element_size` - - `use_resize_observer` + - `use_element_size` + - `use_resize_observer` ### Fixes 🍕 @@ -170,16 +174,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixes 🍕 - Fixed SSR for - - use_timestamp - - use_raf_fn - - use_idle + - use_timestamp + - use_raf_fn + - use_idle ## [0.8.1] - 2023-10-28 ### Fixes 🍕 - Using strings for `ElementMaybeSignal` and `ElementsMaybeSignal` is now SSR safe. - - This fixes specifically `use_color_mode` to work on the server. + - This fixes specifically `use_color_mode` to work on the server. ## [0.8.0] - 2023-10-24 @@ -234,17 +238,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `use_mutation_observer_with_options` now takes a `UseMutationObserverOptions` instead of a `web_sys::MutationObserverInit`. - `use_websocket`: - - takes now a `&str` instead of a `String` as its `url` parameter. - - same for the returned `send` method. - - The `ready_state` return type is now renamed to `ConnectionReadyState` instead of `UseWebSocketReadyState`. - - The returned signals `ready_state`, `message`, `message_bytes` have now the type - `Signal<...>` instead of `ReadSignal<...>` to make them more consistent with other functions. - - The options `reconnect_limit` and `reconnect_interval` now take a `u64` instead of `Option` to improve DX. - - The option `manual` has been renamed to `immediate` to make it more consistent with other functions. - To port please note that `immediate` is the inverse of `manual` (`immediate` = `!manual`). - - Added documentation how pass it ergonomically as context. + - takes now a `&str` instead of a `String` as its `url` parameter. + - same for the returned `send` method. + - The `ready_state` return type is now renamed to `ConnectionReadyState` instead of `UseWebSocketReadyState`. + - The returned signals `ready_state`, `message`, `message_bytes` have now the type + `Signal<...>` instead of `ReadSignal<...>` to make them more consistent with other functions. + - The options `reconnect_limit` and `reconnect_interval` now take a `u64` instead of `Option` to improve DX. + - The option `manual` has been renamed to `immediate` to make it more consistent with other functions. + To port please note that `immediate` is the inverse of `manual` (`immediate` = `!manual`). + - Added documentation how pass it ergonomically as context. - `use_color_mode`: - - The optional `on_changed` handler parameters have changed slightly. Please refer to the docs for more details. + - The optional `on_changed` handler parameters have changed slightly. Please refer to the docs for more details. - Throttled or debounced functions cannot be `FnOnce` anymore. - All traits `ClonableFn...` have been removed. @@ -255,16 +259,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Callback in `use_raf_fn` doesn't require to be cloneable anymore - All (!) functions can now be safely called on the server. Specifically this includes the following that before panicked on the server: - - `use_scroll` - - `use_event_listener` - - `use_element_hover` - - `on_click_outside` - - `use_drop_zone` - - `use_element_size` - - `use_element_visibility` - - `use_resize_observer` - - `use_intersection_observer` - - `use_mutation_observer` + - `use_scroll` + - `use_event_listener` + - `use_element_hover` + - `on_click_outside` + - `use_drop_zone` + - `use_element_size` + - `use_element_visibility` + - `use_resize_observer` + - `use_intersection_observer` + - `use_mutation_observer` ### Fixes 🍕 @@ -304,18 +308,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The following functions now accept a `MaybeRwSignal` as their initial/default value which means you can use a synchronized `RwSignal` in those places. - - `use_color_mode` - - `use_cycle_list` - - `use_favicon` - - `use_storage` - - `use_local_storage` - - `use_session_storage` + - `use_color_mode` + - `use_cycle_list` + - `use_favicon` + - `use_storage` + - `use_local_storage` + - `use_session_storage` - Instead of returning `ReadSignal`, the following functions now return `Signal`. - - `use_color_mode` - - `use_favicon` - - `use_storage` - - `use_local_storage` - - `use_session_storage` + - `use_color_mode` + - `use_favicon` + - `use_storage` + - `use_local_storage` + - `use_session_storage` ### Fixes 🍕 @@ -388,11 +392,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - You can now specify a `&str` or `Signal` with CSS selectors wherever a node ref is accepted - Callbacks of the following functions no longer require `Clone` - - `use_resize_observer` - - `use_intersection_observer` + - `use_resize_observer` + - `use_intersection_observer` - These functions now also accept multiple target elements in addition to a single one: - - `use_resize_observer` - - `use_intersection_observer` + - `use_resize_observer` + - `use_intersection_observer` ### New Functions 🚀 diff --git a/Cargo.toml b/Cargo.toml index e26ee4e..d7a8d19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ actix-web = { version = "4", optional = true, default-features = false } async-trait = "0.1" base64 = { version = "0.21", optional = true } cfg-if = "1" +bincode = { version = "1", optional = true } cookie = { version = "0.18", features = ["percent-encode"] } default-struct-builder = "0.5" futures-util = "0.3" @@ -141,12 +142,13 @@ actix = ["dep:actix-web", "dep:leptos_actix", "dep:http0_2"] axum = ["dep:leptos_axum", "dep:http1"] docs = [] math = ["num"] -prost = ["base64", "dep:prost"] -serde = ["dep:serde", "serde_json"] +prost = ["dep:prost"] +json_serde = ["dep:serde_json", "dep:serde"] spin = ["dep:leptos-spin", "dep:http1"] ssr = [] -msgpack = ["dep:rmp-serde", "dep:serde"] +msgpack_serde = ["dep:rmp-serde", "dep:serde"] +bincode_serde = ["dep:bincode", "dep:serde"] wasm_ssr = [] [package.metadata.docs.rs] -features = ["math", "docs", "ssr", "prost", "serde"] +features = ["math", "docs", "ssr", "prost", "json_serde", "msgpack_serde", "bincode_serde"] diff --git a/docs/book/src/codecs.md b/docs/book/src/codecs.md new file mode 100644 index 0000000..2e54fa2 --- /dev/null +++ b/docs/book/src/codecs.md @@ -0,0 +1,156 @@ +# Encoding and Decoding Data + +Several functions encode and decode data for storing it and/or sending it over the network. To do this, codecs +located at [`src/utils/codecs`](https://github.com/Synphonyte/leptos-use/tree/main/src/utils/codecs) are used. They +implement the traits [`Encoder`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/mod.rs#L9) with the +method `encode` and [`Decoder`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/mod.rs#L17) with the +method `decode`. + +There are two types of codecs: One that encodes as binary data (`Vec[u8]`) and another type that encodes as +strings (`String`). There is also an adapter +[`Base64`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/base64.rs) that can be used to +wrap a binary codec and make it a string codec by representing the binary data as a base64 string. + +## Available Codecs + +### String Codecs + +- [**`FromToStringCodec` + **](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/from_to_string.rs) +- [**`JsonSerdeCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/json_serde.rs)** + +### Binary Codecs + +- [**`FromToBytesCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/binary/from_to_bytes.rs) +- [**`BincodeSerdeCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/binary/bincode_serde.rs) +- [**`MsgpackSerdeCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/binary/msgpack_serde.rs) + +### Adapters + +- [**`Base64`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/base64.rs) — + Wraps a binary codec and make it a string codec by representing the binary data as a base64 string. +- [**`OptionCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/option.rs) — + Wraps a string codec that encodes `T` to create a codec that encodes `Option`. + +## Example + +In this example, a codec is given to [`use_cookie`](browser/use_cookie.md) that stores data as a string in the JSON +format. Since cookies can only store strings, we have to use string codecs here. + +```rust,noplayground +# use leptos::*; +# use leptos_use::use_cookie; +# use serde::{Deserialize, Serialize}; + +# #[component] +# pub fn App(cx: Scope) -> impl IntoView { +#[derive(Serialize, Deserialize, Clone)] +struct MyState { + chicken_count: i32, + egg_count: i32, +} + +let (cookie, set_cookie) = use_cookie::("my-state-cookie"); +# view! {} +# } +``` + +## Custom Codecs + +If you don't find a suitable codecs for your needs, you can implement your own; it's straightforward! If you want to +create a string codec, you can look +at [`JsonSerdeCodec`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/json_serde.rs). +In case it's a binary codec, have a look +at [`BincodeSerdeCodec`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/binary/bincode_serde.rs). + +## Versioning + +Versioning is the process of handling long-term data that can outlive our code. + +For example, we could have a settings struct whose members change over time. We might eventually +add timezone support, and we might then remove support for a thousands separator for numbers. +Each change results in a new possible version of the stored data. If we stored these settings +in browser storage, we would need to handle all possible versions of the data format that can +occur. If we don't offer versioning, then all settings could revert to the default every time we +encounter an old format. + +How best to handle versioning depends on the codec involved: + +- The `FromToStringCodec` can avoid versioning entirely by keeping + to primitive types. In our example above, we could have decomposed the settings struct into + separate timezone and number separator fields. These would be encoded as strings and stored as + two separate key-value fields in the browser rather than a single field. If a field is missing, + then the value intentionally would fall back to the default without interfering with the other + field. + +- The `ProstCodec` uses [Protocol buffers](https://protobuf.dev/overview/) + designed to solve the problem of long-term storage. It provides semantics for versioning that + are not present in JSON or other formats. + +- The codecs that use serde under the hood can rely on serde or by + providing their own manual version handling. See the next sections for more details. + +### Rely on `serde` + +A simple way to avoid complex versioning is to rely on serde's [field attributes](https://serde.rs/field-attrs.html) +such as [`serde(default)`](https://serde.rs/field-attrs.html#default) +and [`serde(rename = "...")`](https://serde.rs/field-attrs.html#rename). + +### Manual Version Handling + +We look at the example of the `JsonSerdeCodec` in this section. + +To implement version handling, we parse the JSON generically then transform the +resulting `JsValue` before decoding it into our struct again. + +Let's look at an example. + + ```rust,noplayground + # use leptos::*; + # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions}; + # use serde::{Deserialize, Serialize}; + # use serde_json::json; + # use leptos_use::utils::{Encoder, Decoder}; + # + # pub fn Demo() -> impl IntoView { + #[derive(Serialize, Deserialize, Clone, Default, PartialEq)] + pub struct MyState { + pub hello: String, + // This field was added in a later version + pub greeting: String, + } + + pub struct MyStateCodec; + + impl Encoder for MyStateCodec { + type Error = serde_json::Error; + type Encoded = String; + + fn encode(val: &MyState) -> Result { + serde_json::to_string(val) + } + } + + impl Decoder for MyStateCodec { + type Error = serde_json::Error; + type Encoded = str; + + fn decode(stored_value: &Self::Encoded) -> Result { + let mut val: serde_json::Value = serde_json::from_str(stored_value)?; + // add "greeting": "Hello" to the object if it's missing + if let Some(obj) = val.as_object_mut() { + if !obj.contains_key("greeting") { + obj.insert("greeting".to_string(), json!("Hello")); + } + serde_json::from_value(val) + } else { + Ok(MyState::default()) + } + } + } + + // Then use it like the following just as any other codec. + let (get, set, remove) = use_local_storage::("my-struct-key"); + # view! { } + # } + ``` diff --git a/src/storage/use_local_storage.rs b/src/storage/use_local_storage.rs index a392406..24d3582 100644 --- a/src/storage/use_local_storage.rs +++ b/src/storage/use_local_storage.rs @@ -1,5 +1,5 @@ use super::{use_storage_with_options, StorageType, UseStorageOptions}; -use crate::utils::StringCodec; +use crate::utils::{Decoder, Encoder}; use leptos::signal_prelude::*; /// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). @@ -15,23 +15,23 @@ pub fn use_local_storage( ) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + Default + PartialEq, - C: StringCodec + Default, + C: Encoder + Decoder, { use_storage_with_options::( StorageType::Local, key, - UseStorageOptions::::default(), + UseStorageOptions::>::Error, >::Error>::default(), ) } /// Accepts [`UseStorageOptions`]. See [`use_local_storage`] for details. pub fn use_local_storage_with_options( key: impl AsRef, - options: UseStorageOptions, + options: UseStorageOptions>::Error, >::Error>, ) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + PartialEq, - C: StringCodec + Default, + C: Encoder + Decoder, { use_storage_with_options::(StorageType::Local, key, options) } diff --git a/src/storage/use_session_storage.rs b/src/storage/use_session_storage.rs index 3edf36e..bb13cfe 100644 --- a/src/storage/use_session_storage.rs +++ b/src/storage/use_session_storage.rs @@ -1,5 +1,5 @@ use super::{use_storage_with_options, StorageType, UseStorageOptions}; -use crate::utils::StringCodec; +use crate::utils::{Decoder, Encoder}; use leptos::signal_prelude::*; /// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). @@ -15,23 +15,23 @@ pub fn use_session_storage( ) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + Default + PartialEq, - C: StringCodec + Default, + C: Encoder + Decoder, { use_storage_with_options::( StorageType::Session, key, - UseStorageOptions::::default(), + UseStorageOptions::>::Error, >::Error>::default(), ) } /// Accepts [`UseStorageOptions`]. See [`use_session_storage`] for details. pub fn use_session_storage_with_options( key: impl AsRef, - options: UseStorageOptions, + options: UseStorageOptions>::Error, >::Error>, ) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + PartialEq, - C: StringCodec + Default, + C: Encoder + Decoder, { use_storage_with_options::(StorageType::Session, key, options) } diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 39aa5ea..3bb466c 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -1,9 +1,9 @@ +use crate::utils::{CodecError, Decoder, Encoder}; use crate::{ core::{MaybeRwSignal, StorageType}, - utils::{FilterOptions, StringCodec}, + utils::FilterOptions, }; use default_struct_builder::DefaultBuilder; -use leptos::leptos_dom::HydrationCtx; use leptos::*; use std::rc::Rc; use thiserror::Error; @@ -25,12 +25,13 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// The specified key is where data is stored. All values are stored as UTF-16 strings which /// is then encoded and decoded via the given [`Codec`]. This value is synced with other calls using /// the same key on the smae page and across tabs for local storage. -/// See [`UseStorageOptions`] to see how behaviour can be further customised. +/// See [`UseStorageOptions`] to see how behavior can be further customised. /// -/// See [`StringCodec`] for more details on how to handle versioning — dealing with data that can outlast your code. +/// Values are (en)decoded via the given codec. You can use any of the string codecs or a +/// binary codec wrapped in [`Base64`]. /// -/// > To use the [`JsonCodec`], you will need to add the `"serde"` feature to your project's `Cargo.toml`. -/// > To use [`ProstCodec`], add the feature `"prost"`. +/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are +/// available and what feature flags they require. /// /// ## Example /// @@ -38,12 +39,12 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// # use leptos::*; /// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage}; /// # use serde::{Deserialize, Serialize}; -/// # use leptos_use::utils::{FromToStringCodec, JsonCodec, ProstCodec}; +/// # use leptos_use::utils::{FromToStringCodec, JsonSerdeCodec, ProstCodec, Base64}; /// # /// # #[component] /// # pub fn Demo() -> impl IntoView { /// // Binds a struct: -/// let (state, set_state, _) = use_local_storage::("my-state"); +/// let (state, set_state, _) = use_local_storage::("my-state"); /// /// // Binds a bool, stored as a string: /// let (flag, set_flag, remove_flag) = use_session_storage::("my-flag"); @@ -51,10 +52,10 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// // Binds a number, stored as a string: /// let (count, set_count, _) = use_session_storage::("my-count"); /// // Binds a number, stored in JSON: -/// let (count, set_count, _) = use_session_storage::("my-count-kept-in-js"); +/// let (count, set_count, _) = use_session_storage::("my-count-kept-in-js"); /// /// // Bind string with SessionStorage stored in ProtoBuf format: -/// let (id, set_id, _) = use_storage::( +/// let (id, set_id, _) = use_storage::>( /// StorageType::Session, /// "my-id", /// ); @@ -81,10 +82,6 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// } /// ``` /// -/// ## Create Your Own Custom Codec -/// -/// All you need to do is to implement the [`StringCodec`] trait together with `Default` and `Clone`. -/// /// ## Server-Side Rendering /// /// On the server the returned signals will just read/manipulate the `initial_value` without persistence. @@ -96,7 +93,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// /// ``` /// # use leptos::*; -/// # use leptos_use::storage::use_local_storage; +/// # use leptos_use::storage::use_session_storage; /// # use leptos_use::utils::FromToStringCodec; /// # /// # #[component] @@ -104,7 +101,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// let (flag, set_flag, _) = use_session_storage::("my-flag"); /// /// view! { -/// +/// ///
Some conditional content
///
/// } @@ -134,13 +131,13 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// # /// # #[component] /// # pub fn Example() -> impl IntoView { -/// let (flag, set_flag, _) = use_session_storage_with_options::( +/// let (flag, set_flag, _) = use_local_storage_with_options::( /// "my-flag", /// UseStorageOptions::default().delay_during_hydration(true), /// ); /// /// view! { -/// +/// ///
Some conditional content
///
/// } @@ -153,7 +150,7 @@ pub fn use_storage( ) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Default + Clone + PartialEq, - C: StringCodec + Default, + C: Encoder + Decoder, { use_storage_with_options::(storage_type, key, UseStorageOptions::default()) } @@ -162,11 +159,11 @@ where pub fn use_storage_with_options( storage_type: StorageType, key: impl AsRef, - options: UseStorageOptions, + options: UseStorageOptions>::Error, >::Error>, ) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + PartialEq, - C: StringCodec + Default, + C: Encoder + Decoder, { let UseStorageOptions { on_error, @@ -176,17 +173,15 @@ where delay_during_hydration, } = options; - let codec = C::default(); - let (data, set_data) = initial_value.into_signal(); let default = data.get_untracked(); #[cfg(feature = "ssr")] { - let _ = codec; let _ = on_error; let _ = listen_to_storage_changes; let _ = filter; + let _ = delay_during_hydration; let _ = storage_type; let _ = key; let _ = INTERNAL_STORAGE_EVENT; @@ -278,7 +273,7 @@ where }; // Fetch initial value - if delay_during_hydration && HydrationCtx::is_hydrating() { + if delay_during_hydration && leptos::leptos_dom::HydrationCtx::is_hydrating() { request_animation_frame(fetch_from_storage.clone()); } else { fetch_from_storage(); @@ -381,7 +376,7 @@ where /// Session handling errors returned by [`use_storage_with_options`]. #[derive(Error, Debug)] -pub enum UseStorageError { +pub enum UseStorageError { #[error("storage not available")] StorageNotAvailable(JsValue), #[error("storage not returned from window")] @@ -395,18 +390,18 @@ pub enum UseStorageError { #[error("failed to notify item changed")] NotifyItemChangedFailed(JsValue), #[error("failed to encode / decode item value")] - ItemCodecError(Err), + ItemCodecError(CodecError), } /// Options for use with [`use_local_storage_with_options`], [`use_session_storage_with_options`] and [`use_storage_with_options`]. #[derive(DefaultBuilder)] -pub struct UseStorageOptions +pub struct UseStorageOptions where T: 'static, { // Callback for when an error occurs #[builder(skip)] - on_error: Rc)>, + on_error: Rc)>, // Whether to continuously listen to changes from browser storage listen_to_storage_changes: bool, // Initial value to use when the storage key is not set @@ -430,7 +425,7 @@ fn handle_error( result.map_err(|err| (on_error)(err)) } -impl Default for UseStorageOptions { +impl Default for UseStorageOptions { fn default() -> Self { Self { on_error: Rc::new(|_err| ()), @@ -442,9 +437,9 @@ impl Default for UseStorageOptions { } } -impl UseStorageOptions { +impl UseStorageOptions { /// Optional callback whenever an error occurs. - pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { + pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { Self { on_error: Rc::new(on_error), ..self diff --git a/src/use_broadcast_channel.rs b/src/use_broadcast_channel.rs index f3d07d9..62cfac4 100644 --- a/src/use_broadcast_channel.rs +++ b/src/use_broadcast_channel.rs @@ -1,4 +1,4 @@ -use crate::utils::StringCodec; +use crate::utils::{CodecError, Decoder, Encoder}; use crate::{ js, use_event_listener, use_event_listener_with_options, use_supported, UseEventListenerOptions, }; @@ -44,13 +44,17 @@ use wasm_bindgen::JsValue; /// # } /// ``` /// -/// Just like with [`use_storage`] you can use different codecs for encoding and decoding. +/// Values are (en)decoded via the given codec. You can use any of the string codecs or a +/// binary codec wrapped in [`Base64`]. +/// +/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are +/// available and what feature flags they require. /// /// ``` /// # use leptos::*; /// # use serde::{Deserialize, Serialize}; /// # use leptos_use::use_broadcast_channel; -/// # use leptos_use::utils::JsonCodec; +/// # use leptos_use::utils::JsonSerdeCodec; /// # /// // Data sent in JSON must implement Serialize, Deserialize: /// #[derive(Serialize, Deserialize, Clone, PartialEq)] @@ -61,35 +65,35 @@ use wasm_bindgen::JsValue; /// /// # #[component] /// # fn Demo() -> impl IntoView { -/// use_broadcast_channel::("everyting-is-awesome"); +/// use_broadcast_channel::("everyting-is-awesome"); /// # view! { } /// # } /// ``` -/// -/// ## Create Your Own Custom Codec -/// -/// All you need to do is to implement the [`StringCodec`] trait together with `Default` and `Clone`. pub fn use_broadcast_channel( name: &str, -) -> UseBroadcastChannelReturn +) -> UseBroadcastChannelReturn< + T, + impl Fn(&T) + Clone, + impl Fn() + Clone, + >::Error, + >::Error, +> where - C: StringCodec + Default + Clone, + C: Encoder + Decoder, { let is_supported = use_supported(|| js!("BroadcastChannel" in &window())); let (is_closed, set_closed) = create_signal(false); let (channel, set_channel) = create_signal(None::); let (message, set_message) = create_signal(None::); - let (error, set_error) = create_signal(None::>); - - let codec = C::default(); + let (error, set_error) = create_signal( + None::>::Error, >::Error>>, + ); let post = { - let codec = codec.clone(); - move |data: &T| { if let Some(channel) = channel.get_untracked() { - match codec.encode(data) { + match C::encode(data) { Ok(msg) => { channel .post_message(&msg.into()) @@ -99,7 +103,9 @@ where .ok(); } Err(err) => { - set_error.set(Some(UseBroadcastChannelError::Encode(err))); + set_error.set(Some(UseBroadcastChannelError::Codec(CodecError::Encode( + err, + )))); } } } @@ -125,11 +131,13 @@ where ev::message, move |event| { if let Some(data) = event.data().as_string() { - match codec.decode(data) { + match C::decode(&data) { Ok(msg) => { set_message.set(Some(msg)); } - Err(err) => set_error.set(Some(UseBroadcastChannelError::Decode(err))), + Err(err) => set_error.set(Some(UseBroadcastChannelError::Codec( + CodecError::Decode(err), + ))), } } else { set_error.set(Some(UseBroadcastChannelError::ValueNotString)); @@ -167,12 +175,13 @@ where } /// Return type of [`use_broadcast_channel`]. -pub struct UseBroadcastChannelReturn +pub struct UseBroadcastChannelReturn where T: 'static, PFn: Fn(&T) + Clone, CFn: Fn() + Clone, - Err: 'static, + E: 'static, + D: 'static, { /// `true` if this browser supports `BroadcastChannel`s. pub is_supported: Signal, @@ -190,22 +199,20 @@ where pub close: CFn, /// Latest error as reported by the `messageerror` event. - pub error: Signal>>, + pub error: Signal>>, /// Wether the channel is closed pub is_closed: Signal, } -#[derive(Debug, Error, Clone)] -pub enum UseBroadcastChannelError { +#[derive(Debug, Error)] +pub enum UseBroadcastChannelError { #[error("failed to post message")] PostMessage(JsValue), #[error("channel message error")] MessageEvent(web_sys::MessageEvent), - #[error("failed to encode value")] - Encode(Err), - #[error("failed to decode value")] - Decode(Err), + #[error("failed to (de)encode value")] + Codec(CodecError), #[error("received value is not a string")] ValueNotString, } diff --git a/src/use_cookie.rs b/src/use_cookie.rs index 4fe20d8..e860585 100644 --- a/src/use_cookie.rs +++ b/src/use_cookie.rs @@ -1,7 +1,7 @@ #![allow(clippy::too_many_arguments)] use crate::core::now; -use crate::utils::StringCodec; +use crate::utils::{CodecError, Decoder, Encoder}; use cookie::time::{Duration, OffsetDateTime}; pub use cookie::SameSite; use cookie::{Cookie, CookieJar}; @@ -56,7 +56,11 @@ use std::rc::Rc; /// # } /// ``` /// -/// See [`StringCodec`] for details on how to handle versioning — dealing with data that can outlast your code. +/// Values are (en)decoded via the given codec. You can use any of the string codecs or a +/// binary codec wrapped in [`Base64`]. +/// +/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are +/// available and what feature flags they require. /// /// ## Cookie attributes /// @@ -101,7 +105,7 @@ use std::rc::Rc; /// # use leptos::*; /// # use serde::{Deserialize, Serialize}; /// # use leptos_use::{use_cookie_with_options, UseCookieOptions}; -/// # use leptos_use::utils::JsonCodec; +/// # use leptos_use::utils::JsonSerdeCodec; /// # /// # #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] /// # pub struct Auth { @@ -111,7 +115,7 @@ use std::rc::Rc; /// # /// # #[component] /// # fn Demo() -> impl IntoView { -/// use_cookie_with_options::( +/// use_cookie_with_options::( /// "auth", /// UseCookieOptions::default() /// .ssr_cookies_header_getter(|| { @@ -136,7 +140,7 @@ use std::rc::Rc; /// All you need to do is to implement the [`StringCodec`] trait together with `Default` and `Clone`. pub fn use_cookie(cookie_name: &str) -> (Signal>, WriteSignal>) where - C: StringCodec + Default + Clone, + C: Encoder + Decoder, T: Clone, { use_cookie_with_options::(cookie_name, UseCookieOptions::default()) @@ -145,10 +149,10 @@ where /// Version of [`use_cookie`] that takes [`UseCookieOptions`]. pub fn use_cookie_with_options( cookie_name: &str, - options: UseCookieOptions, + options: UseCookieOptions>::Error, >::Error>, ) -> (Signal>, WriteSignal>) where - C: StringCodec + Default + Clone, + C: Encoder + Decoder, T: Clone, { let UseCookieOptions { @@ -181,7 +185,6 @@ where let (cookie, set_cookie) = create_signal(None::); let jar = store_value(CookieJar::new()); - let codec = C::default(); if !has_expired { let ssr_cookies_header_getter = Rc::clone(&ssr_cookies_header_getter); @@ -193,9 +196,8 @@ where set_cookie.set( jar.get(cookie_name) .and_then(|c| { - codec - .decode(c.value().to_string()) - .map_err(|err| on_error(err)) + C::decode(c.value()) + .map_err(|err| on_error(CodecError::Decode(err))) .ok() }) .or(default_value), @@ -213,19 +215,19 @@ where #[cfg(not(feature = "ssr"))] { + use crate::utils::{FromToStringCodec, OptionCodec}; use crate::{ use_broadcast_channel, watch_pausable, UseBroadcastChannelReturn, WatchPausableReturn, }; let UseBroadcastChannelReturn { message, post, .. } = - use_broadcast_channel::, OptionStringCodec>(&format!( + use_broadcast_channel::, OptionCodec>(&format!( "leptos-use:cookies:{cookie_name}" )); let on_cookie_change = { let cookie_name = cookie_name.to_owned(); let ssr_cookies_header_getter = Rc::clone(&ssr_cookies_header_getter); - let codec = codec.clone(); let on_error = Rc::clone(&on_error); let domain = domain.clone(); let path = path.clone(); @@ -238,7 +240,7 @@ where let value = cookie.with_untracked(|cookie| { cookie .as_ref() - .and_then(|cookie| codec.encode(cookie).map_err(|err| on_error(err)).ok()) + .and_then(|cookie| C::encode(cookie).map_err(|err| on_error(err)).ok()) }); if value @@ -290,7 +292,7 @@ where pause(); if let Some(message) = message { - match codec.decode(message.clone()) { + match C::decode(&message) { Ok(value) => { let ssr_cookies_header_getter = Rc::clone(&ssr_cookies_header_getter); @@ -359,9 +361,11 @@ where if !readonly { let value = cookie .with_untracked(|cookie| { - cookie - .as_ref() - .map(|cookie| codec.encode(&cookie).map_err(|err| on_error(err)).ok()) + cookie.as_ref().map(|cookie| { + C::encode(&cookie) + .map_err(|err| on_error(CodecError::Encode(err))) + .ok() + }) }) .flatten(); jar.update_value(|jar| { @@ -387,7 +391,7 @@ where /// Options for [`use_cookie_with_options`]. #[derive(DefaultBuilder)] -pub struct UseCookieOptions { +pub struct UseCookieOptions { /// [`Max-Age` of the cookie](https://tools.ietf.org/html/rfc6265#section-5.2.2) in milliseconds. The returned signal will turn to `None` after the max age is reached. /// Default: `None` /// @@ -464,10 +468,10 @@ pub struct UseCookieOptions { ssr_set_cookie: Rc, /// Callback for encoding/decoding errors. Defaults to logging the error to the console. - on_error: Rc, + on_error: Rc)>, } -impl Default for UseCookieOptions { +impl Default for UseCookieOptions { #[allow(dead_code)] fn default() -> Self { Self { @@ -879,21 +883,3 @@ fn load_and_parse_cookie_jar( jar }) } - -#[derive(Default, Copy, Clone)] -struct OptionStringCodec; - -impl StringCodec> for OptionStringCodec { - type Error = (); - - fn encode(&self, val: &Option) -> Result { - match val { - Some(val) => Ok(format!("~<|Some|>~{val}")), - None => Ok("~<|None|>~".to_owned()), - } - } - - fn decode(&self, str: String) -> Result, Self::Error> { - Ok(str.strip_prefix("~<|Some|>~").map(|v| v.to_owned())) - } -} diff --git a/src/use_event_source.rs b/src/use_event_source.rs index 5227cac..8e490cf 100644 --- a/src/use_event_source.rs +++ b/src/use_event_source.rs @@ -1,6 +1,6 @@ use crate::core::ConnectionReadyState; -use crate::utils::StringCodec; -use crate::{js, use_event_listener}; +use crate::utils::Decoder; +use crate::{js, use_event_listener, ReconnectLimit}; use default_struct_builder::DefaultBuilder; use leptos::*; use std::cell::Cell; @@ -18,14 +18,16 @@ use thiserror::Error; /// /// ## Usage /// -/// Values are decoded via the given [`Codec`]. +/// Values are decoded via the given decoder. You can use any of the string codecs or a +/// binary codec wrapped in [`Base64`]. +/// +/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are +/// available and what feature flags they require. /// -/// > To use the [`JsonCodec`], you will need to add the `"serde"` feature to your project's `Cargo.toml`. -/// > To use [`ProstCodec`], add the feature `"prost"`. /// /// ``` /// # use leptos::*; -/// # use leptos_use::{use_event_source, UseEventSourceReturn, utils::JsonCodec}; +/// # use leptos_use::{use_event_source, UseEventSourceReturn, utils::JsonSerdeCodec}; /// # use serde::{Deserialize, Serialize}; /// # /// #[derive(Serialize, Deserialize, Clone, PartialEq)] @@ -38,7 +40,7 @@ use thiserror::Error; /// # fn Demo() -> impl IntoView { /// let UseEventSourceReturn { /// ready_state, data, error, close, .. -/// } = use_event_source::("https://event-source-url"); +/// } = use_event_source::("https://event-source-url"); /// # /// # view! { } /// # } @@ -85,7 +87,7 @@ use thiserror::Error; /// /// ``` /// # use leptos::*; -/// # use leptos_use::{use_event_source_with_options, UseEventSourceReturn, UseEventSourceOptions, utils::FromToStringCodec}; +/// # use leptos_use::{use_event_source_with_options, UseEventSourceReturn, UseEventSourceOptions, utils::FromToStringCodec, ReconnectLimit}; /// # /// # #[component] /// # fn Demo() -> impl IntoView { @@ -94,7 +96,7 @@ use thiserror::Error; /// } = use_event_source_with_options::( /// "https://event-source-url", /// UseEventSourceOptions::default() -/// .reconnect_limit(5) // at most 5 attempts +/// .reconnect_limit(ReconnectLimit::Limited(5)) // at most 5 attempts /// .reconnect_interval(2000) // wait for 2 seconds between attempts /// ); /// # @@ -113,22 +115,21 @@ pub fn use_event_source( ) -> UseEventSourceReturn where T: Clone + PartialEq + 'static, - C: StringCodec + Default, + C: Decoder, { - use_event_source_with_options(url, UseEventSourceOptions::::default()) + use_event_source_with_options::(url, UseEventSourceOptions::::default()) } /// Version of [`use_event_source`] that takes a `UseEventSourceOptions`. See [`use_event_source`] for how to use. pub fn use_event_source_with_options( url: &str, - options: UseEventSourceOptions, + options: UseEventSourceOptions, ) -> UseEventSourceReturn where T: Clone + PartialEq + 'static, - C: StringCodec + Default, + C: Decoder, { let UseEventSourceOptions { - codec, reconnect_limit, reconnect_interval, on_failed, @@ -151,7 +152,7 @@ where let set_data_from_string = move |data_string: Option| { if let Some(data_string) = data_string { - match codec.decode(data_string) { + match C::decode(&data_string) { Ok(data) => set_data.set(Some(data)), Err(err) => set_error.set(Some(UseEventSourceError::Deserialize(err))), } @@ -213,12 +214,15 @@ where // only reconnect if EventSource isn't reconnecting by itself // this is the case when the connection is closed (readyState is 2) - if es.ready_state() == 2 && !explicitly_closed.get() && reconnect_limit > 0 { + if es.ready_state() == 2 + && !explicitly_closed.get() + && matches!(reconnect_limit, ReconnectLimit::Limited(_)) + { es.close(); retried.set(retried.get() + 1); - if retried.get() < reconnect_limit { + if reconnect_limit.is_exceeded_by(retried.get()) { set_timeout( move || { if let Some(init) = init.get_value() { @@ -312,16 +316,13 @@ where /// Options for [`use_event_source_with_options`]. #[derive(DefaultBuilder)] -pub struct UseEventSourceOptions +pub struct UseEventSourceOptions where T: 'static, - C: StringCodec, { - /// Decodes from the received String to a value of type `T`. - codec: C, - - /// Retry times. Defaults to 3. - reconnect_limit: u64, + /// Retry times. Defaults to `ReconnectLimit::Limited(3)`. Use `ReconnectLimit::Infinite` for + /// infinite retries. + reconnect_limit: ReconnectLimit, /// Retry interval in ms. Defaults to 3000. reconnect_interval: u64, @@ -344,11 +345,10 @@ where _marker: PhantomData, } -impl + Default> Default for UseEventSourceOptions { +impl Default for UseEventSourceOptions { fn default() -> Self { Self { - codec: C::default(), - reconnect_limit: 3, + reconnect_limit: ReconnectLimit::default(), reconnect_interval: 3000, on_failed: Rc::new(|| {}), immediate: true, diff --git a/src/utils/codecs/bin/bincode_serde.rs b/src/utils/codecs/bin/bincode_serde.rs new file mode 100644 index 0000000..0442fc7 --- /dev/null +++ b/src/utils/codecs/bin/bincode_serde.rs @@ -0,0 +1,46 @@ +use crate::utils::{Decoder, Encoder}; +use serde::{Deserialize, Serialize}; + +/// A codec that relies on `bincode` adn `serde` to encode data in the bincode format. +/// +/// This is only available with the **`bincode` feature** enabled. +pub struct BincodeSerdeCodec; + +impl Encoder for BincodeSerdeCodec { + type Error = bincode::Error; + type Encoded = Vec; + + fn encode(val: &T) -> Result { + bincode::serialize(val) + } +} + +impl Decoder for BincodeSerdeCodec { + type Error = bincode::Error; + type Encoded = [u8]; + + fn decode(val: &Self::Encoded) -> Result { + bincode::deserialize(val) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bincode_codec() { + #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] + struct Test { + s: String, + i: i32, + } + let t = Test { + s: String::from("party time 🎉"), + i: 42, + }; + let enc = BincodeSerdeCodec::encode(&t).unwrap(); + let dec: Test = BincodeSerdeCodec::decode(&enc).unwrap(); + assert_eq!(dec, t); + } +} diff --git a/src/utils/codecs/bin/from_to_bytes.rs b/src/utils/codecs/bin/from_to_bytes.rs index 74537dc..9ef9bc8 100644 --- a/src/utils/codecs/bin/from_to_bytes.rs +++ b/src/utils/codecs/bin/from_to_bytes.rs @@ -1,7 +1,10 @@ -use super::BinCodec; +use crate::utils::{Decoder, Encoder}; use thiserror::Error; -#[derive(Copy, Clone, Default, PartialEq)] +/// A binary codec that uses rust own binary encoding functions to encode and decode data. +/// This can be used if you want to encode only primitives and don't want to rely on third party +/// crates like `bincode` or `rmp-serde`. If you have more complex data check out +/// [`BincodeSerdeCodec`] or [`MsgpackSerdeCodec`]. pub struct FromToBytesCodec; #[derive(Error, Debug)] @@ -15,14 +18,20 @@ pub enum FromToBytesCodecError { macro_rules! impl_bin_codec_for_number { ($num:ty) => { - impl BinCodec<$num> for FromToBytesCodec { - type Error = FromToBytesCodecError; + impl Encoder<$num> for FromToBytesCodec { + type Error = (); + type Encoded = Vec; - fn encode(&self, val: &$num) -> Result, Self::Error> { + fn encode(val: &$num) -> Result { Ok(val.to_be_bytes().to_vec()) } + } - fn decode(&self, val: &[u8]) -> Result<$num, Self::Error> { + impl Decoder<$num> for FromToBytesCodec { + type Error = FromToBytesCodecError; + type Encoded = [u8]; + + fn decode(val: &Self::Encoded) -> Result<$num, Self::Error> { Ok(<$num>::from_be_bytes(val.try_into()?)) } } @@ -50,30 +59,54 @@ impl_bin_codec_for_number!(usize); impl_bin_codec_for_number!(f32); impl_bin_codec_for_number!(f64); -impl BinCodec for FromToBytesCodec { - type Error = FromToBytesCodecError; +impl Encoder for FromToBytesCodec { + type Error = (); + type Encoded = Vec; - fn encode(&self, val: &bool) -> Result, Self::Error> { - let codec = FromToBytesCodec; + fn encode(val: &bool) -> Result { let num: u8 = if *val { 1 } else { 0 }; - codec.encode(&num) + Self::encode(&num) } +} - fn decode(&self, val: &[u8]) -> Result { - let codec = FromToBytesCodec; - let num: u8 = codec.decode(val)?; +impl Decoder for FromToBytesCodec { + type Error = FromToBytesCodecError; + type Encoded = [u8]; + + fn decode(val: &Self::Encoded) -> Result { + let num: u8 = Self::decode(val)?; Ok(num != 0) } } -impl BinCodec for FromToBytesCodec { - type Error = FromToBytesCodecError; +impl Encoder for FromToBytesCodec { + type Error = (); + type Encoded = Vec; - fn encode(&self, val: &String) -> Result, Self::Error> { + fn encode(val: &String) -> Result { Ok(val.as_bytes().to_vec()) } +} - fn decode(&self, val: &[u8]) -> Result { +impl Decoder for FromToBytesCodec { + type Error = FromToBytesCodecError; + type Encoded = [u8]; + + fn decode(val: &Self::Encoded) -> Result { Ok(String::from_utf8(val.to_vec())?) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fromtobytes_codec() { + let t = 50; + + let enc: Vec = FromToBytesCodec::encode(&t).unwrap(); + let dec: i32 = FromToBytesCodec::decode(enc.as_slice()).unwrap(); + assert_eq!(dec, t); + } +} diff --git a/src/utils/codecs/bin/mod.rs b/src/utils/codecs/bin/mod.rs index c28996d..dfcbc72 100644 --- a/src/utils/codecs/bin/mod.rs +++ b/src/utils/codecs/bin/mod.rs @@ -1,15 +1,16 @@ +#[cfg(feature = "bincode_serde")] +mod bincode_serde; mod from_to_bytes; +#[cfg(feature = "msgpack_serde")] +mod msgpack_serde; +#[cfg(feature = "prost")] +mod prost; +#[cfg(feature = "bincode")] +pub use bincode_serde::*; #[allow(unused_imports)] pub use from_to_bytes::*; - -/// A codec for encoding and decoding values to and from strings. -/// These strings are intended to be sent over the network. -pub trait BinCodec: Clone + 'static { - /// The error type returned when encoding or decoding fails. - type Error; - /// Encodes a value to a string. - fn encode(&self, val: &T) -> Result, Self::Error>; - /// Decodes a string to a value. Should be able to decode any string encoded by [`encode`]. - fn decode(&self, val: &[u8]) -> Result; -} +#[cfg(feature = "msgpack")] +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 new file mode 100644 index 0000000..719a72f --- /dev/null +++ b/src/utils/codecs/bin/msgpack_serde.rs @@ -0,0 +1,46 @@ +use crate::utils::{Decoder, Encoder}; +use serde::{Deserialize, Serialize}; + +/// A codec that relies on `rmp-serde` to encode data in the msgpack format. +/// +/// This is only available with the **`msgpack` feature** enabled. +pub struct MsgpackSerdeCodec; + +impl Encoder for MsgpackSerdeCodec { + type Error = rmp_serde::encode::Error; + type Encoded = Vec; + + fn encode(val: &T) -> Result { + rmp_serde::to_vec(val) + } +} + +impl Decoder for MsgpackSerdeCodec { + type Error = rmp_serde::decode::Error; + type Encoded = [u8]; + + fn decode(val: &Self::Encoded) -> Result { + rmp_serde::from_slice(val) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_msgpack_codec() { + #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] + struct Test { + s: String, + i: i32, + } + let t = Test { + s: String::from("party time 🎉"), + i: 42, + }; + let enc = MsgpackSerdeCodec::encode(&t).unwrap(); + let dec: Test = MsgpackSerdeCodec::decode(&enc).unwrap(); + assert_eq!(dec, t); + } +} diff --git a/src/utils/codecs/bin/prost.rs b/src/utils/codecs/bin/prost.rs new file mode 100644 index 0000000..850c190 --- /dev/null +++ b/src/utils/codecs/bin/prost.rs @@ -0,0 +1,76 @@ +use crate::utils::{Decoder, Encoder}; + +/// A codec for storing ProtoBuf messages that relies on [`prost`](https://github.com/tokio-rs/prost) to parse. +/// +/// [Protocol buffers](https://protobuf.dev/overview/) is a serialisation format useful for +/// long-term storage. It provides semantics for versioning that are not present in JSON or other +/// formats. [`prost`] is a Rust implementation of Protocol Buffers. +/// +/// This codec uses [`prost`](https://github.com/tokio-rs/prost) to encode the message into a byte stream. +/// To use it with local storage in the example below we wrap it with [`Base64`] to represent the bytes as a string. +/// +/// ## Example +/// ``` +/// # use leptos::*; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions}; +/// # use leptos_use::utils::{Base64, ProstCodec}; +/// # +/// # pub fn Demo() -> impl IntoView { +/// // Primitive types: +/// let (get, set, remove) = use_local_storage::>("my-key"); +/// +/// // Structs: +/// #[derive(Clone, PartialEq, prost::Message)] +/// pub struct MyState { +/// #[prost(string, tag = "1")] +/// pub hello: String, +/// } +/// let (get, set, remove) = use_local_storage::>("my-struct-key"); +/// # view! { } +/// # } +/// ``` +/// +/// Note: we've defined and used the `prost` attribute here for brevity. Alternate usage would be to +/// describe the message in a .proto file and use [`prost_build`](https://docs.rs/prost-build) to +/// auto-generate the Rust code. +pub struct ProstCodec; + +impl Encoder for ProstCodec { + type Error = (); + type Encoded = Vec; + + fn encode(val: &T) -> Result { + let buf = val.encode_to_vec(); + Ok(buf) + } +} + +impl Decoder for ProstCodec { + type Error = prost::DecodeError; + type Encoded = [u8]; + + fn decode(val: &Self::Encoded) -> Result { + T::decode(val) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_prost_codec() { + #[derive(Clone, PartialEq, prost::Message)] + struct Test { + #[prost(string, tag = "1")] + s: String, + #[prost(int32, tag = "2")] + i: i32, + } + let t = Test { + s: String::from("party time 🎉"), + i: 42, + }; + assert_eq!(ProstCodec::decode(&ProstCodec::encode(&t).unwrap()), Ok(t)); + } +} diff --git a/src/utils/codecs/mod.rs b/src/utils/codecs/mod.rs index 259545f..2f266ad 100644 --- a/src/utils/codecs/mod.rs +++ b/src/utils/codecs/mod.rs @@ -1,4 +1,46 @@ mod bin; mod string; +pub use bin::*; pub use string::*; +use thiserror::Error; + +/// Trait every encoder must implement. +pub trait Encoder: 'static { + type Error; + type Encoded; + + fn encode(val: &T) -> Result; +} + +/// Trait every decoder must implement. +pub trait Decoder: 'static { + type Error; + type Encoded: ?Sized; + + 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}")] + Encode(E), + #[error("failed to decode: {0}")] + Decode(D), +} diff --git a/src/utils/codecs/string/base64.rs b/src/utils/codecs/string/base64.rs new file mode 100644 index 0000000..baa0587 --- /dev/null +++ b/src/utils/codecs/string/base64.rs @@ -0,0 +1,67 @@ +use crate::utils::{Decoder, Encoder}; +use base64::Engine; +use thiserror::Error; + +/// Wraps a binary codec and make it a string codec by representing the binary data as a base64 +/// string. +/// +/// Only available with the **`base64` feature** enabled. +/// +/// Example: +/// +/// ``` +/// # use leptos_use::utils::{Base64, MsgpackSerdeCodec, Encoder, Decoder}; +/// # use serde::{Serialize, Deserialize}; +/// # +/// #[derive(Serialize, Deserialize, PartialEq, Debug)] +/// struct MyState { +/// chicken_count: u32, +/// egg_count: u32, +/// farm_name: String, +/// } +/// +/// let original_value = MyState { +/// chicken_count: 10, +/// egg_count: 20, +/// farm_name: "My Farm".to_owned(), +/// }; +/// +/// let encoded: String = Base64::::encode(&original_value).unwrap(); +/// let decoded: MyState = Base64::::decode(&encoded).unwrap(); +/// +/// assert_eq!(decoded, original_value); +/// ``` +pub struct Base64(C); + +#[derive(Error, Debug, PartialEq)] +pub enum Base64DecodeError { + #[error("failed to decode base64: {0}")] + DecodeBase64(#[from] base64::DecodeError), + #[error("failed to decode: {0}")] + Decoder(Err), +} + +impl Encoder for Base64 +where + E: Encoder>, +{ + type Error = E::Error; + type Encoded = String; + + fn encode(val: &T) -> Result { + Ok(base64::engine::general_purpose::STANDARD.encode(E::encode(val)?)) + } +} + +impl Decoder for Base64 +where + D: Decoder, +{ + type Error = Base64DecodeError; + type Encoded = str; + + fn decode(val: &Self::Encoded) -> Result { + let buf = base64::engine::general_purpose::STANDARD.decode(val)?; + D::decode(&buf).map_err(|err| Base64DecodeError::Decoder(err)) + } +} diff --git a/src/utils/codecs/string/from_to_string.rs b/src/utils/codecs/string/from_to_string.rs index 6d37350..46ebca7 100644 --- a/src/utils/codecs/string/from_to_string.rs +++ b/src/utils/codecs/string/from_to_string.rs @@ -1,9 +1,11 @@ -use super::StringCodec; +use crate::utils::{Decoder, Encoder}; use std::str::FromStr; -/// A codec for strings that relies on [`FromStr`] and [`ToString`] to parse. +/// A string codec that relies on [`FromStr`] and [`ToString`]. It can encode anything that +/// implements [`ToString`] and decode anything that implements [`FromStr`]. /// -/// This makes simple key / value easy to use for primitive types. It is also useful for encoding simple data structures without depending on serde. +/// This makes simple key / value easy to use for primitive types. It is also useful for encoding +/// simply data structures without depending on third party crates like serde and serde_json. /// /// ## Example /// ``` @@ -16,18 +18,23 @@ use std::str::FromStr; /// # view! { } /// # } /// ``` -#[derive(Copy, Clone, Default, PartialEq)] pub struct FromToStringCodec; -impl StringCodec for FromToStringCodec { - type Error = T::Err; +impl Encoder for FromToStringCodec { + type Error = (); + type Encoded = String; - fn encode(&self, val: &T) -> Result { + fn encode(val: &T) -> Result { Ok(val.to_string()) } +} - fn decode(&self, str: String) -> Result { - T::from_str(&str) +impl Decoder for FromToStringCodec { + type Error = T::Err; + type Encoded = str; + + fn decode(val: &Self::Encoded) -> Result { + T::from_str(val) } } @@ -38,8 +45,7 @@ mod tests { #[test] fn test_string_codec() { let s = String::from("party time 🎉"); - let codec = FromToStringCodec; - assert_eq!(codec.encode(&s), Ok(s.clone())); - assert_eq!(codec.decode(s.clone()), Ok(s)); + assert_eq!(FromToStringCodec::encode(&s), Ok(s.clone())); + assert_eq!(FromToStringCodec::decode(&s), Ok(s)); } } diff --git a/src/utils/codecs/string/json.rs b/src/utils/codecs/string/json.rs deleted file mode 100644 index e34bd29..0000000 --- a/src/utils/codecs/string/json.rs +++ /dev/null @@ -1,154 +0,0 @@ -use super::StringCodec; - -/// A codec for storing JSON messages that relies on [`serde_json`] to parse. -/// -/// ## Example -/// ``` -/// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions}; -/// # use serde::{Deserialize, Serialize}; -/// # use leptos_use::utils::JsonCodec; -/// # -/// # pub fn Demo() -> impl IntoView { -/// // Primitive types: -/// let (get, set, remove) = use_local_storage::("my-key"); -/// -/// // Structs: -/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)] -/// pub struct MyState { -/// pub hello: String, -/// } -/// let (get, set, remove) = use_local_storage::("my-struct-key"); -/// # view! { } -/// # } -/// ``` -/// -/// ## Versioning -/// -/// If the JSON decoder fails, the storage hook will return `T::Default` dropping the stored JSON value. See [`Codec`](super::Codec) for general information on codec versioning. -/// -/// ### Rely on serde -/// This codec uses [`serde_json`] under the hood. A simple way to avoid complex versioning is to rely on serde's [field attributes](https://serde.rs/field-attrs.html) such as [`serde(default)`](https://serde.rs/field-attrs.html#default) and [`serde(rename = "...")`](https://serde.rs/field-attrs.html#rename). -/// -/// ### String replacement -/// Previous versions of leptos-use offered a `merge_defaults` fn to rewrite the encoded value. This is possible by wrapping the codec but should be avoided. -/// -/// ``` -/// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions}; -/// # use serde::{Deserialize, Serialize}; -/// # use leptos_use::utils::StringCodec; -/// # -/// # pub fn Demo() -> impl IntoView { -/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)] -/// pub struct MyState { -/// pub hello: String, -/// pub greeting: String, -/// } -/// -/// #[derive(Clone, Default)] -/// pub struct MyStateCodec(); -/// impl StringCodec for MyStateCodec { -/// type Error = serde_json::Error; -/// -/// fn encode(&self, val: &MyState) -> Result { -/// serde_json::to_string(val) -/// } -/// -/// fn decode(&self, stored_value: String) -> Result { -/// let default_value = MyState::default(); -/// let rewritten = if stored_value.contains(r#""greeting":"#) { -/// stored_value -/// } else { -/// // add "greeting": "Hello" to the string -/// stored_value.replace("}", &format!(r#""greeting": "{}"}}"#, default_value.greeting)) -/// }; -/// serde_json::from_str(&rewritten) -/// } -/// } -/// -/// let (get, set, remove) = use_local_storage::("my-struct-key"); -/// # view! { } -/// # } -/// ``` -/// -/// ### Transform a `JsValue` -/// A better alternative to string replacement might be to parse the JSON then transform the resulting `JsValue` before decoding it to to your struct again. -/// -/// ``` -/// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions}; -/// # use serde::{Deserialize, Serialize}; -/// # use serde_json::json; -/// # use leptos_use::utils::StringCodec; -/// # -/// # pub fn Demo() -> impl IntoView { -/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)] -/// pub struct MyState { -/// pub hello: String, -/// pub greeting: String, -/// } -/// -/// #[derive(Clone, Default)] -/// pub struct MyStateCodec(); -/// impl StringCodec for MyStateCodec { -/// type Error = serde_json::Error; -/// -/// fn encode(&self, val: &MyState) -> Result { -/// serde_json::to_string(val) -/// } -/// -/// fn decode(&self, stored_value: String) -> Result { -/// let mut val: serde_json::Value = serde_json::from_str(&stored_value)?; -/// // add "greeting": "Hello" to the object if it's missing -/// if let Some(obj) = val.as_object_mut() { -/// if !obj.contains_key("greeting") { -/// obj.insert("greeting".to_string(), json!("Hello")); -/// } -/// serde_json::from_value(val) -/// } else { -/// Ok(MyState::default()) -/// } -/// } -/// } -/// -/// let (get, set, remove) = use_local_storage::("my-struct-key"); -/// # view! { } -/// # } -/// ``` -#[derive(Copy, Clone, Default, PartialEq)] -pub struct JsonCodec; - -impl StringCodec for JsonCodec { - type Error = serde_json::Error; - - fn encode(&self, val: &T) -> Result { - serde_json::to_string(val) - } - - fn decode(&self, str: String) -> Result { - serde_json::from_str(&str) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_json_codec() { - #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] - struct Test { - s: String, - i: i32, - } - let t = Test { - s: String::from("party time 🎉"), - i: 42, - }; - let codec = JsonCodec; - let enc = codec.encode(&t).unwrap(); - let dec: Test = codec.decode(enc).unwrap(); - assert_eq!(dec, t); - } -} diff --git a/src/utils/codecs/string/json_serde.rs b/src/utils/codecs/string/json_serde.rs new file mode 100644 index 0000000..c1b75a8 --- /dev/null +++ b/src/utils/codecs/string/json_serde.rs @@ -0,0 +1,67 @@ +use crate::utils::{Decoder, Encoder}; + +/// A codec for encoding JSON messages that relies on [`serde_json`]. +/// +/// Only available with the **`json` feature** enabled. +/// +/// ## Example +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions}; +/// # use serde::{Deserialize, Serialize}; +/// # use leptos_use::utils::JsonSerdeCodec; +/// # +/// # pub fn Demo() -> impl IntoView { +/// // Primitive types: +/// let (get, set, remove) = use_local_storage::("my-key"); +/// +/// // Structs: +/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)] +/// pub struct MyState { +/// pub hello: String, +/// } +/// let (get, set, remove) = use_local_storage::("my-struct-key"); +/// # view! { } +/// # } +/// ``` +pub struct JsonSerdeCodec; + +impl Encoder for JsonSerdeCodec { + type Error = serde_json::Error; + type Encoded = String; + + fn encode(val: &T) -> Result { + serde_json::to_string(val) + } +} + +impl Decoder for JsonSerdeCodec { + type Error = serde_json::Error; + type Encoded = str; + + fn decode(val: &Self::Encoded) -> Result { + serde_json::from_str(val) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_json_codec() { + #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] + struct Test { + s: String, + i: i32, + } + let t = Test { + s: String::from("party time 🎉"), + i: 42, + }; + let enc = JsonSerdeCodec::encode(&t).unwrap(); + let dec: Test = JsonSerdeCodec::decode(&enc).unwrap(); + assert_eq!(dec, t); + } +} diff --git a/src/utils/codecs/string/mod.rs b/src/utils/codecs/string/mod.rs index 96c16b7..dc251f2 100644 --- a/src/utils/codecs/string/mod.rs +++ b/src/utils/codecs/string/mod.rs @@ -1,36 +1,13 @@ +#[cfg(feature = "base64")] +mod base64; mod from_to_string; -#[cfg(feature = "serde_json")] -mod json; -#[cfg(feature = "prost")] -mod prost; +#[cfg(feature = "json_serde")] +mod json_serde; +mod option; +#[cfg(feature = "base64")] +pub use base64::*; pub use from_to_string::*; -#[cfg(feature = "serde_json")] -pub use json::*; -#[cfg(feature = "prost")] -pub use prost::*; - -/// A codec for encoding and decoding values to and from strings. -/// These strings are intended to be stored in browser storage or sent over the network. -/// -/// ## Versioning -/// -/// Versioning is the process of handling long-term data that can outlive our code. -/// -/// For example we could have a settings struct whose members change over time. We might eventually add timezone support and we might then remove support for a thousand separator on numbers. Each change results in a new possible version of the stored data. If we stored these settings in browser storage we would need to handle all possible versions of the data format that can occur. If we don't offer versioning then all settings could revert to the default every time we encounter an old format. -/// -/// How best to handle versioning depends on the codec involved: -/// -/// - The [`StringCodec`](super::StringCodec) can avoid versioning entirely by keeping to privimitive types. In our example above, we could have decomposed the settings struct into separate timezone and number separator fields. These would be encoded as strings and stored as two separate key-value fields in the browser rather than a single field. If a field is missing then the value intentionally would fallback to the default without interfering with the other field. -/// -/// - The [`ProstCodec`](super::ProstCodec) uses [Protocol buffers](https://protobuf.dev/overview/) designed to solve the problem of long-term storage. It provides semantics for versioning that are not present in JSON or other formats. -/// -/// - The [`JsonCodec`](super::JsonCodec) stores data as JSON. We can then rely on serde or by providing our own manual version handling. See the codec for more details. -pub trait StringCodec: Clone + 'static { - /// The error type returned when encoding or decoding fails. - type Error; - /// Encodes a value to a string. - fn encode(&self, val: &T) -> Result; - /// Decodes a string to a value. Should be able to decode any string encoded by [`encode`]. - fn decode(&self, str: String) -> Result; -} +#[cfg(feature = "json_serde")] +pub use json_serde::*; +pub use option::*; diff --git a/src/utils/codecs/string/option.rs b/src/utils/codecs/string/option.rs new file mode 100644 index 0000000..12b1669 --- /dev/null +++ b/src/utils/codecs/string/option.rs @@ -0,0 +1,45 @@ +use crate::utils::{Decoder, Encoder}; + +/// Wraps a string codec that encodes `T` to create a codec that encodes `Option`. +/// +/// Example: +/// +/// ``` +/// # use leptos_use::utils::{OptionCodec, FromToStringCodec, Encoder, Decoder}; +/// # +/// let original_value = Some(4); +/// let encoded = OptionCodec::::encode(&original_value).unwrap(); +/// let decoded = OptionCodec::::decode(&encoded).unwrap(); +/// +/// assert_eq!(decoded, original_value); +/// ``` +pub struct OptionCodec(C); + +impl Encoder> for OptionCodec +where + E: Encoder, +{ + type Error = E::Error; + type Encoded = String; + + fn encode(val: &Option) -> Result { + match val { + Some(val) => Ok(format!("~<|Some|>~{}", E::encode(val)?)), + None => Ok("~<|None|>~".to_owned()), + } + } +} + +impl Decoder> for OptionCodec +where + D: Decoder, +{ + type Error = D::Error; + type Encoded = str; + + fn decode(str: &Self::Encoded) -> Result, Self::Error> { + str.strip_prefix("~<|Some|>~") + .map(|v| D::decode(v)) + .transpose() + } +} diff --git a/src/utils/codecs/string/prost.rs b/src/utils/codecs/string/prost.rs deleted file mode 100644 index 9b49d42..0000000 --- a/src/utils/codecs/string/prost.rs +++ /dev/null @@ -1,80 +0,0 @@ -use super::StringCodec; -use base64::Engine; -use thiserror::Error; - -/// A codec for storing ProtoBuf messages that relies on [`prost`] to parse. -/// -/// [Protocol buffers](https://protobuf.dev/overview/) is a serialisation format useful for long-term storage. It provides semantics for versioning that are not present in JSON or other formats. [`prost`] is a Rust implementation of Protocol Buffers. -/// -/// This codec uses [`prost`] to encode the message and then [`base64`](https://docs.rs/base64) to represent the bytes as a string. -/// -/// ## Example -/// ``` -/// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions}; -/// # use leptos_use::utils::ProstCodec; -/// # -/// # pub fn Demo() -> impl IntoView { -/// // Primitive types: -/// let (get, set, remove) = use_local_storage::("my-key"); -/// -/// // Structs: -/// #[derive(Clone, PartialEq, prost::Message)] -/// pub struct MyState { -/// #[prost(string, tag = "1")] -/// pub hello: String, -/// } -/// let (get, set, remove) = use_local_storage::("my-struct-key"); -/// # view! { } -/// # } -/// ``` -/// -/// Note: we've defined and used the `prost` attribute here for brevity. Alternate usage would be to describe the message in a .proto file and use [`prost_build`](https://docs.rs/prost-build) to auto-generate the Rust code. -#[derive(Copy, Clone, Default, PartialEq)] -pub struct ProstCodec; - -#[derive(Error, Debug, PartialEq)] -pub enum ProstCodecError { - #[error("failed to decode base64")] - DecodeBase64(base64::DecodeError), - #[error("failed to decode protobuf")] - DecodeProst(#[from] prost::DecodeError), -} - -impl StringCodec for ProstCodec { - type Error = ProstCodecError; - - fn encode(&self, val: &T) -> Result { - let buf = val.encode_to_vec(); - Ok(base64::engine::general_purpose::STANDARD.encode(buf)) - } - - fn decode(&self, str: String) -> Result { - let buf = base64::engine::general_purpose::STANDARD - .decode(str) - .map_err(ProstCodecError::DecodeBase64)?; - T::decode(buf.as_slice()).map_err(ProstCodecError::DecodeProst) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_prost_codec() { - #[derive(Clone, PartialEq, prost::Message)] - struct Test { - #[prost(string, tag = "1")] - s: String, - #[prost(int32, tag = "2")] - i: i32, - } - let t = Test { - s: String::from("party time 🎉"), - i: 42, - }; - let codec = ProstCodec; - assert_eq!(codec.decode(codec.encode(&t).unwrap()), Ok(t)); - } -} From 4d42f7234af46f1bfc253393313d69c533bbab41 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Mon, 1 Jul 2024 02:29:27 +0100 Subject: [PATCH 13/24] 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) } } From 661a84af2421943d4bb88d1016a9c5f0edf177c6 Mon Sep 17 00:00:00 2001 From: Michael Scofield Date: Mon, 8 Jul 2024 16:09:24 +0200 Subject: [PATCH 14/24] Make `use_derive_signal!` public --- src/is_err.rs | 3 +-- src/is_none.rs | 3 +-- src/is_ok.rs | 3 +-- src/is_some.rs | 3 +-- src/use_to_string.rs | 3 +-- src/utils/mod.rs | 1 - src/utils/use_derive_signal.rs | 3 +-- 7 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/is_err.rs b/src/is_err.rs index 95102b3..2d6e5e1 100644 --- a/src/is_err.rs +++ b/src/is_err.rs @@ -1,7 +1,6 @@ -use crate::utils::use_derive_signal; use leptos::*; -use_derive_signal!( +crate::use_derive_signal!( /// Reactive `Result::is_err()`. /// /// ## Usage diff --git a/src/is_none.rs b/src/is_none.rs index d70ff97..e6bf349 100644 --- a/src/is_none.rs +++ b/src/is_none.rs @@ -1,7 +1,6 @@ -use crate::utils::use_derive_signal; use leptos::*; -use_derive_signal!( +crate::use_derive_signal!( /// Reactive `Option::is_none()`. /// /// ## Usage diff --git a/src/is_ok.rs b/src/is_ok.rs index 38cefcb..c568e82 100644 --- a/src/is_ok.rs +++ b/src/is_ok.rs @@ -1,7 +1,6 @@ -use crate::utils::use_derive_signal; use leptos::*; -use_derive_signal!( +crate::use_derive_signal!( /// Reactive `Result::is_ok()`. /// /// ## Usage diff --git a/src/is_some.rs b/src/is_some.rs index c75ec07..a9ea32e 100644 --- a/src/is_some.rs +++ b/src/is_some.rs @@ -1,7 +1,6 @@ -use crate::utils::use_derive_signal; use leptos::*; -use_derive_signal!( +crate::use_derive_signal!( /// Reactive `Option::is_some()`. /// /// ## Usage diff --git a/src/use_to_string.rs b/src/use_to_string.rs index 6cceb16..445c83d 100644 --- a/src/use_to_string.rs +++ b/src/use_to_string.rs @@ -1,7 +1,6 @@ -use crate::utils::use_derive_signal; use leptos::*; -use_derive_signal!( +crate::use_derive_signal!( /// Reactive `ToString::to_string()`. /// /// ## Usage diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 25f19eb..d5a1fca 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -13,4 +13,3 @@ pub use is::*; pub(crate) use js_value_from_to_string::*; pub use pausable::*; pub(crate) use signal_filtered::*; -pub(crate) use use_derive_signal::*; diff --git a/src/utils/use_derive_signal.rs b/src/utils/use_derive_signal.rs index 1bb1212..1fafc75 100644 --- a/src/utils/use_derive_signal.rs +++ b/src/utils/use_derive_signal.rs @@ -1,3 +1,4 @@ +#[macro_export] macro_rules! use_derive_signal { ( $(#[$outer:meta])* @@ -14,5 +15,3 @@ macro_rules! use_derive_signal { } }; } - -pub(crate) use use_derive_signal; From e4ad9f11aff10f68bdc641ff01de80e756ad8cdd Mon Sep 17 00:00:00 2001 From: Maccesch Date: Mon, 8 Jul 2024 17:10:29 +0100 Subject: [PATCH 15/24] codecs extracted into crate codee --- .github/FUNDING.yml | 2 +- .github/workflows/ci.yml | 8 +- .github/workflows/tests.yml | 8 +- Cargo.toml | 17 ++-- docs/book/src/SUMMARY.md | 1 + docs/book/src/options.md | 22 +++++ examples/use_storage/Cargo.toml | 3 +- examples/use_websocket/Cargo.toml | 3 +- examples/use_websocket/src/main.rs | 2 +- src/storage/use_local_storage.rs | 2 +- src/storage/use_session_storage.rs | 2 +- src/storage/use_storage.rs | 9 +- src/use_broadcast_channel.rs | 6 +- src/use_color_mode.rs | 2 +- src/use_cookie.rs | 10 +- src/use_event_source.rs | 11 ++- src/use_websocket.rs | 8 +- src/utils/codecs/bin/bincode_serde.rs | 45 --------- src/utils/codecs/bin/from_to_bytes.rs | 112 ---------------------- src/utils/codecs/bin/mod.rs | 16 ---- src/utils/codecs/bin/msgpack_serde.rs | 45 --------- src/utils/codecs/bin/prost.rs | 76 --------------- src/utils/codecs/hybrid.rs | 108 --------------------- src/utils/codecs/mod.rs | 32 ------- src/utils/codecs/string/base64.rs | 67 ------------- src/utils/codecs/string/from_to_string.rs | 51 ---------- src/utils/codecs/string/json_serde.rs | 67 ------------- src/utils/codecs/string/mod.rs | 13 --- src/utils/codecs/string/option.rs | 45 --------- src/utils/mod.rs | 2 - 30 files changed, 70 insertions(+), 725 deletions(-) create mode 100644 docs/book/src/options.md delete mode 100644 src/utils/codecs/bin/bincode_serde.rs delete mode 100644 src/utils/codecs/bin/from_to_bytes.rs delete mode 100644 src/utils/codecs/bin/mod.rs delete mode 100644 src/utils/codecs/bin/msgpack_serde.rs delete mode 100644 src/utils/codecs/bin/prost.rs delete mode 100644 src/utils/codecs/hybrid.rs delete mode 100644 src/utils/codecs/mod.rs delete mode 100644 src/utils/codecs/string/base64.rs delete mode 100644 src/utils/codecs/string/from_to_string.rs delete mode 100644 src/utils/codecs/string/json_serde.rs delete mode 100644 src/utils/codecs/string/mod.rs delete mode 100644 src/utils/codecs/string/option.rs diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e2c8ca1..8187b1c 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: [Synphonyte]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: [Synphonyte] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cc0813..95f75d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,13 +30,13 @@ jobs: - name: Check formatting run: cargo fmt --check - name: Clippy - run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings + run: cargo clippy --features docs,math --tests -- -D warnings - name: Run tests (general) - run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64 + run: cargo test --features math,docs,ssr - name: Run tests (axum) - run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64,axum --doc use_cookie::use_cookie + run: cargo test --features math,docs,ssr,axum --doc use_cookie::use_cookie - name: Run tests (actix) - run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64,actix --doc use_cookie::use_cookie + run: cargo test --features math,docs,ssr,actix --doc use_cookie::use_cookie #### mdbook - name: Install mdbook I diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 559a729..68ee5cf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,10 +23,10 @@ jobs: - name: Check formatting run: cargo fmt --check - name: Clippy - run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings + run: cargo clippy --features docs,math --tests -- -D warnings - name: Run tests (general) - run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64 + run: cargo test --features math,docs,ssr - name: Run tests (axum) - run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64,axum --doc use_cookie::use_cookie + run: cargo test --features math,docs,ssr,axum --doc use_cookie::use_cookie - name: Run tests (actix) - run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64,actix --doc use_cookie::use_cookie + run: cargo test --features math,docs,ssr,actix --doc use_cookie::use_cookie diff --git a/Cargo.toml b/Cargo.toml index d7a8d19..49474ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,8 @@ homepage = "https://leptos-use.rs" [dependencies] actix-web = { version = "4", optional = true, default-features = false } async-trait = "0.1" -base64 = { version = "0.21", optional = true } cfg-if = "1" -bincode = { version = "1", optional = true } +codee = "0.1" cookie = { version = "0.18", features = ["percent-encode"] } default-struct-builder = "0.5" futures-util = "0.3" @@ -33,10 +32,6 @@ leptos_actix = { version = "0.6", optional = true } leptos-spin = { version = "0.1", optional = true } num = { version = "0.4", optional = true } paste = "1" -prost = { version = "0.12", optional = true } -rmp-serde = { version = "1.1", optional = true } -serde = { version = "1", optional = true } -serde_json = { version = "1", optional = true } thiserror = "1" wasm-bindgen = "0.2.92" wasm-bindgen-futures = "0.4" @@ -136,19 +131,19 @@ features = [ getrandom = { version = "0.2", features = ["js"] } leptos_meta = "0.6" rand = "0.8" +codee = { version = "0.1", features = ["json_serde", "msgpack_serde", "base64", "prost"] } +serde = { version = "1", features = ["derive"] } [features] actix = ["dep:actix-web", "dep:leptos_actix", "dep:http0_2"] axum = ["dep:leptos_axum", "dep:http1"] docs = [] math = ["num"] -prost = ["dep:prost"] -json_serde = ["dep:serde_json", "dep:serde"] spin = ["dep:leptos-spin", "dep:http1"] ssr = [] -msgpack_serde = ["dep:rmp-serde", "dep:serde"] -bincode_serde = ["dep:bincode", "dep:serde"] wasm_ssr = [] [package.metadata.docs.rs] -features = ["math", "docs", "ssr", "prost", "json_serde", "msgpack_serde", "bincode_serde"] +features = ["math", "docs", "ssr"] +rustdoc-args = ["--cfg=web_sys_unstable_apis"] +rustc-args = ["--cfg=web_sys_unstable_apis"] \ No newline at end of file diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index d43fb8a..5c32d80 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -2,6 +2,7 @@ [Introduction](introduction.md) [Get Started](get_started.md) +[Options](options.md) [Element Parameters](element_parameters.md) [Server-Side Rendering](server_side_rendering.md) [Encoding and Decoding Data](codecs.md) diff --git a/docs/book/src/options.md b/docs/book/src/options.md new file mode 100644 index 0000000..fe312ca --- /dev/null +++ b/docs/book/src/options.md @@ -0,0 +1,22 @@ +# Options + +Most functions in Leptos-Use come with a version `..._with_options`. For example `use_css_var` has a +version `use_css_var_with_options`. As the name suggests, you can provide additional options to those versions of the +functions. + +These options are defined as structs with the corresponding PascalCase name. For our example `use_css_var_with_options` +the name of the struct is `UseCssVarOptions`. Every option struct implements `Default` and the builder pattern to +make it easy to change only the values needed. This can look like the following example. + +```rust +let (color, set_color) = use_css_var_with_options( + "--color", + UseCssVarOptions::default() + .target(el) + .initial_value("#eee"), +); +``` + +Here only the values `target` and `initial_value` are changed and everything else is left to default. + +TODO : automatic conversion like Fn and Option \ No newline at end of file diff --git a/examples/use_storage/Cargo.toml b/examples/use_storage/Cargo.toml index 98ebbf3..4a53065 100644 --- a/examples/use_storage/Cargo.toml +++ b/examples/use_storage/Cargo.toml @@ -4,11 +4,12 @@ version = "0.1.0" edition = "2021" [dependencies] +codee = { path = "../../../codee", features = ["json_serde"] } leptos = { version = "0.6", features = ["nightly", "csr"] } console_error_panic_hook = "0.1" console_log = "1" log = "0.4" -leptos-use = { path = "../..", features = ["docs", "json_serde"] } +leptos-use = { path = "../..", features = ["docs"] } web-sys = "0.3" serde = "1.0.163" diff --git a/examples/use_websocket/Cargo.toml b/examples/use_websocket/Cargo.toml index 76baa77..971690c 100644 --- a/examples/use_websocket/Cargo.toml +++ b/examples/use_websocket/Cargo.toml @@ -5,10 +5,11 @@ edition = "2021" [dependencies] leptos = { version = "0.6", features = ["nightly", "csr"] } +codee = { path = "../../../codee", features = ["msgpack_serde"] } console_error_panic_hook = "0.1" console_log = "1" log = "0.4" -leptos-use = { path = "../..", features = ["docs", "msgpack_serde"] } +leptos-use = { path = "../..", features = ["docs"] } serde = { version = "1", features = ["derive"] } web-sys = "0.3" diff --git a/examples/use_websocket/src/main.rs b/examples/use_websocket/src/main.rs index 7a5e370..477f3e2 100644 --- a/examples/use_websocket/src/main.rs +++ b/examples/use_websocket/src/main.rs @@ -6,7 +6,7 @@ use leptos_use::{ }; use serde::{Deserialize, Serialize}; -use leptos_use::utils::{FromToStringCodec, MsgpackSerdeCodec}; +use codee::{binary::MsgpackSerdeCodec, string::FromToStringCodec}; use web_sys::{CloseEvent, Event}; #[derive(Serialize, Deserialize, Debug)] diff --git a/src/storage/use_local_storage.rs b/src/storage/use_local_storage.rs index 24d3582..41811e6 100644 --- a/src/storage/use_local_storage.rs +++ b/src/storage/use_local_storage.rs @@ -1,5 +1,5 @@ use super::{use_storage_with_options, StorageType, UseStorageOptions}; -use crate::utils::{Decoder, Encoder}; +use codee::{Decoder, Encoder}; use leptos::signal_prelude::*; /// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). diff --git a/src/storage/use_session_storage.rs b/src/storage/use_session_storage.rs index bb13cfe..b377edf 100644 --- a/src/storage/use_session_storage.rs +++ b/src/storage/use_session_storage.rs @@ -1,5 +1,5 @@ use super::{use_storage_with_options, StorageType, UseStorageOptions}; -use crate::utils::{Decoder, Encoder}; +use codee::{Decoder, Encoder}; use leptos::signal_prelude::*; /// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 1df4133..e98591e 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -1,8 +1,8 @@ -use crate::utils::{CodecError, Decoder, Encoder}; use crate::{ core::{MaybeRwSignal, StorageType}, utils::FilterOptions, }; +use codee::{CodecError, Decoder, Encoder}; use default_struct_builder::DefaultBuilder; use leptos::*; use std::rc::Rc; @@ -39,7 +39,8 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// # use leptos::*; /// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage}; /// # use serde::{Deserialize, Serialize}; -/// # use leptos_use::utils::{FromToStringCodec, JsonSerdeCodec, ProstCodec, Base64}; +/// # use codee::string::{FromToStringCodec, JsonSerdeCodec, Base64}; +/// # use codee::binary::ProstCodec; /// # /// # #[component] /// # pub fn Demo() -> impl IntoView { @@ -94,7 +95,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// ``` /// # use leptos::*; /// # use leptos_use::storage::use_session_storage; -/// # use leptos_use::utils::FromToStringCodec; +/// # use codee::string::FromToStringCodec; /// # /// # #[component] /// # pub fn Example() -> impl IntoView { @@ -127,7 +128,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// ``` /// # use leptos::*; /// # use leptos_use::storage::{use_local_storage_with_options, UseStorageOptions}; -/// # use leptos_use::utils::FromToStringCodec; +/// # use codee::string::FromToStringCodec; /// # /// # #[component] /// # pub fn Example() -> impl IntoView { diff --git a/src/use_broadcast_channel.rs b/src/use_broadcast_channel.rs index 62cfac4..fe9d823 100644 --- a/src/use_broadcast_channel.rs +++ b/src/use_broadcast_channel.rs @@ -1,7 +1,7 @@ -use crate::utils::{CodecError, Decoder, Encoder}; use crate::{ js, use_event_listener, use_event_listener_with_options, use_supported, UseEventListenerOptions, }; +use codee::{CodecError, Decoder, Encoder}; use leptos::*; use thiserror::Error; use wasm_bindgen::JsValue; @@ -23,7 +23,7 @@ use wasm_bindgen::JsValue; /// ``` /// # use leptos::*; /// # use leptos_use::{use_broadcast_channel, UseBroadcastChannelReturn}; -/// # use leptos_use::utils::FromToStringCodec; +/// # use codee::string::FromToStringCodec; /// # /// # #[component] /// # fn Demo() -> impl IntoView { @@ -54,7 +54,7 @@ use wasm_bindgen::JsValue; /// # use leptos::*; /// # use serde::{Deserialize, Serialize}; /// # use leptos_use::use_broadcast_channel; -/// # use leptos_use::utils::JsonSerdeCodec; +/// # use codee::string::JsonSerdeCodec; /// # /// // Data sent in JSON must implement Serialize, Deserialize: /// #[derive(Serialize, Deserialize, Clone, PartialEq)] diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index af35e7f..5e0ba98 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -2,8 +2,8 @@ use crate::core::url; use crate::core::StorageType; use crate::core::{ElementMaybeSignal, MaybeRwSignal}; use crate::storage::{use_storage_with_options, UseStorageOptions}; -use crate::utils::FromToStringCodec; use crate::{sync_signal_with_options, use_cookie, use_preferred_dark, SyncSignalOptions}; +use codee::string::FromToStringCodec; use default_struct_builder::DefaultBuilder; use leptos::*; use std::fmt::{Display, Formatter}; diff --git a/src/use_cookie.rs b/src/use_cookie.rs index e2d6aaf..6f53d8e 100644 --- a/src/use_cookie.rs +++ b/src/use_cookie.rs @@ -1,7 +1,7 @@ #![allow(clippy::too_many_arguments)] use crate::core::now; -use crate::utils::{CodecError, Decoder, Encoder}; +use codee::{CodecError, Decoder, Encoder}; use cookie::time::{Duration, OffsetDateTime}; pub use cookie::SameSite; use cookie::{Cookie, CookieJar}; @@ -30,7 +30,7 @@ use std::rc::Rc; /// ``` /// # use leptos::*; /// # use leptos_use::use_cookie; -/// # use leptos_use::utils::FromToStringCodec; +/// # use codee::string::FromToStringCodec; /// # use rand::prelude::*; /// /// # @@ -70,7 +70,7 @@ use std::rc::Rc; /// # use cookie::SameSite; /// # use leptos::*; /// # use leptos_use::{use_cookie_with_options, UseCookieOptions}; -/// # use leptos_use::utils::FromToStringCodec; +/// # use codee::string::FromToStringCodec; /// # /// # #[component] /// # fn Demo() -> impl IntoView { @@ -105,7 +105,7 @@ use std::rc::Rc; /// # use leptos::*; /// # use serde::{Deserialize, Serialize}; /// # use leptos_use::{use_cookie_with_options, UseCookieOptions}; -/// # use leptos_use::utils::JsonSerdeCodec; +/// # use codee::string::JsonSerdeCodec; /// # /// # #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] /// # pub struct Auth { @@ -215,10 +215,10 @@ where #[cfg(not(feature = "ssr"))] { - use crate::utils::{FromToStringCodec, OptionCodec}; use crate::{ use_broadcast_channel, watch_pausable, UseBroadcastChannelReturn, WatchPausableReturn, }; + use codee::string::{FromToStringCodec, OptionCodec}; let UseBroadcastChannelReturn { message, post, .. } = use_broadcast_channel::, OptionCodec>(&format!( diff --git a/src/use_event_source.rs b/src/use_event_source.rs index fe7dcaa..e158324 100644 --- a/src/use_event_source.rs +++ b/src/use_event_source.rs @@ -1,6 +1,6 @@ use crate::core::ConnectionReadyState; -use crate::utils::Decoder; use crate::{js, use_event_listener, ReconnectLimit}; +use codee::Decoder; use default_struct_builder::DefaultBuilder; use leptos::*; use std::cell::Cell; @@ -27,7 +27,8 @@ use thiserror::Error; /// /// ``` /// # use leptos::*; -/// # use leptos_use::{use_event_source, UseEventSourceReturn, utils::JsonSerdeCodec}; +/// # use leptos_use::{use_event_source, UseEventSourceReturn}; +/// # use codee::string::JsonSerdeCodec; /// # use serde::{Deserialize, Serialize}; /// # /// #[derive(Serialize, Deserialize, Clone, PartialEq)] @@ -56,7 +57,8 @@ use thiserror::Error; /// /// ``` /// # use leptos::*; -/// # use leptos_use::{use_event_source_with_options, UseEventSourceReturn, UseEventSourceOptions, utils::FromToStringCodec}; +/// # use leptos_use::{use_event_source_with_options, UseEventSourceReturn, UseEventSourceOptions}; +/// # use codee::string::FromToStringCodec; /// # /// # #[component] /// # fn Demo() -> impl IntoView { @@ -87,7 +89,8 @@ use thiserror::Error; /// /// ``` /// # use leptos::*; -/// # use leptos_use::{use_event_source_with_options, UseEventSourceReturn, UseEventSourceOptions, utils::FromToStringCodec, ReconnectLimit}; +/// # use leptos_use::{use_event_source_with_options, UseEventSourceReturn, UseEventSourceOptions, ReconnectLimit}; +/// # use codee::string::FromToStringCodec; /// # /// # #[component] /// # fn Demo() -> impl IntoView { diff --git a/src/use_websocket.rs b/src/use_websocket.rs index 79f022b..d7c41e4 100644 --- a/src/use_websocket.rs +++ b/src/use_websocket.rs @@ -8,7 +8,7 @@ use std::time::Duration; use thiserror::Error; use crate::core::ConnectionReadyState; -use crate::utils::{ +use codee::{ CodecError, Decoder, Encoder, HybridCoderError, HybridDecoder, HybridEncoder, IsBinary, }; use default_struct_builder::DefaultBuilder; @@ -31,7 +31,7 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket}; /// /// ``` /// # use leptos::*; -/// # use leptos_use::utils::FromToStringCodec; +/// # use codee::string::FromToStringCodec; /// # use leptos_use::{use_websocket, UseWebSocketReturn}; /// # use leptos_use::core::ConnectionReadyState; /// # @@ -81,7 +81,7 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket}; /// /// ``` /// # use leptos::*; -/// # use leptos_use::utils::MsgpackSerdeCodec; +/// # use codee::binary::MsgpackSerdeCodec; /// # use leptos_use::{use_websocket, UseWebSocketReturn}; /// # use serde::{Deserialize, Serialize}; /// # @@ -164,7 +164,7 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket}; /// /// ``` /// # use leptos::*; -/// # use leptos_use::utils::FromToStringCodec; +/// # use codee::string::FromToStringCodec; /// # use leptos_use::{use_websocket, UseWebSocketReturn}; /// # use std::rc::Rc; /// # #[derive(Clone)] diff --git a/src/utils/codecs/bin/bincode_serde.rs b/src/utils/codecs/bin/bincode_serde.rs deleted file mode 100644 index db1e309..0000000 --- a/src/utils/codecs/bin/bincode_serde.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::utils::{Decoder, Encoder}; - -/// A codec that relies on `bincode` adn `serde` to encode data in the bincode format. -/// -/// This is only available with the **`bincode` feature** enabled. -pub struct BincodeSerdeCodec; - -impl Encoder for BincodeSerdeCodec { - type Error = bincode::Error; - type Encoded = Vec; - - fn encode(val: &T) -> Result { - bincode::serialize(val) - } -} - -impl Decoder for BincodeSerdeCodec { - type Error = bincode::Error; - type Encoded = [u8]; - - fn decode(val: &Self::Encoded) -> Result { - bincode::deserialize(val) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_bincode_codec() { - #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] - struct Test { - s: String, - i: i32, - } - let t = Test { - s: String::from("party time 🎉"), - i: 42, - }; - let enc = BincodeSerdeCodec::encode(&t).unwrap(); - let dec: Test = BincodeSerdeCodec::decode(&enc).unwrap(); - assert_eq!(dec, t); - } -} diff --git a/src/utils/codecs/bin/from_to_bytes.rs b/src/utils/codecs/bin/from_to_bytes.rs deleted file mode 100644 index 9ef9bc8..0000000 --- a/src/utils/codecs/bin/from_to_bytes.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::utils::{Decoder, Encoder}; -use thiserror::Error; - -/// A binary codec that uses rust own binary encoding functions to encode and decode data. -/// This can be used if you want to encode only primitives and don't want to rely on third party -/// crates like `bincode` or `rmp-serde`. If you have more complex data check out -/// [`BincodeSerdeCodec`] or [`MsgpackSerdeCodec`]. -pub struct FromToBytesCodec; - -#[derive(Error, Debug)] -pub enum FromToBytesCodecError { - #[error("failed to convert byte slice to byte array")] - InvalidByteSlice(#[from] std::array::TryFromSliceError), - - #[error("failed to convert byte array to string")] - InvalidString(#[from] std::string::FromUtf8Error), -} - -macro_rules! impl_bin_codec_for_number { - ($num:ty) => { - impl Encoder<$num> for FromToBytesCodec { - type Error = (); - type Encoded = Vec; - - fn encode(val: &$num) -> Result { - Ok(val.to_be_bytes().to_vec()) - } - } - - impl Decoder<$num> for FromToBytesCodec { - type Error = FromToBytesCodecError; - type Encoded = [u8]; - - fn decode(val: &Self::Encoded) -> Result<$num, Self::Error> { - Ok(<$num>::from_be_bytes(val.try_into()?)) - } - } - }; -} - -impl_bin_codec_for_number!(i8); -impl_bin_codec_for_number!(u8); - -impl_bin_codec_for_number!(i16); -impl_bin_codec_for_number!(u16); - -impl_bin_codec_for_number!(i32); -impl_bin_codec_for_number!(u32); - -impl_bin_codec_for_number!(i64); -impl_bin_codec_for_number!(u64); - -impl_bin_codec_for_number!(i128); -impl_bin_codec_for_number!(u128); - -impl_bin_codec_for_number!(isize); -impl_bin_codec_for_number!(usize); - -impl_bin_codec_for_number!(f32); -impl_bin_codec_for_number!(f64); - -impl Encoder for FromToBytesCodec { - type Error = (); - type Encoded = Vec; - - fn encode(val: &bool) -> Result { - let num: u8 = if *val { 1 } else { 0 }; - Self::encode(&num) - } -} - -impl Decoder for FromToBytesCodec { - type Error = FromToBytesCodecError; - type Encoded = [u8]; - - fn decode(val: &Self::Encoded) -> Result { - let num: u8 = Self::decode(val)?; - Ok(num != 0) - } -} - -impl Encoder for FromToBytesCodec { - type Error = (); - type Encoded = Vec; - - fn encode(val: &String) -> Result { - Ok(val.as_bytes().to_vec()) - } -} - -impl Decoder for FromToBytesCodec { - type Error = FromToBytesCodecError; - type Encoded = [u8]; - - fn decode(val: &Self::Encoded) -> Result { - Ok(String::from_utf8(val.to_vec())?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_fromtobytes_codec() { - let t = 50; - - let enc: Vec = FromToBytesCodec::encode(&t).unwrap(); - let dec: i32 = FromToBytesCodec::decode(enc.as_slice()).unwrap(); - assert_eq!(dec, t); - } -} diff --git a/src/utils/codecs/bin/mod.rs b/src/utils/codecs/bin/mod.rs deleted file mode 100644 index 8698906..0000000 --- a/src/utils/codecs/bin/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[cfg(feature = "bincode_serde")] -mod bincode_serde; -mod from_to_bytes; -#[cfg(feature = "msgpack_serde")] -mod msgpack_serde; -#[cfg(feature = "prost")] -mod prost; - -#[cfg(feature = "bincode_serde")] -pub use bincode_serde::*; -#[allow(unused_imports)] -pub use from_to_bytes::*; -#[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 deleted file mode 100644 index 77778c8..0000000 --- a/src/utils/codecs/bin/msgpack_serde.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::utils::{Decoder, Encoder}; - -/// A codec that relies on `rmp-serde` to encode data in the msgpack format. -/// -/// This is only available with the **`msgpack` feature** enabled. -pub struct MsgpackSerdeCodec; - -impl Encoder for MsgpackSerdeCodec { - type Error = rmp_serde::encode::Error; - type Encoded = Vec; - - fn encode(val: &T) -> Result { - rmp_serde::to_vec(val) - } -} - -impl Decoder for MsgpackSerdeCodec { - type Error = rmp_serde::decode::Error; - type Encoded = [u8]; - - fn decode(val: &Self::Encoded) -> Result { - rmp_serde::from_slice(val) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_msgpack_codec() { - #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] - struct Test { - s: String, - i: i32, - } - let t = Test { - s: String::from("party time 🎉"), - i: 42, - }; - let enc = MsgpackSerdeCodec::encode(&t).unwrap(); - let dec: Test = MsgpackSerdeCodec::decode(&enc).unwrap(); - assert_eq!(dec, t); - } -} diff --git a/src/utils/codecs/bin/prost.rs b/src/utils/codecs/bin/prost.rs deleted file mode 100644 index 850c190..0000000 --- a/src/utils/codecs/bin/prost.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::utils::{Decoder, Encoder}; - -/// A codec for storing ProtoBuf messages that relies on [`prost`](https://github.com/tokio-rs/prost) to parse. -/// -/// [Protocol buffers](https://protobuf.dev/overview/) is a serialisation format useful for -/// long-term storage. It provides semantics for versioning that are not present in JSON or other -/// formats. [`prost`] is a Rust implementation of Protocol Buffers. -/// -/// This codec uses [`prost`](https://github.com/tokio-rs/prost) to encode the message into a byte stream. -/// To use it with local storage in the example below we wrap it with [`Base64`] to represent the bytes as a string. -/// -/// ## Example -/// ``` -/// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions}; -/// # use leptos_use::utils::{Base64, ProstCodec}; -/// # -/// # pub fn Demo() -> impl IntoView { -/// // Primitive types: -/// let (get, set, remove) = use_local_storage::>("my-key"); -/// -/// // Structs: -/// #[derive(Clone, PartialEq, prost::Message)] -/// pub struct MyState { -/// #[prost(string, tag = "1")] -/// pub hello: String, -/// } -/// let (get, set, remove) = use_local_storage::>("my-struct-key"); -/// # view! { } -/// # } -/// ``` -/// -/// Note: we've defined and used the `prost` attribute here for brevity. Alternate usage would be to -/// describe the message in a .proto file and use [`prost_build`](https://docs.rs/prost-build) to -/// auto-generate the Rust code. -pub struct ProstCodec; - -impl Encoder for ProstCodec { - type Error = (); - type Encoded = Vec; - - fn encode(val: &T) -> Result { - let buf = val.encode_to_vec(); - Ok(buf) - } -} - -impl Decoder for ProstCodec { - type Error = prost::DecodeError; - type Encoded = [u8]; - - fn decode(val: &Self::Encoded) -> Result { - T::decode(val) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_prost_codec() { - #[derive(Clone, PartialEq, prost::Message)] - struct Test { - #[prost(string, tag = "1")] - s: String, - #[prost(int32, tag = "2")] - i: i32, - } - let t = Test { - s: String::from("party time 🎉"), - i: 42, - }; - assert_eq!(ProstCodec::decode(&ProstCodec::encode(&t).unwrap()), Ok(t)); - } -} diff --git a/src/utils/codecs/hybrid.rs b/src/utils/codecs/hybrid.rs deleted file mode 100644 index 42ccdc8..0000000 --- a/src/utils/codecs/hybrid.rs +++ /dev/null @@ -1,108 +0,0 @@ -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 deleted file mode 100644 index 708ec1d..0000000 --- a/src/utils/codecs/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -mod bin; -mod hybrid; -mod string; - -pub use bin::*; -pub use hybrid::*; -pub use string::*; -use thiserror::Error; - -/// Trait every encoder must implement. -pub trait Encoder: 'static { - type Error; - type Encoded; - - fn encode(val: &T) -> Result; -} - -/// Trait every decoder must implement. -pub trait Decoder: 'static { - type Error; - type Encoded: ?Sized; - - fn decode(val: &Self::Encoded) -> Result; -} - -#[derive(Error, Debug)] -pub enum CodecError { - #[error("failed to encode: {0}")] - Encode(E), - #[error("failed to decode: {0}")] - Decode(D), -} diff --git a/src/utils/codecs/string/base64.rs b/src/utils/codecs/string/base64.rs deleted file mode 100644 index b9f0265..0000000 --- a/src/utils/codecs/string/base64.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::utils::{Decoder, Encoder}; -use base64::Engine; -use thiserror::Error; - -/// Wraps a binary codec and make it a string codec by representing the binary data as a base64 -/// string. -/// -/// Only available with the **`base64` feature** enabled. -/// -/// Example: -/// -/// ``` -/// # use leptos_use::utils::{Base64, MsgpackSerdeCodec, Encoder, Decoder}; -/// # use serde::{Serialize, Deserialize}; -/// # -/// #[derive(Serialize, Deserialize, PartialEq, Debug)] -/// struct MyState { -/// chicken_count: u32, -/// egg_count: u32, -/// farm_name: String, -/// } -/// -/// let original_value = MyState { -/// chicken_count: 10, -/// egg_count: 20, -/// farm_name: "My Farm".to_owned(), -/// }; -/// -/// let encoded: String = Base64::::encode(&original_value).unwrap(); -/// let decoded: MyState = Base64::::decode(&encoded).unwrap(); -/// -/// assert_eq!(decoded, original_value); -/// ``` -pub struct Base64(C); - -#[derive(Error, Debug, PartialEq)] -pub enum Base64DecodeError { - #[error("failed to decode base64: {0}")] - DecodeBase64(#[from] base64::DecodeError), - #[error("failed to decode: {0}")] - Decoder(Err), -} - -impl Encoder for Base64 -where - E: Encoder>, -{ - type Error = E::Error; - type Encoded = String; - - fn encode(val: &T) -> Result { - Ok(base64::engine::general_purpose::STANDARD.encode(E::encode(val)?)) - } -} - -impl Decoder for Base64 -where - D: Decoder, -{ - type Error = Base64DecodeError; - type Encoded = str; - - fn decode(val: &Self::Encoded) -> Result { - let buf = base64::engine::general_purpose::STANDARD.decode(val)?; - D::decode(&buf).map_err(Base64DecodeError::Decoder) - } -} diff --git a/src/utils/codecs/string/from_to_string.rs b/src/utils/codecs/string/from_to_string.rs deleted file mode 100644 index 46ebca7..0000000 --- a/src/utils/codecs/string/from_to_string.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::utils::{Decoder, Encoder}; -use std::str::FromStr; - -/// A string codec that relies on [`FromStr`] and [`ToString`]. It can encode anything that -/// implements [`ToString`] and decode anything that implements [`FromStr`]. -/// -/// This makes simple key / value easy to use for primitive types. It is also useful for encoding -/// simply data structures without depending on third party crates like serde and serde_json. -/// -/// ## Example -/// ``` -/// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions}; -/// # use leptos_use::utils::FromToStringCodec; -/// # -/// # pub fn Demo() -> impl IntoView { -/// let (get, set, remove) = use_local_storage::("my-key"); -/// # view! { } -/// # } -/// ``` -pub struct FromToStringCodec; - -impl Encoder for FromToStringCodec { - type Error = (); - type Encoded = String; - - fn encode(val: &T) -> Result { - Ok(val.to_string()) - } -} - -impl Decoder for FromToStringCodec { - type Error = T::Err; - type Encoded = str; - - fn decode(val: &Self::Encoded) -> Result { - T::from_str(val) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_string_codec() { - let s = String::from("party time 🎉"); - assert_eq!(FromToStringCodec::encode(&s), Ok(s.clone())); - assert_eq!(FromToStringCodec::decode(&s), Ok(s)); - } -} diff --git a/src/utils/codecs/string/json_serde.rs b/src/utils/codecs/string/json_serde.rs deleted file mode 100644 index c1b75a8..0000000 --- a/src/utils/codecs/string/json_serde.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::utils::{Decoder, Encoder}; - -/// A codec for encoding JSON messages that relies on [`serde_json`]. -/// -/// Only available with the **`json` feature** enabled. -/// -/// ## Example -/// -/// ``` -/// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions}; -/// # use serde::{Deserialize, Serialize}; -/// # use leptos_use::utils::JsonSerdeCodec; -/// # -/// # pub fn Demo() -> impl IntoView { -/// // Primitive types: -/// let (get, set, remove) = use_local_storage::("my-key"); -/// -/// // Structs: -/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)] -/// pub struct MyState { -/// pub hello: String, -/// } -/// let (get, set, remove) = use_local_storage::("my-struct-key"); -/// # view! { } -/// # } -/// ``` -pub struct JsonSerdeCodec; - -impl Encoder for JsonSerdeCodec { - type Error = serde_json::Error; - type Encoded = String; - - fn encode(val: &T) -> Result { - serde_json::to_string(val) - } -} - -impl Decoder for JsonSerdeCodec { - type Error = serde_json::Error; - type Encoded = str; - - fn decode(val: &Self::Encoded) -> Result { - serde_json::from_str(val) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_json_codec() { - #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] - struct Test { - s: String, - i: i32, - } - let t = Test { - s: String::from("party time 🎉"), - i: 42, - }; - let enc = JsonSerdeCodec::encode(&t).unwrap(); - let dec: Test = JsonSerdeCodec::decode(&enc).unwrap(); - assert_eq!(dec, t); - } -} diff --git a/src/utils/codecs/string/mod.rs b/src/utils/codecs/string/mod.rs deleted file mode 100644 index dc251f2..0000000 --- a/src/utils/codecs/string/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[cfg(feature = "base64")] -mod base64; -mod from_to_string; -#[cfg(feature = "json_serde")] -mod json_serde; -mod option; - -#[cfg(feature = "base64")] -pub use base64::*; -pub use from_to_string::*; -#[cfg(feature = "json_serde")] -pub use json_serde::*; -pub use option::*; diff --git a/src/utils/codecs/string/option.rs b/src/utils/codecs/string/option.rs deleted file mode 100644 index 12b1669..0000000 --- a/src/utils/codecs/string/option.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::utils::{Decoder, Encoder}; - -/// Wraps a string codec that encodes `T` to create a codec that encodes `Option`. -/// -/// Example: -/// -/// ``` -/// # use leptos_use::utils::{OptionCodec, FromToStringCodec, Encoder, Decoder}; -/// # -/// let original_value = Some(4); -/// let encoded = OptionCodec::::encode(&original_value).unwrap(); -/// let decoded = OptionCodec::::decode(&encoded).unwrap(); -/// -/// assert_eq!(decoded, original_value); -/// ``` -pub struct OptionCodec(C); - -impl Encoder> for OptionCodec -where - E: Encoder, -{ - type Error = E::Error; - type Encoded = String; - - fn encode(val: &Option) -> Result { - match val { - Some(val) => Ok(format!("~<|Some|>~{}", E::encode(val)?)), - None => Ok("~<|None|>~".to_owned()), - } - } -} - -impl Decoder> for OptionCodec -where - D: Decoder, -{ - type Error = D::Error; - type Encoded = str; - - fn decode(str: &Self::Encoded) -> Result, Self::Error> { - str.strip_prefix("~<|Some|>~") - .map(|v| D::decode(v)) - .transpose() - } -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index d5a1fca..906f4e1 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,3 @@ -mod codecs; mod filters; mod is; mod js; @@ -7,7 +6,6 @@ mod pausable; mod signal_filtered; mod use_derive_signal; -pub use codecs::*; pub use filters::*; pub use is::*; pub(crate) use js_value_from_to_string::*; From 9c035d50d8fb9265a5d2761af57372e5c45f3ecd Mon Sep 17 00:00:00 2001 From: Maccesch Date: Mon, 8 Jul 2024 17:16:54 +0100 Subject: [PATCH 16/24] added docs and changelog --- CHANGELOG.md | 28 ++++++++++++-------- docs/book/src/SUMMARY.md | 1 + docs/book/src/utilities/use_derive_signal.md | 5 ++++ src/utils/use_derive_signal.rs | 3 +++ 4 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 docs/book/src/utilities/use_derive_signal.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a9d18f2..a95080c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,23 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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>`. +- Codecs: + - All codecs now live in their own crate `codee` + - There are now binary codecs in addition to string codecs. + - `FromToBytesCodec` + - `WebpackSerdeCodec` + - `BincodeSerdeCodec` + - `ProstCodec` (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 - New book chapter about codecs +- The macro `use_derive_signal!` is now exported (thanks to @mscofield0). ### Breaking Changes 🛠 @@ -35,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. + - All of these structs, traits and features now live in their own crate called `codee` - `use_websocket`: - `UseWebsocketOptions` has been renamed to `UseWebSocketOptions` (uppercase S) to be consistent with the return type. diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 5c32d80..ba3a16b 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -104,6 +104,7 @@ - [is_some](utilities/is_some.md) - [use_cycle_list](utilities/use_cycle_list.md) - [use_debounce_fn](utilities/use_debounce_fn.md) +- [use_derive_signal!](utilities/use_derive_signal.md) - [use_supported](utilities/use_supported.md) - [use_throttle_fn](utilities/use_throttle_fn.md) - [use_to_string](utilities/use_to_string.md) diff --git a/docs/book/src/utilities/use_derive_signal.md b/docs/book/src/utilities/use_derive_signal.md new file mode 100644 index 0000000..a9ee135 --- /dev/null +++ b/docs/book/src/utilities/use_derive_signal.md @@ -0,0 +1,5 @@ +# use_derive_signal! + +Macro to easily create helper functions that derive a signal using a piece of code. + +See [`is_ok`](is_ok.md) or [`use_to_string`](use_to_string.md) as examples. diff --git a/src/utils/use_derive_signal.rs b/src/utils/use_derive_signal.rs index 1fafc75..1e7e9c2 100644 --- a/src/utils/use_derive_signal.rs +++ b/src/utils/use_derive_signal.rs @@ -1,3 +1,6 @@ +/// Macro to easily create helper functions that derive a signal using a piece of code. +/// +/// See [`is_ok`] or [`use_to_string`] as examples. #[macro_export] macro_rules! use_derive_signal { ( From bb017746b9d8e3325ff3e03e810205fa221b8fa4 Mon Sep 17 00:00:00 2001 From: Hector Candelaria Date: Sat, 13 Jul 2024 16:28:27 -0400 Subject: [PATCH 17/24] Feat: Add getUserMedia API implementation Introduced `use_user_media` function and structure to create a reactive streaming interface for media input devices. --- CHANGELOG.md | 4 + Cargo.toml | 3 +- docs/book/src/SUMMARY.md | 1 + docs/book/src/browser/use_user_media.md | 3 + examples/Cargo.toml | 1 + examples/use_user_media/Cargo.toml | 16 + examples/use_user_media/README.md | 23 ++ examples/use_user_media/Trunk.toml | 2 + examples/use_user_media/index.html | 7 + examples/use_user_media/input.css | 3 + examples/use_user_media/rust-toolchain.toml | 2 + examples/use_user_media/src/main.rs | 57 ++++ examples/use_user_media/style/output.css | 350 ++++++++++++++++++++ examples/use_user_media/tailwind.config.js | 15 + src/lib.rs | 2 + src/use_user_media.rs | 204 ++++++++++++ 16 files changed, 692 insertions(+), 1 deletion(-) create mode 100644 docs/book/src/browser/use_user_media.md create mode 100644 examples/use_user_media/Cargo.toml create mode 100644 examples/use_user_media/README.md create mode 100644 examples/use_user_media/Trunk.toml create mode 100644 examples/use_user_media/index.html create mode 100644 examples/use_user_media/input.css create mode 100644 examples/use_user_media/rust-toolchain.toml create mode 100644 examples/use_user_media/src/main.rs create mode 100644 examples/use_user_media/style/output.css create mode 100644 examples/use_user_media/tailwind.config.js create mode 100644 src/use_user_media.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a95080c..4d3e51b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### New Functions 🚀 + +- `use_user_media` + ### New Features 🚀 - Codecs: diff --git a/Cargo.toml b/Cargo.toml index 49474ef..aa60808 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ features = [ "MediaDevices", "MediaQueryList", "MediaStream", + "MediaStreamConstraints", "MediaStreamTrack", "MessageEvent", "MouseEvent", @@ -146,4 +147,4 @@ wasm_ssr = [] [package.metadata.docs.rs] features = ["math", "docs", "ssr"] rustdoc-args = ["--cfg=web_sys_unstable_apis"] -rustc-args = ["--cfg=web_sys_unstable_apis"] \ No newline at end of file +rustc-args = ["--cfg=web_sys_unstable_apis"] diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index ba3a16b..502a1a6 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -49,6 +49,7 @@ - [use_preferred_contrast](browser/use_preferred_contrast.md) - [use_preferred_dark](browser/use_preferred_dark.md) - [use_service_worker](browser/use_service_worker.md) +- [use_user_media](browser/use_user_media.md) - [use_web_notification](browser/use_web_notification.md) # Sensors diff --git a/docs/book/src/browser/use_user_media.md b/docs/book/src/browser/use_user_media.md new file mode 100644 index 0000000..3e39cca --- /dev/null +++ b/docs/book/src/browser/use_user_media.md @@ -0,0 +1,3 @@ +# use_user_media + + diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 4d56bbf..f428959 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -55,6 +55,7 @@ members = [ "use_throttle_fn", "use_timeout_fn", "use_timestamp", + "use_user_media", "use_web_notification", "use_websocket", "use_webtransport", diff --git a/examples/use_user_media/Cargo.toml b/examples/use_user_media/Cargo.toml new file mode 100644 index 0000000..ffd38b8 --- /dev/null +++ b/examples/use_user_media/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "use_user_media" +version = "0.1.0" +edition = "2021" + +[dependencies] +leptos = { version = "0.6", features = ["nightly", "csr"] } +console_error_panic_hook = "0.1" +console_log = "1" +log = "0.4" +leptos-use = { path = "../..", features = ["docs"] } +web-sys = "0.3" + +[dev-dependencies] +wasm-bindgen = "0.2" +wasm-bindgen-test = "0.3.0" diff --git a/examples/use_user_media/README.md b/examples/use_user_media/README.md new file mode 100644 index 0000000..8b47003 --- /dev/null +++ b/examples/use_user_media/README.md @@ -0,0 +1,23 @@ +A simple example for `use_user_media`. + +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 +npm install -D tailwindcss @tailwindcss/forms +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_user_media/Trunk.toml b/examples/use_user_media/Trunk.toml new file mode 100644 index 0000000..3e4be08 --- /dev/null +++ b/examples/use_user_media/Trunk.toml @@ -0,0 +1,2 @@ +[build] +public_url = "/demo/" \ No newline at end of file diff --git a/examples/use_user_media/index.html b/examples/use_user_media/index.html new file mode 100644 index 0000000..ae249a6 --- /dev/null +++ b/examples/use_user_media/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/use_user_media/input.css b/examples/use_user_media/input.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/examples/use_user_media/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/examples/use_user_media/rust-toolchain.toml b/examples/use_user_media/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/examples/use_user_media/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/examples/use_user_media/src/main.rs b/examples/use_user_media/src/main.rs new file mode 100644 index 0000000..54d406c --- /dev/null +++ b/examples/use_user_media/src/main.rs @@ -0,0 +1,57 @@ +use leptos::*; +use leptos_use::docs::demo_or_body; +use leptos_use::{use_user_media, UseUserMediaReturn}; + +#[component] +fn Demo() -> impl IntoView { + let video_ref = create_node_ref::(); + + let UseUserMediaReturn { + stream, + enabled, + set_enabled, + .. + } = use_user_media(); + + create_effect(move |_| { + match stream.get() { + Some(Ok(s)) => { + video_ref.get().map(|v| v.set_src_object(Some(&s))); + return; + } + Some(Err(e)) => logging::error!("Failed to get media stream: {:?}", e), + None => logging::log!("No stream yet"), + } + + video_ref.get().map(|v| v.set_src_object(None)); + }); + + view! { +
+
+ +
+ +
+ +
+
+ } +} + +fn main() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + + mount_to(demo_or_body(), || { + view! { } + }) +} diff --git a/examples/use_user_media/style/output.css b/examples/use_user_media/style/output.css new file mode 100644 index 0000000..e26a0da --- /dev/null +++ b/examples/use_user_media/style/output.css @@ -0,0 +1,350 @@ +[type='text'],input:where(:not([type])),[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + border-radius: 0px; + padding-top: 0.5rem; + padding-right: 0.75rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + font-size: 1rem; + line-height: 1.5rem; + --tw-shadow: 0 0 #0000; +} + +[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + border-color: #2563eb; +} + +input::-moz-placeholder, textarea::-moz-placeholder { + color: #6b7280; + opacity: 1; +} + +input::placeholder,textarea::placeholder { + color: #6b7280; + opacity: 1; +} + +::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +::-webkit-date-and-time-value { + min-height: 1.5em; + text-align: inherit; +} + +::-webkit-datetime-edit { + display: inline-flex; +} + +::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field { + padding-top: 0; + padding-bottom: 0; +} + +select { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + padding-right: 2.5rem; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; +} + +[multiple],[size]:where(select:not([size="1"])) { + background-image: initial; + background-position: initial; + background-repeat: unset; + background-size: initial; + padding-right: 0.75rem; + -webkit-print-color-adjust: unset; + print-color-adjust: unset; +} + +[type='checkbox'],[type='radio'] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding: 0; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + display: inline-block; + vertical-align: middle; + background-origin: border-box; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + flex-shrink: 0; + height: 1rem; + width: 1rem; + color: #2563eb; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + --tw-shadow: 0 0 #0000; +} + +[type='checkbox'] { + border-radius: 0px; +} + +[type='radio'] { + border-radius: 100%; +} + +[type='checkbox']:focus,[type='radio']:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 2px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); +} + +[type='checkbox']:checked,[type='radio']:checked { + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +[type='checkbox']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); +} + +@media (forced-colors: active) { + [type='checkbox']:checked { + -webkit-appearance: auto; + -moz-appearance: auto; + appearance: auto; + } +} + +[type='radio']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); +} + +@media (forced-colors: active) { + [type='radio']:checked { + -webkit-appearance: auto; + -moz-appearance: auto; + appearance: auto; + } +} + +[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='checkbox']:indeterminate { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +@media (forced-colors: active) { + [type='checkbox']:indeterminate { + -webkit-appearance: auto; + -moz-appearance: auto; + appearance: auto; + } +} + +[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='file'] { + background: unset; + border-color: inherit; + border-width: 0; + border-radius: 0; + padding: 0; + font-size: unset; + line-height: inherit; +} + +[type='file']:focus { + outline: 1px solid ButtonText; + outline: 1px auto -webkit-focus-ring-color; +} + +*, ::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: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +::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: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +.static { + position: static; +} + +.flex { + display: flex; +} + +.h-96 { + height: 24rem; +} + +.w-auto { + width: auto; +} + +.flex-col { + flex-direction: column; +} + +.gap-4 { + gap: 1rem; +} + +.text-center { + text-align: center; +} + +.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)); + } +} \ No newline at end of file diff --git a/examples/use_user_media/tailwind.config.js b/examples/use_user_media/tailwind.config.js new file mode 100644 index 0000000..bc09f5e --- /dev/null +++ b/examples/use_user_media/tailwind.config.js @@ -0,0 +1,15 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: { + files: ["*.html", "./src/**/*.rs", "../../src/docs/**/*.rs"], + }, + theme: { + extend: {}, + }, + corePlugins: { + preflight: false, + }, + plugins: [ + require('@tailwindcss/forms'), + ], +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 47c50b0..41112af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ mod is_none; mod is_ok; mod is_some; mod on_click_outside; +mod use_user_media; mod signal_debounced; mod signal_throttled; mod sync_signal; @@ -90,6 +91,7 @@ pub use is_none::*; pub use is_ok::*; pub use is_some::*; pub use on_click_outside::*; +pub use use_user_media::*; pub use signal_debounced::*; pub use signal_throttled::*; pub use sync_signal::*; diff --git a/src/use_user_media.rs b/src/use_user_media.rs new file mode 100644 index 0000000..2336f38 --- /dev/null +++ b/src/use_user_media.rs @@ -0,0 +1,204 @@ +use crate::core::MaybeRwSignal; +use cfg_if::cfg_if; +use default_struct_builder::DefaultBuilder; +use leptos::*; +use wasm_bindgen::{JsCast, JsValue}; + +/// Reactive [`mediaDevices.getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) streaming. +/// +/// ## Demo +/// +/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_user_media) +/// +/// ## Usage +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::{use_user_media, UseUserMediaReturn}; +/// # +/// # #[component] +/// # fn Demo() -> impl IntoView { +/// let video_ref = create_node_ref::(); +/// +/// let UseUserMediaReturn { stream, start, .. } = use_user_media(); +/// +/// start(); +/// +/// create_effect(move |_| +/// video_ref.get().map(|v| { +/// match stream.get() { +/// Some(Ok(s)) => v.set_src_object(Some(&s)), +/// Some(Err(e)) => logging::error!("Failed to get media stream: {:?}", e), +/// None => logging::log!("No stream yet"), +/// } +/// }) +/// ); +/// +/// view! { } +/// # } +/// ``` +/// +/// ## Server-Side Rendering +/// +/// On the server calls to `start` or any other way to enable the stream will be ignored +/// and the stream will always be `None`. +pub fn use_user_media() -> UseUserMediaReturn { + use_user_media_with_options(UseUserMediaOptions::default()) +} + +/// Version of [`use_user_media`] that takes a `UseUserMediaOptions`. See [`use_user_media`] for how to use. +pub fn use_user_media_with_options( + options: UseUserMediaOptions, +) -> UseUserMediaReturn { + let UseUserMediaOptions { + enabled, + video, + audio, + .. + } = options; + + let (enabled, set_enabled) = enabled.into_signal(); + + let (stream, set_stream) = create_signal(None::>); + + let _start = move || async move { + cfg_if! { if #[cfg(not(feature = "ssr"))] { + if stream.get_untracked().is_some() { + return; + } + + let stream = create_media(video, audio).await; + + set_stream.update(|s| *s = Some(stream)); + } else { + let _ = video; + let _ = audio; + }} + }; + + let _stop = move || { + if let Some(Ok(stream)) = stream.get_untracked() { + for track in stream.get_tracks() { + track.unchecked_ref::().stop(); + } + } + + set_stream.set(None); + }; + + let start = move || { + cfg_if! { if #[cfg(not(feature = "ssr"))] { + spawn_local(async move { + _start().await; + stream.with_untracked(move |stream| { + if let Some(Ok(_)) = stream { + set_enabled.set(true); + } + }); + }); + }} + }; + + let stop = move || { + _stop(); + set_enabled.set(false); + }; + + let _ = watch( + move || enabled.get(), + move |enabled, _, _| { + if *enabled { + spawn_local(async move { + _start().await; + }); + } else { + _stop(); + } + }, + true, + ); + UseUserMediaReturn { + stream: stream.into(), + start, + stop, + enabled, + set_enabled, + } +} + +#[cfg(not(feature = "ssr"))] +async fn create_media(video: bool, audio: bool) -> Result { + use crate::js_fut; + use crate::use_window::use_window; + + let media = use_window() + .navigator() + .ok_or_else(|| JsValue::from_str("Failed to access window.navigator")) + .and_then(|n| n.media_devices())?; + + let mut constraints = web_sys::MediaStreamConstraints::new(); + if video { + constraints.video(&JsValue::from(true)); + } + if audio { + constraints.audio(&JsValue::from(true)); + } + + let promise = media.get_user_media_with_constraints(&constraints)?; + let res = js_fut!(promise).await?; + + Ok::<_, JsValue>(web_sys::MediaStream::unchecked_from_js(res)) +} + +/// Options for [`use_user_media_with_options`]. +/// Either or both constraints must be specified. +/// If the browser cannot find all media tracks with the specified types that meet the constraints given, +/// then the returned promise is rejected with `NotFoundError` +#[derive(DefaultBuilder, Clone, Copy, Debug)] +pub struct UseUserMediaOptions { + /// If the stream is enabled. Defaults to `false`. + enabled: MaybeRwSignal, + /// Constraint parameter describing video media type requested + /// The default value is `false`. + video: bool, + /// Constraint parameter describing audio media type requested + /// The default value is `false`. + audio: bool, +} + +impl Default for UseUserMediaOptions { + fn default() -> Self { + Self { + enabled: false.into(), + video: true, + audio: false, + } + } +} + +/// Return type of [`use_user_media`]. +#[derive(Clone)] +pub struct UseUserMediaReturn +where + StartFn: Fn() + Clone, + StopFn: Fn() + Clone, +{ + /// The current [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) if it exists. + /// Initially this is `None` until `start` resolved successfully. + /// In case the stream couldn't be started, for example because the user didn't grant permission, + /// this has the value `Some(Err(...))`. + pub stream: Signal>>, + + /// Starts the screen streaming. Triggers the ask for permission if not already granted. + pub start: StartFn, + + /// Stops the screen streaming + pub stop: StopFn, + + /// A value of `true` indicates that the returned [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) + /// has resolved successfully and thus the stream is enabled. + pub enabled: Signal, + + /// A value of `true` is the same as calling `start()` whereas `false` is the same as calling `stop()`. + pub set_enabled: WriteSignal, +} From 6918314bfffc781af5fed051be5d79957d176fac Mon Sep 17 00:00:00 2001 From: Brian Carlsen Date: Mon, 15 Jul 2024 02:43:48 +0200 Subject: [PATCH 18/24] Uses caller tracking for filtered signals for better warning reporting. --- src/utils/signal_filtered.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/signal_filtered.rs b/src/utils/signal_filtered.rs index 3a81446..1e7ad03 100644 --- a/src/utils/signal_filtered.rs +++ b/src/utils/signal_filtered.rs @@ -8,6 +8,7 @@ macro_rules! signal_filtered { ) => { paste! { $(#[$outer])* + #[track_caller] pub fn []( value: S, ms: impl Into> + 'static, @@ -27,6 +28,7 @@ macro_rules! signal_filtered { /// See #[$simple_func_doc] /// for how to use. + #[track_caller] pub fn []( value: S, ms: impl Into> + 'static, From c26a96d8a671497ffb1613fb6b10d52dea8edc4e Mon Sep 17 00:00:00 2001 From: Marc-Stefan Cassola Date: Tue, 16 Jul 2024 19:57:30 +0100 Subject: [PATCH 19/24] =?UTF-8?q?use=5Fstorage=20now=20always=20writes=20t?= =?UTF-8?q?he=20default=20value=20to=20storage=20if=20the=20key=20doesn?= =?UTF-8?q?=E2=80=99t=20exist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/storage/use_storage.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index e98591e..1c31fea 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -272,13 +272,6 @@ where } }; - // Fetch initial value - if delay_during_hydration && leptos::leptos_dom::HydrationCtx::is_hydrating() { - request_animation_frame(fetch_from_storage.clone()); - } else { - fetch_from_storage(); - } - // Fires when storage needs to be fetched let notify = create_trigger(); @@ -329,6 +322,13 @@ where ); } + // Fetch initial value + if delay_during_hydration && leptos::leptos_dom::HydrationCtx::is_hydrating() { + request_animation_frame(fetch_from_storage.clone()); + } else { + fetch_from_storage(); + } + if listen_to_storage_changes { let check_key = key.as_ref().to_owned(); // Listen to global storage events From 0975bdcfeff8a926781a5ed0e48f647853bfe031 Mon Sep 17 00:00:00 2001 From: Marc-Stefan Cassola Date: Tue, 16 Jul 2024 19:59:55 +0100 Subject: [PATCH 20/24] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d3e51b..19a7ee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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`. +- `use_storage` now always saves the default value to storage if the key doesn't exist yet. ### Fixes 🍕 From e192eff406a4555ac88ef5e5458b0cb6f112e3e8 Mon Sep 17 00:00:00 2001 From: Marc-Stefan Cassola Date: Tue, 16 Jul 2024 20:12:09 +0100 Subject: [PATCH 21/24] use_cookie signal tries to change cookie headers during SSR Closes #124 --- src/use_cookie.rs | 50 +++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/src/use_cookie.rs b/src/use_cookie.rs index 6f53d8e..e61192f 100644 --- a/src/use_cookie.rs +++ b/src/use_cookie.rs @@ -90,7 +90,9 @@ use std::rc::Rc; /// This works equally well on the server or the client. /// On the server this function reads the cookie from the HTTP request header and writes it back into /// the HTTP response header according to options (if provided). -/// The returned `WriteSignal` will not affect the cookie headers on the server. +/// The returned `WriteSignal` may not affect the cookie headers on the server! It will try and write +/// the headers buy if this happens after the headers have already been streamed to the client then +/// this will have no effect. /// /// > If you're using `axum` you have to enable the `"axum"` feature in your Cargo.toml. /// > In case it's `actix-web` enable the feature `"actix"`, for `spin` enable `"spin"`. @@ -361,29 +363,31 @@ where #[cfg(feature = "ssr")] { if !readonly { - let value = cookie - .with_untracked(|cookie| { - cookie.as_ref().map(|cookie| { - C::encode(cookie) - .map_err(|err| on_error(CodecError::Encode(err))) - .ok() + create_isomorphic_effect(move |_| { + let value = cookie + .with(|cookie| { + cookie.as_ref().map(|cookie| { + C::encode(cookie) + .map_err(|err| on_error(CodecError::Encode(err))) + .ok() + }) }) - }) - .flatten(); - jar.update_value(|jar| { - write_server_cookie( - cookie_name, - value, - jar, - max_age, - expires, - domain, - path, - same_site, - secure, - http_only, - ssr_set_cookie, - ) + .flatten(); + jar.update_value(|jar| { + write_server_cookie( + cookie_name, + value, + jar, + max_age, + expires, + domain, + path, + same_site, + secure, + http_only, + ssr_set_cookie, + ) + }); }); } } From a5c67b4f7fb1d8ec3c57b7c4a751f751c7a6df6b Mon Sep 17 00:00:00 2001 From: Marc-Stefan Cassola Date: Tue, 16 Jul 2024 20:14:05 +0100 Subject: [PATCH 22/24] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19a7ee7..0bf1185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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 +- Changing the signal returned by `use_cookie` now tries and changes the headers during SSR. - New book chapter about codecs - The macro `use_derive_signal!` is now exported (thanks to @mscofield0). From af2fcb188cd18497cc3ecadafa2ecd154923394a Mon Sep 17 00:00:00 2001 From: Hector Candelaria Date: Thu, 18 Jul 2024 00:04:57 -0400 Subject: [PATCH 23/24] Fix: correct button misalignment Replaced the CSS selector `~` with `+` to ensure proper spacing between adjacent buttons. This adjustment targets only the immediate sibling buttons. --- docs/book/src/demo.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/book/src/demo.css b/docs/book/src/demo.css index 809ee87..f092756 100644 --- a/docs/book/src/demo.css +++ b/docs/book/src/demo.css @@ -64,7 +64,7 @@ cursor: not-allowed; } -.demo-container button ~ button { +.demo-container button + button { margin-left: 0.8rem; } @@ -134,4 +134,4 @@ border-width: 2px; border-style: dashed; padding: 1.5rem; -} \ No newline at end of file +} From 2bc537292067842a8481746d106de49d7c78fca3 Mon Sep 17 00:00:00 2001 From: Hector Candelaria Date: Thu, 18 Jul 2024 00:09:06 -0400 Subject: [PATCH 24/24] Docs: correct grammar in function comment Updated comment to fix grammar. --- src/use_cookie.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/use_cookie.rs b/src/use_cookie.rs index e61192f..67336f8 100644 --- a/src/use_cookie.rs +++ b/src/use_cookie.rs @@ -11,7 +11,7 @@ use std::rc::Rc; /// SSR-friendly and reactive cookie access. /// -/// You can use this function multiple times in your for the same cookie and they're signals will synchronize +/// You can use this function multiple times for the same cookie and their signals will synchronize /// (even across windows/tabs). But there is no way to listen to changes to `document.cookie` directly so in case /// something outside of this function changes the cookie, the signal will **not** be updated. ///