From fcda13de8d8473156032622c847c4c5590d96387 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Thu, 26 Oct 2023 11:44:59 +0100 Subject: [PATCH 01/50] Add thiserror dependency --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 9b19c02..60f1b5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ num = { version = "0.4", optional = true } paste = "1" serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } +thiserror = "1.0" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" From 2f3c72f2bb17d419e4368fc8a7509cb4bf810f01 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Thu, 26 Oct 2023 11:46:47 +0100 Subject: [PATCH 02/50] Prototype use_storage replacement that uses TryFrom --- src/lib.rs | 2 + src/use_storage.rs | 165 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 src/use_storage.rs diff --git a/src/lib.rs b/src/lib.rs index a9700c1..c16506d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,7 @@ mod use_raf_fn; mod use_scroll; mod use_service_worker; mod use_sorted; +mod use_storage; mod use_supported; mod use_throttle_fn; mod use_timestamp; @@ -109,6 +110,7 @@ pub use use_raf_fn::*; pub use use_scroll::*; pub use use_service_worker::*; pub use use_sorted::*; +pub use use_storage::*; pub use use_supported::*; pub use use_throttle_fn::*; pub use use_timestamp::*; diff --git a/src/use_storage.rs b/src/use_storage.rs new file mode 100644 index 0000000..b5b8edb --- /dev/null +++ b/src/use_storage.rs @@ -0,0 +1,165 @@ +use crate::{use_event_listener_with_options, use_window, UseEventListenerOptions}; +use leptos::*; +use std::rc::Rc; +use thiserror::Error; +use wasm_bindgen::JsValue; +use web_sys::Storage; + +#[derive(Clone)] +pub struct UseStorageOptions { + on_error: Rc)>, +} + +/// Session handling errors returned by [`use_storage`]. +#[derive(Error, Debug)] +pub enum UseStorageError { + #[error("window not available")] + WindowReturnedNone, + #[error("storage not available")] + StorageNotAvailable(JsValue), + #[error("storage not returned from window")] + StorageReturnedNone, + #[error("failed to get item")] + GetItemFailed(JsValue), + #[error("failed to set item")] + SetItemFailed(JsValue), + #[error("failed to delete item")] + RemoveItemFailed(JsValue), + #[error("failed to parse item value")] + ParseItemError(Err), +} + +/// Hook for using local storage. Returns a result of a signal and a setter / deleter. +pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + PartialEq + TryFrom + ToString, + T::Error: std::fmt::Debug, +{ + use_local_storage_with_options(key, UseStorageOptions::default()) +} + +/// Hook for using local storage. Returns a result of a signal and a setter / deleter. +pub fn use_local_storage_with_options( + key: impl AsRef, + options: UseStorageOptions, +) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + PartialEq + TryFrom + ToString, +{ + // TODO ssr + let UseStorageOptions { on_error } = options; + let storage: Result = handle_error(&on_error, try_storage()); + + let initial_value = storage + .to_owned() + // Get initial item from storage + .and_then(|s| { + let result = s + .get_item(key.as_ref()) + .map_err(UseStorageError::GetItemFailed); + handle_error(&on_error, result) + }) + .unwrap_or_default(); + // Attempt to parse the item string + let initial_value = parse_item(initial_value, &on_error); + let (data, set_data) = create_signal(initial_value); + + // Update storage value + let set_value = { + let storage = storage.to_owned(); + let key = key.as_ref().to_owned(); + let on_error = on_error.to_owned(); + move |value: Option| { + let key = key.as_str(); + // Attempt to update storage + let _ = storage.as_ref().map(|storage| { + let result = match value { + // Update + Some(ref value) => storage + .set_item(key, &value.to_string()) + .map_err(UseStorageError::SetItemFailed), + // Remove + None => storage + .remove_item(key) + .map_err(UseStorageError::RemoveItemFailed), + }; + handle_error(&on_error, result) + }); + + // Notify signal of change + set_data.set(value); + } + }; + + // Listen for storage events + // Note: we only receive events from other tabs / windows, not from internal updates. + let _ = { + let key = key.as_ref().to_owned(); + use_event_listener_with_options( + use_window(), + leptos::ev::storage, + move |ev| { + // Update storage value if our key matches + if let Some(k) = ev.key() { + if k == key { + let value = parse_item(ev.new_value(), &on_error); + set_data.set(value) + } + } else { + // All keys deleted + set_data.set(None) + } + }, + UseEventListenerOptions::default().passive(true), + ) + }; + + let value = create_memo(move |_| data.get().unwrap_or_default()); + (value, set_value) +} + +fn try_storage() -> Result> { + use_window() + .as_ref() + .ok_or_else(|| UseStorageError::WindowReturnedNone)? + .local_storage() + .map_err(|err| UseStorageError::StorageNotAvailable(err))? + .ok_or_else(|| UseStorageError::StorageReturnedNone) +} + +/// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling. +fn handle_error( + on_error: &Rc)>, + result: Result>, +) -> Result { + result.or_else(|err| Err((on_error)(err))) +} + +fn parse_item>( + str: Option, + on_error: &Rc)>, +) -> Option { + str.map(|str| { + let result = T::try_from(str).map_err(UseStorageError::ParseItemError); + handle_error(&on_error, result) + }) + .transpose() + // We've sent our error so unwrap to drop () error + .unwrap_or_default() +} + +impl Default for UseStorageOptions { + fn default() -> Self { + Self { + on_error: Rc::new(|_err| ()), + } + } +} + +impl UseStorageOptions { + pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { + Self { + on_error: Rc::new(on_error), + } + } +} From 0f6b4aadc6f71fdc8f791f050919d0c8825815ba Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Thu, 26 Oct 2023 12:01:04 +0100 Subject: [PATCH 03/50] Add prototype use_storage option to listen to changes --- src/use_storage.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/use_storage.rs b/src/use_storage.rs index b5b8edb..80ea6af 100644 --- a/src/use_storage.rs +++ b/src/use_storage.rs @@ -8,6 +8,7 @@ use web_sys::Storage; #[derive(Clone)] pub struct UseStorageOptions { on_error: Rc)>, + listen_to_storage_changes: bool, } /// Session handling errors returned by [`use_storage`]. @@ -47,7 +48,10 @@ where T: Clone + Default + PartialEq + TryFrom + ToString, { // TODO ssr - let UseStorageOptions { on_error } = options; + let UseStorageOptions { + on_error, + listen_to_storage_changes, + } = options; let storage: Result = handle_error(&on_error, try_storage()); let initial_value = storage @@ -93,9 +97,9 @@ where // Listen for storage events // Note: we only receive events from other tabs / windows, not from internal updates. - let _ = { + if listen_to_storage_changes { let key = key.as_ref().to_owned(); - use_event_listener_with_options( + let _ = use_event_listener_with_options( use_window(), leptos::ev::storage, move |ev| { @@ -111,7 +115,7 @@ where } }, UseEventListenerOptions::default().passive(true), - ) + ); }; let value = create_memo(move |_| data.get().unwrap_or_default()); @@ -152,6 +156,7 @@ impl Default for UseStorageOptions { fn default() -> Self { Self { on_error: Rc::new(|_err| ()), + listen_to_storage_changes: true, } } } @@ -160,6 +165,14 @@ impl UseStorageOptions { pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { Self { on_error: Rc::new(on_error), + ..self + } + } + + pub fn listen_to_storage_changes(self, listen_to_storage_changes: bool) -> Self { + Self { + listen_to_storage_changes, + ..self } } } From c6d9ea28e575808782fd57532ea42b48a1b4b9e8 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 10:05:02 +0100 Subject: [PATCH 04/50] Add codec (String, prost, and serde) to use_storage --- Cargo.toml | 5 +- src/lib.rs | 1 + src/use_storage.rs | 185 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 165 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 60f1b5d..a694929 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ repository = "https://github.com/Synphonyte/leptos-use" homepage = "https://leptos-use.rs" [dependencies] +base64 = { version = "0.21", optional = true } cfg-if = "1" default-struct-builder = "0.5" futures-util = "0.3" @@ -22,6 +23,7 @@ lazy_static = "1" leptos = "0.5" num = { version = "0.4", optional = true } paste = "1" +prost = { version = "0.11", optional = true } serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } thiserror = "1.0" @@ -93,11 +95,10 @@ features = [ [features] docs = [] math = ["num"] -storage = ["serde", "serde_json", "web-sys/StorageEvent"] +storage = ["base64", "serde", "serde_json", "web-sys/StorageEvent"] ssr = [] [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg=web_sys_unstable_apis"] rustc-args = ["--cfg=web_sys_unstable_apis"] - diff --git a/src/lib.rs b/src/lib.rs index c16506d..0501fd2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,7 @@ mod use_raf_fn; mod use_scroll; mod use_service_worker; mod use_sorted; +#[cfg(feature = "storage")] mod use_storage; mod use_supported; mod use_throttle_fn; diff --git a/src/use_storage.rs b/src/use_storage.rs index 80ea6af..b2d71bd 100644 --- a/src/use_storage.rs +++ b/src/use_storage.rs @@ -1,13 +1,14 @@ use crate::{use_event_listener_with_options, use_window, UseEventListenerOptions}; use leptos::*; -use std::rc::Rc; +use std::{rc::Rc, str::FromStr}; use thiserror::Error; use wasm_bindgen::JsValue; use web_sys::Storage; #[derive(Clone)] -pub struct UseStorageOptions { - on_error: Rc)>, +pub struct UseStorageOptions> { + codec: C, + on_error: Rc)>, listen_to_storage_changes: bool, } @@ -26,29 +27,30 @@ pub enum UseStorageError { SetItemFailed(JsValue), #[error("failed to delete item")] RemoveItemFailed(JsValue), - #[error("failed to parse item value")] - ParseItemError(Err), + #[error("failed to encode / decode item value")] + ItemCodecError(Err), } /// Hook for using local storage. Returns a result of a signal and a setter / deleter. pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> ()) where - T: Clone + Default + PartialEq + TryFrom + ToString, - T::Error: std::fmt::Debug, + T: Clone + Default + FromStr + PartialEq + ToString, { - use_local_storage_with_options(key, UseStorageOptions::default()) + use_local_storage_with_options(key, UseStorageOptions::string_codec()) } /// Hook for using local storage. Returns a result of a signal and a setter / deleter. -pub fn use_local_storage_with_options( +pub fn use_local_storage_with_options( key: impl AsRef, - options: UseStorageOptions, + options: UseStorageOptions, ) -> (Memo, impl Fn(Option) -> ()) where - T: Clone + Default + PartialEq + TryFrom + ToString, + T: Clone + Default + PartialEq, + C: Codec, { // TODO ssr let UseStorageOptions { + codec, on_error, listen_to_storage_changes, } = options; @@ -65,13 +67,14 @@ where }) .unwrap_or_default(); // Attempt to parse the item string - let initial_value = parse_item(initial_value, &on_error); + let initial_value = decode_item(&codec, initial_value, &on_error); let (data, set_data) = create_signal(initial_value); // Update storage value let set_value = { let storage = storage.to_owned(); let key = key.as_ref().to_owned(); + let codec = codec.to_owned(); let on_error = on_error.to_owned(); move |value: Option| { let key = key.as_str(); @@ -79,9 +82,14 @@ where let _ = storage.as_ref().map(|storage| { let result = match value { // Update - Some(ref value) => storage - .set_item(key, &value.to_string()) - .map_err(UseStorageError::SetItemFailed), + Some(ref value) => codec + .encode(&value) + .map_err(UseStorageError::ItemCodecError) + .and_then(|enc_value| { + storage + .set_item(key, &enc_value) + .map_err(UseStorageError::SetItemFailed) + }), // Remove None => storage .remove_item(key) @@ -106,7 +114,7 @@ where // Update storage value if our key matches if let Some(k) = ev.key() { if k == key { - let value = parse_item(ev.new_value(), &on_error); + let value = decode_item(&codec, ev.new_value(), &on_error); set_data.set(value) } } else { @@ -139,12 +147,13 @@ fn handle_error( result.or_else(|err| Err((on_error)(err))) } -fn parse_item>( +fn decode_item>( + codec: &C, str: Option, - on_error: &Rc)>, + on_error: &Rc)>, ) -> Option { str.map(|str| { - let result = T::try_from(str).map_err(UseStorageError::ParseItemError); + let result = codec.decode(str).map_err(UseStorageError::ItemCodecError); handle_error(&on_error, result) }) .transpose() @@ -152,17 +161,16 @@ fn parse_item>( .unwrap_or_default() } -impl Default for UseStorageOptions { - fn default() -> Self { +impl> UseStorageOptions { + fn new(codec: C) -> Self { Self { + codec, on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, } } -} -impl UseStorageOptions { - 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 @@ -176,3 +184,132 @@ impl UseStorageOptions { } } } + +pub trait Codec: Clone + 'static { + type Error; + fn encode(&self, val: &T) -> Result; + fn decode(&self, str: String) -> Result; +} + +#[derive(Clone, PartialEq)] +pub struct StringCodec(); + +impl Codec for StringCodec { + type Error = T::Err; + + fn encode(&self, val: &T) -> Result { + Ok(val.to_string()) + } + + fn decode(&self, str: String) -> Result { + T::from_str(&str) + } +} + +impl UseStorageOptions { + pub fn string_codec() -> Self { + Self::new(StringCodec()) + } +} + +#[derive(Clone, 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), +} + +use base64::Engine; +impl Codec 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) + } +} + +impl UseStorageOptions { + pub fn prost_codec() -> Self { + Self::new(ProstCodec()) + } +} + +#[derive(Clone, PartialEq)] +pub struct JsonCodec(); + +impl Codec 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) + } +} + +impl UseStorageOptions { + pub fn json_codec() -> Self { + Self::new(JsonCodec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_string_codec() { + let s = String::from("party time 🎉"); + let codec = StringCodec(); + assert_eq!(codec.encode(&s), Ok(s.clone())); + assert_eq!(codec.decode(s.clone()), Ok(s)); + } + + #[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)); + } + + #[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); + } +} From 0fb4f7e6a1e9e26d106df7538f25df2fda8f8a6e Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 11:56:47 +0100 Subject: [PATCH 05/50] Problem: only local storage is available. Provide session and generic web_sys::Storage fns --- src/use_storage.rs | 96 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 15 deletions(-) diff --git a/src/use_storage.rs b/src/use_storage.rs index b2d71bd..7988d13 100644 --- a/src/use_storage.rs +++ b/src/use_storage.rs @@ -3,7 +3,7 @@ use leptos::*; use std::{rc::Rc, str::FromStr}; use thiserror::Error; use wasm_bindgen::JsValue; -use web_sys::Storage; +use web_sys::{Storage, Window}; #[derive(Clone)] pub struct UseStorageOptions> { @@ -36,11 +36,74 @@ pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option where T: Clone + Default + FromStr + PartialEq + ToString, { - use_local_storage_with_options(key, UseStorageOptions::string_codec()) + use_storage_with_options(get_local_storage, key, UseStorageOptions::string_codec()) +} + +pub fn use_local_storage_with_options( + key: impl AsRef, + options: UseStorageOptions, +) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + PartialEq, + C: Codec, +{ + use_storage_with_options(|w| w.local_storage(), key, options) +} + +fn get_local_storage(w: &Window) -> Result, JsValue> { + w.local_storage() +} + +/// Hook for using session storage. Returns a result of a signal and a setter / deleter. +pub fn use_session_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + FromStr + PartialEq + ToString, +{ + use_storage_with_options(get_session_storage, key, UseStorageOptions::string_codec()) +} + +pub fn use_session_storage_with_options( + key: impl AsRef, + options: UseStorageOptions, +) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + PartialEq, + C: Codec, +{ + use_storage_with_options(get_session_storage, key, options) +} + +fn get_session_storage(w: &Window) -> Result, JsValue> { + w.session_storage() +} + +/// Hook for using custom storage. Returns a result of a signal and a setter / deleter. +pub fn use_custom_storage( + storage: impl Into, + key: impl AsRef, +) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + FromStr + PartialEq + ToString, +{ + use_custom_storage_with_options(storage, key, UseStorageOptions::string_codec()) +} + +pub fn use_custom_storage_with_options( + storage: impl Into, + key: impl AsRef, + options: UseStorageOptions, +) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + PartialEq, + C: Codec, +{ + let storage = storage.into(); + use_storage_with_options(|_| Ok(Some(storage)), key, options) } /// Hook for using local storage. Returns a result of a signal and a setter / deleter. -pub fn use_local_storage_with_options( +fn use_storage_with_options( + get_storage: impl FnOnce(&Window) -> Result, JsValue>, key: impl AsRef, options: UseStorageOptions, ) -> (Memo, impl Fn(Option) -> ()) @@ -54,11 +117,22 @@ where on_error, listen_to_storage_changes, } = options; - let storage: Result = handle_error(&on_error, try_storage()); + // Get storage API + let storage = use_window() + .as_ref() + .ok_or(UseStorageError::WindowReturnedNone) + .and_then(|w| { + get_storage(w) + .map_err(UseStorageError::StorageNotAvailable) + .and_then(|s| s.ok_or(UseStorageError::StorageReturnedNone)) + }); + let storage: Result = handle_error(&on_error, storage); + + // Fetch initial value (undecoded) let initial_value = storage .to_owned() - // Get initial item from storage + // Pull from storage .and_then(|s| { let result = s .get_item(key.as_ref()) @@ -66,8 +140,9 @@ where handle_error(&on_error, result) }) .unwrap_or_default(); - // Attempt to parse the item string + // Decode initial value let initial_value = decode_item(&codec, initial_value, &on_error); + let (data, set_data) = create_signal(initial_value); // Update storage value @@ -130,15 +205,6 @@ where (value, set_value) } -fn try_storage() -> Result> { - use_window() - .as_ref() - .ok_or_else(|| UseStorageError::WindowReturnedNone)? - .local_storage() - .map_err(|err| UseStorageError::StorageNotAvailable(err))? - .ok_or_else(|| UseStorageError::StorageReturnedNone) -} - /// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling. fn handle_error( on_error: &Rc)>, From 4538097ab130543637d0d2b831cc2646f9739fb1 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 12:37:41 +0100 Subject: [PATCH 06/50] Problem: use_color_mode relies on specifying backing via StorageType. Expose use_storage_with_options and use StorageType --- src/use_storage.rs | 65 ++++++++++------------------------------------ 1 file changed, 14 insertions(+), 51 deletions(-) diff --git a/src/use_storage.rs b/src/use_storage.rs index 7988d13..cac53bb 100644 --- a/src/use_storage.rs +++ b/src/use_storage.rs @@ -1,9 +1,10 @@ -use crate::{use_event_listener_with_options, use_window, UseEventListenerOptions}; +use crate::{ + core::StorageType, use_event_listener_with_options, use_window, UseEventListenerOptions, +}; use leptos::*; use std::{rc::Rc, str::FromStr}; use thiserror::Error; use wasm_bindgen::JsValue; -use web_sys::{Storage, Window}; #[derive(Clone)] pub struct UseStorageOptions> { @@ -15,8 +16,6 @@ pub struct UseStorageOptions> { /// Session handling errors returned by [`use_storage`]. #[derive(Error, Debug)] pub enum UseStorageError { - #[error("window not available")] - WindowReturnedNone, #[error("storage not available")] StorageNotAvailable(JsValue), #[error("storage not returned from window")] @@ -36,7 +35,7 @@ pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option where T: Clone + Default + FromStr + PartialEq + ToString, { - use_storage_with_options(get_local_storage, key, UseStorageOptions::string_codec()) + use_storage_with_options(StorageType::Local, key, UseStorageOptions::string_codec()) } pub fn use_local_storage_with_options( @@ -47,11 +46,7 @@ where T: Clone + Default + PartialEq, C: Codec, { - use_storage_with_options(|w| w.local_storage(), key, options) -} - -fn get_local_storage(w: &Window) -> Result, JsValue> { - w.local_storage() + use_storage_with_options(StorageType::Local, key, options) } /// Hook for using session storage. Returns a result of a signal and a setter / deleter. @@ -59,7 +54,7 @@ pub fn use_session_storage(key: impl AsRef) -> (Memo, impl Fn(Option< where T: Clone + Default + FromStr + PartialEq + ToString, { - use_storage_with_options(get_session_storage, key, UseStorageOptions::string_codec()) + use_storage_with_options(StorageType::Session, key, UseStorageOptions::string_codec()) } pub fn use_session_storage_with_options( @@ -70,40 +65,12 @@ where T: Clone + Default + PartialEq, C: Codec, { - use_storage_with_options(get_session_storage, key, options) + use_storage_with_options(StorageType::Session, key, options) } -fn get_session_storage(w: &Window) -> Result, JsValue> { - w.session_storage() -} - -/// Hook for using custom storage. Returns a result of a signal and a setter / deleter. -pub fn use_custom_storage( - storage: impl Into, - key: impl AsRef, -) -> (Memo, impl Fn(Option) -> ()) -where - T: Clone + Default + FromStr + PartialEq + ToString, -{ - use_custom_storage_with_options(storage, key, UseStorageOptions::string_codec()) -} - -pub fn use_custom_storage_with_options( - storage: impl Into, - key: impl AsRef, - options: UseStorageOptions, -) -> (Memo, impl Fn(Option) -> ()) -where - T: Clone + Default + PartialEq, - C: Codec, -{ - let storage = storage.into(); - use_storage_with_options(|_| Ok(Some(storage)), key, options) -} - -/// Hook for using local storage. Returns a result of a signal and a setter / deleter. +/// Hook for using any kind of storage. Returns a result of a signal and a setter / deleter. fn use_storage_with_options( - get_storage: impl FnOnce(&Window) -> Result, JsValue>, + storage_type: StorageType, key: impl AsRef, options: UseStorageOptions, ) -> (Memo, impl Fn(Option) -> ()) @@ -119,15 +86,11 @@ where } = options; // Get storage API - let storage = use_window() - .as_ref() - .ok_or(UseStorageError::WindowReturnedNone) - .and_then(|w| { - get_storage(w) - .map_err(UseStorageError::StorageNotAvailable) - .and_then(|s| s.ok_or(UseStorageError::StorageReturnedNone)) - }); - let storage: Result = handle_error(&on_error, storage); + let storage = storage_type + .into_storage() + .map_err(UseStorageError::StorageNotAvailable) + .and_then(|s| s.ok_or(UseStorageError::StorageReturnedNone)); + let storage = handle_error(&on_error, storage); // Fetch initial value (undecoded) let initial_value = storage From bdeadba508eb9e035e9f9e61e0e2d255e492e2ce Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 12:39:58 +0100 Subject: [PATCH 07/50] Delete existing storage/ in favour of use_storage.rs --- src/storage/mod.rs | 8 - src/storage/shared.rs | 96 ------ src/storage/use_local_storage.rs | 22 -- src/storage/use_session_storage.rs | 22 -- src/storage/use_storage.rs | 498 ----------------------------- 5 files changed, 646 deletions(-) delete mode 100644 src/storage/mod.rs delete mode 100644 src/storage/shared.rs delete mode 100644 src/storage/use_local_storage.rs delete mode 100644 src/storage/use_session_storage.rs delete mode 100644 src/storage/use_storage.rs diff --git a/src/storage/mod.rs b/src/storage/mod.rs deleted file mode 100644 index bc0844b..0000000 --- a/src/storage/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod shared; -mod use_local_storage; -mod use_session_storage; -mod use_storage; - -pub use use_local_storage::*; -pub use use_session_storage::*; -pub use use_storage::*; diff --git a/src/storage/shared.rs b/src/storage/shared.rs deleted file mode 100644 index bd9ef93..0000000 --- a/src/storage/shared.rs +++ /dev/null @@ -1,96 +0,0 @@ -use crate::filter_builder_methods; -use crate::storage::{StorageType, UseStorageError, UseStorageOptions}; -use crate::utils::{DebounceOptions, FilterOptions, ThrottleOptions}; -use default_struct_builder::DefaultBuilder; -use leptos::*; -use std::rc::Rc; - -macro_rules! use_specific_storage { - ($(#[$outer:meta])* - $storage_name:ident - #[$simple_func:meta] - ) => { - paste! { - $(#[$outer])* - pub fn []( - key: &str, - defaults: D, - ) -> (Signal, WriteSignal, impl Fn() + Clone) - where - for<'de> T: Serialize + Deserialize<'de> + Clone + 'static, - D: Into>, - T: Clone, - { - [](key, defaults, UseSpecificStorageOptions::default()) - } - - /// Version of - #[$simple_func] - /// that accepts [`UseSpecificStorageOptions`]. See - #[$simple_func] - /// for how to use. - pub fn []( - key: &str, - defaults: D, - options: UseSpecificStorageOptions, - ) -> (Signal, WriteSignal, impl Fn() + Clone) - where - for<'de> T: Serialize + Deserialize<'de> + Clone + 'static, - D: Into>, - T: Clone, - { - use_storage_with_options(key, defaults, options.into_storage_options(StorageType::[<$storage_name:camel>])) - } - } - }; -} - -pub(crate) use use_specific_storage; - -/// Options for [`use_local_storage_with_options`]. -// #[doc(cfg(feature = "storage"))] -#[derive(DefaultBuilder)] -pub struct UseSpecificStorageOptions { - /// Listen to changes to this storage key from somewhere else. Defaults to true. - listen_to_storage_changes: bool, - /// If no value for the give key is found in the storage, write it. Defaults to true. - write_defaults: bool, - /// Takes the serialized (json) stored value and the default value and returns a merged version. - /// Defaults to simply returning the stored value. - merge_defaults: fn(&str, &T) -> String, - /// Optional callback whenever an error occurs. The callback takes an argument of type [`UseStorageError`]. - on_error: Rc, - - /// Debounce or throttle the writing to storage whenever the value changes. - filter: FilterOptions, -} - -impl Default for UseSpecificStorageOptions { - fn default() -> Self { - Self { - listen_to_storage_changes: true, - write_defaults: true, - merge_defaults: |stored_value, _default_value| stored_value.to_string(), - on_error: Rc::new(|_| ()), - filter: Default::default(), - } - } -} - -impl UseSpecificStorageOptions { - pub fn into_storage_options(self, storage_type: StorageType) -> UseStorageOptions { - UseStorageOptions { - storage_type, - listen_to_storage_changes: self.listen_to_storage_changes, - write_defaults: self.write_defaults, - merge_defaults: self.merge_defaults, - on_error: self.on_error, - filter: self.filter, - } - } - - filter_builder_methods!( - /// the serializing and storing into storage - filter - ); -} diff --git a/src/storage/use_local_storage.rs b/src/storage/use_local_storage.rs deleted file mode 100644 index 3ed7716..0000000 --- a/src/storage/use_local_storage.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::core::MaybeRwSignal; -use crate::storage::shared::{use_specific_storage, UseSpecificStorageOptions}; -use crate::storage::{use_storage_with_options, StorageType}; -use leptos::*; -use paste::paste; -use serde::{Deserialize, Serialize}; - -use_specific_storage!( - /// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) - /// - /// ## Usage - /// - /// Please refer to [`use_storage`] - /// - /// ## See also - /// - /// * [`use_storage`] - /// * [`use_session_storage`] - // #[doc(cfg(feature = "storage"))] - local - /// [`use_local_storage`] -); diff --git a/src/storage/use_session_storage.rs b/src/storage/use_session_storage.rs deleted file mode 100644 index dca9b95..0000000 --- a/src/storage/use_session_storage.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::core::MaybeRwSignal; -use crate::storage::shared::{use_specific_storage, UseSpecificStorageOptions}; -use crate::storage::{use_storage_with_options, StorageType}; -use leptos::*; -use paste::paste; -use serde::{Deserialize, Serialize}; - -use_specific_storage!( - /// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) - /// - /// ## Usage - /// - /// Please refer to [`use_storage`] - /// - /// ## See also - /// - /// * [`use_storage`] - /// * [`use_local_storage`] - // #[doc(cfg(feature = "storage"))] - session - /// [`use_session_storage`] -); diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs deleted file mode 100644 index 7122882..0000000 --- a/src/storage/use_storage.rs +++ /dev/null @@ -1,498 +0,0 @@ -#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports, dead_code))] - -use crate::core::MaybeRwSignal; -use crate::utils::FilterOptions; -use crate::{ - filter_builder_methods, use_event_listener, watch_pausable_with_options, DebounceOptions, - ThrottleOptions, WatchOptions, WatchPausableReturn, -}; -use cfg_if::cfg_if; -use default_struct_builder::DefaultBuilder; -use js_sys::Reflect; -use leptos::*; -use serde::{Deserialize, Serialize}; -use serde_json::Error; -use std::rc::Rc; -use std::time::Duration; -use wasm_bindgen::{JsCast, JsValue}; - -pub use crate::core::StorageType; - -const CUSTOM_STORAGE_EVENT_NAME: &str = "leptos-use-storage"; - -/// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) / [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). -/// -/// ## Demo -/// -/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_storage) -/// -/// ## Usage -/// -/// It returns a triplet `(read_signal, write_signal, delete_from_storage_func)` of type `(ReadSignal, WriteSignal, Fn())`. -/// -/// Values are (de-)serialized to/from JSON using [`serde`](https://serde.rs/). -/// -/// ``` -/// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_storage, use_storage_with_options, UseStorageOptions}; -/// # use serde::{Deserialize, Serialize}; -/// # -/// #[derive(Serialize, Deserialize, Clone)] -/// pub struct MyState { -/// pub hello: String, -/// pub greeting: String, -/// } -/// -/// # pub fn Demo() -> impl IntoView { -/// // bind struct. Must be serializable. -/// let (state, set_state, _) = use_storage( -/// "my-state", -/// MyState { -/// hello: "hi".to_string(), -/// greeting: "Hello".to_string() -/// }, -/// ); // returns Signal -/// -/// // bind bool. -/// let (flag, set_flag, remove_flag) = use_storage("my-flag", true); // returns Signal -/// -/// // bind number -/// let (count, set_count, _) = use_storage("my-count", 0); // returns Signal -/// -/// // bind string with SessionStorage -/// let (id, set_id, _) = use_storage_with_options( -/// "my-id", -/// "some_string_id".to_string(), -/// UseStorageOptions::default().storage_type(StorageType::Session), -/// ); -/// # view! { } -/// # } -/// ``` -/// -/// ## Merge Defaults -/// -/// By default, [`use_storage`] will use the value from storage if it is present and ignores the default value. -/// Be aware that when you add more properties to the default value, the key might be `None` -/// (in the case of an `Option` field) if client's storage does not have that key -/// or deserialization might fail altogether. -/// -/// Let's say you had a struct `MyState` that has been saved to storage -/// -/// ```ignore -/// #[derive(Serialize, Deserialize, Clone)] -/// struct MyState { -/// hello: String, -/// } -/// -/// let (state, .. ) = use_storage("my-state", MyState { hello: "hello" }); -/// ``` -/// -/// Now, in a newer version you added a field `greeting` to `MyState`. -/// -/// ```ignore -/// #[derive(Serialize, Deserialize, Clone)] -/// struct MyState { -/// hello: String, -/// greeting: String, -/// } -/// -/// let (state, .. ) = use_storage( -/// "my-state", -/// MyState { hello: "hi", greeting: "whatsup" }, -/// ); // fails to deserialize -> default value -/// ``` -/// -/// This will fail to deserialize the stored string `{"hello": "hello"}` because it has no field `greeting`. -/// Hence it just uses the new default value provided and the previously saved value is lost. -/// -/// To mitigate that you can provide a `merge_defaults` option. This is a pure function pointer -/// that takes the serialized (to json) stored value and the default value as arguments -/// and should return the serialized merged value. -/// -/// ``` -/// # use leptos::*; -/// # use leptos_use::storage::{use_storage_with_options, UseStorageOptions}; -/// # use serde::{Deserialize, Serialize}; -/// # -/// #[derive(Serialize, Deserialize, Clone)] -/// pub struct MyState { -/// pub hello: String, -/// pub greeting: String, -/// } -/// # -/// # pub fn Demo() -> impl IntoView { -/// let (state, set_state, _) = use_storage_with_options( -/// "my-state", -/// MyState { -/// hello: "hi".to_string(), -/// greeting: "Hello".to_string() -/// }, -/// UseStorageOptions::::default().merge_defaults(|stored_value, default_value| { -/// if stored_value.contains(r#""greeting":"#) { -/// stored_value.to_string() -/// } else { -/// // add "greeting": "Hello" to the string -/// stored_value.replace("}", &format!(r#""greeting": "{}"}}"#, default_value.greeting)) -/// } -/// }), -/// ); -/// # -/// # view! { } -/// # } -/// ``` -/// -/// ## Filter Storage Write -/// -/// You can specify `debounce` or `throttle` options for limiting writes to storage. -/// -/// ## Server-Side Rendering -/// -/// On the server this falls back to a `create_signal(default)` and an empty remove function. -/// -/// ## See also -/// -/// * [`use_local_storage`] -/// * [`use_session_storage`] -// #[doc(cfg(feature = "storage"))] -pub fn use_storage(key: &str, defaults: D) -> (Signal, WriteSignal, impl Fn() + Clone) -where - for<'de> T: Serialize + Deserialize<'de> + Clone + 'static, - D: Into>, - T: Clone, -{ - use_storage_with_options(key, defaults, UseStorageOptions::default()) -} - -/// Version of [`use_storage`] that accepts [`UseStorageOptions`]. See [`use_storage`] for how to use. -// #[doc(cfg(feature = "storage"))] -pub fn use_storage_with_options( - key: &str, - defaults: D, - options: UseStorageOptions, -) -> (Signal, WriteSignal, impl Fn() + Clone) -where - for<'de> T: Serialize + Deserialize<'de> + Clone + 'static, - D: Into>, - T: Clone, -{ - let defaults = defaults.into(); - - let UseStorageOptions { - storage_type, - listen_to_storage_changes, - write_defaults, - merge_defaults, - on_error, - filter, - } = options; - - let (data, set_data) = defaults.into_signal(); - - let raw_init = data.get_untracked(); - - cfg_if! { if #[cfg(feature = "ssr")] { - let remove: Rc = Rc::new(|| {}); - } else { - let storage = storage_type.into_storage(); - - let remove: Rc = match storage { - Ok(Some(storage)) => { - let write = { - let on_error = on_error.clone(); - let storage = storage.clone(); - let key = key.to_string(); - - Rc::new(move |v: &T| { - match serde_json::to_string(&v) { - Ok(ref serialized) => match storage.get_item(&key) { - Ok(old_value) => { - if old_value.as_ref() != Some(serialized) { - if let Err(e) = storage.set_item(&key, serialized) { - on_error(UseStorageError::StorageAccessError(e)); - } else { - let mut event_init = web_sys::CustomEventInit::new(); - event_init.detail( - &StorageEventDetail { - key: Some(key.clone()), - old_value, - new_value: Some(serialized.clone()), - storage_area: Some(storage.clone()), - } - .into(), - ); - - // importantly this should _not_ be a StorageEvent since those cannot - // be constructed with a non-built-in storage area - let _ = window().dispatch_event( - &web_sys::CustomEvent::new_with_event_init_dict( - CUSTOM_STORAGE_EVENT_NAME, - &event_init, - ) - .expect("Failed to create CustomEvent"), - ); - } - } - } - Err(e) => { - on_error.clone()(UseStorageError::StorageAccessError(e)); - } - }, - Err(e) => { - on_error.clone()(UseStorageError::SerializationError(e)); - } - } - }) - }; - - let read = { - let storage = storage.clone(); - let on_error = on_error.clone(); - let key = key.to_string(); - let raw_init = raw_init.clone(); - - Rc::new( - move |event_detail: Option| -> Option { - let serialized_init = match serde_json::to_string(&raw_init) { - Ok(serialized) => Some(serialized), - Err(e) => { - on_error.clone()(UseStorageError::DefaultSerializationError(e)); - None - } - }; - - let raw_value = if let Some(event_detail) = event_detail { - event_detail.new_value - } else { - match storage.get_item(&key) { - Ok(raw_value) => match raw_value { - Some(raw_value) => Some(merge_defaults(&raw_value, &raw_init)), - None => serialized_init.clone(), - }, - Err(e) => { - on_error.clone()(UseStorageError::StorageAccessError(e)); - None - } - } - }; - - match raw_value { - Some(raw_value) => match serde_json::from_str(&raw_value) { - Ok(v) => Some(v), - Err(e) => { - on_error.clone()(UseStorageError::SerializationError(e)); - None - } - }, - None => { - if let Some(serialized_init) = &serialized_init { - if write_defaults { - if let Err(e) = storage.set_item(&key, serialized_init) { - on_error(UseStorageError::StorageAccessError(e)); - } - } - } - - Some(raw_init.clone()) - } - } - }, - ) - }; - - let WatchPausableReturn { - pause: pause_watch, - resume: resume_watch, - .. - } = watch_pausable_with_options( - move || data.get(), - move |data, _, _| Rc::clone(&write)(data), - WatchOptions::default().filter(filter), - ); - - let update = { - let key = key.to_string(); - let storage = storage.clone(); - let raw_init = raw_init.clone(); - - Rc::new(move |event_detail: Option| { - if let Some(event_detail) = &event_detail { - if event_detail.storage_area != Some(storage.clone()) { - return; - } - - match &event_detail.key { - None => { - set_data.set(raw_init.clone()); - return; - } - Some(event_key) => { - if event_key != &key { - return; - } - } - }; - } - - pause_watch(); - - if let Some(value) = read(event_detail.clone()) { - set_data.set(value); - } - - if event_detail.is_some() { - // use timeout to avoid infinite loop - let resume = resume_watch.clone(); - let _ = set_timeout_with_handle(resume, Duration::ZERO); - } else { - resume_watch(); - } - }) - }; - - let update_from_custom_event = { - let update = Rc::clone(&update); - - move |event: web_sys::CustomEvent| { - let update = Rc::clone(&update); - queue_microtask(move || update(Some(event.into()))) - } - }; - - let update_from_storage_event = { - let update = Rc::clone(&update); - - move |event: web_sys::StorageEvent| update(Some(event.into())) - }; - - if listen_to_storage_changes { - let _ = use_event_listener(window(), ev::storage, update_from_storage_event); - let _ = use_event_listener( - window(), - ev::Custom::new(CUSTOM_STORAGE_EVENT_NAME), - update_from_custom_event, - ); - } - - update(None); - - let k = key.to_string(); - - Rc::new(move || { - let _ = storage.remove_item(&k); - }) - } - Err(e) => { - on_error(UseStorageError::NoStorage(e)); - Rc::new(move || {}) - } - _ => { - // do nothing - Rc::new(move || {}) - } - }; - }} - - (data, set_data, move || remove()) -} - -#[derive(Clone)] -pub struct StorageEventDetail { - pub key: Option, - pub old_value: Option, - pub new_value: Option, - pub storage_area: Option, -} - -impl From for StorageEventDetail { - fn from(event: web_sys::StorageEvent) -> Self { - Self { - key: event.key(), - old_value: event.old_value(), - new_value: event.new_value(), - storage_area: event.storage_area(), - } - } -} - -impl From for StorageEventDetail { - fn from(event: web_sys::CustomEvent) -> Self { - let detail = event.detail(); - Self { - key: get_optional_string(&detail, "key"), - old_value: get_optional_string(&detail, "oldValue"), - new_value: get_optional_string(&detail, "newValue"), - storage_area: Reflect::get(&detail, &"storageArea".into()) - .map(|v| v.dyn_into::().ok()) - .unwrap_or_default(), - } - } -} - -impl From for JsValue { - fn from(event: StorageEventDetail) -> Self { - let obj = js_sys::Object::new(); - - let _ = Reflect::set(&obj, &"key".into(), &event.key.into()); - let _ = Reflect::set(&obj, &"oldValue".into(), &event.old_value.into()); - let _ = Reflect::set(&obj, &"newValue".into(), &event.new_value.into()); - let _ = Reflect::set(&obj, &"storageArea".into(), &event.storage_area.into()); - - obj.into() - } -} - -fn get_optional_string(v: &JsValue, key: &str) -> Option { - Reflect::get(v, &key.into()) - .map(|v| v.as_string()) - .unwrap_or_default() -} - -/// Error type for use_storage_with_options -// #[doc(cfg(feature = "storage"))] -pub enum UseStorageError { - NoStorage(JsValue), - StorageAccessError(JsValue), - CustomStorageAccessError(E), - SerializationError(Error), - DefaultSerializationError(Error), -} - -/// Options for [`use_storage_with_options`]. -// #[doc(cfg(feature = "storage"))] -#[derive(DefaultBuilder)] -pub struct UseStorageOptions { - /// Type of storage. Can be `Local` (default), `Session` or `Custom(web_sys::Storage)` - pub(crate) storage_type: StorageType, - /// Listen to changes to this storage key from somewhere else. Defaults to true. - pub(crate) listen_to_storage_changes: bool, - /// If no value for the give key is found in the storage, write it. Defaults to true. - pub(crate) write_defaults: bool, - /// Takes the serialized (json) stored value and the default value and returns a merged version. - /// Defaults to simply returning the stored value. - pub(crate) merge_defaults: fn(&str, &T) -> String, - /// Optional callback whenever an error occurs. The callback takes an argument of type [`UseStorageError`]. - pub(crate) on_error: Rc, - - /// Debounce or throttle the writing to storage whenever the value changes. - pub(crate) filter: FilterOptions, -} - -impl Default for UseStorageOptions { - fn default() -> Self { - Self { - storage_type: Default::default(), - listen_to_storage_changes: true, - write_defaults: true, - merge_defaults: |stored_value, _default_value| stored_value.to_string(), - on_error: Rc::new(|_| ()), - filter: Default::default(), - } - } -} - -impl UseStorageOptions { - filter_builder_methods!( - /// the serializing and storing into storage - filter - ); -} From f3c87a1c50777aeed06bedc2698e7d4f3106a024 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 12:41:42 +0100 Subject: [PATCH 08/50] Move use_storage.rs under storage/ --- src/lib.rs | 4 ---- src/storage/mod.rs | 4 ++++ src/{ => storage}/use_storage.rs | 0 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 src/storage/mod.rs rename src/{ => storage}/use_storage.rs (100%) diff --git a/src/lib.rs b/src/lib.rs index 0501fd2..7ca96a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,6 @@ pub mod core; pub mod docs; #[cfg(feature = "math")] pub mod math; -#[cfg(feature = "storage")] pub mod storage; pub mod utils; @@ -57,8 +56,6 @@ mod use_raf_fn; mod use_scroll; mod use_service_worker; mod use_sorted; -#[cfg(feature = "storage")] -mod use_storage; mod use_supported; mod use_throttle_fn; mod use_timestamp; @@ -111,7 +108,6 @@ pub use use_raf_fn::*; pub use use_scroll::*; pub use use_service_worker::*; pub use use_sorted::*; -pub use use_storage::*; pub use use_supported::*; pub use use_throttle_fn::*; pub use use_timestamp::*; diff --git a/src/storage/mod.rs b/src/storage/mod.rs new file mode 100644 index 0000000..cfb8520 --- /dev/null +++ b/src/storage/mod.rs @@ -0,0 +1,4 @@ +#[cfg(feature = "storage")] +mod use_storage; + +pub use use_storage::*; diff --git a/src/use_storage.rs b/src/storage/use_storage.rs similarity index 100% rename from src/use_storage.rs rename to src/storage/use_storage.rs From fdb9f672d1c28d10c0b566434f3828af67b23215 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 13:57:36 +0100 Subject: [PATCH 09/50] Apply use_signal default value via an optional signal --- src/storage/use_storage.rs | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index cac53bb..38b1dd8 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -1,5 +1,6 @@ use crate::{ - core::StorageType, use_event_listener_with_options, use_window, UseEventListenerOptions, + core::{MaybeRwSignal, StorageType}, + use_event_listener_with_options, use_window, UseEventListenerOptions, }; use leptos::*; use std::{rc::Rc, str::FromStr}; @@ -7,10 +8,11 @@ use thiserror::Error; use wasm_bindgen::JsValue; #[derive(Clone)] -pub struct UseStorageOptions> { +pub struct UseStorageOptions> { codec: C, on_error: Rc)>, listen_to_storage_changes: bool, + default_value: MaybeSignal, } /// Session handling errors returned by [`use_storage`]. @@ -43,7 +45,7 @@ pub fn use_local_storage_with_options( options: UseStorageOptions, ) -> (Memo, impl Fn(Option) -> ()) where - T: Clone + Default + PartialEq, + T: Clone + PartialEq, C: Codec, { use_storage_with_options(StorageType::Local, key, options) @@ -62,7 +64,7 @@ pub fn use_session_storage_with_options( options: UseStorageOptions, ) -> (Memo, impl Fn(Option) -> ()) where - T: Clone + Default + PartialEq, + T: Clone + PartialEq, C: Codec, { use_storage_with_options(StorageType::Session, key, options) @@ -75,7 +77,7 @@ fn use_storage_with_options( options: UseStorageOptions, ) -> (Memo, impl Fn(Option) -> ()) where - T: Clone + Default + PartialEq, + T: Clone + PartialEq, C: Codec, { // TODO ssr @@ -83,6 +85,7 @@ where codec, on_error, listen_to_storage_changes, + default_value, } = options; // Get storage API @@ -164,7 +167,9 @@ where ); }; - let value = create_memo(move |_| data.get().unwrap_or_default()); + // Apply default value + let value = create_memo(move |_| data.get().unwrap_or_else(|| default_value.get())); + (value, set_value) } @@ -190,12 +195,13 @@ fn decode_item>( .unwrap_or_default() } -impl> UseStorageOptions { +impl> UseStorageOptions { fn new(codec: C) -> Self { Self { codec, on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, + default_value: MaybeSignal::default(), } } @@ -212,6 +218,13 @@ impl> UseStorageOptions { ..self } } + + pub fn default_value(self, values: impl Into>) -> Self { + Self { + default_value: values.into().into_signal().0.into(), + ..self + } + } } pub trait Codec: Clone + 'static { @@ -235,7 +248,7 @@ impl Codec for StringCodec { } } -impl UseStorageOptions { +impl UseStorageOptions { pub fn string_codec() -> Self { Self::new(StringCodec()) } @@ -269,7 +282,7 @@ impl Codec for ProstCodec { } } -impl UseStorageOptions { +impl UseStorageOptions { pub fn prost_codec() -> Self { Self::new(ProstCodec()) } @@ -290,7 +303,9 @@ impl Codec for JsonCodec { } } -impl UseStorageOptions { +impl + UseStorageOptions +{ pub fn json_codec() -> Self { Self::new(JsonCodec()) } From 7bfd0690476a06f2e946e70550abadccce611a58 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 14:15:40 +0100 Subject: [PATCH 10/50] Expose generalised use of use_storage_with_options --- src/storage/use_storage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 38b1dd8..1f4e992 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -71,7 +71,7 @@ where } /// Hook for using any kind of storage. Returns a result of a signal and a setter / deleter. -fn use_storage_with_options( +pub fn use_storage_with_options( storage_type: StorageType, key: impl AsRef, options: UseStorageOptions, From 7b5456a4618f93c93b971880f52ac0f5fdd00d00 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 14:27:53 +0100 Subject: [PATCH 11/50] Remove storage feature --- Cargo.toml | 2 +- examples/ssr/Cargo.toml | 2 +- examples/use_color_mode/Cargo.toml | 2 +- examples/use_storage/Cargo.toml | 2 +- src/core/storage.rs | 1 - src/storage/mod.rs | 1 - src/use_color_mode.rs | 68 ++++++++++-------------------- 7 files changed, 26 insertions(+), 52 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a694929..8405ad8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ features = [ "ServiceWorkerRegistration", "ServiceWorkerState", "Storage", + "StorageEvent", "Touch", "TouchEvent", "TouchList", @@ -95,7 +96,6 @@ features = [ [features] docs = [] math = ["num"] -storage = ["base64", "serde", "serde_json", "web-sys/StorageEvent"] ssr = [] [package.metadata.docs.rs] diff --git a/examples/ssr/Cargo.toml b/examples/ssr/Cargo.toml index 5010f37..782e066 100644 --- a/examples/ssr/Cargo.toml +++ b/examples/ssr/Cargo.toml @@ -15,7 +15,7 @@ leptos = { version = "0.5", features = ["nightly"] } leptos_axum = { version = "0.5", optional = true } leptos_meta = { version = "0.5", features = ["nightly"] } leptos_router = { version = "0.5", features = ["nightly"] } -leptos-use = { path = "../..", features = ["storage"] } +leptos-use = { path = "../.." } log = "0.4" simple_logger = "4" tokio = { version = "1.25.0", optional = true } diff --git a/examples/use_color_mode/Cargo.toml b/examples/use_color_mode/Cargo.toml index 308c9c2..0b58b63 100644 --- a/examples/use_color_mode/Cargo.toml +++ b/examples/use_color_mode/Cargo.toml @@ -8,7 +8,7 @@ leptos = { version = "0.5", features = ["nightly", "csr"] } console_error_panic_hook = "0.1" console_log = "1" log = "0.4" -leptos-use = { path = "../..", features = ["docs", "storage"] } +leptos-use = { path = "../..", features = ["docs"] } web-sys = "0.3" [dev-dependencies] diff --git a/examples/use_storage/Cargo.toml b/examples/use_storage/Cargo.toml index 09446d5..6686fe8 100644 --- a/examples/use_storage/Cargo.toml +++ b/examples/use_storage/Cargo.toml @@ -8,7 +8,7 @@ leptos = { version = "0.5", features = ["nightly", "csr"] } console_error_panic_hook = "0.1" console_log = "1" log = "0.4" -leptos-use = { path = "../..", features = ["docs", "storage"] } +leptos-use = { path = "../..", features = ["docs", "prost", "serde"] } web-sys = "0.3" serde = "1.0.163" diff --git a/src/core/storage.rs b/src/core/storage.rs index 72da8fa..8390217 100644 --- a/src/core/storage.rs +++ b/src/core/storage.rs @@ -2,7 +2,6 @@ use leptos::window; use wasm_bindgen::JsValue; /// Local or session storage or a custom store that is a `web_sys::Storage`. -// #[doc(cfg(feature = "storage"))] #[derive(Default)] pub enum StorageType { #[default] diff --git a/src/storage/mod.rs b/src/storage/mod.rs index cfb8520..2320414 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,4 +1,3 @@ -#[cfg(feature = "storage")] mod use_storage; pub use use_storage::*; diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index 3bffcb5..86efba4 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -1,13 +1,9 @@ use crate::core::{ElementMaybeSignal, MaybeRwSignal}; -#[cfg(feature = "storage")] use crate::storage::{use_storage_with_options, UseStorageOptions}; -#[cfg(feature = "storage")] -use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use crate::core::StorageType; use crate::use_preferred_dark; -use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::*; use std::marker::PhantomData; @@ -255,49 +251,29 @@ pub enum ColorMode { Custom(String), } -cfg_if! { if #[cfg(feature = "storage")] { - fn get_store_signal( - initial_value: MaybeRwSignal, - storage_signal: Option>, - storage_key: &str, - storage_enabled: bool, - storage: StorageType, - listen_to_storage_changes: bool, - ) -> (Signal, WriteSignal) { - if let Some(storage_signal) = storage_signal { - let (store, set_store) = storage_signal.split(); - (store.into(), set_store) - } else if storage_enabled { - let (store, set_store, _) = use_storage_with_options( - storage_key, - initial_value, - UseStorageOptions::default() - .listen_to_storage_changes(listen_to_storage_changes) - .storage_type(storage), - ); - - (store, set_store) - } else { - initial_value.into_signal() - } +fn get_store_signal( + initial_value: MaybeRwSignal, + storage_signal: Option>, + storage_key: &str, + storage_enabled: bool, + storage: StorageType, + listen_to_storage_changes: bool, +) -> (Signal, WriteSignal) { + if let Some(storage_signal) = storage_signal { + let (store, set_store) = storage_signal.split(); + (store.into(), set_store) + } else if storage_enabled { + use_storage_with_options( + storage_key, + initial_value, + UseStorageOptions::default() + .listen_to_storage_changes(listen_to_storage_changes) + .storage_type(storage), + ) + } else { + initial_value.into_signal() } -} else { - fn get_store_signal( - initial_value: MaybeRwSignal, - storage_signal: Option>, - _storage_key: &str, - _storage_enabled: bool, - _storage: StorageType, - _listen_to_storage_changes: bool, - ) -> (Signal, WriteSignal) { - if let Some(storage_signal) = storage_signal { - let (store, set_store) = storage_signal.split(); - (store.into(), set_store) - } else { - initial_value.into_signal() - } - } -}} +} impl Display for ColorMode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { From 861633dd1e84accd462cb915f3d5bc8b6a13df5f Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 14:32:03 +0100 Subject: [PATCH 12/50] Gate prost and serde dependencies behind features --- Cargo.toml | 2 + src/storage/codec_json.rs | 46 +++++++++++++++++++ src/storage/codec_prost.rs | 58 ++++++++++++++++++++++++ src/storage/mod.rs | 4 ++ src/storage/use_storage.rs | 93 +------------------------------------- 5 files changed, 111 insertions(+), 92 deletions(-) create mode 100644 src/storage/codec_json.rs create mode 100644 src/storage/codec_prost.rs diff --git a/Cargo.toml b/Cargo.toml index 8405ad8..4de6bbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,8 @@ features = [ [features] docs = [] math = ["num"] +prost = ["base64", "dep:prost"] +serde = ["dep:serde", "serde_json"] ssr = [] [package.metadata.docs.rs] diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs new file mode 100644 index 0000000..fc065a7 --- /dev/null +++ b/src/storage/codec_json.rs @@ -0,0 +1,46 @@ +use super::{Codec, UseStorageOptions}; + +#[derive(Clone, PartialEq)] +pub struct JsonCodec(); + +impl Codec 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) + } +} + +impl + UseStorageOptions +{ + pub fn json_codec() -> Self { + Self::new(JsonCodec()) + } +} + +#[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/storage/codec_prost.rs b/src/storage/codec_prost.rs new file mode 100644 index 0000000..0b0bfa7 --- /dev/null +++ b/src/storage/codec_prost.rs @@ -0,0 +1,58 @@ +use super::{Codec, UseStorageOptions}; +use base64::Engine; +use thiserror::Error; + +#[derive(Clone, 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 Codec 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) + } +} + +impl UseStorageOptions { + pub fn prost_codec() -> Self { + Self::new(ProstCodec()) + } +} + +#[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/storage/mod.rs b/src/storage/mod.rs index 2320414..eeae93e 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,3 +1,7 @@ +#[cfg(feature = "serde")] +mod codec_json; +#[cfg(feature = "prost")] +mod codec_prost; mod use_storage; pub use use_storage::*; diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 1f4e992..c296805 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -196,7 +196,7 @@ fn decode_item>( } impl> UseStorageOptions { - fn new(codec: C) -> Self { + pub(super) fn new(codec: C) -> Self { Self { codec, on_error: Rc::new(|_err| ()), @@ -254,63 +254,6 @@ impl UseStorageOptions } } -#[derive(Clone, 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), -} - -use base64::Engine; -impl Codec 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) - } -} - -impl UseStorageOptions { - pub fn prost_codec() -> Self { - Self::new(ProstCodec()) - } -} - -#[derive(Clone, PartialEq)] -pub struct JsonCodec(); - -impl Codec 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) - } -} - -impl - UseStorageOptions -{ - pub fn json_codec() -> Self { - Self::new(JsonCodec()) - } -} - #[cfg(test)] mod tests { use super::*; @@ -322,38 +265,4 @@ mod tests { assert_eq!(codec.encode(&s), Ok(s.clone())); assert_eq!(codec.decode(s.clone()), Ok(s)); } - - #[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)); - } - - #[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); - } } From bbebd8a67f036dfd9cd7d5a2628caafa278f4e0c Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 14:50:29 +0100 Subject: [PATCH 13/50] use_storage SSR should return signal with defaults --- src/storage/use_storage.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index c296805..d048b9c 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -2,6 +2,7 @@ use crate::{ core::{MaybeRwSignal, StorageType}, use_event_listener_with_options, use_window, UseEventListenerOptions, }; +use cfg_if::cfg_if; use leptos::*; use std::{rc::Rc, str::FromStr}; use thiserror::Error; @@ -80,7 +81,17 @@ where T: Clone + PartialEq, C: Codec, { - // TODO ssr + cfg_if! { if #[cfg(feature = "ssr")] { + let (data, set_data) = create_signal(None); + let set_value = move |value: Option| { + set_data.set(value); + }; + let value = create_memo(move |_| data.get().unwrap_or_default()); + return (value, set_value); + } else { + // Continue + }} + let UseStorageOptions { codec, on_error, From d56c5cf51430e7d8c7addf77192af204f563f95b Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 16:08:51 +0100 Subject: [PATCH 14/50] Problem: use_storage defaults to StringCodec. Allow turbo fish usage and rely on Default --- src/storage/codec_json.rs | 2 +- src/storage/codec_prost.rs | 2 +- src/storage/mod.rs | 5 +++++ src/storage/use_storage.rs | 43 +++++++++++++++++++++++++++++--------- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs index fc065a7..a8c140f 100644 --- a/src/storage/codec_json.rs +++ b/src/storage/codec_json.rs @@ -1,6 +1,6 @@ use super::{Codec, UseStorageOptions}; -#[derive(Clone, PartialEq)] +#[derive(Clone, Default, PartialEq)] pub struct JsonCodec(); impl Codec for JsonCodec { diff --git a/src/storage/codec_prost.rs b/src/storage/codec_prost.rs index 0b0bfa7..74d1856 100644 --- a/src/storage/codec_prost.rs +++ b/src/storage/codec_prost.rs @@ -2,7 +2,7 @@ use super::{Codec, UseStorageOptions}; use base64::Engine; use thiserror::Error; -#[derive(Clone, PartialEq)] +#[derive(Clone, Default, PartialEq)] pub struct ProstCodec(); #[derive(Error, Debug, PartialEq)] diff --git a/src/storage/mod.rs b/src/storage/mod.rs index eeae93e..e5b09ed 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -4,4 +4,9 @@ mod codec_json; mod codec_prost; mod use_storage; +pub use crate::core::StorageType; +#[cfg(feature = "serde")] +pub use codec_json::*; +#[cfg(feature = "prost")] +pub use codec_prost::*; pub use use_storage::*; diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index d048b9c..27b1452 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -34,17 +34,22 @@ pub enum UseStorageError { } /// Hook for using local storage. Returns a result of a signal and a setter / deleter. -pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> ()) +pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> () + Clone) where - T: Clone + Default + FromStr + PartialEq + ToString, + T: Clone + Default + PartialEq, + C: Codec + Default, { - use_storage_with_options(StorageType::Local, key, UseStorageOptions::string_codec()) + use_storage_with_options( + StorageType::Local, + key, + UseStorageOptions::::default(), + ) } pub fn use_local_storage_with_options( key: impl AsRef, options: UseStorageOptions, -) -> (Memo, impl Fn(Option) -> ()) +) -> (Memo, impl Fn(Option) -> () + Clone) where T: Clone + PartialEq, C: Codec, @@ -53,17 +58,24 @@ where } /// Hook for using session storage. Returns a result of a signal and a setter / deleter. -pub fn use_session_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> ()) +pub fn use_session_storage( + key: impl AsRef, +) -> (Memo, impl Fn(Option) -> () + Clone) where - T: Clone + Default + FromStr + PartialEq + ToString, + T: Clone + Default + PartialEq, + C: Codec + Default, { - use_storage_with_options(StorageType::Session, key, UseStorageOptions::string_codec()) + use_storage_with_options( + StorageType::Session, + key, + UseStorageOptions::::default(), + ) } pub fn use_session_storage_with_options( key: impl AsRef, options: UseStorageOptions, -) -> (Memo, impl Fn(Option) -> ()) +) -> (Memo, impl Fn(Option) -> () + Clone) where T: Clone + PartialEq, C: Codec, @@ -76,7 +88,7 @@ pub fn use_storage_with_options( storage_type: StorageType, key: impl AsRef, options: UseStorageOptions, -) -> (Memo, impl Fn(Option) -> ()) +) -> (Memo, impl Fn(Option) -> () + Clone) where T: Clone + PartialEq, C: Codec, @@ -206,6 +218,17 @@ fn decode_item>( .unwrap_or_default() } +impl + Default> Default for UseStorageOptions { + fn default() -> Self { + Self { + codec: C::default(), + on_error: Rc::new(|_err| ()), + listen_to_storage_changes: true, + default_value: MaybeSignal::default(), + } + } +} + impl> UseStorageOptions { pub(super) fn new(codec: C) -> Self { Self { @@ -244,7 +267,7 @@ pub trait Codec: Clone + 'static { fn decode(&self, str: String) -> Result; } -#[derive(Clone, PartialEq)] +#[derive(Clone, Default, PartialEq)] pub struct StringCodec(); impl Codec for StringCodec { From 1371b81b6777d40ad4a8fedb63124e6b2f3b1a1b Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 19:51:02 +0100 Subject: [PATCH 15/50] Problem: use_storage value setter differs from existing API too much. Use WriteSignal + separate deleter fn --- src/storage/use_storage.rs | 108 +++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 41 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 27b1452..7a65fee 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -13,7 +13,7 @@ pub struct UseStorageOptions> { codec: C, on_error: Rc)>, listen_to_storage_changes: bool, - default_value: MaybeSignal, + default_value: MaybeRwSignal, } /// Session handling errors returned by [`use_storage`]. @@ -34,7 +34,9 @@ pub enum UseStorageError { } /// Hook for using local storage. Returns a result of a signal and a setter / deleter. -pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> () + Clone) +pub fn use_local_storage( + key: impl AsRef, +) -> (Signal, WriteSignal, impl Fn() -> () + Clone) where T: Clone + Default + PartialEq, C: Codec + Default, @@ -49,7 +51,7 @@ where pub fn use_local_storage_with_options( key: impl AsRef, options: UseStorageOptions, -) -> (Memo, impl Fn(Option) -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() -> () + Clone) where T: Clone + PartialEq, C: Codec, @@ -60,7 +62,7 @@ where /// Hook for using session storage. Returns a result of a signal and a setter / deleter. pub fn use_session_storage( key: impl AsRef, -) -> (Memo, impl Fn(Option) -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() -> () + Clone) where T: Clone + Default + PartialEq, C: Codec + Default, @@ -75,7 +77,7 @@ where pub fn use_session_storage_with_options( key: impl AsRef, options: UseStorageOptions, -) -> (Memo, impl Fn(Option) -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() -> () + Clone) where T: Clone + PartialEq, C: Codec, @@ -88,7 +90,7 @@ pub fn use_storage_with_options( storage_type: StorageType, key: impl AsRef, options: UseStorageOptions, -) -> (Memo, impl Fn(Option) -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() -> () + Clone) where T: Clone + PartialEq, C: Codec, @@ -99,7 +101,7 @@ where set_data.set(value); }; let value = create_memo(move |_| data.get().unwrap_or_default()); - return (value, set_value); + return (value, set_value, || ()); } else { // Continue }} @@ -118,7 +120,7 @@ where .and_then(|s| s.ok_or(UseStorageError::StorageReturnedNone)); let storage = handle_error(&on_error, storage); - // Fetch initial value (undecoded) + // Fetch initial value let initial_value = storage .to_owned() // Pull from storage @@ -129,71 +131,95 @@ where handle_error(&on_error, result) }) .unwrap_or_default(); - // Decode initial value let initial_value = decode_item(&codec, initial_value, &on_error); - let (data, set_data) = create_signal(initial_value); + // Data signal: use initial value or falls back to default value. + let (default_value, set_default_value) = default_value.into_signal(); + let (data, set_data) = match initial_value { + Some(initial_value) => { + let (data, set_data) = create_signal(initial_value); + (data.into(), set_data) + } + None => (default_value, set_default_value), + }; - // Update storage value - let set_value = { + // If data is removed from browser storage, revert to default value + let revert_data = move || { + set_data.set(default_value.get_untracked()); + }; + + // Update storage value on change + { let storage = storage.to_owned(); - let key = key.as_ref().to_owned(); let codec = codec.to_owned(); + let key = key.as_ref().to_owned(); let on_error = on_error.to_owned(); - move |value: Option| { - let key = key.as_str(); - // Attempt to update storage - let _ = storage.as_ref().map(|storage| { - let result = match value { - // Update - Some(ref value) => codec + let _ = watch( + move || data.get(), + move |value, _, _| { + let key = key.as_str(); + if let Ok(storage) = &storage { + let result = codec .encode(&value) .map_err(UseStorageError::ItemCodecError) .and_then(|enc_value| { + // Set storage -- this sends an event to other pages storage .set_item(key, &enc_value) .map_err(UseStorageError::SetItemFailed) - }), - // Remove - None => storage - .remove_item(key) - .map_err(UseStorageError::RemoveItemFailed), - }; - handle_error(&on_error, result) - }); - - // Notify signal of change - set_data.set(value); - } + }); + let _ = handle_error(&on_error, result); + } + }, + false, + ); }; // Listen for storage events // Note: we only receive events from other tabs / windows, not from internal updates. if listen_to_storage_changes { let key = key.as_ref().to_owned(); + let on_error = on_error.to_owned(); let _ = use_event_listener_with_options( use_window(), leptos::ev::storage, move |ev| { + let mut deleted = false; // Update storage value if our key matches if let Some(k) = ev.key() { if k == key { - let value = decode_item(&codec, ev.new_value(), &on_error); - set_data.set(value) + match decode_item(&codec, ev.new_value(), &on_error) { + Some(value) => set_data.set(value), + None => deleted = true, + } } } else { // All keys deleted - set_data.set(None) + deleted = true; + } + if deleted { + revert_data(); } }, UseEventListenerOptions::default().passive(true), ); }; - // Apply default value - let value = create_memo(move |_| data.get().unwrap_or_else(|| default_value.get())); + // Remove from storage fn + let remove = { + let key = key.as_ref().to_owned(); + move || { + let _ = storage.as_ref().map(|storage| { + let result = storage + .remove_item(key.as_ref()) + .map_err(UseStorageError::RemoveItemFailed); + let _ = handle_error(&on_error, result); + revert_data(); + }); + } + }; - (value, set_value) + (data, set_data, remove) } /// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling. @@ -224,7 +250,7 @@ impl + Default> Default for UseStorageOptions< codec: C::default(), on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, - default_value: MaybeSignal::default(), + default_value: MaybeRwSignal::default(), } } } @@ -235,7 +261,7 @@ impl> UseStorageOptions { codec, on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, - default_value: MaybeSignal::default(), + default_value: MaybeRwSignal::default(), } } @@ -255,7 +281,7 @@ impl> UseStorageOptions { pub fn default_value(self, values: impl Into>) -> Self { Self { - default_value: values.into().into_signal().0.into(), + default_value: values.into(), ..self } } From f23d8ad31cf966f510b92bdb06f50634f241c905 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 20:46:53 +0100 Subject: [PATCH 16/50] Problem: use_color_mode doesn't work with new use_storage API. Use StringCodec --- src/use_color_mode.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index 86efba4..af7e2b8 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -1,6 +1,7 @@ use crate::core::{ElementMaybeSignal, MaybeRwSignal}; use crate::storage::{use_storage_with_options, UseStorageOptions}; use std::fmt::{Display, Formatter}; +use std::str::FromStr; use crate::core::StorageType; use crate::use_preferred_dark; @@ -263,13 +264,14 @@ fn get_store_signal( let (store, set_store) = storage_signal.split(); (store.into(), set_store) } else if storage_enabled { - use_storage_with_options( + let (store, set_store, _) = use_storage_with_options( + storage, storage_key, - initial_value, - UseStorageOptions::default() + UseStorageOptions::string_codec() .listen_to_storage_changes(listen_to_storage_changes) - .storage_type(storage), - ) + .default_value(initial_value), + ); + (store, set_store) } else { initial_value.into_signal() } @@ -306,6 +308,14 @@ impl From for ColorMode { } } +impl FromStr for ColorMode { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(ColorMode::from(s)) + } +} + #[derive(DefaultBuilder)] pub struct UseColorModeOptions where From 3ecaade851ae4af5f1a37e5b01c982a66d2f2222 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 28 Oct 2023 12:13:55 +0100 Subject: [PATCH 17/50] Schedule internal use_storage events to notify same page of changes --- src/storage/use_storage.rs | 182 ++++++++++++++++++++++--------------- 1 file changed, 110 insertions(+), 72 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 7a65fee..a225a17 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -1,6 +1,6 @@ use crate::{ core::{MaybeRwSignal, StorageType}, - use_event_listener_with_options, use_window, UseEventListenerOptions, + use_event_listener, use_window, }; use cfg_if::cfg_if; use leptos::*; @@ -8,6 +8,8 @@ use std::{rc::Rc, str::FromStr}; use thiserror::Error; use wasm_bindgen::JsValue; +const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; + #[derive(Clone)] pub struct UseStorageOptions> { codec: C, @@ -29,6 +31,8 @@ pub enum UseStorageError { SetItemFailed(JsValue), #[error("failed to delete item")] RemoveItemFailed(JsValue), + #[error("failed to notify item changed")] + NotifyItemChangedFailed(JsValue), #[error("failed to encode / decode item value")] ItemCodecError(Err), } @@ -120,88 +124,134 @@ where .and_then(|s| s.ok_or(UseStorageError::StorageReturnedNone)); let storage = handle_error(&on_error, storage); - // Fetch initial value - let initial_value = storage - .to_owned() - // Pull from storage - .and_then(|s| { - let result = s - .get_item(key.as_ref()) - .map_err(UseStorageError::GetItemFailed); - handle_error(&on_error, result) - }) - .unwrap_or_default(); - let initial_value = decode_item(&codec, initial_value, &on_error); - - // Data signal: use initial value or falls back to default value. - let (default_value, set_default_value) = default_value.into_signal(); - let (data, set_data) = match initial_value { - Some(initial_value) => { - let (data, set_data) = create_signal(initial_value); - (data.into(), set_data) + // Schedules a storage event microtask. Uses a queue to avoid re-entering the runtime + let dispatch_storage_event = { + let key = key.as_ref().to_owned(); + let on_error = on_error.to_owned(); + move || { + let key = key.to_owned(); + let on_error = on_error.to_owned(); + queue_microtask(move || { + // Note: we cannot construct a full StorageEvent so we _must_ rely on a custom event + let mut custom = web_sys::CustomEventInit::new(); + custom.detail(&JsValue::from_str(&key)); + let result = window() + .dispatch_event( + &web_sys::CustomEvent::new_with_event_init_dict( + INTERNAL_STORAGE_EVENT, + &custom, + ) + .expect("failed to create custom storage event"), + ) + .map_err(UseStorageError::NotifyItemChangedFailed); + let _ = handle_error(&on_error, result); + }) } - None => (default_value, set_default_value), }; - // If data is removed from browser storage, revert to default value - let revert_data = move || { - set_data.set(default_value.get_untracked()); + // Fires when storage needs to be updated + let notify = create_trigger(); + + // Keeps track of how many times we've been notified. Does not increment for calls to set_data + let notify_id = create_memo::(move |prev| { + notify.track(); + prev.map(|prev| prev + 1).unwrap_or_default() + }); + + // Fetch from storage and falls back to the default (possibly a signal) if deleted + let fetcher = { + let storage = storage.to_owned(); + let codec = codec.to_owned(); + let key = key.as_ref().to_owned(); + let on_error = on_error.to_owned(); + let (default, _) = default_value.into_signal(); + create_memo(move |_| { + notify.track(); + storage + .to_owned() + .and_then(|storage| { + // Get directly from storage + let result = storage + .get_item(&key) + .map_err(UseStorageError::GetItemFailed); + handle_error(&on_error, result) + }) + .unwrap_or_default() // Drop handled Err(()) + .map(|encoded| { + // Decode item + let result = codec + .decode(encoded) + .map_err(UseStorageError::ItemCodecError); + handle_error(&on_error, result) + }) + .transpose() + .unwrap_or_default() // Drop handled Err(()) + // Fallback to default + .unwrap_or_else(move || default.get()) + }) }; - // Update storage value on change + // Create mutable data signal from our fetcher + let (data, set_data) = MaybeRwSignal::::from(fetcher).into_signal(); + let data = create_memo(move |_| data.get()); + + // Set storage value on data change { let storage = storage.to_owned(); let codec = codec.to_owned(); let key = key.as_ref().to_owned(); let on_error = on_error.to_owned(); + let dispatch_storage_event = dispatch_storage_event.to_owned(); let _ = watch( - move || data.get(), - move |value, _, _| { - let key = key.as_str(); + move || (notify_id.get(), data.get()), + move |(id, value), prev, _| { + // Skip setting storage on changes from external events. The ID will change on external events. + if prev.map(|(prev_id, _)| *prev_id != *id).unwrap_or_default() { + return; + } + if let Ok(storage) = &storage { + // Encode value let result = codec - .encode(&value) + .encode(value) .map_err(UseStorageError::ItemCodecError) .and_then(|enc_value| { - // Set storage -- this sends an event to other pages + // Set storage -- sends a global event storage - .set_item(key, &enc_value) + .set_item(&key, &enc_value) .map_err(UseStorageError::SetItemFailed) }); - let _ = handle_error(&on_error, result); + let result = handle_error(&on_error, result); + // Send internal storage event + if result.is_ok() { + dispatch_storage_event(); + } } }, false, ); }; - // Listen for storage events - // Note: we only receive events from other tabs / windows, not from internal updates. if listen_to_storage_changes { - let key = key.as_ref().to_owned(); - let on_error = on_error.to_owned(); - let _ = use_event_listener_with_options( + let check_key = key.as_ref().to_owned(); + // Listen to global storage events + let _ = use_event_listener(use_window(), leptos::ev::storage, move |ev| { + let ev_key = ev.key(); + // Key matches or all keys deleted (None) + if ev_key == Some(check_key.clone()) || ev_key.is_none() { + notify.notify() + } + }); + // Listen to internal storage events + let check_key = key.as_ref().to_owned(); + let _ = use_event_listener( use_window(), - leptos::ev::storage, - move |ev| { - let mut deleted = false; - // Update storage value if our key matches - if let Some(k) = ev.key() { - if k == key { - match decode_item(&codec, ev.new_value(), &on_error) { - Some(value) => set_data.set(value), - None => deleted = true, - } - } - } else { - // All keys deleted - deleted = true; - } - if deleted { - revert_data(); + ev::Custom::new(INTERNAL_STORAGE_EVENT), + move |ev: web_sys::CustomEvent| { + if Some(check_key.clone()) == ev.detail().as_string() { + notify.notify() } }, - UseEventListenerOptions::default().passive(true), ); }; @@ -210,16 +260,18 @@ where let key = key.as_ref().to_owned(); move || { let _ = storage.as_ref().map(|storage| { + // Delete directly from storage let result = storage - .remove_item(key.as_ref()) + .remove_item(&key) .map_err(UseStorageError::RemoveItemFailed); let _ = handle_error(&on_error, result); - revert_data(); + notify.notify(); + dispatch_storage_event(); }); } }; - (data, set_data, remove) + (data.into(), set_data, remove) } /// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling. @@ -230,20 +282,6 @@ fn handle_error( result.or_else(|err| Err((on_error)(err))) } -fn decode_item>( - codec: &C, - str: Option, - on_error: &Rc)>, -) -> Option { - str.map(|str| { - let result = codec.decode(str).map_err(UseStorageError::ItemCodecError); - handle_error(&on_error, result) - }) - .transpose() - // We've sent our error so unwrap to drop () error - .unwrap_or_default() -} - impl + Default> Default for UseStorageOptions { fn default() -> Self { Self { From c62adedcf2d9868edb31f92138057a68a9e07fed Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 28 Oct 2023 12:16:05 +0100 Subject: [PATCH 18/50] Update use_storage example for new API --- examples/use_storage/src/main.rs | 40 ++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/examples/use_storage/src/main.rs b/examples/use_storage/src/main.rs index 7f5442f..849f8fa 100644 --- a/examples/use_storage/src/main.rs +++ b/examples/use_storage/src/main.rs @@ -1,28 +1,31 @@ use leptos::*; use leptos_use::docs::{demo_or_body, Note}; -use leptos_use::storage::use_storage; +use leptos_use::storage::{use_local_storage, JsonCodec}; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct BananaState { pub name: String, - pub color: String, - pub size: String, + pub wearing: String, + pub descending: String, pub count: u32, } +impl Default for BananaState { + fn default() -> Self { + Self { + name: "Bananas".to_string(), + wearing: "pyjamas".to_string(), + descending: "stairs".to_string(), + count: 2, + } + } +} + #[component] fn Demo() -> impl IntoView { - let the_default = BananaState { - name: "Banana".to_string(), - color: "Yellow".to_string(), - size: "Medium".to_string(), - count: 0, - }; - - let (state, set_state, _) = use_storage("banana-state", the_default.clone()); - - let (state2, ..) = use_storage("banana-state", the_default.clone()); + let (state, set_state, reset) = use_local_storage::("banana-state"); + let (state2, _, _) = use_local_storage::("banana-state"); view! { impl IntoView { /> impl IntoView { step="1" max="1000" /> +

