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 d06c55f..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,serde + run: cargo test --features math,docs,ssr - 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,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,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..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,serde + run: cargo test --features math,docs,ssr - 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,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,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 e2f8130..f0ab304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated to Leptos 0.7 +## [Unreleased] + +### New Functions 🚀 + +- `use_user_media` + +### New Features 🚀 + +- 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 +- 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). + +### Breaking Changes 🛠 + +- `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. + - 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. + - `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`. +- `use_storage` now always saves the default value to storage if the key doesn't exist yet. + +### 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 + +### 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 🍕 @@ -117,7 +191,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 @@ -147,7 +222,7 @@ 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 @@ -221,7 +296,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 + - 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. @@ -469,4 +544,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. diff --git a/Cargo.toml b/Cargo.toml index 179e113..810b7a0 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"] @@ -15,8 +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" +codee = "0.1" cookie = { version = "0.18", features = ["percent-encode"] } default-struct-builder = "0.5" futures-util = "0.3" @@ -32,11 +32,7 @@ leptos_actix = { version = "0.6", optional = true } leptos-spin = { version = "0.2", optional = true } num = { version = "0.4", optional = true } paste = "1" -prost = { version = "0.12", optional = true } -rmp-serde = { version = "1.1", optional = true } send_wrapper = "0.6.0" -serde = { version = "1", optional = true } -serde_json = { version = "1", optional = true } thiserror = "1" wasm-bindgen = "0.2.92" wasm-bindgen-futures = "0.4" @@ -78,6 +74,7 @@ features = [ "MediaDevices", "MediaQueryList", "MediaStream", + "MediaStreamConstraints", "MediaStreamTrack", "MessageEvent", "MouseEvent", @@ -136,17 +133,19 @@ features = [ getrandom = { version = "0.2", features = ["js"] } leptos_meta = "0.7.0-alpha" 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 = ["base64", "dep:prost"] -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"] +features = ["math", "docs", "ssr"] +rustdoc-args = ["--cfg=web_sys_unstable_apis"] +rustc-args = ["--cfg=web_sys_unstable_apis"] 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/SUMMARY.md b/docs/book/src/SUMMARY.md index 51ca13f..502a1a6 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -2,8 +2,10 @@ [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) [Changelog](changelog.md) [Functions](functions.md) @@ -47,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 @@ -65,6 +68,7 @@ - [use_event_source](network/use_event_source.md) - [use_websocket](network/use_websocket.md) + # Animation @@ -94,12 +98,14 @@ - [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) - [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/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/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/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 +} 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/docs/book/src/server_side_rendering.md b/docs/book/src/server_side_rendering.md index a61d69d..d8cc92a 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 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/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/ssr/src/app.rs b/examples/ssr/src/app.rs index 1ab9506..3f25751 100644 --- a/examples/ssr/src/app.rs +++ b/examples/ssr/src/app.rs @@ -3,7 +3,7 @@ use leptos::ev::{keypress, KeyboardEvent}; use leptos::prelude::*; 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/examples/use_storage/Cargo.toml b/examples/use_storage/Cargo.toml index a4ba692..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", "prost", "serde"] } +leptos-use = { path = "../..", features = ["docs"] } web-sys = "0.3" serde = "1.0.163" 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/examples/use_websocket/Cargo.toml b/examples/use_websocket/Cargo.toml index 2f1f0a5..971690c 100644 --- a/examples/use_websocket/Cargo.toml +++ b/examples/use_websocket/Cargo.toml @@ -5,10 +5,12 @@ 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"] } +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 6374e13..0aaef12 100644 --- a/examples/use_websocket/src/main.rs +++ b/examples/use_websocket/src/main.rs @@ -1,12 +1,20 @@ use leptos::prelude::*; 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 codee::{binary::MsgpackSerdeCodec, string::FromToStringCodec}; 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) = 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 codee::binary::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: Arc, // use Arc to make it easily cloneable +/// send: Arc, // use Arc to make it easily cloneable /// } /// /// impl WebsocketContext { -/// pub fn new(message: Signal>, send: Arc) -> Self { +/// pub fn new(message: Signal>, send: Arc) -> 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::prelude::*; -/// # use leptos_use::{use_websocket, UseWebsocketReturn}; +/// # use codee::string::FromToStringCodec; +/// # use leptos_use::{use_websocket, UseWebSocketReturn}; /// # use std::sync::Arc; /// # #[derive(Clone)] /// # pub struct WebsocketContext { /// # pub message: Signal>, -/// # send: Arc, +/// # send: Arc, /// # } /// # /// # impl WebsocketContext { -/// # pub fn new(message: Signal>, send: Arc) -> Self { +/// # pub fn new(message: Signal>, send: Arc) -> 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, Arc::new(send.clone()))); /// # @@ -163,18 +200,18 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket}; /// /// ``` /// # use leptos::prelude::*; -/// # use leptos_use::{use_websocket, UseWebsocketReturn}; +/// # use leptos_use::{use_websocket, UseWebSocketReturn}; /// # use std::sync::Arc; /// # #[derive(Clone)] /// # pub struct WebsocketContext { /// # pub message: Signal>, -/// # send: Arc, +/// # send: Arc, /// # } /// # /// # 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,12 +284,12 @@ pub fn use_websocket_with_options( let (ready_state, set_ready_state) = signal(ConnectionReadyState::Closed); let (message, set_message) = signal(None); - let (message_bytes, set_message_bytes) = signal(None); let ws_ref: StoredValue>> = StoredValue::new(None); let reconnect_timer_ref: StoredValue> = StoredValue::new(None); let reconnect_times_ref: StoredValue = StoredValue::new(0); + let manually_closed_ref: StoredValue = StoredValue::new(false); let unmounted = Arc::new(AtomicBool::new(false)); @@ -243,10 +300,10 @@ pub fn use_websocket_with_options( let reconnect_ref: StoredValue>> = StoredValue::new(None); reconnect_ref.set_value({ - let ws = ws_ref.get_value(); Some(Arc::new(move || { - if reconnect_times_ref.get_value() < reconnect_limit - && ws.clone().map_or(false, |ws: SendWrapper| { + if !manually_closed_ref.get_value() + && !reconnect_limit.is_exceeded_by(reconnect_times_ref.get_value()) + && ws_ref.get_value().map_or(false, |ws: SendWrapper| { ws.ready_state() != WebSocket::OPEN }) { @@ -267,13 +324,13 @@ pub fn use_websocket_with_options( }); connect_ref.set_value({ - let ws = ws_ref.get_value(); let unmounted = Arc::clone(&unmounted); + let on_error = Arc::clone(&on_error); Some(Arc::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(); } @@ -323,7 +380,9 @@ pub fn use_websocket_with_options( { let unmounted = Arc::clone(&unmounted); let on_message = Arc::clone(&on_message); - let on_message_bytes = Arc::clone(&on_message_bytes); + let on_message_raw = Arc::clone(&on_message_raw); + let on_message_raw_bytes = Arc::clone(&on_message_raw_bytes); + let on_error = Arc::clone(&on_error); let onmessage_closure = Closure::wrap(Box::new(move |e: MessageEvent| { if unmounted.get() { @@ -345,12 +404,27 @@ pub fn use_websocket_with_options( #[cfg(debug_assertions)] let zone = diagnostics::SpecialNonReactiveZone::enter(); - on_message(txt.clone()); + on_message_raw(&txt); #[cfg(debug_assertions)] + SpecialNonReactiveZone::exit(prev); + + match C::decode_str(&txt) { + Ok(val) => { + #[cfg(debug_assertions)] + let prev = SpecialNonReactiveZone::enter(); + + on_message(&val); + + #[cfg(debug_assertions)] drop(zone); - set_message.set(Some(txt)); + set_message.set(Some(val)); + } + Err(err) => { + on_error(CodecError::Decode(err).into()); + } + } }, ); }, @@ -361,12 +435,27 @@ pub fn use_websocket_with_options( #[cfg(debug_assertions)] let zone = diagnostics::SpecialNonReactiveZone::enter(); - on_message_bytes(array.clone()); + on_message_raw_bytes(&array); #[cfg(debug_assertions)] drop(zone); - 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()); + } + } }, ); }) @@ -392,7 +481,7 @@ pub fn use_websocket_with_options( #[cfg(debug_assertions)] let zone = diagnostics::SpecialNonReactiveZone::enter(); - on_error(e); + on_error(UseWebSocketError::Event(e)); #[cfg(debug_assertions)] drop(zone); @@ -439,7 +528,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() { @@ -450,10 +539,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()), + } } } }; @@ -471,7 +578,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(); } @@ -491,33 +598,61 @@ 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)] +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, + } + } +} + +type ArcFnBytes = Arc; + /// Options for [`use_websocket_with_options`]. #[derive(DefaultBuilder)] -pub struct UseWebSocketOptions { +pub struct UseWebSocketOptions +where + T: ?Sized, +{ /// `WebSocket` connect callback. on_open: Arc, + /// `WebSocket` message callback for typed message decoded by codec. + #[builder(skip)] + on_message: Arc, /// `WebSocket` message callback for text. - on_message: Arc, + on_message_raw: Arc, /// `WebSocket` message callback for binary. - on_message_bytes: Arc) + Send + Sync>, + on_message_raw_bytes: ArcFnBytes + Send + Sync, /// `WebSocket` error callback. - on_error: Arc, + #[builder(skip)] + on_error: Arc) + Send + Sync>, /// `WebSocket` close callback. on_close: Arc, - /// 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. @@ -528,15 +663,40 @@ 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: Arc::new(|_| {}), on_message: Arc::new(|_| {}), - on_message_bytes: Arc::new(|_| {}), + on_message_raw: Arc::new(|_| {}), + on_message_raw_bytes: Arc::new(|_| {}), on_error: Arc::new(|_| {}), on_close: Arc::new(|_| {}), - reconnect_limit: 3, + reconnect_limit: ReconnectLimit::default(), reconnect_interval: 3000, immediate: true, protocols: Default::default(), @@ -546,29 +706,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 1d51f03..3c80951 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/from_to_bytes.rs b/src/utils/codecs/bin/from_to_bytes.rs deleted file mode 100644 index 74537dc..0000000 --- a/src/utils/codecs/bin/from_to_bytes.rs +++ /dev/null @@ -1,79 +0,0 @@ -use super::BinCodec; -use thiserror::Error; - -#[derive(Copy, Clone, Default, PartialEq)] -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 BinCodec<$num> for FromToBytesCodec { - type Error = FromToBytesCodecError; - - fn encode(&self, val: &$num) -> Result, Self::Error> { - Ok(val.to_be_bytes().to_vec()) - } - - fn decode(&self, val: &[u8]) -> 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 BinCodec for FromToBytesCodec { - type Error = FromToBytesCodecError; - - fn encode(&self, val: &bool) -> Result, Self::Error> { - let codec = FromToBytesCodec; - let num: u8 = if *val { 1 } else { 0 }; - codec.encode(&num) - } - - fn decode(&self, val: &[u8]) -> Result { - let codec = FromToBytesCodec; - let num: u8 = codec.decode(val)?; - Ok(num != 0) - } -} - -impl BinCodec for FromToBytesCodec { - type Error = FromToBytesCodecError; - - fn encode(&self, val: &String) -> Result, Self::Error> { - Ok(val.as_bytes().to_vec()) - } - - fn decode(&self, val: &[u8]) -> Result { - Ok(String::from_utf8(val.to_vec())?) - } -} diff --git a/src/utils/codecs/bin/mod.rs b/src/utils/codecs/bin/mod.rs deleted file mode 100644 index c28996d..0000000 --- a/src/utils/codecs/bin/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod from_to_bytes; - -#[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; -} diff --git a/src/utils/codecs/mod.rs b/src/utils/codecs/mod.rs deleted file mode 100644 index 259545f..0000000 --- a/src/utils/codecs/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod bin; -mod string; - -pub use string::*; 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 bbe83ae..0000000 --- a/src/utils/codecs/string/from_to_string.rs +++ /dev/null @@ -1,45 +0,0 @@ -use super::StringCodec; -use std::str::FromStr; - -/// A codec for strings that relies on [`FromStr`] and [`ToString`] to parse. -/// -/// This makes simple key / value easy to use for primitive types. It is also useful for encoding simple data structures without depending on serde. -/// -/// ## Example -/// ``` -/// # use leptos::prelude::*; -/// # 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! { } -/// # } -/// ``` -#[derive(Copy, Clone, Default, PartialEq)] -pub struct FromToStringCodec; - -impl StringCodec for FromToStringCodec { - type Error = T::Err; - - fn encode(&self, val: &T) -> Result { - Ok(val.to_string()) - } - - fn decode(&self, str: String) -> Result { - T::from_str(&str) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[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)); - } -} diff --git a/src/utils/codecs/string/json.rs b/src/utils/codecs/string/json.rs deleted file mode 100644 index 3979ce7..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::prelude::*; -/// # 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::prelude::*; -/// # 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::prelude::*; -/// # 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/mod.rs b/src/utils/codecs/string/mod.rs deleted file mode 100644 index 96c16b7..0000000 --- a/src/utils/codecs/string/mod.rs +++ /dev/null @@ -1,36 +0,0 @@ -mod from_to_string; -#[cfg(feature = "serde_json")] -mod json; -#[cfg(feature = "prost")] -mod prost; - -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; -} diff --git a/src/utils/codecs/string/prost.rs b/src/utils/codecs/string/prost.rs deleted file mode 100644 index 05e8902..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::prelude::*; -/// # 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)); - } -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 25f19eb..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,10 +6,8 @@ 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::*; pub use pausable::*; pub(crate) use signal_filtered::*; -pub(crate) use use_derive_signal::*; diff --git a/src/utils/signal_filtered.rs b/src/utils/signal_filtered.rs index 8c920e0..f397e5e 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, diff --git a/src/utils/use_derive_signal.rs b/src/utils/use_derive_signal.rs index 1bb1212..1e7e9c2 100644 --- a/src/utils/use_derive_signal.rs +++ b/src/utils/use_derive_signal.rs @@ -1,3 +1,7 @@ +/// 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 { ( $(#[$outer:meta])* @@ -14,5 +18,3 @@ macro_rules! use_derive_signal { } }; } - -pub(crate) use use_derive_signal;