diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..b891103 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[unstable] +rustflags = ["--cfg=web_sys_unstable_apis"] +rustdocflags = ["--cfg=web_sys_unstable_apis"] \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9401b3d..d0d3d18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ 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] + +### New Functions 🚀 + +- `use_webtransport` + +### Breaking Changes 🛠 + +- 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. + ## [0.9.0] - 2023-12-06 ### New Functions 🚀 @@ -86,7 +98,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `use_document` - `use_window` - `use_geolocation` -- `use_webtransport` - `signal_debounced` - `signal_throttled` diff --git a/src/storage/mod.rs b/src/storage/mod.rs index c5cd494..b3b9487 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,8 +1,3 @@ -#[cfg(feature = "serde")] -mod codec_json; -#[cfg(feature = "prost")] -mod codec_prost; -mod codec_string; mod use_local_storage; mod use_session_storage; mod use_storage; diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 3c66fce..af94564 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -1,7 +1,6 @@ -use crate::storage::StringCodec; use crate::{ core::{MaybeRwSignal, StorageType}, - utils::FilterOptions, + utils::{FilterOptions, StringCodec}, }; use cfg_if::cfg_if; use leptos::*; @@ -71,6 +70,20 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// } /// } /// ``` +/// +/// ## 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. #[inline(always)] pub fn use_storage( storage_type: StorageType, @@ -322,30 +335,6 @@ pub struct UseStorageOptions> { filter: FilterOptions, } -/// A codec for encoding and decoding values to and from UTF-16 strings. These strings are intended to be stored in browser storage. -/// -/// ## 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 Codec: Clone + 'static { - /// The error type returned when encoding or decoding fails. - type Error; - /// Encodes a value to a UTF-16 string. - fn encode(&self, val: &T) -> Result; - /// Decodes a UTF-16 string to a value. Should be able to decode any string encoded by [`encode`]. - fn decode(&self, str: String) -> Result; -} - /// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling. #[cfg(not(feature = "ssr"))] fn handle_error( diff --git a/src/use_webtransport.rs b/src/use_webtransport.rs index 593ca26..902a484 100644 --- a/src/use_webtransport.rs +++ b/src/use_webtransport.rs @@ -424,6 +424,7 @@ impl Default for UseWebTransportOptions { pub enum StreamState { Open, Closed, + ClosedWithError(WebTransportError), } #[async_trait(?Send)] @@ -592,14 +593,20 @@ macro_rules! impl_closable_stream { #[inline(always)] fn close(&self) { - let _ = self.writer.close(); - self.set_state.set(StreamState::Closed); + spawn_local(async { + self.close_async().await.ok(); + }) } async fn close_async(&self) -> Result<(), WebTransportError> { - let _ = JsFuture::from(self.writer.close()) - .await - .map_err(|e| WebTransportError::OnCloseWriter(e))?; + let _ = JsFuture::from(self.writer.close()).await.map_err(|e| { + let error = WebTransportError::FailedToCloseStream(e); + self.set_state + .set(StreamState::ClosedWithError(error.clone())); + error + })?; + + self.set_state.set(StreamState::Closed); Ok(()) } @@ -740,7 +747,7 @@ fn create_bidir_stream( } /// Error enum for [`UseWebTransportOptions::on_error`] -#[derive(Debug)] +#[derive(Debug, Clone, Error)] pub enum WebTransportError { /// The `WebTransport` is not connected yet. Call `open` first. NotConnected, @@ -774,7 +781,7 @@ pub enum SendError { pub enum ReceiveError { #[cfg(feature = "bincode")] #[error("Serialization failed: {0}")] - SerializationFailed(#[from] bincode::Error), + DeserializationFailed(#[from] bincode::Error), #[cfg(feature = "msgpack")] #[error("Serialization failed: {0}")] diff --git a/src/utils/codecs/bin/from_to_bytes.rs b/src/utils/codecs/bin/from_to_bytes.rs new file mode 100644 index 0000000..63f661b --- /dev/null +++ b/src/utils/codecs/bin/from_to_bytes.rs @@ -0,0 +1,78 @@ +use super::BinCodec; + +#[derive(Copy, Clone, Default, PartialEq)] +pub struct FromToBytesCodec; + +#[derive(Error, Debug, PartialEq)] +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 new file mode 100644 index 0000000..64cead0 --- /dev/null +++ b/src/utils/codecs/bin/mod.rs @@ -0,0 +1,14 @@ +mod from_to_bytes; + +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 new file mode 100644 index 0000000..259545f --- /dev/null +++ b/src/utils/codecs/mod.rs @@ -0,0 +1,4 @@ +mod bin; +mod string; + +pub use string::*; diff --git a/src/storage/codec_string.rs b/src/utils/codecs/string/from_to_string.rs similarity index 83% rename from src/storage/codec_string.rs rename to src/utils/codecs/string/from_to_string.rs index c7a8049..71af948 100644 --- a/src/storage/codec_string.rs +++ b/src/utils/codecs/string/from_to_string.rs @@ -1,4 +1,4 @@ -use super::Codec; +use super::StringCodec; use std::str::FromStr; /// A codec for strings that relies on [`FromStr`] and [`ToString`] to parse. @@ -15,10 +15,10 @@ use std::str::FromStr; /// # view! { } /// # } /// ``` -#[derive(Clone, Default, PartialEq)] -pub struct StringCodec; +#[derive(Copy, Clone, Default, PartialEq)] +pub struct FromToStringCodec; -impl Codec for StringCodec { +impl StringCodec for FromToStringCodec { type Error = T::Err; fn encode(&self, val: &T) -> Result { @@ -37,7 +37,7 @@ mod tests { #[test] fn test_string_codec() { let s = String::from("party time 🎉"); - let codec = StringCodec; + let codec = FromToStringCodec; assert_eq!(codec.encode(&s), Ok(s.clone())); assert_eq!(codec.decode(s.clone()), Ok(s)); } diff --git a/src/storage/codec_json.rs b/src/utils/codecs/string/json.rs similarity index 97% rename from src/storage/codec_json.rs rename to src/utils/codecs/string/json.rs index 1a6a3a9..46a513c 100644 --- a/src/storage/codec_json.rs +++ b/src/utils/codecs/string/json.rs @@ -1,4 +1,4 @@ -use super::Codec; +use super::StringCodec; /// A codec for storing JSON messages that relies on [`serde_json`] to parse. /// @@ -113,10 +113,10 @@ use super::Codec; /// # view! { } /// # } /// ``` -#[derive(Clone, Default, PartialEq)] +#[derive(Copy, Clone, Default, PartialEq)] pub struct JsonCodec; -impl Codec for JsonCodec { +impl StringCodec for JsonCodec { type Error = serde_json::Error; fn encode(&self, val: &T) -> Result { diff --git a/src/utils/codecs/string/mod.rs b/src/utils/codecs/string/mod.rs new file mode 100644 index 0000000..2fd2657 --- /dev/null +++ b/src/utils/codecs/string/mod.rs @@ -0,0 +1,22 @@ +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. +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/storage/codec_prost.rs b/src/utils/codecs/string/prost.rs similarity index 95% rename from src/storage/codec_prost.rs rename to src/utils/codecs/string/prost.rs index 2311e28..ea2158e 100644 --- a/src/storage/codec_prost.rs +++ b/src/utils/codecs/string/prost.rs @@ -1,4 +1,4 @@ -use super::Codec; +use super::StringCodec; use base64::Engine; use thiserror::Error; @@ -29,7 +29,7 @@ use thiserror::Error; /// ``` /// /// 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(Clone, Default, PartialEq)] +#[derive(Copy, Clone, Default, PartialEq)] pub struct ProstCodec; #[derive(Error, Debug, PartialEq)] @@ -40,7 +40,7 @@ pub enum ProstCodecError { DecodeProst(#[from] prost::DecodeError), } -impl Codec for ProstCodec { +impl StringCodec for ProstCodec { type Error = ProstCodecError; fn encode(&self, val: &T) -> Result { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 404c372..fc22bc5 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,4 @@ +mod codecs; mod filters; mod is; mod js_value_from_to_string; @@ -5,6 +6,7 @@ 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::*;