"Second " From c24f0a5d45247e4c2ce6b091835a60ae77aff91a Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 28 Oct 2023 12:19:33 +0100 Subject: [PATCH 19/50] Separate StringCodec to separate file --- src/storage/codec_string.rs | 36 ++++++++++++++++++++++++++++ src/storage/mod.rs | 2 ++ src/storage/use_storage.rs | 48 ++++++------------------------------- 3 files changed, 45 insertions(+), 41 deletions(-) create mode 100644 src/storage/codec_string.rs diff --git a/src/storage/codec_string.rs b/src/storage/codec_string.rs new file mode 100644 index 0000000..2d855df --- /dev/null +++ b/src/storage/codec_string.rs @@ -0,0 +1,36 @@ +use super::{Codec, UseStorageOptions}; +use std::str::FromStr; + +#[derive(Clone, Default, PartialEq)] +pub struct StringCodec(); + +impl Codec for StringCodec { + type Error = T::Err; + + fn encode(&self, val: &T) -> Result { + Ok(val.to_string()) + } + + fn decode(&self, str: String) -> Result { + T::from_str(&str) + } +} + +impl UseStorageOptions { + pub fn string_codec() -> Self { + Self::new(StringCodec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_string_codec() { + let s = String::from("party time 🎉"); + let codec = StringCodec(); + assert_eq!(codec.encode(&s), Ok(s.clone())); + assert_eq!(codec.decode(s.clone()), Ok(s)); + } +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs index e5b09ed..7183c63 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -2,6 +2,7 @@ mod codec_json; #[cfg(feature = "prost")] mod codec_prost; +mod codec_string; mod use_storage; pub use crate::core::StorageType; @@ -9,4 +10,5 @@ pub use crate::core::StorageType; pub use codec_json::*; #[cfg(feature = "prost")] pub use codec_prost::*; +pub use codec_string::*; pub use use_storage::*; diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index a225a17..cd2a26b 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -4,12 +4,18 @@ use crate::{ }; use cfg_if::cfg_if; use leptos::*; -use std::{rc::Rc, str::FromStr}; +use std::rc::Rc; use thiserror::Error; use wasm_bindgen::JsValue; const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; +pub trait Codec: Clone + 'static { + type Error; + fn encode(&self, val: &T) -> Result; + fn decode(&self, str: String) -> Result; +} + #[derive(Clone)] pub struct UseStorageOptions> { codec: C, @@ -324,43 +330,3 @@ impl> UseStorageOptions { } } } - -pub trait Codec: Clone + 'static { - type Error; - fn encode(&self, val: &T) -> Result; - fn decode(&self, str: String) -> Result; -} - -#[derive(Clone, Default, PartialEq)] -pub struct StringCodec(); - -impl Codec for StringCodec { - type Error = T::Err; - - fn encode(&self, val: &T) -> Result { - Ok(val.to_string()) - } - - fn decode(&self, str: String) -> Result { - T::from_str(&str) - } -} - -impl UseStorageOptions { - pub fn string_codec() -> Self { - Self::new(StringCodec()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_string_codec() { - let s = String::from("party time 🎉"); - let codec = StringCodec(); - assert_eq!(codec.encode(&s), Ok(s.clone())); - assert_eq!(codec.decode(s.clone()), Ok(s)); - } -} From 50952581de4c8cc77d27397ed233418deffc0b31 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 28 Oct 2023 12:28:26 +0100 Subject: [PATCH 20/50] Clean up of UseStorageOptions default --- src/storage/use_storage.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index cd2a26b..cd08b48 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -288,18 +288,13 @@ fn handle_error( result.or_else(|err| Err((on_error)(err))) } -impl + Default> Default for UseStorageOptions { +impl + Default> Default for UseStorageOptions { fn default() -> Self { - Self { - codec: C::default(), - on_error: Rc::new(|_err| ()), - listen_to_storage_changes: true, - default_value: MaybeRwSignal::default(), - } + Self::new(C::default()) } } -impl> UseStorageOptions { +impl> UseStorageOptions { pub(super) fn new(codec: C) -> Self { Self { codec, From 40fdb5f6b55f2fe029453b13b49d84183041aad2 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 09:45:43 +0000 Subject: [PATCH 21/50] Add use_storage debounce / throttle on write --- src/storage/use_storage.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index cd08b48..498841f 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -1,6 +1,8 @@ use crate::{ core::{MaybeRwSignal, StorageType}, use_event_listener, use_window, + utils::FilterOptions, + watch_with_options, WatchOptions, }; use cfg_if::cfg_if; use leptos::*; @@ -16,12 +18,12 @@ pub trait Codec: Clone + 'static { fn decode(&self, str: String) -> Result; } -#[derive(Clone)] pub struct UseStorageOptions> { codec: C, on_error: Rc)>, listen_to_storage_changes: bool, default_value: MaybeRwSignal, + filter: FilterOptions, } /// Session handling errors returned by [`use_storage`]. @@ -121,6 +123,7 @@ where on_error, listen_to_storage_changes, default_value, + filter, } = options; // Get storage API @@ -208,7 +211,7 @@ where let key = key.as_ref().to_owned(); let on_error = on_error.to_owned(); let dispatch_storage_event = dispatch_storage_event.to_owned(); - let _ = watch( + let _ = watch_with_options( move || (notify_id.get(), data.get()), move |(id, value), prev, _| { // Skip setting storage on changes from external events. The ID will change on external events. @@ -234,7 +237,7 @@ where } } }, - false, + WatchOptions::default().filter(filter), ); }; @@ -301,6 +304,7 @@ impl> UseStorageOptions { on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, default_value: MaybeRwSignal::default(), + filter: FilterOptions::default(), } } @@ -324,4 +328,11 @@ impl> UseStorageOptions { ..self } } + + pub fn filter(self, filter: impl Into) -> Self { + Self { + filter: filter.into(), + ..self + } + } } From 4110a763c970b602000fd78e02271f5d35953d67 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 11:41:59 +0000 Subject: [PATCH 22/50] Document use_storage with examples --- src/storage/codec_json.rs | 22 +++++++++ src/storage/codec_prost.rs | 29 ++++++++++++ src/storage/codec_string.rs | 18 +++++++- src/storage/use_storage.rs | 90 +++++++++++++++++++++++++++++++++++-- 4 files changed, 154 insertions(+), 5 deletions(-) diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs index a8c140f..d3b77b8 100644 --- a/src/storage/codec_json.rs +++ b/src/storage/codec_json.rs @@ -1,5 +1,26 @@ use super::{Codec, UseStorageOptions}; +/// 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_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; +/// # use serde::{Deserialize, Serialize}; +/// # +/// # 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! { } +/// # } +/// ``` #[derive(Clone, Default, PartialEq)] pub struct JsonCodec(); @@ -18,6 +39,7 @@ impl Codec for JsonCodec { impl UseStorageOptions { + /// Constructs a new `UseStorageOptions` with a [`JsonCodec`] for JSON messages. pub fn json_codec() -> Self { Self::new(JsonCodec()) } diff --git a/src/storage/codec_prost.rs b/src/storage/codec_prost.rs index 74d1856..ced8388 100644 --- a/src/storage/codec_prost.rs +++ b/src/storage/codec_prost.rs @@ -2,6 +2,34 @@ use super::{Codec, UseStorageOptions}; 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_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; +/// # use serde::{Deserialize, Serialize}; +/// # +/// # 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(Clone, Default, PartialEq)] pub struct ProstCodec(); @@ -30,6 +58,7 @@ impl Codec for ProstCodec { } impl UseStorageOptions { + /// Constructs a new `UseStorageOptions` with a [`ProstCodec`] for ProtoBuf messages. pub fn prost_codec() -> Self { Self::new(ProstCodec()) } diff --git a/src/storage/codec_string.rs b/src/storage/codec_string.rs index 2d855df..a2dd64b 100644 --- a/src/storage/codec_string.rs +++ b/src/storage/codec_string.rs @@ -1,6 +1,21 @@ use super::{Codec, UseStorageOptions}; 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::*; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; +/// # use serde::{Deserialize, Serialize}; +/// # +/// # pub fn Demo() -> impl IntoView { +/// let (get, set, remove) = use_local_storage::("my-key"); +/// # view! { } +/// # } +/// ``` #[derive(Clone, Default, PartialEq)] pub struct StringCodec(); @@ -16,7 +31,8 @@ impl Codec for StringCodec { } } -impl UseStorageOptions { +impl UseStorageOptions { + /// Constructs a new `UseStorageOptions` with a [`StringCodec`] relying on [`FromStr`] to parse. pub fn string_codec() -> Self { Self::new(StringCodec()) } diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 498841f..108f911 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -12,12 +12,17 @@ use wasm_bindgen::JsValue; const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; +/// A codec for encoding and decoding values to and from UTF-16 strings. These strings are then stored in browser storage. 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; } +/// Options for use with [`use_local_storage_with_options`], [`use_session_storage_with_options`] and [`use_storage_with_options`]. pub struct UseStorageOptions> { codec: C, on_error: Rc)>, @@ -45,7 +50,13 @@ pub enum UseStorageError { ItemCodecError(Err), } -/// Hook for using local storage. Returns a result of a signal and a setter / deleter. +/// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). +/// +/// LocalStorage stores data in the browser with no expiration time. Access is given to all pages from the same origin (e.g., all pages from "https://example.com" share the same origin). While data doesn't expire the user can view, modify and delete all data stored. Browsers allow 5MB of data to be stored. +/// +/// This is contrast to [`use_session_storage`] which clears data when the page session ends and is not shared. +/// +/// See [`use_storage_with_options`] for more details on how to use. pub fn use_local_storage( key: impl AsRef, ) -> (Signal, WriteSignal, impl Fn() -> () + Clone) @@ -60,6 +71,7 @@ where ) } +/// Accepts [`UseStorageOptions`]. See [`use_local_storage`] for details. pub fn use_local_storage_with_options( key: impl AsRef, options: UseStorageOptions, @@ -71,7 +83,13 @@ where use_storage_with_options(StorageType::Local, key, options) } -/// Hook for using session storage. Returns a result of a signal and a setter / deleter. +/// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). +/// +/// SessionStorages stores data in the browser that is deleted when the page session ends. A page session ends when the browser closes the tab. Data is not shared between pages. While data doesn't expire the user can view, modify and delete all data stored. Browsers allow 5MB of data to be stored. +/// +/// Use [`use_local_storage`] to store data that is shared amongst all pages with the same origin and persists between page sessions. +/// +/// See [`use_storage_with_options`] for more details on how to use. pub fn use_session_storage( key: impl AsRef, ) -> (Signal, WriteSignal, impl Fn() -> () + Clone) @@ -86,6 +104,7 @@ where ) } +/// Accepts [`UseStorageOptions`]. See [`use_session_storage`] for details. pub fn use_session_storage_with_options( key: impl AsRef, options: UseStorageOptions, @@ -97,7 +116,65 @@ where use_storage_with_options(StorageType::Session, key, options) } -/// Hook for using any kind of storage. Returns a result of a signal and a setter / deleter. +/// Reactive [Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). +/// +/// * [See a demo](https://leptos-use.rs/storage/use_storage.html) +/// * [See a full example](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_storage) +/// +/// ## Usage +/// +/// Pass a [`StorageType`] to determine the kind of key-value browser storage to use. 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`].Finally, see [`UseStorageOptions`] to see how behaviour can be further customised. +/// +/// Returns a triplet `(read_signal, write_signal, delete_from_storage_fn)`. +/// +/// Signals work as expected and can be used to read and write to storage. The `delete_from_storage_fn` can be called to delete the item from storage. Once deleted the signals will revert back to the default value. +/// +/// ## Example +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; +/// # use serde::{Deserialize, Serialize}; +/// # +/// # pub fn Demo() -> impl IntoView { +/// // Binds a struct: +/// 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"); +/// +/// // 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"); +/// +/// // Bind string with SessionStorage stored in ProtoBuf format: +/// let (id, set_id, _) = use_storage_with_options::( +/// StorageType::Session, +/// "my-id", +/// UseStorageOptions::prost_codec(), +/// ); +/// # view! { } +/// # } +/// +/// // Data stored in JSON must implement Serialize, Deserialize: +/// #[derive(Serialize, Deserialize, Clone, PartialEq)] +/// pub struct MyState { +/// pub hello: String, +/// pub greeting: String, +/// } +/// +/// // Default can be used to implement intial or deleted values. +/// // You can also use a signal via UseStorageOptions::default_value` +/// impl Default for MyState { +/// fn default() -> Self { +/// Self { +/// hello: "hi".to_string(), +/// greeting: "Hello".to_string() +/// } +/// } +/// } +/// ``` pub fn use_storage_with_options( storage_type: StorageType, key: impl AsRef, @@ -107,6 +184,7 @@ where T: Clone + PartialEq, C: Codec, { + /* cfg_if! { if #[cfg(feature = "ssr")] { let (data, set_data) = create_signal(None); let set_value = move |value: Option| { @@ -116,7 +194,7 @@ where return (value, set_value, || ()); } else { // Continue - }} + }}*/ let UseStorageOptions { codec, @@ -308,6 +386,7 @@ impl> UseStorageOptions { } } + /// Optional callback whenever an error occurs. pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { Self { on_error: Rc::new(on_error), @@ -315,6 +394,7 @@ impl> UseStorageOptions { } } + /// 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, @@ -322,6 +402,7 @@ impl> UseStorageOptions { } } + /// Default value to use when the storage key is not set. Accepts a signal. pub fn default_value(self, values: impl Into>) -> Self { Self { default_value: values.into(), @@ -329,6 +410,7 @@ impl> UseStorageOptions { } } + /// Debounce or throttle the writing to storage whenever the value changes. pub fn filter(self, filter: impl Into) -> Self { Self { filter: filter.into(), From 6af4cc0693f709995c3453737715d34bf46d4f1b Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 11:49:03 +0000 Subject: [PATCH 23/50] Switch to relying on a codec's default instead of constructors on UseStorageOptions --- src/storage/codec_json.rs | 11 +---------- src/storage/codec_prost.rs | 9 +-------- src/storage/codec_string.rs | 9 +-------- src/storage/use_storage.rs | 10 +++------- src/use_color_mode.rs | 6 +++--- 5 files changed, 9 insertions(+), 36 deletions(-) diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs index d3b77b8..30cd03a 100644 --- a/src/storage/codec_json.rs +++ b/src/storage/codec_json.rs @@ -1,4 +1,4 @@ -use super::{Codec, UseStorageOptions}; +use super::Codec; /// A codec for storing JSON messages that relies on [`serde_json`] to parse. /// @@ -36,15 +36,6 @@ impl Codec for JsonCodec { } } -impl - UseStorageOptions -{ - /// Constructs a new `UseStorageOptions` with a [`JsonCodec`] for JSON messages. - pub fn json_codec() -> Self { - Self::new(JsonCodec()) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/storage/codec_prost.rs b/src/storage/codec_prost.rs index ced8388..41a55d9 100644 --- a/src/storage/codec_prost.rs +++ b/src/storage/codec_prost.rs @@ -1,4 +1,4 @@ -use super::{Codec, UseStorageOptions}; +use super::Codec; use base64::Engine; use thiserror::Error; @@ -57,13 +57,6 @@ impl Codec for ProstCodec { } } -impl UseStorageOptions { - /// Constructs a new `UseStorageOptions` with a [`ProstCodec`] for ProtoBuf messages. - pub fn prost_codec() -> Self { - Self::new(ProstCodec()) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/storage/codec_string.rs b/src/storage/codec_string.rs index a2dd64b..91ac731 100644 --- a/src/storage/codec_string.rs +++ b/src/storage/codec_string.rs @@ -1,4 +1,4 @@ -use super::{Codec, UseStorageOptions}; +use super::Codec; use std::str::FromStr; /// A codec for strings that relies on [`FromStr`] and [`ToString`] to parse. @@ -31,13 +31,6 @@ impl Codec for StringCodec { } } -impl UseStorageOptions { - /// Constructs a new `UseStorageOptions` with a [`StringCodec`] relying on [`FromStr`] to parse. - pub fn string_codec() -> Self { - Self::new(StringCodec()) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 108f911..14936f8 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -371,21 +371,17 @@ fn handle_error( impl + Default> Default for UseStorageOptions { fn default() -> Self { - Self::new(C::default()) - } -} - -impl> UseStorageOptions { - pub(super) fn new(codec: C) -> Self { Self { - codec, + codec: C::default(), on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, default_value: MaybeRwSignal::default(), filter: FilterOptions::default(), } } +} +impl> UseStorageOptions { /// Optional callback whenever an error occurs. pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { Self { diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index af7e2b8..9c9d567 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -1,5 +1,5 @@ use crate::core::{ElementMaybeSignal, MaybeRwSignal}; -use crate::storage::{use_storage_with_options, UseStorageOptions}; +use crate::storage::{use_storage_with_options, StringCodec, UseStorageOptions}; use std::fmt::{Display, Formatter}; use std::str::FromStr; @@ -264,10 +264,10 @@ fn get_store_signal( let (store, set_store) = storage_signal.split(); (store.into(), set_store) } else if storage_enabled { - let (store, set_store, _) = use_storage_with_options( + let (store, set_store, _) = use_storage_with_options::( storage, storage_key, - UseStorageOptions::string_codec() + UseStorageOptions::default() .listen_to_storage_changes(listen_to_storage_changes) .default_value(initial_value), ); From 64c6f63d681599c26f7e3c233883a9d7ce8e1951 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 11:57:02 +0000 Subject: [PATCH 24/50] Fix dead doc ref --- src/storage/use_storage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 14936f8..5ee9ab0 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -31,7 +31,7 @@ pub struct UseStorageOptions> { filter: FilterOptions, } -/// Session handling errors returned by [`use_storage`]. +/// Session handling errors returned by [`use_storage_with_options`]. #[derive(Error, Debug)] pub enum UseStorageError { #[error("storage not available")] From d267408116d2bee4f9df88f91dbf29bf92d625af Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 11:58:14 +0000 Subject: [PATCH 25/50] Add codec setter to UseStorageOptions --- src/storage/use_storage.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 5ee9ab0..7ae49e9 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -382,6 +382,14 @@ impl + Default> Default for UseStorageOptions { } 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 + } + } + /// Optional callback whenever an error occurs. pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { Self { From f3ea5dcd74cddd37d49e3615bcfb9e0f3230890c Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 12:18:45 +0000 Subject: [PATCH 26/50] Fix use_storage for SSR --- src/storage/use_storage.rs | 310 ++++++++++++++++++------------------- 1 file changed, 153 insertions(+), 157 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 7ae49e9..6ac5875 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -184,18 +184,6 @@ where T: Clone + PartialEq, C: Codec, { - /* - cfg_if! { if #[cfg(feature = "ssr")] { - let (data, set_data) = create_signal(None); - let set_value = move |value: Option| { - set_data.set(value); - }; - let value = create_memo(move |_| data.get().unwrap_or_default()); - return (value, set_value, || ()); - } else { - // Continue - }}*/ - let UseStorageOptions { codec, on_error, @@ -203,162 +191,170 @@ where default_value, filter, } = options; + let (default, _) = default_value.into_signal(); - // Get storage API - let storage = storage_type - .into_storage() - .map_err(UseStorageError::StorageNotAvailable) - .and_then(|s| s.ok_or(UseStorageError::StorageReturnedNone)); - let storage = handle_error(&on_error, storage); + cfg_if! { if #[cfg(feature = "ssr")] { + let (data, set_data) = create_signal(default.get_untracked()); + let remove = move || { + set_data.set(default.get_untracked()); + }; + (data.into(), set_data, remove) + } else { + // Get storage API + let storage = storage_type + .into_storage() + .map_err(UseStorageError::StorageNotAvailable) + .and_then(|s| s.ok_or(UseStorageError::StorageReturnedNone)); + let storage = handle_error(&on_error, storage); - // Schedules a storage event microtask. Uses a queue to avoid re-entering the runtime - let dispatch_storage_event = { - let key = key.as_ref().to_owned(); - let on_error = on_error.to_owned(); - move || { - let key = key.to_owned(); + // Schedules a storage event microtask. Uses a queue to avoid re-entering the runtime + let dispatch_storage_event = { + let key = key.as_ref().to_owned(); let on_error = on_error.to_owned(); - queue_microtask(move || { - // Note: we cannot construct a full StorageEvent so we _must_ rely on a custom event - let mut custom = web_sys::CustomEventInit::new(); - custom.detail(&JsValue::from_str(&key)); - let result = window() - .dispatch_event( - &web_sys::CustomEvent::new_with_event_init_dict( - INTERNAL_STORAGE_EVENT, - &custom, + move || { + let key = key.to_owned(); + let on_error = on_error.to_owned(); + queue_microtask(move || { + // Note: we cannot construct a full StorageEvent so we _must_ rely on a custom event + let mut custom = web_sys::CustomEventInit::new(); + custom.detail(&JsValue::from_str(&key)); + let result = window() + .dispatch_event( + &web_sys::CustomEvent::new_with_event_init_dict( + INTERNAL_STORAGE_EVENT, + &custom, + ) + .expect("failed to create custom storage event"), ) - .expect("failed to create custom storage event"), - ) - .map_err(UseStorageError::NotifyItemChangedFailed); - let _ = handle_error(&on_error, result); - }) - } - }; - - // Fires when storage needs to be updated - let notify = create_trigger(); - - // Keeps track of how many times we've been notified. Does not increment for calls to set_data - let notify_id = create_memo::(move |prev| { - notify.track(); - prev.map(|prev| prev + 1).unwrap_or_default() - }); - - // Fetch from storage and falls back to the default (possibly a signal) if deleted - let fetcher = { - let storage = storage.to_owned(); - let codec = codec.to_owned(); - let key = key.as_ref().to_owned(); - let on_error = on_error.to_owned(); - let (default, _) = default_value.into_signal(); - create_memo(move |_| { - notify.track(); - storage - .to_owned() - .and_then(|storage| { - // Get directly from storage - let result = storage - .get_item(&key) - .map_err(UseStorageError::GetItemFailed); - handle_error(&on_error, result) + .map_err(UseStorageError::NotifyItemChangedFailed); + let _ = handle_error(&on_error, result); }) - .unwrap_or_default() // Drop handled Err(()) - .map(|encoded| { - // Decode item - let result = codec - .decode(encoded) - .map_err(UseStorageError::ItemCodecError); - handle_error(&on_error, result) - }) - .transpose() - .unwrap_or_default() // Drop handled Err(()) - // Fallback to default - .unwrap_or_else(move || default.get()) - }) - }; - - // Create mutable data signal from our fetcher - let (data, set_data) = MaybeRwSignal::::from(fetcher).into_signal(); - let data = create_memo(move |_| data.get()); - - // Set storage value on data change - { - let storage = storage.to_owned(); - let codec = codec.to_owned(); - let key = key.as_ref().to_owned(); - let on_error = on_error.to_owned(); - let dispatch_storage_event = dispatch_storage_event.to_owned(); - let _ = watch_with_options( - move || (notify_id.get(), data.get()), - move |(id, value), prev, _| { - // Skip setting storage on changes from external events. The ID will change on external events. - if prev.map(|(prev_id, _)| *prev_id != *id).unwrap_or_default() { - return; - } - - if let Ok(storage) = &storage { - // Encode value - let result = codec - .encode(value) - .map_err(UseStorageError::ItemCodecError) - .and_then(|enc_value| { - // Set storage -- sends a global event - storage - .set_item(&key, &enc_value) - .map_err(UseStorageError::SetItemFailed) - }); - let result = handle_error(&on_error, result); - // Send internal storage event - if result.is_ok() { - dispatch_storage_event(); - } - } - }, - WatchOptions::default().filter(filter), - ); - }; - - if listen_to_storage_changes { - let check_key = key.as_ref().to_owned(); - // Listen to global storage events - let _ = use_event_listener(use_window(), leptos::ev::storage, move |ev| { - let ev_key = ev.key(); - // Key matches or all keys deleted (None) - if ev_key == Some(check_key.clone()) || ev_key.is_none() { - notify.notify() } + }; + + // Fires when storage needs to be updated + let notify = create_trigger(); + + // Keeps track of how many times we've been notified. Does not increment for calls to set_data + let notify_id = create_memo::(move |prev| { + notify.track(); + prev.map(|prev| prev + 1).unwrap_or_default() }); - // Listen to internal storage events - let check_key = key.as_ref().to_owned(); - let _ = use_event_listener( - use_window(), - ev::Custom::new(INTERNAL_STORAGE_EVENT), - move |ev: web_sys::CustomEvent| { - if Some(check_key.clone()) == ev.detail().as_string() { + + // Fetch from storage and falls back to the default (possibly a signal) if deleted + let fetcher = { + let storage = storage.to_owned(); + let codec = codec.to_owned(); + let key = key.as_ref().to_owned(); + let on_error = on_error.to_owned(); + create_memo(move |_| { + notify.track(); + storage + .to_owned() + .and_then(|storage| { + // Get directly from storage + let result = storage + .get_item(&key) + .map_err(UseStorageError::GetItemFailed); + handle_error(&on_error, result) + }) + .unwrap_or_default() // Drop handled Err(()) + .map(|encoded| { + // Decode item + let result = codec + .decode(encoded) + .map_err(UseStorageError::ItemCodecError); + handle_error(&on_error, result) + }) + .transpose() + .unwrap_or_default() // Drop handled Err(()) + // Fallback to default + .unwrap_or_else(move || default.get()) + }) + }; + + // Create mutable data signal from our fetcher + let (data, set_data) = MaybeRwSignal::::from(fetcher).into_signal(); + let data = create_memo(move |_| data.get()); + + // Set storage value on data change + { + let storage = storage.to_owned(); + let codec = codec.to_owned(); + let key = key.as_ref().to_owned(); + let on_error = on_error.to_owned(); + let dispatch_storage_event = dispatch_storage_event.to_owned(); + let _ = watch_with_options( + move || (notify_id.get(), data.get()), + move |(id, value), prev, _| { + // Skip setting storage on changes from external events. The ID will change on external events. + if prev.map(|(prev_id, _)| *prev_id != *id).unwrap_or_default() { + return; + } + + if let Ok(storage) = &storage { + // Encode value + let result = codec + .encode(value) + .map_err(UseStorageError::ItemCodecError) + .and_then(|enc_value| { + // Set storage -- sends a global event + storage + .set_item(&key, &enc_value) + .map_err(UseStorageError::SetItemFailed) + }); + let result = handle_error(&on_error, result); + // Send internal storage event + if result.is_ok() { + dispatch_storage_event(); + } + } + }, + WatchOptions::default().filter(filter), + ); + }; + + if listen_to_storage_changes { + let check_key = key.as_ref().to_owned(); + // Listen to global storage events + let _ = use_event_listener(use_window(), leptos::ev::storage, move |ev| { + let ev_key = ev.key(); + // Key matches or all keys deleted (None) + if ev_key == Some(check_key.clone()) || ev_key.is_none() { notify.notify() } - }, - ); - }; - - // Remove from storage fn - let remove = { - let key = key.as_ref().to_owned(); - move || { - let _ = storage.as_ref().map(|storage| { - // Delete directly from storage - let result = storage - .remove_item(&key) - .map_err(UseStorageError::RemoveItemFailed); - let _ = handle_error(&on_error, result); - notify.notify(); - dispatch_storage_event(); }); - } - }; + // Listen to internal storage events + let check_key = key.as_ref().to_owned(); + let _ = use_event_listener( + use_window(), + ev::Custom::new(INTERNAL_STORAGE_EVENT), + move |ev: web_sys::CustomEvent| { + if Some(check_key.clone()) == ev.detail().as_string() { + notify.notify() + } + }, + ); + }; - (data.into(), set_data, remove) + // Remove from storage fn + let remove = { + let key = key.as_ref().to_owned(); + move || { + let _ = storage.as_ref().map(|storage| { + // Delete directly from storage + let result = storage + .remove_item(&key) + .map_err(UseStorageError::RemoveItemFailed); + let _ = handle_error(&on_error, result); + notify.notify(); + dispatch_storage_event(); + }); + } + }; + + (data.into(), set_data, remove) + }} } /// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling. From 1a27eb0034dc242d147b60bb6b6c0f7a6f58f778 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 12:20:23 +0000 Subject: [PATCH 27/50] Problem: docs test broken with missing codec constructors --- src/storage/use_storage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 6ac5875..546e2c0 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -152,7 +152,7 @@ where /// let (id, set_id, _) = use_storage_with_options::( /// StorageType::Session, /// "my-id", -/// UseStorageOptions::prost_codec(), +/// UseStorageOptions::default(), /// ); /// # view! { } /// # } From 8168d301e012c67addb3221cdde40fe1252d54f8 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 12:23:33 +0000 Subject: [PATCH 28/50] Update prost --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4de6bbf..9851503 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ lazy_static = "1" leptos = "0.5" num = { version = "0.4", optional = true } paste = "1" -prost = { version = "0.11", optional = true } +prost = { version = "0.12", optional = true } serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } thiserror = "1.0" From 208451f0c2366c07eac542c69aefdf0c6af9f702 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 15:44:20 +0000 Subject: [PATCH 29/50] Update docs to specify that values are synced across multiple use_storage calls on the same page and other tabs / windows --- examples/use_storage/src/main.rs | 2 +- src/storage/use_storage.rs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/use_storage/src/main.rs b/examples/use_storage/src/main.rs index 849f8fa..36abca4 100644 --- a/examples/use_storage/src/main.rs +++ b/examples/use_storage/src/main.rs @@ -71,7 +71,7 @@ fn Demo() -> impl IntoView {

{move || format!("{:#?}", state2.get())}
- "The values are persistent. When you reload the page the values will be the same." + "The values are persistent. When you reload the page or open a second window, the values will be the same." } } diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 546e2c0..dd74213 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -123,12 +123,10 @@ where /// /// ## Usage /// -/// Pass a [`StorageType`] to determine the kind of key-value browser storage to use. 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`].Finally, see [`UseStorageOptions`] to see how behaviour can be further customised. +/// Pass a [`StorageType`] to determine the kind of key-value browser storage to use. 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. /// /// Returns a triplet `(read_signal, write_signal, delete_from_storage_fn)`. /// -/// Signals work as expected and can be used to read and write to storage. The `delete_from_storage_fn` can be called to delete the item from storage. Once deleted the signals will revert back to the default value. -/// /// ## Example /// /// ``` From daab6f944cc0daf41ddf7661688d61bf141ce5ab Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 15:47:52 +0000 Subject: [PATCH 30/50] Remove notice to enable feature storage on use_color_mode --- src/use_color_mode.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index 23d407f..5867bf4 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -13,9 +13,6 @@ use wasm_bindgen::JsCast; /// Reactive color mode (dark / light / customs) with auto data persistence. /// -/// > Data persistence is only enabled when the crate feature **`storage`** is enabled. You -/// can use the function without it but the mode won't be persisted. -/// /// ## Demo /// /// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_color_mode) @@ -55,7 +52,7 @@ use wasm_bindgen::JsCast; /// # /// mode.get(); // ColorMode::Dark or ColorMode::Light /// -/// set_mode.set(ColorMode::Dark); // change to dark mode and persist (with feature `storage`) +/// set_mode.set(ColorMode::Dark); // change to dark mode and persist /// /// set_mode.set(ColorMode::Auto); // change to auto mode /// # From 6e423753b863d2b26f58c397c0aa2b503d18345c Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 15:58:41 +0000 Subject: [PATCH 31/50] Problem: UseColorModeOptions::storage_enabled references storage flag / feature which no longer exists --- src/use_color_mode.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index 5867bf4..ff7031f 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -361,7 +361,7 @@ where /// If the color mode should be persisted. If `true` this required the /// *create feature* **`storage`** to be enabled. - /// Defaults to `true` and is forced to `false` if the feature **`storage`** is not enabled. + /// Defaults to `true`. storage_enabled: bool, /// Emit `auto` mode from state From a28bbb33e4c69b18fe8fcf9267ab0beca6ece3d1 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 12:34:11 +0000 Subject: [PATCH 32/50] Update docs/book for use_storage --- .../storage/{use_storage.md => use_storage_with_options.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename docs/book/src/storage/{use_storage.md => use_storage_with_options.md} (54%) diff --git a/docs/book/src/storage/use_storage.md b/docs/book/src/storage/use_storage_with_options.md similarity index 54% rename from docs/book/src/storage/use_storage.md rename to docs/book/src/storage/use_storage_with_options.md index f937ee4..26c94e7 100644 --- a/docs/book/src/storage/use_storage.md +++ b/docs/book/src/storage/use_storage_with_options.md @@ -1,3 +1,3 @@ -# use_storage +# use_storage_with_options - + From 389b6e681140df6377b9f37c56c6756fecbf70f3 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 4 Nov 2023 13:30:36 +0000 Subject: [PATCH 33/50] Directly use and return read / write signal given by default_value --- src/storage/use_storage.rs | 67 +++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index dd74213..31fa82d 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -189,12 +189,13 @@ where default_value, filter, } = options; - let (default, _) = default_value.into_signal(); + + let (data, set_data) = default_value.into_signal(); + let default = data.get_untracked(); cfg_if! { if #[cfg(feature = "ssr")] { - let (data, set_data) = create_signal(default.get_untracked()); let remove = move || { - set_data.set(default.get_untracked()); + set_data.set(default.clone()); }; (data.into(), set_data, remove) } else { @@ -230,24 +231,14 @@ where } }; - // Fires when storage needs to be updated - let notify = create_trigger(); - - // Keeps track of how many times we've been notified. Does not increment for calls to set_data - let notify_id = create_memo::(move |prev| { - notify.track(); - prev.map(|prev| prev + 1).unwrap_or_default() - }); - - // Fetch from storage and falls back to the default (possibly a signal) if deleted - let fetcher = { + // Fetches direct from browser storage and fills set_data if changed (memo) + let fetch_from_storage = { let storage = storage.to_owned(); let codec = codec.to_owned(); let key = key.as_ref().to_owned(); let on_error = on_error.to_owned(); - create_memo(move |_| { - notify.track(); - storage + move || { + let fetched = storage .to_owned() .and_then(|storage| { // Get directly from storage @@ -265,17 +256,41 @@ where handle_error(&on_error, result) }) .transpose() - .unwrap_or_default() // Drop handled Err(()) - // Fallback to default - .unwrap_or_else(move || default.get()) - }) + .unwrap_or_default(); // Drop handled Err(()) + + match fetched { + Some(value) => { + // Replace data if changed + if value != data.get_untracked() { + set_data.set(value) + } + } + + // Revert to default + None => set_data.set(default.clone()), + }; + } }; - // Create mutable data signal from our fetcher - let (data, set_data) = MaybeRwSignal::::from(fetcher).into_signal(); - let data = create_memo(move |_| data.get()); + // Fetch initial value + fetch_from_storage(); - // Set storage value on data change + // Fires when storage needs to be fetched + let notify = create_trigger(); + + // Refetch from storage. Keeps track of how many times we've been notified. Does not increment for calls to set_data + let notify_id = create_memo::(move |prev| { + notify.track(); + match prev { + None => 1, // Avoid async fetch of initial value + Some(prev) => { + fetch_from_storage(); + prev + 1 + } + } + }); + + // Set item on internal (non-event) page changes to the data signal { let storage = storage.to_owned(); let codec = codec.to_owned(); @@ -310,7 +325,7 @@ where }, WatchOptions::default().filter(filter), ); - }; + } if listen_to_storage_changes { let check_key = key.as_ref().to_owned(); From 16ad1417cfc92ff5656067274b8cb555dad61d98 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 4 Nov 2023 13:33:02 +0000 Subject: [PATCH 34/50] Add link to open new window for use_storage example --- examples/use_storage/src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/use_storage/src/main.rs b/examples/use_storage/src/main.rs index 36abca4..c1a98fa 100644 --- a/examples/use_storage/src/main.rs +++ b/examples/use_storage/src/main.rs @@ -71,7 +71,9 @@ fn Demo() -> impl IntoView {
{move || format!("{:#?}", state2.get())}
- "The values are persistent. When you reload the page or open a second window, the values will be the same." + "The values are persistent. When you reload the page or " + "open a second window" + ", the values will be the same." } } From af653595a5224d1b4ea7db10ee9af70d5c3a194e Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 4 Nov 2023 13:36:01 +0000 Subject: [PATCH 35/50] Problem: use_storage examples have unused imports --- src/storage/codec_json.rs | 2 +- src/storage/codec_prost.rs | 3 +-- src/storage/codec_string.rs | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs index 30cd03a..7e70e02 100644 --- a/src/storage/codec_json.rs +++ b/src/storage/codec_json.rs @@ -5,7 +5,7 @@ use super::Codec; /// ## Example /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, JsonCodec}; /// # use serde::{Deserialize, Serialize}; /// # /// # pub fn Demo() -> impl IntoView { diff --git a/src/storage/codec_prost.rs b/src/storage/codec_prost.rs index 41a55d9..28986b2 100644 --- a/src/storage/codec_prost.rs +++ b/src/storage/codec_prost.rs @@ -11,8 +11,7 @@ use thiserror::Error; /// ## Example /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; -/// # use serde::{Deserialize, Serialize}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, ProstCodec}; /// # /// # pub fn Demo() -> impl IntoView { /// // Primitive types: diff --git a/src/storage/codec_string.rs b/src/storage/codec_string.rs index 91ac731..2c16f34 100644 --- a/src/storage/codec_string.rs +++ b/src/storage/codec_string.rs @@ -8,8 +8,7 @@ use std::str::FromStr; /// ## Example /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; -/// # use serde::{Deserialize, Serialize}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec}; /// # /// # pub fn Demo() -> impl IntoView { /// let (get, set, remove) = use_local_storage::("my-key"); From 9a9f1ba7d92d973481d459fbde26380e67106c0e Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 4 Nov 2023 13:50:41 +0000 Subject: [PATCH 36/50] Problem: use_storage's default_value with a signal might suggest changing the signal also changes the value used on an unset storage value. Rename to initial value and document this behaviour --- src/storage/use_storage.rs | 14 +++++++------- src/use_color_mode.rs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 31fa82d..b1dbd24 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -27,7 +27,7 @@ pub struct UseStorageOptions> { codec: C, on_error: Rc)>, listen_to_storage_changes: bool, - default_value: MaybeRwSignal, + initial_value: MaybeRwSignal, filter: FilterOptions, } @@ -186,11 +186,11 @@ where codec, on_error, listen_to_storage_changes, - default_value, + initial_value, filter, } = options; - let (data, set_data) = default_value.into_signal(); + let (data, set_data) = initial_value.into_signal(); let default = data.get_untracked(); cfg_if! { if #[cfg(feature = "ssr")] { @@ -384,7 +384,7 @@ impl + Default> Default for UseStorageOptions { codec: C::default(), on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, - default_value: MaybeRwSignal::default(), + initial_value: MaybeRwSignal::default(), filter: FilterOptions::default(), } } @@ -415,10 +415,10 @@ impl> UseStorageOptions { } } - /// Default value to use when the storage key is not set. Accepts a signal. - pub fn default_value(self, values: impl Into>) -> 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 { - default_value: values.into(), + initial_value: initial.into(), ..self } } diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index ff7031f..5f2b781 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -266,7 +266,7 @@ fn get_store_signal( storage_key, UseStorageOptions::default() .listen_to_storage_changes(listen_to_storage_changes) - .default_value(initial_value), + .initial_value(initial_value), ); (store, set_store) } else { From 7c3ebf09f4e5f65c1e22638736d1eae3120c0f90 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 4 Nov 2023 15:43:36 +0000 Subject: [PATCH 37/50] Add overview of codec versioning --- src/storage/use_storage.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index b1dbd24..f640d25 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -12,7 +12,21 @@ use wasm_bindgen::JsValue; const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; -/// A codec for encoding and decoding values to and from UTF-16 strings. These strings are then stored in browser storage. +/// 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; @@ -125,6 +139,8 @@ where /// /// Pass a [`StorageType`] to determine the kind of key-value browser storage to use. 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 [`Codec`] for more details on how to handle versioning--dealing with data that can outlast your code. +/// /// Returns a triplet `(read_signal, write_signal, delete_from_storage_fn)`. /// /// ## Example From 808e97c072c990570245f9f243ce0f2993702b4a Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 4 Nov 2023 15:44:09 +0000 Subject: [PATCH 38/50] Add JSON versioning docs + examples --- src/storage/codec_json.rs | 92 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs index 7e70e02..c12d3a7 100644 --- a/src/storage/codec_json.rs +++ b/src/storage/codec_json.rs @@ -21,6 +21,98 @@ use super::Codec; /// # 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_with_options, UseStorageOptions, Codec, JsonCodec}; +/// # use serde::{Deserialize, Serialize}; +/// # +/// # 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 Codec 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_with_options, UseStorageOptions, Codec, JsonCodec}; +/// # use serde::{Deserialize, Serialize}; +/// # use serde_json::json; +/// # +/// # 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 Codec 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(Clone, Default, PartialEq)] pub struct JsonCodec(); From a65e4ddaa1b80be3d06176dd24bb2bb242230337 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 4 Nov 2023 16:33:43 +0000 Subject: [PATCH 39/50] Problem: mdbook refers to use_storage from older API --- docs/book/src/SUMMARY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 4a9c0c5..8a56599 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -10,7 +10,7 @@ - [use_local_storage](storage/use_local_storage.md) - [use_session_storage](storage/use_session_storage.md) -- [use_storage](storage/use_storage.md) +- [use_storage_with_options](storage/use_storage_with_options.md) # Elements From 5b056e8d19744a82659d7e8fb4946c8764092e2b Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 10:43:32 +0000 Subject: [PATCH 40/50] Problem: CI references storage feature. Replace with serde,prost --- .fleet/run.json | 6 +++--- .github/workflows/ci.yml | 4 ++-- .github/workflows/tests.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.fleet/run.json b/.fleet/run.json index 4e4ffb5..a849842 100644 --- a/.fleet/run.json +++ b/.fleet/run.json @@ -3,14 +3,14 @@ { "type": "cargo", "name": "Tests", - "cargoArgs": ["test", "--features", "math,storage,docs"], + "cargoArgs": ["test", "--features", "math,prost,serde,docs"], }, { "type": "cargo", "name": "Clippy", - "cargoArgs": ["+nightly", "clippy", "--features", "math,storage,docs", "--tests", "--", "-D", "warnings"], + "cargoArgs": ["+nightly", "clippy", "--features", "math,prost,serde,docs", "--tests", "--", "-D", "warnings"], "workingDir": "./", }, ] -} \ No newline at end of file +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d883bd8..6ad089d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: - name: Check formatting run: cargo fmt --check - name: Clippy - run: cargo clippy --features storage,docs,math --tests -- -D warnings + run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings - name: Run tests run: cargo test --all-features @@ -110,4 +110,4 @@ jobs: # - name: Publish to Coveralls # uses: coverallsapp/github-action@master # with: -# github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file +# github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 70a765e..f5500fb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,6 +23,6 @@ jobs: - name: Check formatting run: cargo fmt --check - name: Clippy - run: cargo clippy --features storage,docs,math --tests -- -D warnings + run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings - name: Run tests run: cargo test --all-features From 2976d4c03374dbab023d311101af8967710e7ac3 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 10:51:23 +0000 Subject: [PATCH 41/50] Fix clippy lints --- 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 f640d25..466a38a 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -73,7 +73,7 @@ pub enum UseStorageError { /// See [`use_storage_with_options`] for more details on how to use. pub fn use_local_storage( key: impl AsRef, -) -> (Signal, WriteSignal, impl Fn() -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + Default + PartialEq, C: Codec + Default, @@ -89,7 +89,7 @@ where pub fn use_local_storage_with_options( key: impl AsRef, options: UseStorageOptions, -) -> (Signal, WriteSignal, impl Fn() -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + PartialEq, C: Codec, @@ -106,7 +106,7 @@ where /// See [`use_storage_with_options`] for more details on how to use. pub fn use_session_storage( key: impl AsRef, -) -> (Signal, WriteSignal, impl Fn() -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + Default + PartialEq, C: Codec + Default, @@ -122,7 +122,7 @@ where pub fn use_session_storage_with_options( key: impl AsRef, options: UseStorageOptions, -) -> (Signal, WriteSignal, impl Fn() -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + PartialEq, C: Codec, @@ -193,7 +193,7 @@ pub fn use_storage_with_options( storage_type: StorageType, key: impl AsRef, options: UseStorageOptions, -) -> (Signal, WriteSignal, impl Fn() -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + PartialEq, C: Codec, @@ -382,7 +382,7 @@ where } }; - (data.into(), set_data, remove) + (data, set_data, remove) }} } @@ -391,7 +391,7 @@ fn handle_error( on_error: &Rc)>, result: Result>, ) -> Result { - result.or_else(|err| Err((on_error)(err))) + result.map_err(|err| (on_error)(err)) } impl + Default> Default for UseStorageOptions { From 93556a3f8f2fd86b8996c3633c659c842184ed73 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 10:56:46 +0000 Subject: [PATCH 42/50] Fix clippy lint. Remove needless borrow --- src/storage/codec_prost.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/codec_prost.rs b/src/storage/codec_prost.rs index 28986b2..1c90ec8 100644 --- a/src/storage/codec_prost.rs +++ b/src/storage/codec_prost.rs @@ -45,7 +45,7 @@ impl Codec for ProstCodec { fn encode(&self, val: &T) -> Result { let buf = val.encode_to_vec(); - Ok(base64::engine::general_purpose::STANDARD.encode(&buf)) + Ok(base64::engine::general_purpose::STANDARD.encode(buf)) } fn decode(&self, str: String) -> Result { From 94841e4bb5b93a81f2f83b05577ad6316c831073 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 11:05:29 +0000 Subject: [PATCH 43/50] Document fields on UseStorageOptions --- src/storage/use_storage.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 466a38a..6455c4b 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -38,10 +38,15 @@ pub trait Codec: Clone + 'static { /// 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, + // Callback for when an error occurs 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 initial_value: MaybeRwSignal, + // Debounce or throttle the writing to storage whenever the value changes filter: FilterOptions, } From cf33e317a663698672b02ad742ceb7bcd9af1ce8 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 12:56:32 +0000 Subject: [PATCH 44/50] Rename use_storage.rs to align with mdbook reference --- src/storage/{use_storage.rs => use_storage_with_options.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/storage/{use_storage.rs => use_storage_with_options.rs} (100%) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage_with_options.rs similarity index 100% rename from src/storage/use_storage.rs rename to src/storage/use_storage_with_options.rs From 99803a9bcb779405915939d4799d0947e087c45c Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 13:15:13 +0000 Subject: [PATCH 45/50] Organise storage docs for mdbook --- src/storage/mod.rs | 8 +- src/storage/use_local_storage.rs | 36 +++++ src/storage/use_session_storage.rs | 36 +++++ src/storage/use_storage_with_options.rs | 180 ++++++++---------------- 4 files changed, 135 insertions(+), 125 deletions(-) create mode 100644 src/storage/use_local_storage.rs create mode 100644 src/storage/use_session_storage.rs diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 7183c63..46da6cb 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -3,7 +3,9 @@ mod codec_json; #[cfg(feature = "prost")] mod codec_prost; mod codec_string; -mod use_storage; +mod use_local_storage; +mod use_session_storage; +mod use_storage_with_options; pub use crate::core::StorageType; #[cfg(feature = "serde")] @@ -11,4 +13,6 @@ pub use codec_json::*; #[cfg(feature = "prost")] pub use codec_prost::*; pub use codec_string::*; -pub use use_storage::*; +pub use use_local_storage::*; +pub use use_session_storage::*; +pub use use_storage_with_options::*; diff --git a/src/storage/use_local_storage.rs b/src/storage/use_local_storage.rs new file mode 100644 index 0000000..44725ce --- /dev/null +++ b/src/storage/use_local_storage.rs @@ -0,0 +1,36 @@ +use super::{use_storage_with_options, Codec, StorageType, UseStorageOptions}; +use leptos::signal_prelude::*; + +/// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). +/// +/// LocalStorage stores data in the browser with no expiration time. Access is given to all pages from the same origin (e.g., all pages from "https://example.com" share the same origin). While data doesn't expire the user can view, modify and delete all data stored. Browsers allow 5MB of data to be stored. +/// +/// This is contrast to [`use_session_storage`] which clears data when the page session ends and is not shared. +/// +/// ## Usage +/// See [`use_storage_with_options`] for more details on how to use. +pub fn use_local_storage( + key: impl AsRef, +) -> (Signal, WriteSignal, impl Fn() + Clone) +where + T: Clone + Default + PartialEq, + C: Codec + Default, +{ + use_storage_with_options( + StorageType::Local, + key, + UseStorageOptions::::default(), + ) +} + +/// Accepts [`UseStorageOptions`]. See [`use_local_storage`] for details. +pub fn use_local_storage_with_options( + key: impl AsRef, + options: UseStorageOptions, +) -> (Signal, WriteSignal, impl Fn() + Clone) +where + T: Clone + PartialEq, + C: Codec, +{ + use_storage_with_options(StorageType::Local, key, options) +} diff --git a/src/storage/use_session_storage.rs b/src/storage/use_session_storage.rs new file mode 100644 index 0000000..1d7286d --- /dev/null +++ b/src/storage/use_session_storage.rs @@ -0,0 +1,36 @@ +use super::{use_storage_with_options, Codec, StorageType, UseStorageOptions}; +use leptos::signal_prelude::*; + +/// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). +/// +/// SessionStorages stores data in the browser that is deleted when the page session ends. A page session ends when the browser closes the tab. Data is not shared between pages. While data doesn't expire the user can view, modify and delete all data stored. Browsers allow 5MB of data to be stored. +/// +/// Use [`use_local_storage`] to store data that is shared amongst all pages with the same origin and persists between page sessions. +/// +/// ## Usage +/// See [`use_storage_with_options`] for more details on how to use. +pub fn use_session_storage( + key: impl AsRef, +) -> (Signal, WriteSignal, impl Fn() + Clone) +where + T: Clone + Default + PartialEq, + C: Codec + Default, +{ + use_storage_with_options( + StorageType::Session, + key, + UseStorageOptions::::default(), + ) +} + +/// Accepts [`UseStorageOptions`]. See [`use_session_storage`] for details. +pub fn use_session_storage_with_options( + key: impl AsRef, + options: UseStorageOptions, +) -> (Signal, WriteSignal, impl Fn() + Clone) +where + T: Clone + PartialEq, + C: Codec, +{ + use_storage_with_options(StorageType::Session, key, options) +} diff --git a/src/storage/use_storage_with_options.rs b/src/storage/use_storage_with_options.rs index 6455c4b..076f36d 100644 --- a/src/storage/use_storage_with_options.rs +++ b/src/storage/use_storage_with_options.rs @@ -12,129 +12,6 @@ use wasm_bindgen::JsValue; const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; -/// 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; -} - -/// 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, - // Callback for when an error occurs - 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 - initial_value: MaybeRwSignal, - // Debounce or throttle the writing to storage whenever the value changes - filter: FilterOptions, -} - -/// Session handling errors returned by [`use_storage_with_options`]. -#[derive(Error, Debug)] -pub enum UseStorageError { - #[error("storage not available")] - StorageNotAvailable(JsValue), - #[error("storage not returned from window")] - StorageReturnedNone, - #[error("failed to get item")] - GetItemFailed(JsValue), - #[error("failed to set item")] - SetItemFailed(JsValue), - #[error("failed to delete item")] - RemoveItemFailed(JsValue), - #[error("failed to notify item changed")] - NotifyItemChangedFailed(JsValue), - #[error("failed to encode / decode item value")] - ItemCodecError(Err), -} - -/// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). -/// -/// LocalStorage stores data in the browser with no expiration time. Access is given to all pages from the same origin (e.g., all pages from "https://example.com" share the same origin). While data doesn't expire the user can view, modify and delete all data stored. Browsers allow 5MB of data to be stored. -/// -/// This is contrast to [`use_session_storage`] which clears data when the page session ends and is not shared. -/// -/// See [`use_storage_with_options`] for more details on how to use. -pub fn use_local_storage( - key: impl AsRef, -) -> (Signal, WriteSignal, impl Fn() + Clone) -where - T: Clone + Default + PartialEq, - C: Codec + Default, -{ - use_storage_with_options( - StorageType::Local, - key, - UseStorageOptions::::default(), - ) -} - -/// Accepts [`UseStorageOptions`]. See [`use_local_storage`] for details. -pub fn use_local_storage_with_options( - key: impl AsRef, - options: UseStorageOptions, -) -> (Signal, WriteSignal, impl Fn() + Clone) -where - T: Clone + PartialEq, - C: Codec, -{ - use_storage_with_options(StorageType::Local, key, options) -} - -/// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). -/// -/// SessionStorages stores data in the browser that is deleted when the page session ends. A page session ends when the browser closes the tab. Data is not shared between pages. While data doesn't expire the user can view, modify and delete all data stored. Browsers allow 5MB of data to be stored. -/// -/// Use [`use_local_storage`] to store data that is shared amongst all pages with the same origin and persists between page sessions. -/// -/// See [`use_storage_with_options`] for more details on how to use. -pub fn use_session_storage( - key: impl AsRef, -) -> (Signal, WriteSignal, impl Fn() + Clone) -where - T: Clone + Default + PartialEq, - C: Codec + Default, -{ - use_storage_with_options( - StorageType::Session, - key, - UseStorageOptions::::default(), - ) -} - -/// Accepts [`UseStorageOptions`]. See [`use_session_storage`] for details. -pub fn use_session_storage_with_options( - key: impl AsRef, - options: UseStorageOptions, -) -> (Signal, WriteSignal, impl Fn() + Clone) -where - T: Clone + PartialEq, - C: Codec, -{ - use_storage_with_options(StorageType::Session, key, options) -} - /// Reactive [Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). /// /// * [See a demo](https://leptos-use.rs/storage/use_storage.html) @@ -391,6 +268,63 @@ where }} } +/// Session handling errors returned by [`use_storage_with_options`]. +#[derive(Error, Debug)] +pub enum UseStorageError { + #[error("storage not available")] + StorageNotAvailable(JsValue), + #[error("storage not returned from window")] + StorageReturnedNone, + #[error("failed to get item")] + GetItemFailed(JsValue), + #[error("failed to set item")] + SetItemFailed(JsValue), + #[error("failed to delete item")] + RemoveItemFailed(JsValue), + #[error("failed to notify item changed")] + NotifyItemChangedFailed(JsValue), + #[error("failed to encode / decode item value")] + ItemCodecError(Err), +} + +/// 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, + // Callback for when an error occurs + 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 + initial_value: MaybeRwSignal, + // Debounce or throttle the writing to storage whenever the value changes + 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. fn handle_error( on_error: &Rc)>, From 9306e0299c85e864b9870991c4a79d739483f50c Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 13:15:34 +0000 Subject: [PATCH 46/50] Remove storage flag from mdbook --- docs/book/src/storage/use_local_storage.md | 2 +- docs/book/src/storage/use_session_storage.md | 2 +- docs/book/src/storage/use_storage_with_options.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/book/src/storage/use_local_storage.md b/docs/book/src/storage/use_local_storage.md index d39b415..3256f85 100644 --- a/docs/book/src/storage/use_local_storage.md +++ b/docs/book/src/storage/use_local_storage.md @@ -1,3 +1,3 @@ # use_local_storage - + diff --git a/docs/book/src/storage/use_session_storage.md b/docs/book/src/storage/use_session_storage.md index f04e6e5..03ddbab 100644 --- a/docs/book/src/storage/use_session_storage.md +++ b/docs/book/src/storage/use_session_storage.md @@ -1,3 +1,3 @@ # use_session_storage - + diff --git a/docs/book/src/storage/use_storage_with_options.md b/docs/book/src/storage/use_storage_with_options.md index 26c94e7..3fe134f 100644 --- a/docs/book/src/storage/use_storage_with_options.md +++ b/docs/book/src/storage/use_storage_with_options.md @@ -1,3 +1,3 @@ # use_storage_with_options - + From e8c6540369349c7d5b6b2d824071921934b865b5 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 13:16:20 +0000 Subject: [PATCH 47/50] Remove @ prefix on mdbook storage reference --- docs/book/src/SUMMARY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 8a56599..016228b 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -6,7 +6,7 @@ [Changelog](changelog.md) [Functions](functions.md) -# @Storage +# Storage - [use_local_storage](storage/use_local_storage.md) - [use_session_storage](storage/use_session_storage.md) From b26459986edb4cded3c61b156b255310da3203c7 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Sun, 12 Nov 2023 22:50:59 +0000 Subject: [PATCH 48/50] brought back use_storage --- src/storage/codec_json.rs | 6 +++--- src/storage/codec_prost.rs | 2 +- src/storage/codec_string.rs | 2 +- src/storage/mod.rs | 4 ++-- src/storage/use_local_storage.rs | 4 ++-- src/storage/use_session_storage.rs | 4 ++-- ...use_storage_with_options.rs => use_storage.rs} | 15 ++++++++++++++- src/use_color_mode.rs | 2 +- 8 files changed, 26 insertions(+), 13 deletions(-) rename src/storage/{use_storage_with_options.rs => use_storage.rs} (97%) diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs index c12d3a7..5a834a1 100644 --- a/src/storage/codec_json.rs +++ b/src/storage/codec_json.rs @@ -5,7 +5,7 @@ use super::Codec; /// ## Example /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, JsonCodec}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, JsonCodec}; /// # use serde::{Deserialize, Serialize}; /// # /// # pub fn Demo() -> impl IntoView { @@ -34,7 +34,7 @@ use super::Codec; /// /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, Codec, JsonCodec}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, Codec, JsonCodec}; /// # use serde::{Deserialize, Serialize}; /// # /// # pub fn Demo() -> impl IntoView { @@ -75,7 +75,7 @@ use super::Codec; /// /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, Codec, JsonCodec}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, Codec, JsonCodec}; /// # use serde::{Deserialize, Serialize}; /// # use serde_json::json; /// # diff --git a/src/storage/codec_prost.rs b/src/storage/codec_prost.rs index 1c90ec8..e833410 100644 --- a/src/storage/codec_prost.rs +++ b/src/storage/codec_prost.rs @@ -11,7 +11,7 @@ use thiserror::Error; /// ## Example /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, ProstCodec}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, ProstCodec}; /// # /// # pub fn Demo() -> impl IntoView { /// // Primitive types: diff --git a/src/storage/codec_string.rs b/src/storage/codec_string.rs index 2c16f34..1cae942 100644 --- a/src/storage/codec_string.rs +++ b/src/storage/codec_string.rs @@ -8,7 +8,7 @@ use std::str::FromStr; /// ## Example /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, StringCodec}; /// # /// # pub fn Demo() -> impl IntoView { /// let (get, set, remove) = use_local_storage::("my-key"); diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 46da6cb..c5cd494 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -5,7 +5,7 @@ mod codec_prost; mod codec_string; mod use_local_storage; mod use_session_storage; -mod use_storage_with_options; +mod use_storage; pub use crate::core::StorageType; #[cfg(feature = "serde")] @@ -15,4 +15,4 @@ pub use codec_prost::*; pub use codec_string::*; pub use use_local_storage::*; pub use use_session_storage::*; -pub use use_storage_with_options::*; +pub use use_storage::*; diff --git a/src/storage/use_local_storage.rs b/src/storage/use_local_storage.rs index 44725ce..73d864d 100644 --- a/src/storage/use_local_storage.rs +++ b/src/storage/use_local_storage.rs @@ -1,4 +1,4 @@ -use super::{use_storage_with_options, Codec, StorageType, UseStorageOptions}; +use super::{use_storage, Codec, StorageType, UseStorageOptions}; use leptos::signal_prelude::*; /// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). @@ -8,7 +8,7 @@ use leptos::signal_prelude::*; /// This is contrast to [`use_session_storage`] which clears data when the page session ends and is not shared. /// /// ## Usage -/// See [`use_storage_with_options`] for more details on how to use. +/// See [`use_storage`] for more details on how to use. pub fn use_local_storage( key: impl AsRef, ) -> (Signal, WriteSignal, impl Fn() + Clone) diff --git a/src/storage/use_session_storage.rs b/src/storage/use_session_storage.rs index 1d7286d..ae6e1eb 100644 --- a/src/storage/use_session_storage.rs +++ b/src/storage/use_session_storage.rs @@ -1,4 +1,4 @@ -use super::{use_storage_with_options, Codec, StorageType, UseStorageOptions}; +use super::{use_storage, Codec, StorageType, UseStorageOptions}; use leptos::signal_prelude::*; /// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). @@ -8,7 +8,7 @@ use leptos::signal_prelude::*; /// Use [`use_local_storage`] to store data that is shared amongst all pages with the same origin and persists between page sessions. /// /// ## Usage -/// See [`use_storage_with_options`] for more details on how to use. +/// See [`use_storage`] for more details on how to use. pub fn use_session_storage( key: impl AsRef, ) -> (Signal, WriteSignal, impl Fn() + Clone) diff --git a/src/storage/use_storage_with_options.rs b/src/storage/use_storage.rs similarity index 97% rename from src/storage/use_storage_with_options.rs rename to src/storage/use_storage.rs index 076f36d..6456f9f 100644 --- a/src/storage/use_storage_with_options.rs +++ b/src/storage/use_storage.rs @@ -29,7 +29,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; /// # use serde::{Deserialize, Serialize}; /// # /// # pub fn Demo() -> impl IntoView { @@ -71,6 +71,19 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// } /// } /// ``` +#[inline(always)] +pub fn use_storage( + storage_type: StorageType, + key: impl AsRef, +) -> (Signal, WriteSignal, impl Fn() + Clone) +where + T: Clone + PartialEq, + C: Codec, +{ + use_storage_with_options(storage_type, key, UseStorageOptions::default()) +} + +/// Version of [`use_storage`] that accepts [`UseStorageOptions`]. pub fn use_storage_with_options( storage_type: StorageType, key: impl AsRef, diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index 5f2b781..a9dd6fb 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -1,5 +1,5 @@ use crate::core::{ElementMaybeSignal, MaybeRwSignal}; -use crate::storage::{use_storage_with_options, StringCodec, UseStorageOptions}; +use crate::storage::{use_storage, StringCodec, UseStorageOptions}; use std::fmt::{Display, Formatter}; use std::str::FromStr; From 8199d19e924911c76868906db725df816be1668b Mon Sep 17 00:00:00 2001 From: Maccesch Date: Sun, 12 Nov 2023 23:02:36 +0000 Subject: [PATCH 49/50] brought back use_storage in book --- docs/book/src/SUMMARY.md | 2 +- .../storage/{use_storage_with_options.md => use_storage.md} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename docs/book/src/storage/{use_storage_with_options.md => use_storage.md} (58%) diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 016228b..35658f4 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -10,7 +10,7 @@ - [use_local_storage](storage/use_local_storage.md) - [use_session_storage](storage/use_session_storage.md) -- [use_storage_with_options](storage/use_storage_with_options.md) +- [use_storage](storage/use_storage.md) # Elements diff --git a/docs/book/src/storage/use_storage_with_options.md b/docs/book/src/storage/use_storage.md similarity index 58% rename from docs/book/src/storage/use_storage_with_options.md rename to docs/book/src/storage/use_storage.md index 3fe134f..316df04 100644 --- a/docs/book/src/storage/use_storage_with_options.md +++ b/docs/book/src/storage/use_storage.md @@ -1,3 +1,3 @@ -# use_storage_with_options +# use_storage - + From 796ea23ddfaa78e82b5e4cdc401b401b7e295cc5 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Sun, 12 Nov 2023 23:07:10 +0000 Subject: [PATCH 50/50] restored use_storage demo --- src/storage/use_storage.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 6456f9f..bce2f10 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -14,8 +14,9 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// Reactive [Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). /// -/// * [See a demo](https://leptos-use.rs/storage/use_storage.html) -/// * [See a full example](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_storage) +/// ## Demo +/// +/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_storage) /// /// ## Usage ///