From 03f8b19c88439dc1088f3c6c366bdeb07a0f3820 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sun, 15 Jun 2025 00:08:23 -0700 Subject: [PATCH 01/37] Working human readable storage example --- examples/storage/Cargo.toml | 2 + examples/storage/src/main.rs | 54 ++++++++++-- packages/storage/src/client_storage/fs.rs | 56 ++++++++----- packages/storage/src/client_storage/web.rs | 53 +++++++----- packages/storage/src/lib.rs | 98 ++++++++++++++++++++++ 5 files changed, 212 insertions(+), 51 deletions(-) diff --git a/examples/storage/Cargo.toml b/examples/storage/Cargo.toml index 8e9fc91..08ff9d7 100644 --- a/examples/storage/Cargo.toml +++ b/examples/storage/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" [dependencies] dioxus = { workspace = true, features = ["router"] } dioxus_storage = { workspace = true } +serde = "1.0.219" +serde_json = "1.0.140" [features] default = ["desktop"] diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index de1af7a..7b73e34 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -1,6 +1,8 @@ use dioxus::prelude::*; use dioxus_storage::*; +use serde::{de::DeserializeOwned, Serialize}; + fn main() { dioxus_storage::set_dir!(); launch(App); @@ -51,18 +53,20 @@ fn Footer() -> Element { rsx! { div { - Outlet:: { } + Outlet:: {} - p { - "----" - } + p { "----" } {new_window} nav { ul { - li { Link { to: Route::Home {}, "Home" } } - li { Link { to: Route::Storage {}, "Storage" } } + li { + Link { to: Route::Home {}, "Home" } + } + li { + Link { to: Route::Storage {}, "Storage" } + } } } } @@ -79,6 +83,11 @@ fn Storage() -> Element { let mut count_session = use_singleton_persistent(|| 0); let mut count_local = use_synced_storage::("synced".to_string(), || 0); + let mut count_local_human = use_synced_storage::, i32>( + "synced_human".to_string(), + || 0, + ); + rsx!( div { button { @@ -86,7 +95,7 @@ fn Storage() -> Element { *count_session.write() += 1; }, "Click me!" - }, + } "I persist for the current session. Clicked {count_session} times" } div { @@ -95,8 +104,37 @@ fn Storage() -> Element { *count_local.write() += 1; }, "Click me!" - }, + } "I persist across all sessions. Clicked {count_local} times" } + div { + button { + onclick: move |_| { + *count_local_human.write() += 1; + }, + "Click me!" + } + "I persist a human readable value across all sessions. Clicked {count_local_human} times" + } ) } + +// Define a "human readable" storage format which is pretty printed JSON instead of a compressed binary format. +type HumanReadableStorage = LayeredStorage; + +#[derive(Clone)] +struct HumanReadableEncoding; + +impl StorageEncoder for HumanReadableEncoding { + type Value = String; + + fn deserialize(loaded: &Self::Value) -> T { + let parsed: Result = serde_json::from_str(loaded); + // This design probably needs an error handling policy better than panic. + parsed.unwrap() + } + + fn serialize(value: &T) -> Self::Value { + serde_json::to_string_pretty(value).unwrap() + } +} diff --git a/packages/storage/src/client_storage/fs.rs b/packages/storage/src/client_storage/fs.rs index f7fd110..a1984bc 100644 --- a/packages/storage/src/client_storage/fs.rs +++ b/packages/storage/src/client_storage/fs.rs @@ -1,13 +1,12 @@ use crate::{StorageChannelPayload, StorageSubscription}; use dioxus::logger::tracing::trace; -use serde::Serialize; use serde::de::DeserializeOwned; use std::collections::HashMap; use std::io::Write; use std::sync::{OnceLock, RwLock}; use tokio::sync::watch::{Receiver, channel}; -use crate::{StorageBacking, StorageSubscriber, serde_to_string, try_serde_from_string}; +use crate::{StorageBacking, StoragePersistence, StorageSubscriber}; #[doc(hidden)] /// Sets the directory where the storage files are located. @@ -29,53 +28,64 @@ pub fn set_dir_name(name: &str) { static LOCATION: OnceLock = OnceLock::new(); /// Set a value in the configured storage location using the key as the file name. -fn set(key: String, value: &T) { - let as_str = serde_to_string(value); +fn set(key: &str, as_str: &Option) { let path = LOCATION .get() - .expect("Call the set_dir macro before accessing persistant data"); - std::fs::create_dir_all(path).unwrap(); + .expect("Call the set_dir macro before accessing persistent data"); + let file_path = path.join(key); - let mut file = std::fs::File::create(file_path).unwrap(); - file.write_all(as_str.as_bytes()).unwrap(); + + match as_str { + Some(as_str) => { + std::fs::create_dir_all(path).unwrap(); + let mut file = std::fs::File::create(file_path).unwrap(); + file.write_all(as_str.as_bytes()).unwrap() + } + None => match std::fs::remove_file(file_path) { + Ok(_) => {} + Err(error) => match error.kind() { + std::io::ErrorKind::NotFound => {} + _ => Result::Err(error).unwrap(), + }, + }, + } } /// Get a value from the configured storage location using the key as the file name. -fn get(key: &str) -> Option { +fn get(key: &str) -> Option { let path = LOCATION .get() - .expect("Call the set_dir macro before accessing persistant data") + .expect("Call the set_dir macro before accessing persistent data") .join(key); - let s = std::fs::read_to_string(path).ok()?; - try_serde_from_string(&s) + std::fs::read_to_string(path).ok() } #[derive(Clone)] pub struct LocalStorage; -impl StorageBacking for LocalStorage { +/// LocalStorage stores Option. +impl StoragePersistence for LocalStorage { type Key = String; + type Value = Option; + + fn load(key: &Self::Key) -> Self::Value { + get(key) + } - fn set(key: String, value: &T) { - let key_clone = key.clone(); - let value_clone = (*value).clone(); - set(key, value); + fn store(key: Self::Key, value: &Self::Value) { + set(&key, value); // If the subscriptions map is not initialized, we don't need to notify any subscribers. if let Some(subscriptions) = SUBSCRIPTIONS.get() { let read_binding = subscriptions.read().unwrap(); - if let Some(subscription) = read_binding.get(&key_clone) { + if let Some(subscription) = read_binding.get(&key) { subscription .tx - .send(StorageChannelPayload::new(value_clone)) + .send(StorageChannelPayload::new(value.clone())) .unwrap(); } } } - - fn get(key: &String) -> Option { - get(key) - } } // Note that this module contains an optimization that differs from the web version. Dioxus Desktop runs all windows in diff --git a/packages/storage/src/client_storage/web.rs b/packages/storage/src/client_storage/web.rs index b0b224b..37658c6 100644 --- a/packages/storage/src/client_storage/web.rs +++ b/packages/storage/src/client_storage/web.rs @@ -5,29 +5,28 @@ use std::{ use dioxus::logger::tracing::{error, trace}; use once_cell::sync::Lazy; -use serde::{Serialize, de::DeserializeOwned}; +use serde::de::DeserializeOwned; use tokio::sync::watch::{Receiver, channel}; use wasm_bindgen::JsCast; use wasm_bindgen::prelude::Closure; use web_sys::{Storage, window}; -use crate::{ - StorageBacking, StorageChannelPayload, StorageSubscriber, StorageSubscription, serde_to_string, - try_serde_from_string, -}; +use crate::{StorageChannelPayload, StoragePersistence, StorageSubscriber, StorageSubscription}; #[derive(Clone)] pub struct LocalStorage; -impl StorageBacking for LocalStorage { +/// LocalStorage stores Option. +impl StoragePersistence for LocalStorage { type Key = String; + type Value = Option; - fn set(key: String, value: &T) { - set(key, value, WebStorageType::Local); + fn load(key: &Self::Key) -> Self::Value { + get(key, WebStorageType::Local) } - fn get(key: &String) -> Option { - get(key, WebStorageType::Local) + fn store(key: Self::Key, value: &Self::Value) { + set_or_clear(key, value.as_deref(), WebStorageType::Local) } } @@ -96,29 +95,43 @@ static SUBSCRIPTIONS: Lazy>>> = #[derive(Clone)] pub struct SessionStorage; -impl StorageBacking for SessionStorage { +/// LocalStorage stores Option. +impl StoragePersistence for SessionStorage { type Key = String; + type Value = Option; - fn set(key: String, value: &T) { - set(key, value, WebStorageType::Session); + fn load(key: &Self::Key) -> Self::Value { + get(key, WebStorageType::Session) } - fn get(key: &String) -> Option { - get(key, WebStorageType::Session) + fn store(key: Self::Key, value: &Self::Value) { + set_or_clear(key, value.as_deref(), WebStorageType::Session) } } -fn set(key: String, value: &T, storage_type: WebStorageType) { - let as_str = serde_to_string(value); +fn set_or_clear(key: String, value: Option<&str>, storage_type: WebStorageType) { + match value { + Some(str) => set(key, &str, storage_type), + None => clear(key, storage_type), + } +} + +fn set(key: String, as_str: &str, storage_type: WebStorageType) { get_storage_by_type(storage_type) .unwrap() .set_item(&key, &as_str) .unwrap(); } -fn get(key: &str, storage_type: WebStorageType) -> Option { - let s: String = get_storage_by_type(storage_type)?.get_item(key).ok()??; - try_serde_from_string(&s) +fn clear(key: String, storage_type: WebStorageType) { + get_storage_by_type(storage_type) + .unwrap() + .delete(&key) + .unwrap(); +} + +fn get(key: &str, storage_type: WebStorageType) -> Option { + get_storage_by_type(storage_type)?.get_item(key).ok()? } fn get_storage_by_type(storage_type: WebStorageType) -> Option { diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index 89881f0..b4db45d 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -36,6 +36,7 @@ pub use persistence::{ new_persistent, new_singleton_persistent, use_persistent, use_singleton_persistent, }; use std::cell::RefCell; +use std::marker::PhantomData; use std::rc::Rc; use dioxus::prelude::*; @@ -475,6 +476,74 @@ pub trait StorageBacking: Clone + 'static { fn set(key: Self::Key, value: &T); } +/// A trait for the persistence portion of StorageBacking. +pub trait StoragePersistence: Clone + 'static { + /// The key type used to store data in storage + type Key: PartialEq + Clone + Debug + Send + Sync + 'static; + /// The type of value which can be stored. + type Value; + /// Gets a value from storage for the given key + fn load(key: &Self::Key) -> Self::Value; + /// Sets a value in storage for the given key + fn store(key: Self::Key, value: &Self::Value); +} + +/// New trait which can be implemented to define a data format for storage. +pub trait StorageEncoder: Clone + 'static { + /// The type of value which can be stored. + type Value; + fn deserialize(loaded: &Self::Value) -> T; + fn serialize(value: &T) -> Self::Value; +} + +/// A way to create a StorageEncoder out of the two layers. +/// +/// I'm not sure if this is the best way to abstract that. +#[derive(Clone)] +pub struct LayeredStorage { + persistence: PhantomData, + encoder: PhantomData, +} + +/// StorageBacking for LayeredStorage. +impl>, E: StorageEncoder> + StorageBacking for LayeredStorage +{ + type Key = P::Key; + + fn get(key: &Self::Key) -> Option { + let loaded = P::load(key); + match loaded { + Some(t) => E::deserialize(&t), + None => None, + } + } + + fn set(key: Self::Key, value: &T) { + P::store(key, &Some(E::serialize(value))); + } +} + +impl< + Value, + Key, + P: StoragePersistence, Key = Key> + + StorageSubscriber

+ + StorageBacking, + E: StorageEncoder, +> StorageSubscriber> for LayeredStorage +{ + fn subscribe( + key: & as StorageBacking>::Key, + ) -> Receiver { + P::subscribe::(key) + } + + fn unsubscribe(key: & as StorageBacking>::Key) { + P::unsubscribe(key) + } +} + /// A trait for a subscriber to events from a storage backing pub trait StorageSubscriber { /// Subscribes to events from a storage backing for the given key @@ -630,3 +699,32 @@ where } signal } + +#[derive(Clone)] +struct DefaultEncoder; + +impl StorageEncoder for DefaultEncoder { + type Value = String; + + fn deserialize(loaded: &Self::Value) -> T { + // TODO: handle errors + try_serde_from_string(loaded).unwrap() + } + + fn serialize(value: &T) -> Self::Value { + serde_to_string(value) + } +} + +/// StorageBacking using default encoder: handles LocalStorage and other built in storage implementations. +impl>> StorageBacking for P { + type Key = P::Key; + + fn get(key: &Self::Key) -> Option { + LayeredStorage::::get(key) + } + + fn set(key: Self::Key, value: &T) { + LayeredStorage::::set(key, value) + } +} From 7bda9ab576eb2454bf00bec8699276a752f4342b Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Mon, 16 Jun 2025 11:59:40 -0700 Subject: [PATCH 02/37] Messy failed attempt to decouple from serde --- packages/storage/src/client_storage/fs.rs | 5 +- packages/storage/src/lib.rs | 110 +++++++++++++--------- 2 files changed, 69 insertions(+), 46 deletions(-) diff --git a/packages/storage/src/client_storage/fs.rs b/packages/storage/src/client_storage/fs.rs index a1984bc..b4306fe 100644 --- a/packages/storage/src/client_storage/fs.rs +++ b/packages/storage/src/client_storage/fs.rs @@ -1,5 +1,6 @@ use crate::{StorageChannelPayload, StorageSubscription}; use dioxus::logger::tracing::trace; +use serde::Serialize; use serde::de::DeserializeOwned; use std::collections::HashMap; use std::io::Write; @@ -92,8 +93,8 @@ impl StoragePersistence for LocalStorage { // the same thread, meaning that we can just directly notify the subscribers via the same channels, rather than using the // storage event listener. impl StorageSubscriber for LocalStorage { - fn subscribe( - key: &::Key, + fn subscribe( + key: &>::Key, ) -> Receiver { // Initialize the subscriptions map if it hasn't been initialized yet. let subscriptions = SUBSCRIPTIONS.get_or_init(|| RwLock::new(HashMap::new())); diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index b4db45d..fc1451a 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -69,7 +69,7 @@ pub use client_storage::{set_dir_name, set_directory}; /// ``` pub fn use_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal where - S: StorageBacking, + S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { @@ -120,7 +120,7 @@ impl StorageMode { /// ``` pub fn new_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal where - S: StorageBacking, + S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { @@ -145,7 +145,7 @@ where /// The changes to the state will be persisted to storage and all other app sessions will be notified of the change to update their local state. pub fn use_synced_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { @@ -161,7 +161,7 @@ where /// The changes to the state will be persisted to storage and all other app sessions will be notified of the change to update their local state. pub fn new_synced_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { @@ -187,7 +187,7 @@ where /// A hook that creates a StorageEntry with the latest value from storage or the init value if it doesn't exist. pub fn use_storage_entry(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry where - S: StorageBacking, + S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { @@ -203,7 +203,7 @@ pub fn use_synced_storage_entry( init: impl FnOnce() -> T, ) -> SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { @@ -216,7 +216,7 @@ where /// Returns a StorageEntry with the latest value from storage or the init value if it doesn't exist. pub fn new_storage_entry(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry where - S: StorageBacking, + S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static, S::Key: Clone, { @@ -232,7 +232,7 @@ pub fn new_synced_storage_entry( init: impl FnOnce() -> T, ) -> SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, S::Key: Clone, { @@ -242,7 +242,7 @@ where /// Returns a value from storage or the init value if it doesn't exist. pub fn get_from_storage< - S: StorageBacking, + S: StorageBacking, T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, >( key: S::Key, @@ -256,7 +256,7 @@ pub fn get_from_storage< } /// A trait for common functionality between StorageEntry and SyncedStorageEntry -pub trait StorageEntryTrait: +pub trait StorageEntryTrait, T: PartialEq + Clone + 'static>: Clone + 'static { /// Saves the current state to storage @@ -274,7 +274,7 @@ pub trait StorageEntryTrait: /// Creates a hook that will save the state to storage when the state changes fn save_to_storage_on_change(&self) where - S: StorageBacking, + S: StorageBacking, T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, { let entry_clone = self.clone(); @@ -301,7 +301,7 @@ pub trait StorageEntryTrait: /// A wrapper around StorageEntry that provides a channel to subscribe to updates to the underlying storage. #[derive(Clone)] pub struct SyncedStorageEntry< - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, > { /// The underlying StorageEntry that is used to store the data and track the state @@ -312,7 +312,7 @@ pub struct SyncedStorageEntry< impl SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, { pub fn new(key: S::Key, data: T) -> Self { @@ -352,7 +352,7 @@ where impl StorageEntryTrait for SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, { fn save(&self) { @@ -383,7 +383,7 @@ where /// A storage entry that can be used to store data across application reloads. It optionally provides a channel to subscribe to updates to the underlying storage. #[derive(Clone)] pub struct StorageEntry< - S: StorageBacking, + S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static, > { /// The key used to store the data in storage @@ -394,7 +394,7 @@ pub struct StorageEntry< impl StorageEntry where - S: StorageBacking, + S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static, S::Key: Clone, { @@ -412,7 +412,7 @@ where impl StorageEntryTrait for StorageEntry where - S: StorageBacking, + S: StorageBacking, T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, { fn save(&self) { @@ -432,7 +432,7 @@ where } } -impl Deref +impl, T: Serialize + DeserializeOwned + Clone + Send + Sync> Deref for StorageEntry { type Target = Signal; @@ -442,7 +442,7 @@ impl D } } -impl DerefMut +impl, T: Serialize + DeserializeOwned + Clone + Send + Sync> DerefMut for StorageEntry { fn deref_mut(&mut self) -> &mut Signal { @@ -450,7 +450,7 @@ impl D } } -impl Display +impl, T: Display + Serialize + DeserializeOwned + Clone + Send + Sync> Display for StorageEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -458,7 +458,7 @@ impl Debug +impl, T: Debug + Serialize + DeserializeOwned + Clone + Send + Sync> Debug for StorageEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -467,13 +467,13 @@ impl: Clone + 'static { /// The key type used to store data in storage type Key: PartialEq + Clone + Debug + Send + Sync + 'static; /// Gets a value from storage for the given key - fn get(key: &Self::Key) -> Option; + fn get(key: &Self::Key) -> Option; /// Sets a value in storage for the given key - fn set(key: Self::Key, value: &T); + fn set(key: Self::Key, value: &T); } /// A trait for the persistence portion of StorageBacking. @@ -489,37 +489,53 @@ pub trait StoragePersistence: Clone + 'static { } /// New trait which can be implemented to define a data format for storage. -pub trait StorageEncoder: Clone + 'static { +pub trait StorageEncoder: Clone + 'static { /// The type of value which can be stored. type Value; - fn deserialize(loaded: &Self::Value) -> T; - fn serialize(value: &T) -> Self::Value; + fn deserialize(loaded: &Self::Value) -> T; + fn serialize(value: &T) -> Self::Value; } /// A way to create a StorageEncoder out of the two layers. /// /// I'm not sure if this is the best way to abstract that. -#[derive(Clone)] -pub struct LayeredStorage { +pub struct LayeredStorage> { persistence: PhantomData, encoder: PhantomData, + value: PhantomData, +} + +impl> Clone + for LayeredStorage +{ + fn clone(&self) -> Self { + Self { + persistence: self.persistence.clone(), + encoder: self.encoder.clone(), + value: self.value.clone(), + } + } } /// StorageBacking for LayeredStorage. -impl>, E: StorageEncoder> - StorageBacking for LayeredStorage +impl< + T: DeserializeOwned + Clone + 'static, + Value, + P: StoragePersistence>, + E: StorageEncoder, +> StorageBacking for LayeredStorage { type Key = P::Key; - fn get(key: &Self::Key) -> Option { + fn get(key: &Self::Key) -> Option { let loaded = P::load(key); match loaded { - Some(t) => E::deserialize(&t), + Some(t) => Some(E::deserialize(&t)), None => None, } } - fn set(key: Self::Key, value: &T) { + fn set(key: Self::Key, value: &T) { P::store(key, &Some(E::serialize(value))); } } @@ -565,7 +581,7 @@ pub struct StorageSubscription { impl StorageSubscription { pub fn new< - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: DeserializeOwned + Send + Sync + Clone + 'static, >( tx: Sender, @@ -668,7 +684,7 @@ pub(crate) fn use_hydrate_storage( init: Option T>, ) -> Signal where - S: StorageBacking, + S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { @@ -703,28 +719,34 @@ where #[derive(Clone)] struct DefaultEncoder; -impl StorageEncoder for DefaultEncoder { +impl StorageEncoder + for DefaultEncoder +{ type Value = String; - fn deserialize(loaded: &Self::Value) -> T { + fn deserialize(loaded: &Self::Value) -> T { // TODO: handle errors try_serde_from_string(loaded).unwrap() } - fn serialize(value: &T) -> Self::Value { + fn serialize(value: &T) -> Self::Value { serde_to_string(value) } } /// StorageBacking using default encoder: handles LocalStorage and other built in storage implementations. -impl>> StorageBacking for P { +impl< + T: DeserializeOwned + Clone + 'static + Serialize + Send + Sync, + P: StoragePersistence>, +> StorageBacking for P +{ type Key = P::Key; - fn get(key: &Self::Key) -> Option { - LayeredStorage::::get(key) + fn get(key: &Self::Key) -> Option { + LayeredStorage::::get(key) } - fn set(key: Self::Key, value: &T) { - LayeredStorage::::set(key, value) + fn set(key: Self::Key, value: &T) { + LayeredStorage::::set(key, value) } } From 8b1c6900d68d1ea4d466e3eba7019e78ccc66de7 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sun, 20 Jul 2025 00:54:44 -0700 Subject: [PATCH 03/37] Fix build for desktop (crashes at runtime, web does not compile yet) --- examples/storage/src/main.rs | 20 +++--- packages/storage/src/client_storage/fs.rs | 8 ++- packages/storage/src/client_storage/memory.rs | 6 +- packages/storage/src/lib.rs | 69 ++++++++++--------- 4 files changed, 55 insertions(+), 48 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 7b73e34..b764035 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -83,7 +83,7 @@ fn Storage() -> Element { let mut count_session = use_singleton_persistent(|| 0); let mut count_local = use_synced_storage::("synced".to_string(), || 0); - let mut count_local_human = use_synced_storage::, i32>( + let mut count_local_human = use_synced_storage::, i32>( "synced_human".to_string(), || 0, ); @@ -96,7 +96,7 @@ fn Storage() -> Element { }, "Click me!" } - "I persist for the current session. Clicked {count_session} times" + "I persist for the current session. Clicked {count_session} times." } div { button { @@ -105,7 +105,7 @@ fn Storage() -> Element { }, "Click me!" } - "I persist across all sessions. Clicked {count_local} times" + "I persist across all sessions. Clicked {count_local} times." } div { button { @@ -114,27 +114,29 @@ fn Storage() -> Element { }, "Click me!" } - "I persist a human readable value across all sessions. Clicked {count_local_human} times" + "I persist a human readable value across all sessions. Clicked {count_local_human} times." } ) } // Define a "human readable" storage format which is pretty printed JSON instead of a compressed binary format. -type HumanReadableStorage = LayeredStorage; +type HumanReadableStorage = LayeredStorage; #[derive(Clone)] struct HumanReadableEncoding; -impl StorageEncoder for HumanReadableEncoding { - type Value = String; +impl StorageEncoder + for HumanReadableEncoding +{ + type EncodedValue = String; - fn deserialize(loaded: &Self::Value) -> T { + fn deserialize(loaded: &Self::EncodedValue) -> T { let parsed: Result = serde_json::from_str(loaded); // This design probably needs an error handling policy better than panic. parsed.unwrap() } - fn serialize(value: &T) -> Self::Value { + fn serialize(value: &T) -> Self::EncodedValue { serde_json::to_string_pretty(value).unwrap() } } diff --git a/packages/storage/src/client_storage/fs.rs b/packages/storage/src/client_storage/fs.rs index b4306fe..2c04bed 100644 --- a/packages/storage/src/client_storage/fs.rs +++ b/packages/storage/src/client_storage/fs.rs @@ -92,8 +92,10 @@ impl StoragePersistence for LocalStorage { // Note that this module contains an optimization that differs from the web version. Dioxus Desktop runs all windows in // the same thread, meaning that we can just directly notify the subscribers via the same channels, rather than using the // storage event listener. -impl StorageSubscriber for LocalStorage { - fn subscribe( +impl + StorageSubscriber for LocalStorage +{ + fn subscribe( key: &>::Key, ) -> Receiver { // Initialize the subscriptions map if it hasn't been initialized yet. @@ -118,7 +120,7 @@ impl StorageSubscriber for LocalStorage { } } - fn unsubscribe(key: &::Key) { + fn unsubscribe(key: &>::Key) { trace!("Unsubscribing from \"{}\"", key); // Fail silently if unsubscribe is called but the subscriptions map isn't initialized yet. diff --git a/packages/storage/src/client_storage/memory.rs b/packages/storage/src/client_storage/memory.rs index d0433ba..795fa2d 100644 --- a/packages/storage/src/client_storage/memory.rs +++ b/packages/storage/src/client_storage/memory.rs @@ -10,15 +10,15 @@ use crate::StorageBacking; #[derive(Clone)] pub struct SessionStorage; -impl StorageBacking for SessionStorage { +impl StorageBacking for SessionStorage { type Key = String; - fn set(key: String, value: &T) { + fn set(key: String, value: &T) { let session = SessionStore::get_current_session(); session.borrow_mut().insert(key, Arc::new(value.clone())); } - fn get(key: &String) -> Option { + fn get(key: &String) -> Option { let session = SessionStore::get_current_session(); let read_binding = session.borrow(); let value_any = read_binding.get(key)?; diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index fc1451a..c262c1c 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -145,7 +145,7 @@ where /// The changes to the state will be persisted to storage and all other app sessions will be notified of the change to update their local state. pub fn use_synced_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { @@ -161,7 +161,7 @@ where /// The changes to the state will be persisted to storage and all other app sessions will be notified of the change to update their local state. pub fn new_synced_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { @@ -203,7 +203,7 @@ pub fn use_synced_storage_entry( init: impl FnOnce() -> T, ) -> SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { @@ -232,7 +232,7 @@ pub fn new_synced_storage_entry( init: impl FnOnce() -> T, ) -> SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, S::Key: Clone, { @@ -301,7 +301,7 @@ pub trait StorageEntryTrait, T: PartialEq + Clone + 'static /// A wrapper around StorageEntry that provides a channel to subscribe to updates to the underlying storage. #[derive(Clone)] pub struct SyncedStorageEntry< - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, > { /// The underlying StorageEntry that is used to store the data and track the state @@ -312,11 +312,11 @@ pub struct SyncedStorageEntry< impl SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, { pub fn new(key: S::Key, data: T) -> Self { - let channel = S::subscribe::(&key); + let channel = S::subscribe(&key); Self { entry: StorageEntry::new(key, data), channel, @@ -352,7 +352,7 @@ where impl StorageEntryTrait for SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, { fn save(&self) { @@ -420,7 +420,8 @@ where } fn update(&mut self) { - self.data = S::get(&self.key).unwrap_or(self.data); + let read = S::get(&self.key); + self.data.set(read.unwrap()); } fn key(&self) -> &S::Key { @@ -488,12 +489,15 @@ pub trait StoragePersistence: Clone + 'static { fn store(key: Self::Key, value: &Self::Value); } -/// New trait which can be implemented to define a data format for storage. -pub trait StorageEncoder: Clone + 'static { - /// The type of value which can be stored. - type Value; - fn deserialize(loaded: &Self::Value) -> T; - fn serialize(value: &T) -> Self::Value; +/// Defines a data encoding for storage. +/// +/// Encodes a `Value` into `EncodedValue`. +pub trait StorageEncoder: Clone + 'static { + /// The type which the storied entries are encoded into. + type EncodedValue; + /// TODO: support errors for this codepath + fn deserialize(loaded: &Self::EncodedValue) -> Value; + fn serialize(value: &Value) -> Self::EncodedValue; } /// A way to create a StorageEncoder out of the two layers. @@ -522,7 +526,7 @@ impl< T: DeserializeOwned + Clone + 'static, Value, P: StoragePersistence>, - E: StorageEncoder, + E: StorageEncoder, > StorageBacking for LayeredStorage { type Key = P::Key; @@ -544,28 +548,27 @@ impl< Value, Key, P: StoragePersistence, Key = Key> - + StorageSubscriber

- + StorageBacking, - E: StorageEncoder, -> StorageSubscriber> for LayeredStorage + + StorageSubscriber + + StorageBacking, + E: StorageEncoder, + T: DeserializeOwned + Send + Sync + Clone + 'static, +> StorageSubscriber, T> for LayeredStorage { - fn subscribe( - key: & as StorageBacking>::Key, + fn subscribe( + key: & as StorageBacking>::Key, ) -> Receiver { - P::subscribe::(key) + P::subscribe(key) } - fn unsubscribe(key: & as StorageBacking>::Key) { + fn unsubscribe(key: & as StorageBacking>::Key) { P::unsubscribe(key) } } /// A trait for a subscriber to events from a storage backing -pub trait StorageSubscriber { +pub trait StorageSubscriber, T: Clone + 'static> { /// Subscribes to events from a storage backing for the given key - fn subscribe( - key: &S::Key, - ) -> Receiver; + fn subscribe(key: &S::Key) -> Receiver; /// Unsubscribes from events from a storage backing for the given key fn unsubscribe(key: &S::Key); } @@ -581,14 +584,14 @@ pub struct StorageSubscription { impl StorageSubscription { pub fn new< - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: DeserializeOwned + Send + Sync + Clone + 'static, >( tx: Sender, key: S::Key, ) -> Self { let getter = move || { - let data = S::get::(&key).unwrap(); + let data = S::get(&key).unwrap(); StorageChannelPayload::new(data) }; Self { @@ -722,14 +725,14 @@ struct DefaultEncoder; impl StorageEncoder for DefaultEncoder { - type Value = String; + type EncodedValue = String; - fn deserialize(loaded: &Self::Value) -> T { + fn deserialize(loaded: &Self::EncodedValue) -> T { // TODO: handle errors try_serde_from_string(loaded).unwrap() } - fn serialize(value: &T) -> Self::Value { + fn serialize(value: &T) -> Self::EncodedValue { serde_to_string(value) } } From 3f9cee68a87489be31402861323bdb0253d25633 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 23 Aug 2025 17:57:14 -0700 Subject: [PATCH 04/37] Get storage working without a hard dependency on encoding with serde --- examples/storage/src/main.rs | 73 +++++- packages/storage/src/client_storage/fs.rs | 17 +- packages/storage/src/client_storage/memory.rs | 47 +++- packages/storage/src/lib.rs | 224 +++++++++--------- packages/storage/src/persistence.rs | 21 +- 5 files changed, 240 insertions(+), 142 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index b764035..8c83c22 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -75,7 +75,7 @@ fn Footer() -> Element { #[component] fn Home() -> Element { - rsx!("Home") + rsx!( "Home" ) } #[component] @@ -83,11 +83,13 @@ fn Storage() -> Element { let mut count_session = use_singleton_persistent(|| 0); let mut count_local = use_synced_storage::("synced".to_string(), || 0); - let mut count_local_human = use_synced_storage::, i32>( + let mut count_local_human = use_synced_storage::, i32>( "synced_human".to_string(), || 0, ); + // let mut in_memory = use_synced_storage::, i32>("memory".to_string(), || 0); + rsx!( div { button { @@ -117,17 +119,25 @@ fn Storage() -> Element { "I persist a human readable value across all sessions. Clicked {count_local_human} times." } ) + + // div { + // button { + // onclick: move |_| { + // *in_memory.write() += 1; + // }, + // "Click me!" + // } + // "I persist a value without encoding, in memory. Clicked {in_memory} times." + // } } // Define a "human readable" storage format which is pretty printed JSON instead of a compressed binary format. -type HumanReadableStorage = LayeredStorage; +type HumanReadableStorage = LayeredStorage; #[derive(Clone)] struct HumanReadableEncoding; -impl StorageEncoder - for HumanReadableEncoding -{ +impl StorageEncoder for HumanReadableEncoding { type EncodedValue = String; fn deserialize(loaded: &Self::EncodedValue) -> T { @@ -140,3 +150,54 @@ impl StorageEncoder serde_json::to_string_pretty(value).unwrap() } } + +// // In memory cloned format +// type MemoryStorage = LayeredStorage; + +// #[derive(Clone)] +// pub struct InMemoryEncoder; + +// impl StorageEncoder for InMemoryEncoder { +// type EncodedValue = Arc>; + +// fn deserialize(loaded: &Self::EncodedValue) -> T { +// let x = loaded.lock().unwrap(); +// // TODO: handle errors +// x.downcast_ref::().cloned().unwrap() +// } + +// fn serialize(value: &T) -> Self::EncodedValue { +// Arc::new(Mutex::new(value.clone())) +// } +// } + +// impl StoragePersistence for InMemoryEncoder { +// type Key = String; +// type Value = Option>>; + +// fn load(key: &Self::Key) -> Self::Value { +// IN_MEMORY_STORE.read().unwrap().get(key).map(|v| v.clone()) +// } + +// fn store(key: &Self::Key, value: &Self::Value, unencoded: &T) { +// let mut map = IN_MEMORY_STORE.write().unwrap(); +// if let Some(v) = value { +// map.insert(key.clone(), v.clone()); +// } else { +// map.remove(key); +// } +// } +// } + +// impl StorageSubscriber for InMemoryEncoder { +// fn subscribe(key: &::Key) -> Receiver { +// todo!() +// } + +// fn unsubscribe(key: &::Key) { +// todo!() +// } +// } + +// static IN_MEMORY_STORE: LazyLock>>>> = +// LazyLock::new(|| RwLock::default()); diff --git a/packages/storage/src/client_storage/fs.rs b/packages/storage/src/client_storage/fs.rs index 2c04bed..fa8b906 100644 --- a/packages/storage/src/client_storage/fs.rs +++ b/packages/storage/src/client_storage/fs.rs @@ -73,16 +73,21 @@ impl StoragePersistence for LocalStorage { get(key) } - fn store(key: Self::Key, value: &Self::Value) { - set(&key, value); + fn store( + key: &Self::Key, + value: &Self::Value, + unencoded: &T, + ) { + set(key, value); // If the subscriptions map is not initialized, we don't need to notify any subscribers. if let Some(subscriptions) = SUBSCRIPTIONS.get() { let read_binding = subscriptions.read().unwrap(); - if let Some(subscription) = read_binding.get(&key) { + if let Some(subscription) = read_binding.get(key) { subscription .tx - .send(StorageChannelPayload::new(value.clone())) + // .send(StorageChannelPayload::new(value.clone())) + .send(StorageChannelPayload::new(unencoded.clone())) .unwrap(); } } @@ -92,8 +97,8 @@ impl StoragePersistence for LocalStorage { // Note that this module contains an optimization that differs from the web version. Dioxus Desktop runs all windows in // the same thread, meaning that we can just directly notify the subscribers via the same channels, rather than using the // storage event listener. -impl - StorageSubscriber for LocalStorage +impl + StorageSubscriber for LocalStorage { fn subscribe( key: &>::Key, diff --git a/packages/storage/src/client_storage/memory.rs b/packages/storage/src/client_storage/memory.rs index 795fa2d..83bf696 100644 --- a/packages/storage/src/client_storage/memory.rs +++ b/packages/storage/src/client_storage/memory.rs @@ -5,7 +5,7 @@ use std::ops::{Deref, DerefMut}; use std::rc::Rc; use std::sync::Arc; -use crate::StorageBacking; +use crate::{StorageBacking, StorageEncoder, StoragePersistence}; #[derive(Clone)] pub struct SessionStorage; @@ -13,9 +13,11 @@ pub struct SessionStorage; impl StorageBacking for SessionStorage { type Key = String; - fn set(key: String, value: &T) { + fn set(key: &String, value: &T) { let session = SessionStore::get_current_session(); - session.borrow_mut().insert(key, Arc::new(value.clone())); + session + .borrow_mut() + .insert(key.clone(), Arc::new(value.clone())); } fn get(key: &String) -> Option { @@ -26,6 +28,29 @@ impl StorageBacking for SessionStorage { } } +impl StoragePersistence for SessionStorage { + type Key = String; + type Value = Option>; + + fn load(key: &Self::Key) -> Self::Value { + let session = SessionStore::get_current_session(); + let read_binding = session.borrow(); + read_binding.get(key).cloned() + } + + fn store(key: &Self::Key, value: &Self::Value, unencoded: &T) { + let session = SessionStore::get_current_session(); + match value { + Some(value) => { + session.borrow_mut().insert(key.clone(), value.clone()); + } + None => { + session.borrow_mut().remove(key); + } + } + } +} + /// An in-memory session store that is tied to the current Dioxus root context. #[derive(Clone)] struct SessionStore { @@ -67,3 +92,19 @@ impl DerefMut for SessionStore { &mut self.map } } + +#[derive(Clone)] +pub struct InMemoryEncoder; + +impl StorageEncoder for InMemoryEncoder { + type EncodedValue = Arc; + + fn deserialize(loaded: &Self::EncodedValue) -> T { + // TODO: handle errors + loaded.downcast_ref::().cloned().unwrap() + } + + fn serialize(value: &T) -> Self::EncodedValue { + Arc::new(value.clone()) + } +} diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index c262c1c..089a7e8 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -63,15 +63,15 @@ pub use client_storage::{set_dir_name, set_directory}; /// use dioxus_signals::Signal; /// /// // This hook can be used with any storage backing without multiple versions of the hook -/// fn use_user_id() -> Signal where S: StorageBacking { +/// fn use_user_id() -> Signal where S: StorageBacking { /// use_storage::("user-id", || 123) /// } /// ``` pub fn use_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, + S: Clone + StorageBacking, S::Key: Clone, + T: Clone + Send + Sync + PartialEq + 'static, { let mut init = Some(init); let storage = use_hook(|| new_storage::(key, || init.take().unwrap()())); @@ -114,15 +114,15 @@ impl StorageMode { /// use dioxus_signals::Signal; /// /// // This hook can be used with any storage backing without multiple versions of the hook -/// fn user_id() -> Signal where S: StorageBacking { +/// fn user_id() -> Signal where S: StorageBacking { /// new_storage::("user-id", || 123) /// } /// ``` pub fn new_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, + S: Clone + StorageBacking, S::Key: Clone, + T: Clone + Send + Sync + PartialEq + 'static, { let mode = StorageMode::current(); @@ -145,9 +145,9 @@ where /// The changes to the state will be persisted to storage and all other app sessions will be notified of the change to update their local state. pub fn use_synced_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal where - S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, + S: Clone + StorageBacking + StorageSubscriber, S::Key: Clone, + T: Clone + Send + Sync + PartialEq + 'static, { let mut init = Some(init); let storage = use_hook(|| new_synced_storage::(key, || init.take().unwrap()())); @@ -161,9 +161,9 @@ where /// The changes to the state will be persisted to storage and all other app sessions will be notified of the change to update their local state. pub fn new_synced_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal where - S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, + S: Clone + StorageBacking + StorageSubscriber, S::Key: Clone, + T: Clone + Send + Sync + PartialEq + 'static, { let signal = { let mode = StorageMode::current(); @@ -188,8 +188,8 @@ where pub fn use_storage_entry(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry where S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, + T: Clone + Send + Sync + PartialEq + 'static, { let mut init = Some(init); let signal = use_hook(|| new_storage_entry::(key, || init.take().unwrap()())); @@ -203,9 +203,9 @@ pub fn use_synced_storage_entry( init: impl FnOnce() -> T, ) -> SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, + S: StorageBacking + StorageSubscriber, S::Key: Clone, + T: Clone + Send + Sync + PartialEq + 'static, { let mut init = Some(init); let signal = use_hook(|| new_synced_storage_entry::(key, || init.take().unwrap()())); @@ -217,10 +217,9 @@ where pub fn new_storage_entry(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry where S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static, - S::Key: Clone, + T: Send + Sync + 'static, { - let data = get_from_storage::(key.clone(), init); + let data = get_from_storage::(&key, init); StorageEntry::new(key, data) } @@ -232,20 +231,16 @@ pub fn new_synced_storage_entry( init: impl FnOnce() -> T, ) -> SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, - S::Key: Clone, + S: StorageBacking + StorageSubscriber, + T: Clone + PartialEq + Send + Sync + 'static, { - let data = get_from_storage::(key.clone(), init); + let data = get_from_storage::(&key, init); SyncedStorageEntry::new(key, data) } /// Returns a value from storage or the init value if it doesn't exist. -pub fn get_from_storage< - S: StorageBacking, - T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, ->( - key: S::Key, +pub fn get_from_storage, T: Send + Sync + 'static>( + key: &S::Key, init: impl FnOnce() -> T, ) -> T { S::get(&key).unwrap_or_else(|| { @@ -256,9 +251,7 @@ pub fn get_from_storage< } /// A trait for common functionality between StorageEntry and SyncedStorageEntry -pub trait StorageEntryTrait, T: PartialEq + Clone + 'static>: - Clone + 'static -{ +pub trait StorageEntryTrait, T: 'static>: 'static { /// Saves the current state to storage fn save(&self); @@ -274,8 +267,9 @@ pub trait StorageEntryTrait, T: PartialEq + Clone + 'static /// Creates a hook that will save the state to storage when the state changes fn save_to_storage_on_change(&self) where + Self: Clone, S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, + T: Clone + PartialEq + 'static, { let entry_clone = self.clone(); let old = RefCell::new(None); @@ -299,21 +293,30 @@ pub trait StorageEntryTrait, T: PartialEq + Clone + 'static } /// A wrapper around StorageEntry that provides a channel to subscribe to updates to the underlying storage. -#[derive(Clone)] -pub struct SyncedStorageEntry< - S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, -> { +pub struct SyncedStorageEntry, T: 'static> { /// The underlying StorageEntry that is used to store the data and track the state pub(crate) entry: StorageEntry, /// The channel to subscribe to updates to the underlying storage pub(crate) channel: Receiver, } +impl Clone for SyncedStorageEntry +where + S: StorageBacking + StorageSubscriber, + S::Key: Clone, + T: 'static, +{ + fn clone(&self) -> Self { + Self { + entry: self.entry.clone(), + channel: self.channel.clone(), + } + } +} + impl SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, + S: StorageBacking + StorageSubscriber, { pub fn new(key: S::Key, data: T) -> Self { let channel = S::subscribe(&key); @@ -329,7 +332,10 @@ where } /// Creates a hook that will update the state when the underlying storage changes - pub fn subscribe_to_storage(&self) { + pub fn subscribe_to_storage(&self) + where + T: Clone + Send + Sync + PartialEq + 'static, + { let storage_entry_signal = *self.data(); let channel = self.channel.clone(); spawn(async move { @@ -352,8 +358,8 @@ where impl StorageEntryTrait for SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, + S: StorageBacking + StorageSubscriber, + T: Send + Sync + PartialEq + 'static, { fn save(&self) { // We want to save in the following conditions @@ -381,22 +387,30 @@ where } /// A storage entry that can be used to store data across application reloads. It optionally provides a channel to subscribe to updates to the underlying storage. -#[derive(Clone)] -pub struct StorageEntry< - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static, -> { +pub struct StorageEntry, T: 'static> { /// The key used to store the data in storage pub(crate) key: S::Key, /// A signal that can be used to read and modify the state pub(crate) data: Signal, } -impl StorageEntry +impl Clone for StorageEntry where S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static, + T: 'static, S::Key: Clone, +{ + fn clone(&self) -> Self { + Self { + key: self.key.clone(), + data: self.data.clone(), + } + } +} + +impl StorageEntry +where + S: StorageBacking, { /// Creates a new StorageEntry pub fn new(key: S::Key, data: T) -> Self { @@ -413,15 +427,16 @@ where impl StorageEntryTrait for StorageEntry where S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, + T: PartialEq + Send + Sync + 'static, { fn save(&self) { - S::set(self.key.clone(), &*self.data.read()); + S::set(&self.key, &*self.data.read()); } fn update(&mut self) { - let read = S::get(&self.key); - self.data.set(read.unwrap()); + if let Some(value) = S::get(&self.key) { + *self.data.write() = value; + } } fn key(&self) -> &S::Key { @@ -433,9 +448,7 @@ where } } -impl, T: Serialize + DeserializeOwned + Clone + Send + Sync> Deref - for StorageEntry -{ +impl, T: Send + Sync> Deref for StorageEntry { type Target = Signal; fn deref(&self) -> &Signal { @@ -443,15 +456,13 @@ impl, T: Serialize + DeserializeOwned + Clone + Send + Sync } } -impl, T: Serialize + DeserializeOwned + Clone + Send + Sync> DerefMut - for StorageEntry -{ +impl, T: Send + Sync> DerefMut for StorageEntry { fn deref_mut(&mut self) -> &mut Signal { &mut self.data } } -impl, T: Display + Serialize + DeserializeOwned + Clone + Send + Sync> Display +impl, T: Display + Serialize + DeserializeOwned + Send + Sync> Display for StorageEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -459,7 +470,7 @@ impl, T: Display + Serialize + DeserializeOwned + Clone + S } } -impl, T: Debug + Serialize + DeserializeOwned + Clone + Send + Sync> Debug +impl, T: Debug + Serialize + DeserializeOwned + Send + Sync> Debug for StorageEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -468,62 +479,52 @@ impl, T: Debug + Serialize + DeserializeOwned + Clone + Sen } /// A trait for a storage backing -pub trait StorageBacking: Clone + 'static { +pub trait StorageBacking: 'static { /// The key type used to store data in storage - type Key: PartialEq + Clone + Debug + Send + Sync + 'static; + type Key: PartialEq + Debug + Send + Sync + 'static; /// Gets a value from storage for the given key fn get(key: &Self::Key) -> Option; /// Sets a value in storage for the given key - fn set(key: Self::Key, value: &T); + fn set(key: &Self::Key, value: &T); } /// A trait for the persistence portion of StorageBacking. -pub trait StoragePersistence: Clone + 'static { +pub trait StoragePersistence: 'static { /// The key type used to store data in storage - type Key: PartialEq + Clone + Debug + Send + Sync + 'static; + type Key: PartialEq + Debug + Send + Sync + 'static; /// The type of value which can be stored. type Value; /// Gets a value from storage for the given key fn load(key: &Self::Key) -> Self::Value; /// Sets a value in storage for the given key - fn store(key: Self::Key, value: &Self::Value); + fn store(key: &Self::Key, value: &Self::Value, unencoded: &T); } -/// Defines a data encoding for storage. -/// -/// Encodes a `Value` into `EncodedValue`. -pub trait StorageEncoder: Clone + 'static { - /// The type which the storied entries are encoded into. +/// New trait which can be implemented to define a data format for storage. +pub trait StorageEncoder: 'static { + /// The type of value which can be stored. type EncodedValue; - /// TODO: support errors for this codepath - fn deserialize(loaded: &Self::EncodedValue) -> Value; - fn serialize(value: &Value) -> Self::EncodedValue; + fn deserialize(loaded: &Self::EncodedValue) -> T; + fn serialize(value: &T) -> Self::EncodedValue; } /// A way to create a StorageEncoder out of the two layers. /// /// I'm not sure if this is the best way to abstract that. +#[derive(Clone)] pub struct LayeredStorage> { persistence: PhantomData, encoder: PhantomData, value: PhantomData, } -impl> Clone - for LayeredStorage -{ - fn clone(&self) -> Self { - Self { - persistence: self.persistence.clone(), - encoder: self.encoder.clone(), - value: self.value.clone(), - } - } -} - /// StorageBacking for LayeredStorage. +/// T: Use facing type +/// Value: what gets persisted +/// P: Stores a Option +/// E: Translated between T and Value impl< - T: DeserializeOwned + Clone + 'static, + T: 'static + Clone + Send + Sync, Value, P: StoragePersistence>, E: StorageEncoder, @@ -532,27 +533,34 @@ impl< type Key = P::Key; fn get(key: &Self::Key) -> Option { + println!( + "LayeredStorage StorageBacking::get: T: {}, Value: {}", + std::any::type_name::(), + std::any::type_name::() + ); let loaded = P::load(key); - match loaded { - Some(t) => Some(E::deserialize(&t)), - None => None, - } + loaded.as_ref().map(E::deserialize) } - fn set(key: Self::Key, value: &T) { - P::store(key, &Some(E::serialize(value))); + fn set(key: &Self::Key, value: &T) { + println!( + "LayeredStorage StorageBacking::set: T: {}, Value: {}", + std::any::type_name::(), + std::any::type_name::() + ); + P::store(key, &Some(E::serialize(value)), value); } } impl< + T: 'static + Clone + Send + Sync, Value, Key, P: StoragePersistence, Key = Key> - + StorageSubscriber + + StorageSubscriber + StorageBacking, E: StorageEncoder, - T: DeserializeOwned + Send + Sync + Clone + 'static, -> StorageSubscriber, T> for LayeredStorage +> StorageSubscriber> for LayeredStorage { fn subscribe( key: & as StorageBacking>::Key, @@ -566,7 +574,7 @@ impl< } /// A trait for a subscriber to events from a storage backing -pub trait StorageSubscriber, T: Clone + 'static> { +pub trait StorageSubscriber> { /// Subscribes to events from a storage backing for the given key fn subscribe(key: &S::Key) -> Receiver; /// Unsubscribes from events from a storage backing for the given key @@ -583,10 +591,7 @@ pub struct StorageSubscription { } impl StorageSubscription { - pub fn new< - S: StorageBacking + StorageSubscriber, - T: DeserializeOwned + Send + Sync + Clone + 'static, - >( + pub fn new + StorageSubscriber, T: Send + Sync + 'static>( tx: Sender, key: S::Key, ) -> Self { @@ -607,17 +612,17 @@ impl StorageSubscription { } } -/// A payload for a storage channel that contains the latest value from storage. -#[derive(Clone, Debug)] +/// A payload for a storage channel that contains the latest value from storage, unencoded. +#[derive(Debug)] pub struct StorageChannelPayload { - data: Arc, + data: Box, } impl StorageChannelPayload { /// Creates a new StorageChannelPayload pub fn new(data: T) -> Self { Self { - data: Arc::new(data), + data: Box::new(data), } } @@ -629,7 +634,7 @@ impl StorageChannelPayload { impl Default for StorageChannelPayload { fn default() -> Self { - Self { data: Arc::new(()) } + Self { data: Box::new(()) } } } @@ -688,8 +693,7 @@ pub(crate) fn use_hydrate_storage( ) -> Signal where S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, - S::Key: Clone, + T: Clone + Send + Sync + PartialEq + 'static, { let mode = StorageMode::current(); // We read the value from storage and store it here if we are hydrating the client. @@ -722,9 +726,7 @@ where #[derive(Clone)] struct DefaultEncoder; -impl StorageEncoder - for DefaultEncoder -{ +impl StorageEncoder for DefaultEncoder { type EncodedValue = String; fn deserialize(loaded: &Self::EncodedValue) -> T { @@ -739,7 +741,7 @@ impl StorageEnc /// StorageBacking using default encoder: handles LocalStorage and other built in storage implementations. impl< - T: DeserializeOwned + Clone + 'static + Serialize + Send + Sync, + T: Serialize + DeserializeOwned + 'static + Clone + Send + Sync, P: StoragePersistence>, > StorageBacking for P { @@ -749,7 +751,7 @@ impl< LayeredStorage::::get(key) } - fn set(key: Self::Key, value: &T) { + fn set(key: &Self::Key, value: &T) { LayeredStorage::::set(key, value) } } diff --git a/packages/storage/src/persistence.rs b/packages/storage/src/persistence.rs index 64ee5f2..38d6d38 100644 --- a/packages/storage/src/persistence.rs +++ b/packages/storage/src/persistence.rs @@ -1,18 +1,13 @@ +use super::StorageEntryTrait; use crate::SessionStorage; use crate::{new_storage_entry, use_hydrate_storage}; use dioxus::prelude::*; use dioxus_signals::Signal; -use serde::Serialize; -use serde::de::DeserializeOwned; - -use super::StorageEntryTrait; /// A persistent storage hook that can be used to store data across application reloads. /// /// Depending on the platform this uses either local storage or a file storage -pub fn use_persistent< - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, ->( +pub fn use_persistent( key: impl ToString, init: impl FnOnce() -> T, ) -> Signal { @@ -25,9 +20,7 @@ pub fn use_persistent< /// Creates a persistent storage signal that can be used to store data across application reloads. /// /// Depending on the platform this uses either local storage or a file storage -pub fn new_persistent< - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, ->( +pub fn new_persistent( key: impl ToString, init: impl FnOnce() -> T, ) -> Signal { @@ -41,9 +34,7 @@ pub fn new_persistent< /// /// Depending on the platform this uses either local storage or a file storage #[track_caller] -pub fn use_singleton_persistent< - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, ->( +pub fn use_singleton_persistent( init: impl FnOnce() -> T, ) -> Signal { let mut init = Some(init); @@ -58,9 +49,7 @@ pub fn use_singleton_persistent< /// Depending on the platform this uses either local storage or a file storage #[allow(clippy::needless_return)] #[track_caller] -pub fn new_singleton_persistent< - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, ->( +pub fn new_singleton_persistent( init: impl FnOnce() -> T, ) -> Signal { let caller = std::panic::Location::caller(); From 89824a4d392a14af166b3e4739100d3b5e63a818 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Fri, 29 Aug 2025 10:51:33 -0700 Subject: [PATCH 05/37] Dedupe StoragePersistence logic for SessionStorage --- packages/storage/src/client_storage/memory.rs | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/storage/src/client_storage/memory.rs b/packages/storage/src/client_storage/memory.rs index 83bf696..48ccc2f 100644 --- a/packages/storage/src/client_storage/memory.rs +++ b/packages/storage/src/client_storage/memory.rs @@ -11,26 +11,37 @@ use crate::{StorageBacking, StorageEncoder, StoragePersistence}; pub struct SessionStorage; impl StorageBacking for SessionStorage { - type Key = String; + type Key = ::Key; fn set(key: &String, value: &T) { - let session = SessionStore::get_current_session(); - session - .borrow_mut() - .insert(key.clone(), Arc::new(value.clone())); + let encoded: Arc = Arc::new(value.clone()); + store::(key, &Some(encoded), &value); } fn get(key: &String) -> Option { - let session = SessionStore::get_current_session(); - let read_binding = session.borrow(); - let value_any = read_binding.get(key)?; + let value_any = SessionStorage::load(key)?; value_any.downcast_ref::().cloned() } } +type Key = String; +type Value = Option>; + +fn store(key: &Key, value: &Value, _unencoded: &T) { + let session = SessionStore::get_current_session(); + match value { + Some(value) => { + session.borrow_mut().insert(key.clone(), value.clone()); + } + None => { + session.borrow_mut().remove(key); + } + } +} + impl StoragePersistence for SessionStorage { - type Key = String; - type Value = Option>; + type Key = Key; + type Value = Value; fn load(key: &Self::Key) -> Self::Value { let session = SessionStore::get_current_session(); @@ -39,15 +50,7 @@ impl StoragePersistence for SessionStorage { } fn store(key: &Self::Key, value: &Self::Value, unencoded: &T) { - let session = SessionStore::get_current_session(); - match value { - Some(value) => { - session.borrow_mut().insert(key.clone(), value.clone()); - } - None => { - session.borrow_mut().remove(key); - } - } + store(key, value, unencoded); } } From a787ec5440f5795e2ccecddbee775d4e70b0b57e Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Fri, 29 Aug 2025 11:32:02 -0700 Subject: [PATCH 06/37] cleanup memory --- examples/storage/src/main.rs | 93 +++++++------------ packages/storage/src/client_storage/memory.rs | 42 +++++---- 2 files changed, 58 insertions(+), 77 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 8c83c22..e212bb3 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -1,3 +1,8 @@ +use std::{ + any::Any, + sync::{Arc, Mutex}, +}; + use dioxus::prelude::*; use dioxus_storage::*; @@ -75,7 +80,7 @@ fn Footer() -> Element { #[component] fn Home() -> Element { - rsx!( "Home" ) + rsx!("Home") } #[component] @@ -89,6 +94,7 @@ fn Storage() -> Element { ); // let mut in_memory = use_synced_storage::, i32>("memory".to_string(), || 0); + let mut in_memory = use_storage::("memory".to_string(), || 0); rsx!( div { @@ -118,17 +124,16 @@ fn Storage() -> Element { } "I persist a human readable value across all sessions. Clicked {count_local_human} times." } + div { + button { + onclick: move |_| { + *in_memory.write() += 1; + }, + "Click me!" + } + "I persist a value without encoding, in memory. Clicked {in_memory} times." + } ) - - // div { - // button { - // onclick: move |_| { - // *in_memory.write() += 1; - // }, - // "Click me!" - // } - // "I persist a value without encoding, in memory. Clicked {in_memory} times." - // } } // Define a "human readable" storage format which is pretty printed JSON instead of a compressed binary format. @@ -151,53 +156,19 @@ impl StorageEncoder for HumanReadableEncodin } } -// // In memory cloned format -// type MemoryStorage = LayeredStorage; - -// #[derive(Clone)] -// pub struct InMemoryEncoder; - -// impl StorageEncoder for InMemoryEncoder { -// type EncodedValue = Arc>; - -// fn deserialize(loaded: &Self::EncodedValue) -> T { -// let x = loaded.lock().unwrap(); -// // TODO: handle errors -// x.downcast_ref::().cloned().unwrap() -// } - -// fn serialize(value: &T) -> Self::EncodedValue { -// Arc::new(Mutex::new(value.clone())) -// } -// } - -// impl StoragePersistence for InMemoryEncoder { -// type Key = String; -// type Value = Option>>; - -// fn load(key: &Self::Key) -> Self::Value { -// IN_MEMORY_STORE.read().unwrap().get(key).map(|v| v.clone()) -// } - -// fn store(key: &Self::Key, value: &Self::Value, unencoded: &T) { -// let mut map = IN_MEMORY_STORE.write().unwrap(); -// if let Some(v) = value { -// map.insert(key.clone(), v.clone()); -// } else { -// map.remove(key); -// } -// } -// } - -// impl StorageSubscriber for InMemoryEncoder { -// fn subscribe(key: &::Key) -> Receiver { -// todo!() -// } - -// fn unsubscribe(key: &::Key) { -// todo!() -// } -// } - -// static IN_MEMORY_STORE: LazyLock>>>> = -// LazyLock::new(|| RwLock::default()); +#[derive(Clone)] +pub struct InMemoryEncoder; + +impl StorageEncoder for InMemoryEncoder { + type EncodedValue = Arc>; + + fn deserialize(loaded: &Self::EncodedValue) -> T { + let x = loaded.lock().unwrap(); + // TODO: handle errors + x.downcast_ref::().cloned().unwrap() + } + + fn serialize(value: &T) -> Self::EncodedValue { + Arc::new(Mutex::new(value.clone())) + } +} diff --git a/packages/storage/src/client_storage/memory.rs b/packages/storage/src/client_storage/memory.rs index 48ccc2f..7db9e97 100644 --- a/packages/storage/src/client_storage/memory.rs +++ b/packages/storage/src/client_storage/memory.rs @@ -54,6 +54,32 @@ impl StoragePersistence for SessionStorage { } } +/// A StorageEncoder which encodes Optional data by cloning it's content into `Arc` +pub struct ArcEncoder; + +impl StorageEncoder> for ArcEncoder { + type EncodedValue = Value; + + fn deserialize(loaded: &Self::EncodedValue) -> Option { + match loaded { + Some(v) => { + let v: &Arc = v; + // TODO: this downcast failing is currently handled the same as `loaded` being None. + v.downcast_ref::().cloned() + } + None => None, + } + } + + fn serialize(value: &Option) -> Self::EncodedValue { + value.clone().map(|x| { + let arc: Arc = Arc::new(x); + let arc: Arc = arc; + arc + }) + } +} + /// An in-memory session store that is tied to the current Dioxus root context. #[derive(Clone)] struct SessionStore { @@ -95,19 +121,3 @@ impl DerefMut for SessionStore { &mut self.map } } - -#[derive(Clone)] -pub struct InMemoryEncoder; - -impl StorageEncoder for InMemoryEncoder { - type EncodedValue = Arc; - - fn deserialize(loaded: &Self::EncodedValue) -> T { - // TODO: handle errors - loaded.downcast_ref::().cloned().unwrap() - } - - fn serialize(value: &T) -> Self::EncodedValue { - Arc::new(value.clone()) - } -} From a23ed602b45354566fdcad96df1ecae63151f2cd Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Fri, 29 Aug 2025 14:28:40 -0700 Subject: [PATCH 07/37] Refactor: Fix build, but still errors --- examples/storage/src/main.rs | 12 +- packages/storage/src/client_storage/fs.rs | 8 +- packages/storage/src/client_storage/memory.rs | 49 ++--- packages/storage/src/lib.rs | 208 ++++++++++-------- 4 files changed, 148 insertions(+), 129 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index e212bb3..f200198 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -144,11 +144,10 @@ struct HumanReadableEncoding; impl StorageEncoder for HumanReadableEncoding { type EncodedValue = String; + type DecodeError = serde_json::Error; - fn deserialize(loaded: &Self::EncodedValue) -> T { - let parsed: Result = serde_json::from_str(loaded); - // This design probably needs an error handling policy better than panic. - parsed.unwrap() + fn deserialize(loaded: &Self::EncodedValue) -> Result { + serde_json::from_str(loaded) } fn serialize(value: &T) -> Self::EncodedValue { @@ -161,11 +160,12 @@ pub struct InMemoryEncoder; impl StorageEncoder for InMemoryEncoder { type EncodedValue = Arc>; + type DecodeError = (); - fn deserialize(loaded: &Self::EncodedValue) -> T { + fn deserialize(loaded: &Self::EncodedValue) -> Result { let x = loaded.lock().unwrap(); // TODO: handle errors - x.downcast_ref::().cloned().unwrap() + x.downcast_ref::().cloned().ok_or(()) } fn serialize(value: &T) -> Self::EncodedValue { diff --git a/packages/storage/src/client_storage/fs.rs b/packages/storage/src/client_storage/fs.rs index fa8b906..cdcf805 100644 --- a/packages/storage/src/client_storage/fs.rs +++ b/packages/storage/src/client_storage/fs.rs @@ -7,7 +7,7 @@ use std::io::Write; use std::sync::{OnceLock, RwLock}; use tokio::sync::watch::{Receiver, channel}; -use crate::{StorageBacking, StoragePersistence, StorageSubscriber}; +use crate::{StoragePersistence, StorageSubscriber}; #[doc(hidden)] /// Sets the directory where the storage files are located. @@ -100,9 +100,7 @@ impl StoragePersistence for LocalStorage { impl StorageSubscriber for LocalStorage { - fn subscribe( - key: &>::Key, - ) -> Receiver { + fn subscribe(key: &String) -> Receiver { // Initialize the subscriptions map if it hasn't been initialized yet. let subscriptions = SUBSCRIPTIONS.get_or_init(|| RwLock::new(HashMap::new())); @@ -125,7 +123,7 @@ impl } } - fn unsubscribe(key: &>::Key) { + fn unsubscribe(key: &String) { trace!("Unsubscribing from \"{}\"", key); // Fail silently if unsubscribe is called but the subscriptions map isn't initialized yet. diff --git a/packages/storage/src/client_storage/memory.rs b/packages/storage/src/client_storage/memory.rs index 7db9e97..635a67a 100644 --- a/packages/storage/src/client_storage/memory.rs +++ b/packages/storage/src/client_storage/memory.rs @@ -10,18 +10,9 @@ use crate::{StorageBacking, StorageEncoder, StoragePersistence}; #[derive(Clone)] pub struct SessionStorage; -impl StorageBacking for SessionStorage { - type Key = ::Key; - - fn set(key: &String, value: &T) { - let encoded: Arc = Arc::new(value.clone()); - store::(key, &Some(encoded), &value); - } - - fn get(key: &String) -> Option { - let value_any = SessionStorage::load(key)?; - value_any.downcast_ref::().cloned() - } +impl StorageBacking for SessionStorage { + type Encoder = ArcEncoder; + type Persistence = CurrentSession; } type Key = String; @@ -39,7 +30,9 @@ fn store(key: &Key, value: &Value, _unencoded: &T) { } } -impl StoragePersistence for SessionStorage { +pub struct CurrentSession; + +impl StoragePersistence for CurrentSession { type Key = Key; type Value = Value; @@ -49,7 +42,7 @@ impl StoragePersistence for SessionStorage { read_binding.get(key).cloned() } - fn store(key: &Self::Key, value: &Self::Value, unencoded: &T) { + fn store(key: &Self::Key, value: &Self::Value, unencoded: &T) { store(key, value, unencoded); } } @@ -57,26 +50,18 @@ impl StoragePersistence for SessionStorage { /// A StorageEncoder which encodes Optional data by cloning it's content into `Arc` pub struct ArcEncoder; -impl StorageEncoder> for ArcEncoder { - type EncodedValue = Value; - - fn deserialize(loaded: &Self::EncodedValue) -> Option { - match loaded { - Some(v) => { - let v: &Arc = v; - // TODO: this downcast failing is currently handled the same as `loaded` being None. - v.downcast_ref::().cloned() - } - None => None, - } +impl StorageEncoder for ArcEncoder { + type EncodedValue = Arc; + type DecodeError = (); + + fn deserialize(loaded: &Self::EncodedValue) -> Result { + let v: &Arc = loaded; + // TODO: Better error message + v.downcast_ref::().cloned().ok_or(()) } - fn serialize(value: &Option) -> Self::EncodedValue { - value.clone().map(|x| { - let arc: Arc = Arc::new(x); - let arc: Arc = arc; - arc - }) + fn serialize(value: &T) -> Self::EncodedValue { + Arc::new(value.clone()) } } diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index 089a7e8..23698f8 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -67,10 +67,13 @@ pub use client_storage::{set_dir_name, set_directory}; /// use_storage::("user-id", || 123) /// } /// ``` -pub fn use_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal +pub fn use_storage( + key: ::Key, + init: impl FnOnce() -> T, +) -> Signal where S: Clone + StorageBacking, - S::Key: Clone, + ::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { let mut init = Some(init); @@ -118,10 +121,13 @@ impl StorageMode { /// new_storage::("user-id", || 123) /// } /// ``` -pub fn new_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal +pub fn new_storage( + key: ::Key, + init: impl FnOnce() -> T, +) -> Signal where S: Clone + StorageBacking, - S::Key: Clone, + ::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { let mode = StorageMode::current(); @@ -143,10 +149,13 @@ where /// /// This hook returns a Signal that can be used to read and modify the state. /// The changes to the state will be persisted to storage and all other app sessions will be notified of the change to update their local state. -pub fn use_synced_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal +pub fn use_synced_storage( + key: ::Key, + init: impl FnOnce() -> T, +) -> Signal where S: Clone + StorageBacking + StorageSubscriber, - S::Key: Clone, + ::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { let mut init = Some(init); @@ -159,10 +168,13 @@ where /// /// This hook returns a Signal that can be used to read and modify the state. /// The changes to the state will be persisted to storage and all other app sessions will be notified of the change to update their local state. -pub fn new_synced_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal +pub fn new_synced_storage( + key: ::Key, + init: impl FnOnce() -> T, +) -> Signal where S: Clone + StorageBacking + StorageSubscriber, - S::Key: Clone, + ::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { let signal = { @@ -185,10 +197,13 @@ where } /// A hook that creates a StorageEntry with the latest value from storage or the init value if it doesn't exist. -pub fn use_storage_entry(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry +pub fn use_storage_entry( + key: ::Key, + init: impl FnOnce() -> T, +) -> StorageEntry where S: StorageBacking, - S::Key: Clone, + ::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { let mut init = Some(init); @@ -199,12 +214,12 @@ where /// A hook that creates a StorageEntry with the latest value from storage or the init value if it doesn't exist, and provides a channel to subscribe to updates to the underlying storage. pub fn use_synced_storage_entry( - key: S::Key, + key: ::Key, init: impl FnOnce() -> T, ) -> SyncedStorageEntry where S: StorageBacking + StorageSubscriber, - S::Key: Clone, + ::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { let mut init = Some(init); @@ -214,7 +229,10 @@ where } /// Returns a StorageEntry with the latest value from storage or the init value if it doesn't exist. -pub fn new_storage_entry(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry +pub fn new_storage_entry( + key: ::Key, + init: impl FnOnce() -> T, +) -> StorageEntry where S: StorageBacking, T: Send + Sync + 'static, @@ -227,7 +245,7 @@ where /// /// This differs from `storage_entry` in that this one will return a channel to subscribe to updates to the underlying storage. pub fn new_synced_storage_entry( - key: S::Key, + key: ::Key, init: impl FnOnce() -> T, ) -> SyncedStorageEntry where @@ -239,8 +257,8 @@ where } /// Returns a value from storage or the init value if it doesn't exist. -pub fn get_from_storage, T: Send + Sync + 'static>( - key: &S::Key, +pub fn get_from_storage, T: Send + Sync + 'static + Clone>( + key: &::Key, init: impl FnOnce() -> T, ) -> T { S::get(&key).unwrap_or_else(|| { @@ -259,7 +277,7 @@ pub trait StorageEntryTrait, T: 'static>: 'static { fn update(&mut self); /// Gets the key used to store the data in storage - fn key(&self) -> &S::Key; + fn key(&self) -> &::Key; /// Gets the signal that can be used to read and modify the state fn data(&self) -> &Signal; @@ -303,7 +321,7 @@ pub struct SyncedStorageEntry, T: 'static> { impl Clone for SyncedStorageEntry where S: StorageBacking + StorageSubscriber, - S::Key: Clone, + ::Key: Clone, T: 'static, { fn clone(&self) -> Self { @@ -318,7 +336,7 @@ impl SyncedStorageEntry where S: StorageBacking + StorageSubscriber, { - pub fn new(key: S::Key, data: T) -> Self { + pub fn new(key: ::Key, data: T) -> Self { let channel = S::subscribe(&key); Self { entry: StorageEntry::new(key, data), @@ -356,7 +374,7 @@ where } } -impl StorageEntryTrait for SyncedStorageEntry +impl StorageEntryTrait for SyncedStorageEntry where S: StorageBacking + StorageSubscriber, T: Send + Sync + PartialEq + 'static, @@ -377,7 +395,7 @@ where self.entry.update(); } - fn key(&self) -> &S::Key { + fn key(&self) -> &::Key { self.entry.key() } @@ -389,7 +407,7 @@ where /// A storage entry that can be used to store data across application reloads. It optionally provides a channel to subscribe to updates to the underlying storage. pub struct StorageEntry, T: 'static> { /// The key used to store the data in storage - pub(crate) key: S::Key, + pub(crate) key: ::Key, /// A signal that can be used to read and modify the state pub(crate) data: Signal, } @@ -398,7 +416,7 @@ impl Clone for StorageEntry where S: StorageBacking, T: 'static, - S::Key: Clone, + ::Key: Clone, { fn clone(&self) -> Self { Self { @@ -413,7 +431,7 @@ where S: StorageBacking, { /// Creates a new StorageEntry - pub fn new(key: S::Key, data: T) -> Self { + pub fn new(key: ::Key, data: T) -> Self { Self { key, data: Signal::new_in_scope( @@ -424,7 +442,7 @@ where } } -impl StorageEntryTrait for StorageEntry +impl StorageEntryTrait for StorageEntry where S: StorageBacking, T: PartialEq + Send + Sync + 'static, @@ -439,7 +457,7 @@ where } } - fn key(&self) -> &S::Key { + fn key(&self) -> &::Key { &self.key } @@ -480,12 +498,32 @@ impl, T: Debug + Serialize + DeserializeOwned + Send + Sync /// A trait for a storage backing pub trait StorageBacking: 'static { - /// The key type used to store data in storage - type Key: PartialEq + Debug + Send + Sync + 'static; + type Encoder: StorageEncoder; + type Persistence: StoragePersistence< + Value = Option<>::EncodedValue>, + >; + /// Gets a value from storage for the given key - fn get(key: &Self::Key) -> Option; + fn get( + key: &<>::Persistence as StoragePersistence>::Key, + ) -> Option { + let loaded = Self::Persistence::load(key); + match loaded { + // TODO: this treats None the same as failed decodes + Some(x) => Self::Encoder::deserialize(&x).ok(), + None => None, + } + } /// Sets a value in storage for the given key - fn set(key: &Self::Key, value: &T); + /// + /// TODO: this provides no way to clear (store None) + fn set(key: &<>::Persistence as StoragePersistence>::Key, value: &T) + where + T: 'static + Clone + Send + Sync, + { + let encoded = Self::Encoder::serialize(value); + Self::Persistence::store(key, &Some(encoded), &Some(value).cloned()); + } } /// A trait for the persistence portion of StorageBacking. @@ -504,7 +542,8 @@ pub trait StoragePersistence: 'static { pub trait StorageEncoder: 'static { /// The type of value which can be stored. type EncodedValue; - fn deserialize(loaded: &Self::EncodedValue) -> T; + type DecodeError: Debug; + fn deserialize(loaded: &Self::EncodedValue) -> Result; fn serialize(value: &T) -> Self::EncodedValue; } @@ -524,51 +563,32 @@ pub struct LayeredStorage /// E: Translated between T and Value impl< - T: 'static + Clone + Send + Sync, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, Value, P: StoragePersistence>, E: StorageEncoder, > StorageBacking for LayeredStorage { - type Key = P::Key; - - fn get(key: &Self::Key) -> Option { - println!( - "LayeredStorage StorageBacking::get: T: {}, Value: {}", - std::any::type_name::(), - std::any::type_name::() - ); - let loaded = P::load(key); - loaded.as_ref().map(E::deserialize) - } - - fn set(key: &Self::Key, value: &T) { - println!( - "LayeredStorage StorageBacking::set: T: {}, Value: {}", - std::any::type_name::(), - std::any::type_name::() - ); - P::store(key, &Some(E::serialize(value)), value); - } + type Encoder = E; + type Persistence = P; } impl< - T: 'static + Clone + Send + Sync, + T: 'static + Clone + Send + Sync + Serialize + DeserializeOwned, Value, - Key, - P: StoragePersistence, Key = Key> + P: StoragePersistence> + StorageSubscriber - + StorageBacking, + + StorageBacking, E: StorageEncoder, > StorageSubscriber> for LayeredStorage { - fn subscribe( - key: & as StorageBacking>::Key, - ) -> Receiver { + fn subscribe(key: &

::Key) -> Receiver { P::subscribe(key) } - fn unsubscribe(key: & as StorageBacking>::Key) { + fn unsubscribe( + key: &< as StorageBacking>::Persistence as StoragePersistence>::Key, + ) { P::unsubscribe(key) } } @@ -576,9 +596,11 @@ impl< /// A trait for a subscriber to events from a storage backing pub trait StorageSubscriber> { /// Subscribes to events from a storage backing for the given key - fn subscribe(key: &S::Key) -> Receiver; + fn subscribe( + key: &::Key, + ) -> Receiver; /// Unsubscribes from events from a storage backing for the given key - fn unsubscribe(key: &S::Key); + fn unsubscribe(key: &::Key); } /// A struct to hold information about processing a storage event. @@ -593,7 +615,7 @@ pub struct StorageSubscription { impl StorageSubscription { pub fn new + StorageSubscriber, T: Send + Sync + 'static>( tx: Sender, - key: S::Key, + key: ::Key, ) -> Self { let getter = move || { let data = S::get(&key).unwrap(); @@ -670,19 +692,42 @@ pub(crate) fn serde_from_string(value: &str) -> T { } /// Deserializes and decompresses a value from a string and returns None if there is an error. -pub(crate) fn try_serde_from_string(value: &str) -> Option { +pub(crate) fn try_serde_from_string( + value: &str, +) -> Result> { + let fail = |description: String| FailedDecode::from(value.to_string(), description); + let mut bytes: Vec = Vec::new(); let mut chars = value.chars(); while let Some(c) = chars.next() { - let n1 = c.to_digit(16)?; - let c2 = chars.next()?; - let n2 = c2.to_digit(16)?; + let n1 = c + .to_digit(16) + .ok_or_else(|| fail("decode error 1".to_string()))?; + let c2 = chars + .next() + .ok_or_else(|| fail("decode error 2".to_string()))?; + let n2 = c2 + .to_digit(16) + .ok_or_else(|| fail("decode error 3".to_string()))?; bytes.push((n1 * 16 + n2) as u8); } match yazi::decompress(&bytes, yazi::Format::Zlib) { - Ok((decompressed, _)) => ciborium::from_reader(std::io::Cursor::new(decompressed)).ok(), - Err(_) => None, + Ok((decompressed, _)) => ciborium::from_reader(std::io::Cursor::new(decompressed)) + .map_err(|err| fail(format!("ciborium Error: {err}"))), + Err(err) => Result::Err(fail(format!("yazi Error: {err:?}"))), + } +} + +#[derive(Debug)] +pub struct FailedDecode { + pub from: From, + pub description: String, +} + +impl FailedDecode { + fn from(from: T, description: String) -> FailedDecode { + FailedDecode { from, description } } } @@ -724,14 +769,14 @@ where } #[derive(Clone)] -struct DefaultEncoder; +pub struct DefaultEncoder; impl StorageEncoder for DefaultEncoder { type EncodedValue = String; + type DecodeError = FailedDecode; - fn deserialize(loaded: &Self::EncodedValue) -> T { - // TODO: handle errors - try_serde_from_string(loaded).unwrap() + fn deserialize(loaded: &Self::EncodedValue) -> Result { + try_serde_from_string::(loaded) } fn serialize(value: &T) -> Self::EncodedValue { @@ -740,18 +785,9 @@ impl StorageEncoder for DefaultEncoder { } /// StorageBacking using default encoder: handles LocalStorage and other built in storage implementations. -impl< - T: Serialize + DeserializeOwned + 'static + Clone + Send + Sync, - P: StoragePersistence>, -> StorageBacking for P +impl StorageBacking + for LocalStorage { - type Key = P::Key; - - fn get(key: &Self::Key) -> Option { - LayeredStorage::::get(key) - } - - fn set(key: &Self::Key, value: &T) { - LayeredStorage::::set(key, value) - } + type Encoder = DefaultEncoder; + type Persistence = LocalStorage; } From d3a28dd91a0d6f2ee8c6391efc85f259cfa83f7e Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Fri, 29 Aug 2025 15:26:21 -0700 Subject: [PATCH 08/37] Fix error --- packages/storage/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index 23698f8..823e988 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -365,9 +365,10 @@ where let payload = channel.borrow_and_update(); *storage_entry_signal.write() = payload .data - .downcast_ref::() + .downcast_ref::>() .expect("Type mismatch with storage entry") - .clone(); + .clone() + .expect("Expected storage entry to be Some"); } } }); From 6eb19e4fe65b55d351da1885bbeb0c6bade7f598 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Fri, 29 Aug 2025 15:47:52 -0700 Subject: [PATCH 09/37] cleanup example --- examples/storage/src/main.rs | 24 ------------------- packages/storage/src/client_storage/memory.rs | 6 ++--- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index f200198..6114fa3 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -1,8 +1,3 @@ -use std::{ - any::Any, - sync::{Arc, Mutex}, -}; - use dioxus::prelude::*; use dioxus_storage::*; @@ -93,7 +88,6 @@ fn Storage() -> Element { || 0, ); - // let mut in_memory = use_synced_storage::, i32>("memory".to_string(), || 0); let mut in_memory = use_storage::("memory".to_string(), || 0); rsx!( @@ -154,21 +148,3 @@ impl StorageEncoder for HumanReadableEncodin serde_json::to_string_pretty(value).unwrap() } } - -#[derive(Clone)] -pub struct InMemoryEncoder; - -impl StorageEncoder for InMemoryEncoder { - type EncodedValue = Arc>; - type DecodeError = (); - - fn deserialize(loaded: &Self::EncodedValue) -> Result { - let x = loaded.lock().unwrap(); - // TODO: handle errors - x.downcast_ref::().cloned().ok_or(()) - } - - fn serialize(value: &T) -> Self::EncodedValue { - Arc::new(Mutex::new(value.clone())) - } -} diff --git a/packages/storage/src/client_storage/memory.rs b/packages/storage/src/client_storage/memory.rs index 635a67a..283bdc4 100644 --- a/packages/storage/src/client_storage/memory.rs +++ b/packages/storage/src/client_storage/memory.rs @@ -12,7 +12,7 @@ pub struct SessionStorage; impl StorageBacking for SessionStorage { type Encoder = ArcEncoder; - type Persistence = CurrentSession; + type Persistence = SessionStorage; } type Key = String; @@ -30,9 +30,7 @@ fn store(key: &Key, value: &Value, _unencoded: &T) { } } -pub struct CurrentSession; - -impl StoragePersistence for CurrentSession { +impl StoragePersistence for SessionStorage { type Key = Key; type Value = Value; From fcc1331471e6153114d31bba849d8bdc0e27fe3c Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Fri, 29 Aug 2025 18:05:52 -0700 Subject: [PATCH 10/37] Fix web build --- packages/storage/src/client_storage/web.rs | 38 +++++++++++++++++----- packages/storage/src/lib.rs | 2 +- packages/storage/src/persistence.rs | 18 +++++++--- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/packages/storage/src/client_storage/web.rs b/packages/storage/src/client_storage/web.rs index 37658c6..54a2ee5 100644 --- a/packages/storage/src/client_storage/web.rs +++ b/packages/storage/src/client_storage/web.rs @@ -5,13 +5,25 @@ use std::{ use dioxus::logger::tracing::{error, trace}; use once_cell::sync::Lazy; +use serde::Serialize; use serde::de::DeserializeOwned; use tokio::sync::watch::{Receiver, channel}; use wasm_bindgen::JsCast; use wasm_bindgen::prelude::Closure; use web_sys::{Storage, window}; -use crate::{StorageChannelPayload, StoragePersistence, StorageSubscriber, StorageSubscription}; +use crate::{ + DefaultEncoder, StorageBacking, StorageChannelPayload, StoragePersistence, StorageSubscriber, + StorageSubscription, +}; + +/// StorageBacking using default encoder +impl StorageBacking + for SessionStorage +{ + type Encoder = DefaultEncoder; + type Persistence = LocalStorage; +} #[derive(Clone)] pub struct LocalStorage; @@ -25,15 +37,19 @@ impl StoragePersistence for LocalStorage { get(key, WebStorageType::Local) } - fn store(key: Self::Key, value: &Self::Value) { - set_or_clear(key, value.as_deref(), WebStorageType::Local) + fn store( + key: &Self::Key, + value: &Self::Value, + _unencoded: &T, + ) { + set_or_clear(key.clone(), value.as_deref(), WebStorageType::Local) } } -impl StorageSubscriber for LocalStorage { - fn subscribe( - key: &String, - ) -> Receiver { +impl + StorageSubscriber for LocalStorage +{ + fn subscribe(key: &String) -> Receiver { let read_binding = SUBSCRIPTIONS.read().unwrap(); match read_binding.get(key) { Some(subscription) => subscription.tx.subscribe(), @@ -104,8 +120,12 @@ impl StoragePersistence for SessionStorage { get(key, WebStorageType::Session) } - fn store(key: Self::Key, value: &Self::Value) { - set_or_clear(key, value.as_deref(), WebStorageType::Session) + fn store( + key: &Self::Key, + value: &Self::Value, + _unencoded: &T, + ) { + set_or_clear(key.clone(), value.as_deref(), WebStorageType::Session) } } diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index 823e988..c06ed26 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -785,7 +785,7 @@ impl StorageEncoder for DefaultEncoder { } } -/// StorageBacking using default encoder: handles LocalStorage and other built in storage implementations. +/// StorageBacking using default encoder impl StorageBacking for LocalStorage { diff --git a/packages/storage/src/persistence.rs b/packages/storage/src/persistence.rs index 38d6d38..505e5df 100644 --- a/packages/storage/src/persistence.rs +++ b/packages/storage/src/persistence.rs @@ -3,11 +3,15 @@ use crate::SessionStorage; use crate::{new_storage_entry, use_hydrate_storage}; use dioxus::prelude::*; use dioxus_signals::Signal; +use serde::Serialize; +use serde::de::DeserializeOwned; /// A persistent storage hook that can be used to store data across application reloads. /// /// Depending on the platform this uses either local storage or a file storage -pub fn use_persistent( +pub fn use_persistent< + T: Clone + Send + Sync + PartialEq + 'static + Serialize + DeserializeOwned, +>( key: impl ToString, init: impl FnOnce() -> T, ) -> Signal { @@ -20,7 +24,9 @@ pub fn use_persistent( /// Creates a persistent storage signal that can be used to store data across application reloads. /// /// Depending on the platform this uses either local storage or a file storage -pub fn new_persistent( +pub fn new_persistent< + T: Clone + Send + Sync + PartialEq + 'static + Serialize + DeserializeOwned, +>( key: impl ToString, init: impl FnOnce() -> T, ) -> Signal { @@ -34,7 +40,9 @@ pub fn new_persistent( /// /// Depending on the platform this uses either local storage or a file storage #[track_caller] -pub fn use_singleton_persistent( +pub fn use_singleton_persistent< + T: Clone + Send + Sync + PartialEq + 'static + Serialize + DeserializeOwned, +>( init: impl FnOnce() -> T, ) -> Signal { let mut init = Some(init); @@ -49,7 +57,9 @@ pub fn use_singleton_persistent( /// Depending on the platform this uses either local storage or a file storage #[allow(clippy::needless_return)] #[track_caller] -pub fn new_singleton_persistent( +pub fn new_singleton_persistent< + T: Clone + Send + Sync + PartialEq + 'static + Serialize + DeserializeOwned, +>( init: impl FnOnce() -> T, ) -> Signal { let caller = std::panic::Location::caller(); From 906b6c68741acaa928749e0ab78c622111518d86 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Fri, 29 Aug 2025 18:53:06 -0700 Subject: [PATCH 11/37] Split out default_encoder --- packages/storage/src/client_storage/web.rs | 4 +- packages/storage/src/default_encoder.rs | 76 +++++++++++++++++++ packages/storage/src/lib.rs | 86 +--------------------- packages/storage/src/persistence.rs | 11 +-- 4 files changed, 88 insertions(+), 89 deletions(-) create mode 100644 packages/storage/src/default_encoder.rs diff --git a/packages/storage/src/client_storage/web.rs b/packages/storage/src/client_storage/web.rs index 54a2ee5..33a9a83 100644 --- a/packages/storage/src/client_storage/web.rs +++ b/packages/storage/src/client_storage/web.rs @@ -13,8 +13,8 @@ use wasm_bindgen::prelude::Closure; use web_sys::{Storage, window}; use crate::{ - DefaultEncoder, StorageBacking, StorageChannelPayload, StoragePersistence, StorageSubscriber, - StorageSubscription, + StorageBacking, StorageChannelPayload, StoragePersistence, StorageSubscriber, + StorageSubscription, default_encoder::DefaultEncoder, }; /// StorageBacking using default encoder diff --git a/packages/storage/src/default_encoder.rs b/packages/storage/src/default_encoder.rs new file mode 100644 index 0000000..e3afeb5 --- /dev/null +++ b/packages/storage/src/default_encoder.rs @@ -0,0 +1,76 @@ +use super::FailedDecode; +use super::StorageEncoder; + +use serde::Serialize; +use serde::de::DeserializeOwned; + +/// Default [StorageEncoder]. +/// +/// Uses a non-human readable format. +/// Format uses Serde, and is compressed and then encoded into a utf8 compatible string. +#[derive(Clone)] +pub struct DefaultEncoder; + +impl StorageEncoder for DefaultEncoder { + type EncodedValue = String; + type DecodeError = FailedDecode; + + fn deserialize(loaded: &Self::EncodedValue) -> Result { + try_serde_from_string::(loaded) + } + + fn serialize(value: &T) -> Self::EncodedValue { + serde_to_string(value) + } +} + +// Helper functions + +/// Serializes a value to a string and compresses it. +fn serde_to_string(value: &T) -> String { + let mut serialized = Vec::new(); + ciborium::into_writer(value, &mut serialized).unwrap(); + let compressed = yazi::compress( + &serialized, + yazi::Format::Zlib, + yazi::CompressionLevel::BestSize, + ) + .unwrap(); + let as_str: String = compressed + .iter() + .flat_map(|u| { + [ + char::from_digit(((*u & 0xF0) >> 4).into(), 16).unwrap(), + char::from_digit((*u & 0x0F).into(), 16).unwrap(), + ] + .into_iter() + }) + .collect(); + as_str +} + +/// Deserializes and decompresses a value from a string and returns None if there is an error. +fn try_serde_from_string(value: &str) -> Result> { + let fail = |description: String| FailedDecode::from(value.to_string(), description); + + let mut bytes: Vec = Vec::new(); + let mut chars = value.chars(); + while let Some(c) = chars.next() { + let n1 = c + .to_digit(16) + .ok_or_else(|| fail("decode error 1".to_string()))?; + let c2 = chars + .next() + .ok_or_else(|| fail("decode error 2".to_string()))?; + let n2 = c2 + .to_digit(16) + .ok_or_else(|| fail("decode error 3".to_string()))?; + bytes.push((n1 * 16 + n2) as u8); + } + + match yazi::decompress(&bytes, yazi::Format::Zlib) { + Ok((decompressed, _)) => ciborium::from_reader(std::io::Cursor::new(decompressed)) + .map_err(|err| fail(format!("ciborium Error: {err}"))), + Err(err) => Result::Err(fail(format!("yazi Error: {err:?}"))), + } +} diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index c06ed26..bcc5d2b 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -27,6 +27,7 @@ //! ``` mod client_storage; +mod default_encoder; mod persistence; pub use client_storage::{LocalStorage, SessionStorage}; @@ -481,17 +482,13 @@ impl, T: Send + Sync> DerefMut for StorageEntry { } } -impl, T: Display + Serialize + DeserializeOwned + Send + Sync> Display - for StorageEntry -{ +impl, T: Display> Display for StorageEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.data.fmt(f) } } -impl, T: Debug + Serialize + DeserializeOwned + Send + Sync> Debug - for StorageEntry -{ +impl, T: Debug> Debug for StorageEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.data.fmt(f) } @@ -661,65 +658,6 @@ impl Default for StorageChannelPayload { } } -// Helper functions - -/// Serializes a value to a string and compresses it. -pub(crate) fn serde_to_string(value: &T) -> String { - let mut serialized = Vec::new(); - ciborium::into_writer(value, &mut serialized).unwrap(); - let compressed = yazi::compress( - &serialized, - yazi::Format::Zlib, - yazi::CompressionLevel::BestSize, - ) - .unwrap(); - let as_str: String = compressed - .iter() - .flat_map(|u| { - [ - char::from_digit(((*u & 0xF0) >> 4).into(), 16).unwrap(), - char::from_digit((*u & 0x0F).into(), 16).unwrap(), - ] - .into_iter() - }) - .collect(); - as_str -} - -#[allow(unused)] -/// Deserializes a value from a string and unwraps errors. -pub(crate) fn serde_from_string(value: &str) -> T { - try_serde_from_string(value).unwrap() -} - -/// Deserializes and decompresses a value from a string and returns None if there is an error. -pub(crate) fn try_serde_from_string( - value: &str, -) -> Result> { - let fail = |description: String| FailedDecode::from(value.to_string(), description); - - let mut bytes: Vec = Vec::new(); - let mut chars = value.chars(); - while let Some(c) = chars.next() { - let n1 = c - .to_digit(16) - .ok_or_else(|| fail("decode error 1".to_string()))?; - let c2 = chars - .next() - .ok_or_else(|| fail("decode error 2".to_string()))?; - let n2 = c2 - .to_digit(16) - .ok_or_else(|| fail("decode error 3".to_string()))?; - bytes.push((n1 * 16 + n2) as u8); - } - - match yazi::decompress(&bytes, yazi::Format::Zlib) { - Ok((decompressed, _)) => ciborium::from_reader(std::io::Cursor::new(decompressed)) - .map_err(|err| fail(format!("ciborium Error: {err}"))), - Err(err) => Result::Err(fail(format!("yazi Error: {err:?}"))), - } -} - #[derive(Debug)] pub struct FailedDecode { pub from: From, @@ -769,26 +707,10 @@ where signal } -#[derive(Clone)] -pub struct DefaultEncoder; - -impl StorageEncoder for DefaultEncoder { - type EncodedValue = String; - type DecodeError = FailedDecode; - - fn deserialize(loaded: &Self::EncodedValue) -> Result { - try_serde_from_string::(loaded) - } - - fn serialize(value: &T) -> Self::EncodedValue { - serde_to_string(value) - } -} - /// StorageBacking using default encoder impl StorageBacking for LocalStorage { - type Encoder = DefaultEncoder; + type Encoder = default_encoder::DefaultEncoder; type Persistence = LocalStorage; } diff --git a/packages/storage/src/persistence.rs b/packages/storage/src/persistence.rs index 505e5df..64ee5f2 100644 --- a/packages/storage/src/persistence.rs +++ b/packages/storage/src/persistence.rs @@ -1,4 +1,3 @@ -use super::StorageEntryTrait; use crate::SessionStorage; use crate::{new_storage_entry, use_hydrate_storage}; use dioxus::prelude::*; @@ -6,11 +5,13 @@ use dioxus_signals::Signal; use serde::Serialize; use serde::de::DeserializeOwned; +use super::StorageEntryTrait; + /// A persistent storage hook that can be used to store data across application reloads. /// /// Depending on the platform this uses either local storage or a file storage pub fn use_persistent< - T: Clone + Send + Sync + PartialEq + 'static + Serialize + DeserializeOwned, + T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, >( key: impl ToString, init: impl FnOnce() -> T, @@ -25,7 +26,7 @@ pub fn use_persistent< /// /// Depending on the platform this uses either local storage or a file storage pub fn new_persistent< - T: Clone + Send + Sync + PartialEq + 'static + Serialize + DeserializeOwned, + T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, >( key: impl ToString, init: impl FnOnce() -> T, @@ -41,7 +42,7 @@ pub fn new_persistent< /// Depending on the platform this uses either local storage or a file storage #[track_caller] pub fn use_singleton_persistent< - T: Clone + Send + Sync + PartialEq + 'static + Serialize + DeserializeOwned, + T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, >( init: impl FnOnce() -> T, ) -> Signal { @@ -58,7 +59,7 @@ pub fn use_singleton_persistent< #[allow(clippy::needless_return)] #[track_caller] pub fn new_singleton_persistent< - T: Clone + Send + Sync + PartialEq + 'static + Serialize + DeserializeOwned, + T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, >( init: impl FnOnce() -> T, ) -> Signal { From a122b2b266f1bfbea0ee36fe6956f4d535dd5c02 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Fri, 29 Aug 2025 20:12:33 -0700 Subject: [PATCH 12/37] Generic StoragePersistence --- packages/storage/src/client_storage/fs.rs | 8 +- packages/storage/src/client_storage/memory.rs | 4 +- packages/storage/src/client_storage/web.rs | 16 +--- packages/storage/src/lib.rs | 84 ++++++++++--------- 4 files changed, 54 insertions(+), 58 deletions(-) diff --git a/packages/storage/src/client_storage/fs.rs b/packages/storage/src/client_storage/fs.rs index cdcf805..cb76620 100644 --- a/packages/storage/src/client_storage/fs.rs +++ b/packages/storage/src/client_storage/fs.rs @@ -65,7 +65,7 @@ fn get(key: &str) -> Option { pub struct LocalStorage; /// LocalStorage stores Option. -impl StoragePersistence for LocalStorage { +impl StoragePersistence for LocalStorage { type Key = String; type Value = Option; @@ -73,11 +73,7 @@ impl StoragePersistence for LocalStorage { get(key) } - fn store( - key: &Self::Key, - value: &Self::Value, - unencoded: &T, - ) { + fn store(key: &Self::Key, value: &Self::Value, unencoded: &T) { set(key, value); // If the subscriptions map is not initialized, we don't need to notify any subscribers. diff --git a/packages/storage/src/client_storage/memory.rs b/packages/storage/src/client_storage/memory.rs index 283bdc4..ea4c5f1 100644 --- a/packages/storage/src/client_storage/memory.rs +++ b/packages/storage/src/client_storage/memory.rs @@ -30,7 +30,7 @@ fn store(key: &Key, value: &Value, _unencoded: &T) { } } -impl StoragePersistence for SessionStorage { +impl StoragePersistence for SessionStorage { type Key = Key; type Value = Value; @@ -40,7 +40,7 @@ impl StoragePersistence for SessionStorage { read_binding.get(key).cloned() } - fn store(key: &Self::Key, value: &Self::Value, unencoded: &T) { + fn store(key: &Self::Key, value: &Self::Value, unencoded: &T) { store(key, value, unencoded); } } diff --git a/packages/storage/src/client_storage/web.rs b/packages/storage/src/client_storage/web.rs index 33a9a83..3c5c0ea 100644 --- a/packages/storage/src/client_storage/web.rs +++ b/packages/storage/src/client_storage/web.rs @@ -29,7 +29,7 @@ impl StorageBac pub struct LocalStorage; /// LocalStorage stores Option. -impl StoragePersistence for LocalStorage { +impl StoragePersistence for LocalStorage { type Key = String; type Value = Option; @@ -37,11 +37,7 @@ impl StoragePersistence for LocalStorage { get(key, WebStorageType::Local) } - fn store( - key: &Self::Key, - value: &Self::Value, - _unencoded: &T, - ) { + fn store(key: &Self::Key, value: &Self::Value, _unencoded: &T) { set_or_clear(key.clone(), value.as_deref(), WebStorageType::Local) } } @@ -112,7 +108,7 @@ static SUBSCRIPTIONS: Lazy>>> = pub struct SessionStorage; /// LocalStorage stores Option. -impl StoragePersistence for SessionStorage { +impl StoragePersistence for SessionStorage { type Key = String; type Value = Option; @@ -120,11 +116,7 @@ impl StoragePersistence for SessionStorage { get(key, WebStorageType::Session) } - fn store( - key: &Self::Key, - value: &Self::Value, - _unencoded: &T, - ) { + fn store(key: &Self::Key, value: &Self::Value, _unencoded: &T) { set_or_clear(key.clone(), value.as_deref(), WebStorageType::Session) } } diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index bcc5d2b..3657c91 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -69,12 +69,12 @@ pub use client_storage::{set_dir_name, set_directory}; /// } /// ``` pub fn use_storage( - key: ::Key, + key: >>::Key, init: impl FnOnce() -> T, ) -> Signal where S: Clone + StorageBacking, - ::Key: Clone, + >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { let mut init = Some(init); @@ -123,12 +123,12 @@ impl StorageMode { /// } /// ``` pub fn new_storage( - key: ::Key, + key: >>::Key, init: impl FnOnce() -> T, ) -> Signal where S: Clone + StorageBacking, - ::Key: Clone, + >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { let mode = StorageMode::current(); @@ -151,12 +151,12 @@ where /// This hook returns a Signal that can be used to read and modify the state. /// The changes to the state will be persisted to storage and all other app sessions will be notified of the change to update their local state. pub fn use_synced_storage( - key: ::Key, + key: >>::Key, init: impl FnOnce() -> T, ) -> Signal where S: Clone + StorageBacking + StorageSubscriber, - ::Key: Clone, + >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { let mut init = Some(init); @@ -170,12 +170,12 @@ where /// This hook returns a Signal that can be used to read and modify the state. /// The changes to the state will be persisted to storage and all other app sessions will be notified of the change to update their local state. pub fn new_synced_storage( - key: ::Key, + key: >>::Key, init: impl FnOnce() -> T, ) -> Signal where S: Clone + StorageBacking + StorageSubscriber, - ::Key: Clone, + >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { let signal = { @@ -199,12 +199,12 @@ where /// A hook that creates a StorageEntry with the latest value from storage or the init value if it doesn't exist. pub fn use_storage_entry( - key: ::Key, + key: >>::Key, init: impl FnOnce() -> T, ) -> StorageEntry where S: StorageBacking, - ::Key: Clone, + >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { let mut init = Some(init); @@ -215,12 +215,12 @@ where /// A hook that creates a StorageEntry with the latest value from storage or the init value if it doesn't exist, and provides a channel to subscribe to updates to the underlying storage. pub fn use_synced_storage_entry( - key: ::Key, + key: >>::Key, init: impl FnOnce() -> T, ) -> SyncedStorageEntry where S: StorageBacking + StorageSubscriber, - ::Key: Clone, + >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { let mut init = Some(init); @@ -231,7 +231,7 @@ where /// Returns a StorageEntry with the latest value from storage or the init value if it doesn't exist. pub fn new_storage_entry( - key: ::Key, + key: >>::Key, init: impl FnOnce() -> T, ) -> StorageEntry where @@ -246,7 +246,7 @@ where /// /// This differs from `storage_entry` in that this one will return a channel to subscribe to updates to the underlying storage. pub fn new_synced_storage_entry( - key: ::Key, + key: >>::Key, init: impl FnOnce() -> T, ) -> SyncedStorageEntry where @@ -259,7 +259,7 @@ where /// Returns a value from storage or the init value if it doesn't exist. pub fn get_from_storage, T: Send + Sync + 'static + Clone>( - key: &::Key, + key: &>>::Key, init: impl FnOnce() -> T, ) -> T { S::get(&key).unwrap_or_else(|| { @@ -278,7 +278,7 @@ pub trait StorageEntryTrait, T: 'static>: 'static { fn update(&mut self); /// Gets the key used to store the data in storage - fn key(&self) -> &::Key; + fn key(&self) -> &>>::Key; /// Gets the signal that can be used to read and modify the state fn data(&self) -> &Signal; @@ -322,7 +322,7 @@ pub struct SyncedStorageEntry, T: 'static> { impl Clone for SyncedStorageEntry where S: StorageBacking + StorageSubscriber, - ::Key: Clone, + >>::Key: Clone, T: 'static, { fn clone(&self) -> Self { @@ -337,7 +337,7 @@ impl SyncedStorageEntry where S: StorageBacking + StorageSubscriber, { - pub fn new(key: ::Key, data: T) -> Self { + pub fn new(key: >>::Key, data: T) -> Self { let channel = S::subscribe(&key); Self { entry: StorageEntry::new(key, data), @@ -397,7 +397,7 @@ where self.entry.update(); } - fn key(&self) -> &::Key { + fn key(&self) -> &>>::Key { self.entry.key() } @@ -409,7 +409,7 @@ where /// A storage entry that can be used to store data across application reloads. It optionally provides a channel to subscribe to updates to the underlying storage. pub struct StorageEntry, T: 'static> { /// The key used to store the data in storage - pub(crate) key: ::Key, + pub(crate) key: >>::Key, /// A signal that can be used to read and modify the state pub(crate) data: Signal, } @@ -418,7 +418,7 @@ impl Clone for StorageEntry where S: StorageBacking, T: 'static, - ::Key: Clone, + >>::Key: Clone, { fn clone(&self) -> Self { Self { @@ -433,7 +433,7 @@ where S: StorageBacking, { /// Creates a new StorageEntry - pub fn new(key: ::Key, data: T) -> Self { + pub fn new(key: >>::Key, data: T) -> Self { Self { key, data: Signal::new_in_scope( @@ -459,7 +459,7 @@ where } } - fn key(&self) -> &::Key { + fn key(&self) -> &>>::Key { &self.key } @@ -498,12 +498,13 @@ impl, T: Debug> Debug for StorageEntry { pub trait StorageBacking: 'static { type Encoder: StorageEncoder; type Persistence: StoragePersistence< - Value = Option<>::EncodedValue>, - >; + Option, + Value = Option<>::EncodedValue>, + >; /// Gets a value from storage for the given key fn get( - key: &<>::Persistence as StoragePersistence>::Key, + key: &<>::Persistence as StoragePersistence>>::Key, ) -> Option { let loaded = Self::Persistence::load(key); match loaded { @@ -515,8 +516,10 @@ pub trait StorageBacking: 'static { /// Sets a value in storage for the given key /// /// TODO: this provides no way to clear (store None) - fn set(key: &<>::Persistence as StoragePersistence>::Key, value: &T) - where + fn set( + key: &<>::Persistence as StoragePersistence>>::Key, + value: &T, + ) where T: 'static + Clone + Send + Sync, { let encoded = Self::Encoder::serialize(value); @@ -525,7 +528,7 @@ pub trait StorageBacking: 'static { } /// A trait for the persistence portion of StorageBacking. -pub trait StoragePersistence: 'static { +pub trait StoragePersistence: 'static { /// The key type used to store data in storage type Key: PartialEq + Debug + Send + Sync + 'static; /// The type of value which can be stored. @@ -533,7 +536,7 @@ pub trait StoragePersistence: 'static { /// Gets a value from storage for the given key fn load(key: &Self::Key) -> Self::Value; /// Sets a value in storage for the given key - fn store(key: &Self::Key, value: &Self::Value, unencoded: &T); + fn store(key: &Self::Key, value: &Self::Value, unencoded: &T); } /// New trait which can be implemented to define a data format for storage. @@ -549,7 +552,8 @@ pub trait StorageEncoder: 'static { /// /// I'm not sure if this is the best way to abstract that. #[derive(Clone)] -pub struct LayeredStorage> { +pub struct LayeredStorage>, Encoder: StorageEncoder> +{ persistence: PhantomData, encoder: PhantomData, value: PhantomData, @@ -563,7 +567,7 @@ pub struct LayeredStorage>, + P: StoragePersistence, Value = Option>, E: StorageEncoder, > StorageBacking for LayeredStorage { @@ -574,18 +578,22 @@ impl< impl< T: 'static + Clone + Send + Sync + Serialize + DeserializeOwned, Value, - P: StoragePersistence> + P: StoragePersistence, Value = Option> + StorageSubscriber + StorageBacking, E: StorageEncoder, > StorageSubscriber> for LayeredStorage { - fn subscribe(key: &

::Key) -> Receiver { + fn subscribe( + key: &

>>::Key, + ) -> Receiver { P::subscribe(key) } fn unsubscribe( - key: &< as StorageBacking>::Persistence as StoragePersistence>::Key, + key: &< as StorageBacking>::Persistence as StoragePersistence< + Option, + >>::Key, ) { P::unsubscribe(key) } @@ -595,10 +603,10 @@ impl< pub trait StorageSubscriber> { /// Subscribes to events from a storage backing for the given key fn subscribe( - key: &::Key, + key: &>>::Key, ) -> Receiver; /// Unsubscribes from events from a storage backing for the given key - fn unsubscribe(key: &::Key); + fn unsubscribe(key: &>>::Key); } /// A struct to hold information about processing a storage event. @@ -613,7 +621,7 @@ pub struct StorageSubscription { impl StorageSubscription { pub fn new + StorageSubscriber, T: Send + Sync + 'static>( tx: Sender, - key: ::Key, + key: >>::Key, ) -> Self { let getter = move || { let data = S::get(&key).unwrap(); From 46163369450671299046885ab626774aa84e276e Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Fri, 29 Aug 2025 20:14:30 -0700 Subject: [PATCH 13/37] StorageSubscription references StoragePersistence not StorageBacking --- packages/storage/src/lib.rs | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index 3657c91..7a3dc33 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -155,7 +155,7 @@ pub fn use_synced_storage( init: impl FnOnce() -> T, ) -> Signal where - S: Clone + StorageBacking + StorageSubscriber, + S: Clone + StorageBacking + StorageSubscriber, >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { @@ -174,7 +174,7 @@ pub fn new_synced_storage( init: impl FnOnce() -> T, ) -> Signal where - S: Clone + StorageBacking + StorageSubscriber, + S: Clone + StorageBacking + StorageSubscriber, >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { @@ -219,7 +219,7 @@ pub fn use_synced_storage_entry( init: impl FnOnce() -> T, ) -> SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { @@ -250,7 +250,7 @@ pub fn new_synced_storage_entry( init: impl FnOnce() -> T, ) -> SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Clone + PartialEq + Send + Sync + 'static, { let data = get_from_storage::(&key, init); @@ -321,7 +321,7 @@ pub struct SyncedStorageEntry, T: 'static> { impl Clone for SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, >>::Key: Clone, T: 'static, { @@ -335,7 +335,7 @@ where impl SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, { pub fn new(key: >>::Key, data: T) -> Self { let channel = S::subscribe(&key); @@ -378,7 +378,7 @@ where impl StorageEntryTrait for SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Send + Sync + PartialEq + 'static, { fn save(&self) { @@ -565,7 +565,7 @@ pub struct LayeredStorage>, Encoder /// P: Stores a Option /// E: Translated between T and Value impl< - T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, + T: 'static, Value, P: StoragePersistence, Value = Option>, E: StorageEncoder, @@ -576,13 +576,13 @@ impl< } impl< - T: 'static + Clone + Send + Sync + Serialize + DeserializeOwned, + T: 'static, Value, P: StoragePersistence, Value = Option> + StorageSubscriber + StorageBacking, E: StorageEncoder, -> StorageSubscriber> for LayeredStorage +> StorageSubscriber for LayeredStorage { fn subscribe( key: &

>>::Key, @@ -600,13 +600,13 @@ impl< } /// A trait for a subscriber to events from a storage backing -pub trait StorageSubscriber> { +pub trait StorageSubscriber>> { /// Subscribes to events from a storage backing for the given key fn subscribe( - key: &>>::Key, + key: &>>::Key, ) -> Receiver; /// Unsubscribes from events from a storage backing for the given key - fn unsubscribe(key: &>>::Key); + fn unsubscribe(key: &>>::Key); } /// A struct to hold information about processing a storage event. @@ -619,7 +619,10 @@ pub struct StorageSubscription { } impl StorageSubscription { - pub fn new + StorageSubscriber, T: Send + Sync + 'static>( + pub fn new< + S: StorageBacking + StorageSubscriber, + T: Send + Sync + 'static, + >( tx: Sender, key: >>::Key, ) -> Self { From a581bcd75cd9c1c49dea3a1d149a691c028c95b9 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Fri, 29 Aug 2025 20:29:44 -0700 Subject: [PATCH 14/37] Make use_persistent and related use LocalStorage to match docs --- packages/storage/src/client_storage/fs.rs | 1 + packages/storage/src/client_storage/memory.rs | 3 ++- packages/storage/src/client_storage/web.rs | 10 ++++++++-- packages/storage/src/persistence.rs | 10 ++++++---- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/storage/src/client_storage/fs.rs b/packages/storage/src/client_storage/fs.rs index cb76620..3071536 100644 --- a/packages/storage/src/client_storage/fs.rs +++ b/packages/storage/src/client_storage/fs.rs @@ -61,6 +61,7 @@ fn get(key: &str) -> Option { std::fs::read_to_string(path).ok() } +/// [StoragePersistence] backed by a file. #[derive(Clone)] pub struct LocalStorage; diff --git a/packages/storage/src/client_storage/memory.rs b/packages/storage/src/client_storage/memory.rs index ea4c5f1..ce71963 100644 --- a/packages/storage/src/client_storage/memory.rs +++ b/packages/storage/src/client_storage/memory.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use crate::{StorageBacking, StorageEncoder, StoragePersistence}; +/// [StoragePersistence] backed by the current [SessionStore]. #[derive(Clone)] pub struct SessionStorage; @@ -45,7 +46,7 @@ impl StoragePersistence for SessionStorage { } } -/// A StorageEncoder which encodes Optional data by cloning it's content into `Arc` +/// A [StorageEncoder] which encodes Optional data by cloning it's content into `Arc` pub struct ArcEncoder; impl StorageEncoder for ArcEncoder { diff --git a/packages/storage/src/client_storage/web.rs b/packages/storage/src/client_storage/web.rs index 3c5c0ea..ef283fe 100644 --- a/packages/storage/src/client_storage/web.rs +++ b/packages/storage/src/client_storage/web.rs @@ -25,6 +25,7 @@ impl StorageBac type Persistence = LocalStorage; } +/// [WebStorageType::Local] backed [StoragePersistence]. #[derive(Clone)] pub struct LocalStorage; @@ -38,7 +39,7 @@ impl StoragePersistence for LocalStorage { } fn store(key: &Self::Key, value: &Self::Value, _unencoded: &T) { - set_or_clear(key.clone(), value.as_deref(), WebStorageType::Local) + store(key, value, WebStorageType::Local) } } @@ -104,6 +105,7 @@ static SUBSCRIPTIONS: Lazy>>> = Arc::new(RwLock::new(HashMap::new())) }); +/// [WebStorageType::Session] backed [StoragePersistence]. #[derive(Clone)] pub struct SessionStorage; @@ -117,10 +119,14 @@ impl StoragePersistence for SessionStorage { } fn store(key: &Self::Key, value: &Self::Value, _unencoded: &T) { - set_or_clear(key.clone(), value.as_deref(), WebStorageType::Session) + store(key, value, WebStorageType::Session) } } +fn store(key: &String, value: &Option, storage_type: WebStorageType) { + set_or_clear(key.clone(), value.as_deref(), WebStorageType::Session) +} + fn set_or_clear(key: String, value: Option<&str>, storage_type: WebStorageType) { match value { Some(str) => set(key, &str, storage_type), diff --git a/packages/storage/src/persistence.rs b/packages/storage/src/persistence.rs index 64ee5f2..79b4f4a 100644 --- a/packages/storage/src/persistence.rs +++ b/packages/storage/src/persistence.rs @@ -1,4 +1,6 @@ -use crate::SessionStorage; +//! Storage utilities which implicitly use [LocalStorage]. + +use crate::LocalStorage; use crate::{new_storage_entry, use_hydrate_storage}; use dioxus::prelude::*; use dioxus_signals::Signal; @@ -18,7 +20,7 @@ pub fn use_persistent< ) -> Signal { let mut init = Some(init); let storage = use_hook(|| new_persistent(key.to_string(), || init.take().unwrap()())); - use_hydrate_storage::(storage, init); + use_hydrate_storage::(storage, init); storage } @@ -31,7 +33,7 @@ pub fn new_persistent< key: impl ToString, init: impl FnOnce() -> T, ) -> Signal { - let storage_entry = new_storage_entry::(key.to_string(), init); + let storage_entry = new_storage_entry::(key.to_string(), init); storage_entry.save_to_storage_on_change(); storage_entry.data } @@ -48,7 +50,7 @@ pub fn use_singleton_persistent< ) -> Signal { let mut init = Some(init); let signal = use_hook(|| new_singleton_persistent(|| init.take().unwrap()())); - use_hydrate_storage::(signal, init); + use_hydrate_storage::(signal, init); signal } From 314a4dc2b7511675ca6da827f387b90e138ad7f1 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Fri, 29 Aug 2025 21:37:56 -0700 Subject: [PATCH 15/37] Make StorageEntry use StoragePersistence instead of StorageBacking, and some cleanup --- examples/storage/src/main.rs | 33 +++++++++------ packages/storage/src/client_storage/memory.rs | 2 + packages/storage/src/client_storage/web.rs | 2 +- packages/storage/src/lib.rs | 42 +++++++++---------- packages/storage/src/persistence.rs | 4 +- 5 files changed, 47 insertions(+), 36 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 6114fa3..cf5660e 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -80,57 +80,64 @@ fn Home() -> Element { #[component] fn Storage() -> Element { - let mut count_session = use_singleton_persistent(|| 0); - let mut count_local = use_synced_storage::("synced".to_string(), || 0); + // Uses default encoder and LocalStorage implicitly. + let mut count_persistent = use_persistent("persistent".to_string(), || 0); + // Uses session storage with the default encoder. + let mut count_session = use_storage::("session".to_string(), || 0); + + // Uses local storage with the default encoder. + let mut count_local = use_synced_storage::("local".to_string(), || 0); + + // Uses LocalStorage with our custom human readable encoder let mut count_local_human = use_synced_storage::, i32>( - "synced_human".to_string(), + "local_human".to_string(), || 0, ); - let mut in_memory = use_storage::("memory".to_string(), || 0); - rsx!( div { button { onclick: move |_| { - *count_session.write() += 1; + *count_persistent.write() += 1; }, "Click me!" } - "I persist for the current session. Clicked {count_session} times." + "Persisted (but not synced): Clicked {count_persistent} times." } div { button { onclick: move |_| { - *count_local.write() += 1; + *count_session.write() += 1; }, "Click me!" } - "I persist across all sessions. Clicked {count_local} times." + "Session: Clicked {count_session} times." } div { button { onclick: move |_| { - *count_local_human.write() += 1; + *count_local.write() += 1; }, "Click me!" } - "I persist a human readable value across all sessions. Clicked {count_local_human} times." + "Local: Clicked {count_local} times." } div { button { onclick: move |_| { - *in_memory.write() += 1; + *count_local_human.write() += 1; }, "Click me!" } - "I persist a value without encoding, in memory. Clicked {in_memory} times." + "Human readable persisted: Clicked {count_local_human} times." } ) } // Define a "human readable" storage format which is pretty printed JSON instead of a compressed binary format. +// +// `Storage` must have `Value=Option` for this to work as that is what the encoder encodes to. type HumanReadableStorage = LayeredStorage; #[derive(Clone)] diff --git a/packages/storage/src/client_storage/memory.rs b/packages/storage/src/client_storage/memory.rs index ce71963..5dd56d2 100644 --- a/packages/storage/src/client_storage/memory.rs +++ b/packages/storage/src/client_storage/memory.rs @@ -8,6 +8,8 @@ use std::sync::Arc; use crate::{StorageBacking, StorageEncoder, StoragePersistence}; /// [StoragePersistence] backed by the current [SessionStore]. +/// +/// Skips encoding, and just stores data using `Arc>`. #[derive(Clone)] pub struct SessionStorage; diff --git a/packages/storage/src/client_storage/web.rs b/packages/storage/src/client_storage/web.rs index ef283fe..4c60bb8 100644 --- a/packages/storage/src/client_storage/web.rs +++ b/packages/storage/src/client_storage/web.rs @@ -124,7 +124,7 @@ impl StoragePersistence for SessionStorage { } fn store(key: &String, value: &Option, storage_type: WebStorageType) { - set_or_clear(key.clone(), value.as_deref(), WebStorageType::Session) + set_or_clear(key.clone(), value.as_deref(), storage_type) } fn set_or_clear(key: String, value: Option<&str>, storage_type: WebStorageType) { diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index 7a3dc33..a26f3b2 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -140,7 +140,7 @@ where _ => { // Otherwise the client is rendered normally, so we can just use the storage entry. let storage_entry = new_storage_entry::(key, init); - storage_entry.save_to_storage_on_change(); + StorageEntryTrait::::save_to_storage_on_change(&storage_entry); storage_entry.data } } @@ -201,7 +201,7 @@ where pub fn use_storage_entry( key: >>::Key, init: impl FnOnce() -> T, -) -> StorageEntry +) -> StorageEntry where S: StorageBacking, >>::Key: Clone, @@ -209,7 +209,7 @@ where { let mut init = Some(init); let signal = use_hook(|| new_storage_entry::(key, || init.take().unwrap()())); - use_hydrate_storage::(*signal.data(), init); + use_hydrate_storage::(*StorageEntryTrait::::data(&signal), init); signal } @@ -233,7 +233,7 @@ where pub fn new_storage_entry( key: >>::Key, init: impl FnOnce() -> T, -) -> StorageEntry +) -> StorageEntry where S: StorageBacking, T: Send + Sync + 'static, @@ -314,7 +314,7 @@ pub trait StorageEntryTrait, T: 'static>: 'static { /// A wrapper around StorageEntry that provides a channel to subscribe to updates to the underlying storage. pub struct SyncedStorageEntry, T: 'static> { /// The underlying StorageEntry that is used to store the data and track the state - pub(crate) entry: StorageEntry, + pub(crate) entry: StorageEntry, /// The channel to subscribe to updates to the underlying storage pub(crate) channel: Receiver, } @@ -390,15 +390,15 @@ where return; } } - self.entry.save(); + StorageEntryTrait::::save(&self.entry); } fn update(&mut self) { - self.entry.update(); + StorageEntryTrait::::update(&mut self.entry); } fn key(&self) -> &>>::Key { - self.entry.key() + StorageEntryTrait::::key(&self.entry) } fn data(&self) -> &Signal { @@ -407,18 +407,18 @@ where } /// A storage entry that can be used to store data across application reloads. It optionally provides a channel to subscribe to updates to the underlying storage. -pub struct StorageEntry, T: 'static> { +pub struct StorageEntry>, T: 'static> { /// The key used to store the data in storage - pub(crate) key: >>::Key, + pub(crate) key: P::Key, /// A signal that can be used to read and modify the state pub(crate) data: Signal, } -impl Clone for StorageEntry +impl Clone for StorageEntry where - S: StorageBacking, + P: StoragePersistence>, T: 'static, - >>::Key: Clone, + P::Key: Clone, { fn clone(&self) -> Self { Self { @@ -428,12 +428,12 @@ where } } -impl StorageEntry +impl StorageEntry where - S: StorageBacking, + P: StoragePersistence>, { /// Creates a new StorageEntry - pub fn new(key: >>::Key, data: T) -> Self { + pub fn new(key: P::Key, data: T) -> Self { Self { key, data: Signal::new_in_scope( @@ -444,7 +444,7 @@ where } } -impl StorageEntryTrait for StorageEntry +impl, T: Clone> StorageEntryTrait for StorageEntry where S: StorageBacking, T: PartialEq + Send + Sync + 'static, @@ -468,7 +468,7 @@ where } } -impl, T: Send + Sync> Deref for StorageEntry { +impl>, T: Send + Sync> Deref for StorageEntry { type Target = Signal; fn deref(&self) -> &Signal { @@ -476,19 +476,19 @@ impl, T: Send + Sync> Deref for StorageEntry { } } -impl, T: Send + Sync> DerefMut for StorageEntry { +impl>, T: Send + Sync> DerefMut for StorageEntry { fn deref_mut(&mut self) -> &mut Signal { &mut self.data } } -impl, T: Display> Display for StorageEntry { +impl>, T: Display> Display for StorageEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.data.fmt(f) } } -impl, T: Debug> Debug for StorageEntry { +impl>, T: Debug> Debug for StorageEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.data.fmt(f) } diff --git a/packages/storage/src/persistence.rs b/packages/storage/src/persistence.rs index 79b4f4a..c733b80 100644 --- a/packages/storage/src/persistence.rs +++ b/packages/storage/src/persistence.rs @@ -1,4 +1,6 @@ //! Storage utilities which implicitly use [LocalStorage]. +//! +//! These do not sync: if another session writes to them it will not trigger an update. use crate::LocalStorage; use crate::{new_storage_entry, use_hydrate_storage}; @@ -34,7 +36,7 @@ pub fn new_persistent< init: impl FnOnce() -> T, ) -> Signal { let storage_entry = new_storage_entry::(key.to_string(), init); - storage_entry.save_to_storage_on_change(); + StorageEntryTrait::::save_to_storage_on_change(&storage_entry); storage_entry.data } From 5ea76ca3e65dee0e386f418828797819390f6f0d Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 10:22:02 -0700 Subject: [PATCH 16/37] Add todos for issues --- examples/storage/src/main.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index cf5660e..343a80f 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -80,15 +80,19 @@ fn Home() -> Element { #[component] fn Storage() -> Element { + // TODO: maybe this should sync? (It does not currently) // Uses default encoder and LocalStorage implicitly. let mut count_persistent = use_persistent("persistent".to_string(), || 0); + // TODO: in web multiple tabs share this state: it does not seem to use session storage. // Uses session storage with the default encoder. let mut count_session = use_storage::("session".to_string(), || 0); + // TODO: this does not sync in web // Uses local storage with the default encoder. let mut count_local = use_synced_storage::("local".to_string(), || 0); + // TODO: this does not sync in web // Uses LocalStorage with our custom human readable encoder let mut count_local_human = use_synced_storage::, i32>( "local_human".to_string(), From 74b1cc43f36958f8983caed0ebb6c02e11a3e91c Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 10:52:05 -0700 Subject: [PATCH 17/37] Fix web session storage --- examples/storage/src/main.rs | 5 ++--- packages/storage/src/client_storage/web.rs | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 343a80f..573e6e9 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -84,7 +84,6 @@ fn Storage() -> Element { // Uses default encoder and LocalStorage implicitly. let mut count_persistent = use_persistent("persistent".to_string(), || 0); - // TODO: in web multiple tabs share this state: it does not seem to use session storage. // Uses session storage with the default encoder. let mut count_session = use_storage::("session".to_string(), || 0); @@ -107,7 +106,7 @@ fn Storage() -> Element { }, "Click me!" } - "Persisted (but not synced): Clicked {count_persistent} times." + "Persisted to local storage (but not synced): Clicked {count_persistent} times." } div { button { @@ -134,7 +133,7 @@ fn Storage() -> Element { }, "Click me!" } - "Human readable persisted: Clicked {count_local_human} times." + "Human readable local: Clicked {count_local_human} times." } ) } diff --git a/packages/storage/src/client_storage/web.rs b/packages/storage/src/client_storage/web.rs index 4c60bb8..c9053f9 100644 --- a/packages/storage/src/client_storage/web.rs +++ b/packages/storage/src/client_storage/web.rs @@ -22,7 +22,7 @@ impl StorageBac for SessionStorage { type Encoder = DefaultEncoder; - type Persistence = LocalStorage; + type Persistence = SessionStorage; } /// [WebStorageType::Local] backed [StoragePersistence]. @@ -109,7 +109,7 @@ static SUBSCRIPTIONS: Lazy>>> = #[derive(Clone)] pub struct SessionStorage; -/// LocalStorage stores Option. +/// SessionStorage stores Option. impl StoragePersistence for SessionStorage { type Key = String; type Value = Option; From 6cd6b6591a9e9937883a51e0a0a6aa83ab739437 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 11:00:49 -0700 Subject: [PATCH 18/37] Better document storage type selection in "persistence" --- packages/storage/src/persistence.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/storage/src/persistence.rs b/packages/storage/src/persistence.rs index c733b80..0fb367c 100644 --- a/packages/storage/src/persistence.rs +++ b/packages/storage/src/persistence.rs @@ -1,6 +1,10 @@ //! Storage utilities which implicitly use [LocalStorage]. //! //! These do not sync: if another session writes to them it will not trigger an update. +//! +//! TODO: +//! Documentation implies this just needs to live across reloads, which for web would be session storage, but for desktop would require local storage. +//! Since docs currently say "local storage" local storage is being used. use crate::LocalStorage; use crate::{new_storage_entry, use_hydrate_storage}; @@ -11,6 +15,8 @@ use serde::de::DeserializeOwned; use super::StorageEntryTrait; +type Storage = LocalStorage; + /// A persistent storage hook that can be used to store data across application reloads. /// /// Depending on the platform this uses either local storage or a file storage @@ -22,7 +28,7 @@ pub fn use_persistent< ) -> Signal { let mut init = Some(init); let storage = use_hook(|| new_persistent(key.to_string(), || init.take().unwrap()())); - use_hydrate_storage::(storage, init); + use_hydrate_storage::(storage, init); storage } @@ -35,8 +41,8 @@ pub fn new_persistent< key: impl ToString, init: impl FnOnce() -> T, ) -> Signal { - let storage_entry = new_storage_entry::(key.to_string(), init); - StorageEntryTrait::::save_to_storage_on_change(&storage_entry); + let storage_entry = new_storage_entry::(key.to_string(), init); + StorageEntryTrait::::save_to_storage_on_change(&storage_entry); storage_entry.data } @@ -52,7 +58,7 @@ pub fn use_singleton_persistent< ) -> Signal { let mut init = Some(init); let signal = use_hook(|| new_singleton_persistent(|| init.take().unwrap()())); - use_hydrate_storage::(signal, init); + use_hydrate_storage::(signal, init); signal } From 01bf9f6d13eebdd8395311011efb3ad0ba216699 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 12:04:27 -0700 Subject: [PATCH 19/37] Fix sync of local storage for web --- examples/storage/src/main.rs | 1 - packages/storage/src/lib.rs | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 573e6e9..01c0b42 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -87,7 +87,6 @@ fn Storage() -> Element { // Uses session storage with the default encoder. let mut count_session = use_storage::("session".to_string(), || 0); - // TODO: this does not sync in web // Uses local storage with the default encoder. let mut count_local = use_synced_storage::("local".to_string(), || 0); diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index a26f3b2..c8b0215 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -369,6 +369,7 @@ where .downcast_ref::>() .expect("Type mismatch with storage entry") .clone() + // Currently there is no API exposed to clear storage, so it should never be changed to None .expect("Expected storage entry to be Some"); } } @@ -618,6 +619,7 @@ pub struct StorageSubscription { pub(crate) tx: Arc>, } +/// Sends an Option over the channel, with None representing the storage being empty. impl StorageSubscription { pub fn new< S: StorageBacking + StorageSubscriber, @@ -627,7 +629,7 @@ impl StorageSubscription { key: >>::Key, ) -> Self { let getter = move || { - let data = S::get(&key).unwrap(); + let data = S::get(&key); StorageChannelPayload::new(data) }; Self { From 0c5665cb15762a0b6dcc4284d002ce3ff0fe01f4 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 12:52:48 -0700 Subject: [PATCH 20/37] Add some tracking --- examples/storage/src/main.rs | 4 +++- packages/storage/src/lib.rs | 13 ++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 01c0b42..b44e77d 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -1,4 +1,4 @@ -use dioxus::prelude::*; +use dioxus::{logger::tracing, prelude::*}; use dioxus_storage::*; use serde::{de::DeserializeOwned, Serialize}; @@ -80,6 +80,8 @@ fn Home() -> Element { #[component] fn Storage() -> Element { + tracing::debug!("-- Start --"); + // TODO: maybe this should sync? (It does not currently) // Uses default encoder and LocalStorage implicitly. let mut count_persistent = use_persistent("persistent".to_string(), || 0); diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index c8b0215..dff27aa 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -31,7 +31,7 @@ mod default_encoder; mod persistence; pub use client_storage::{LocalStorage, SessionStorage}; -use dioxus::logger::tracing::trace; +use dioxus::logger::tracing::{self, trace}; use futures_util::stream::StreamExt; pub use persistence::{ new_persistent, new_singleton_persistent, use_persistent, use_singleton_persistent, @@ -510,8 +510,15 @@ pub trait StorageBacking: 'static { let loaded = Self::Persistence::load(key); match loaded { // TODO: this treats None the same as failed decodes - Some(x) => Self::Encoder::deserialize(&x).ok(), - None => None, + Some(x) => { + let deserialized = Self::Encoder::deserialize(&x); + tracing::debug!("Deserialized error: {0:?}", deserialized.as_ref().err()); + deserialized.ok() + } + None => { + tracing::debug!("Got None for key {key:?}"); + None + } } } /// Sets a value in storage for the given key From f24db9c8746e5386bfd3409253ba87bd6cd0688c Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 13:28:10 -0700 Subject: [PATCH 21/37] Fix sync with custom encoding --- examples/storage/src/main.rs | 3 -- packages/storage/src/client_storage/fs.rs | 11 +++--- packages/storage/src/client_storage/web.rs | 11 +++--- packages/storage/src/lib.rs | 41 ++++++++-------------- 4 files changed, 29 insertions(+), 37 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index b44e77d..e92e21a 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -80,8 +80,6 @@ fn Home() -> Element { #[component] fn Storage() -> Element { - tracing::debug!("-- Start --"); - // TODO: maybe this should sync? (It does not currently) // Uses default encoder and LocalStorage implicitly. let mut count_persistent = use_persistent("persistent".to_string(), || 0); @@ -92,7 +90,6 @@ fn Storage() -> Element { // Uses local storage with the default encoder. let mut count_local = use_synced_storage::("local".to_string(), || 0); - // TODO: this does not sync in web // Uses LocalStorage with our custom human readable encoder let mut count_local_human = use_synced_storage::, i32>( "local_human".to_string(), diff --git a/packages/storage/src/client_storage/fs.rs b/packages/storage/src/client_storage/fs.rs index 3071536..62f7374 100644 --- a/packages/storage/src/client_storage/fs.rs +++ b/packages/storage/src/client_storage/fs.rs @@ -1,4 +1,4 @@ -use crate::{StorageChannelPayload, StorageSubscription}; +use crate::{StorageBacking, StorageChannelPayload, StorageEncoder, StorageSubscription}; use dioxus::logger::tracing::trace; use serde::Serialize; use serde::de::DeserializeOwned; @@ -94,8 +94,11 @@ impl StoragePersistence for LocalStorage { // Note that this module contains an optimization that differs from the web version. Dioxus Desktop runs all windows in // the same thread, meaning that we can just directly notify the subscribers via the same channels, rather than using the // storage event listener. -impl - StorageSubscriber for LocalStorage +impl< + T: Send + Sync + Serialize + DeserializeOwned + Clone + 'static, + E: StorageEncoder, + S: StorageBacking, +> StorageSubscriber for LocalStorage { fn subscribe(key: &String) -> Receiver { // Initialize the subscriptions map if it hasn't been initialized yet. @@ -109,7 +112,7 @@ impl None => { drop(read_binding); let (tx, rx) = channel::(StorageChannelPayload::default()); - let subscription = StorageSubscription::new::(tx, key.clone()); + let subscription = StorageSubscription::new::(tx, key.clone()); subscriptions .write() diff --git a/packages/storage/src/client_storage/web.rs b/packages/storage/src/client_storage/web.rs index c9053f9..8db301f 100644 --- a/packages/storage/src/client_storage/web.rs +++ b/packages/storage/src/client_storage/web.rs @@ -13,7 +13,7 @@ use wasm_bindgen::prelude::Closure; use web_sys::{Storage, window}; use crate::{ - StorageBacking, StorageChannelPayload, StoragePersistence, StorageSubscriber, + StorageBacking, StorageChannelPayload, StorageEncoder, StoragePersistence, StorageSubscriber, StorageSubscription, default_encoder::DefaultEncoder, }; @@ -43,8 +43,11 @@ impl StoragePersistence for LocalStorage { } } -impl - StorageSubscriber for LocalStorage +impl< + T: Send + Sync + Serialize + DeserializeOwned + Clone + 'static, + E: StorageEncoder, + S: StorageBacking, +> StorageSubscriber for LocalStorage { fn subscribe(key: &String) -> Receiver { let read_binding = SUBSCRIPTIONS.read().unwrap(); @@ -53,7 +56,7 @@ impl None => { drop(read_binding); let (tx, rx) = channel::(StorageChannelPayload::default()); - let subscription = StorageSubscription::new::(tx, key.clone()); + let subscription = StorageSubscription::new::(tx, key.clone()); SUBSCRIPTIONS .write() .unwrap() diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index dff27aa..fc95042 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -155,7 +155,7 @@ pub fn use_synced_storage( init: impl FnOnce() -> T, ) -> Signal where - S: Clone + StorageBacking + StorageSubscriber, + S: Clone + StorageBacking + StorageSubscriber, >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { @@ -174,7 +174,7 @@ pub fn new_synced_storage( init: impl FnOnce() -> T, ) -> Signal where - S: Clone + StorageBacking + StorageSubscriber, + S: Clone + StorageBacking + StorageSubscriber, >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { @@ -219,7 +219,7 @@ pub fn use_synced_storage_entry( init: impl FnOnce() -> T, ) -> SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { @@ -250,7 +250,7 @@ pub fn new_synced_storage_entry( init: impl FnOnce() -> T, ) -> SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Clone + PartialEq + Send + Sync + 'static, { let data = get_from_storage::(&key, init); @@ -321,7 +321,7 @@ pub struct SyncedStorageEntry, T: 'static> { impl Clone for SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, >>::Key: Clone, T: 'static, { @@ -335,7 +335,7 @@ where impl SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, { pub fn new(key: >>::Key, data: T) -> Self { let channel = S::subscribe(&key); @@ -379,7 +379,7 @@ where impl StorageEntryTrait for SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, T: Send + Sync + PartialEq + 'static, { fn save(&self) { @@ -586,35 +586,27 @@ impl< impl< T: 'static, Value, - P: StoragePersistence, Value = Option> - + StorageSubscriber - + StorageBacking, + P: StoragePersistence, Value = Option> + StorageSubscriber, E: StorageEncoder, -> StorageSubscriber for LayeredStorage +> StorageSubscriber for LayeredStorage { - fn subscribe( - key: &

>>::Key, - ) -> Receiver { + fn subscribe(key: &P::Key) -> Receiver { P::subscribe(key) } - fn unsubscribe( - key: &< as StorageBacking>::Persistence as StoragePersistence< - Option, - >>::Key, - ) { + fn unsubscribe(key: &P::Key) { P::unsubscribe(key) } } /// A trait for a subscriber to events from a storage backing -pub trait StorageSubscriber>> { +pub trait StorageSubscriber> { /// Subscribes to events from a storage backing for the given key fn subscribe( - key: &>>::Key, + key: &>>::Key, ) -> Receiver; /// Unsubscribes from events from a storage backing for the given key - fn unsubscribe(key: &>>::Key); + fn unsubscribe(key: &>>::Key); } /// A struct to hold information about processing a storage event. @@ -628,10 +620,7 @@ pub struct StorageSubscription { /// Sends an Option over the channel, with None representing the storage being empty. impl StorageSubscription { - pub fn new< - S: StorageBacking + StorageSubscriber, - T: Send + Sync + 'static, - >( + pub fn new, T: Send + Sync + 'static>( tx: Sender, key: >>::Key, ) -> Self { From bd4b945096107d0cd1b6acc33c0e8eb2443d0084 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 13:30:27 -0700 Subject: [PATCH 22/37] use trace --- packages/storage/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index fc95042..55cce94 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -509,14 +509,14 @@ pub trait StorageBacking: 'static { ) -> Option { let loaded = Self::Persistence::load(key); match loaded { - // TODO: this treats None the same as failed decodes + // TODO: this treats None the same as failed decodes. Some(x) => { let deserialized = Self::Encoder::deserialize(&x); - tracing::debug!("Deserialized error: {0:?}", deserialized.as_ref().err()); + trace!("Deserialized error: {0:?}", deserialized.as_ref().err()); deserialized.ok() } None => { - tracing::debug!("Got None for key {key:?}"); + trace!("Got None for key {key:?}"); None } } From d58fc0d612a707f34a8c203ea96f2c1e433643e1 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 13:47:40 -0700 Subject: [PATCH 23/37] Use warn! --- examples/storage/src/main.rs | 2 +- packages/storage/src/lib.rs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index e92e21a..1fdea45 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -1,4 +1,4 @@ -use dioxus::{logger::tracing, prelude::*}; +use dioxus::prelude::*; use dioxus_storage::*; use serde::{de::DeserializeOwned, Serialize}; diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index 55cce94..2ed2977 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -31,7 +31,7 @@ mod default_encoder; mod persistence; pub use client_storage::{LocalStorage, SessionStorage}; -use dioxus::logger::tracing::{self, trace}; +use dioxus::logger::tracing::{trace, warn}; use futures_util::stream::StreamExt; pub use persistence::{ new_persistent, new_singleton_persistent, use_persistent, use_singleton_persistent, @@ -512,11 +512,13 @@ pub trait StorageBacking: 'static { // TODO: this treats None the same as failed decodes. Some(x) => { let deserialized = Self::Encoder::deserialize(&x); - trace!("Deserialized error: {0:?}", deserialized.as_ref().err()); + if let Err(err) = &deserialized { + warn!("Deserialization error: {err:?}"); + } deserialized.ok() } None => { - trace!("Got None for key {key:?}"); + warn!("Got None for key {key:?}"); None } } From 0ce57e424d564fc249537e53d505d1d489dccb97 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 14:15:27 -0700 Subject: [PATCH 24/37] Cleanup --- packages/storage/src/lib.rs | 14 +++++--------- packages/storage/src/persistence.rs | 9 +++++---- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index 2ed2977..34d11f4 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -73,7 +73,7 @@ pub fn use_storage( init: impl FnOnce() -> T, ) -> Signal where - S: Clone + StorageBacking, + S: StorageBacking, >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { @@ -127,7 +127,7 @@ pub fn new_storage( init: impl FnOnce() -> T, ) -> Signal where - S: Clone + StorageBacking, + S: StorageBacking, >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { @@ -504,9 +504,7 @@ pub trait StorageBacking: 'static { >; /// Gets a value from storage for the given key - fn get( - key: &<>::Persistence as StoragePersistence>>::Key, - ) -> Option { + fn get(key: &>>::Key) -> Option { let loaded = Self::Persistence::load(key); match loaded { // TODO: this treats None the same as failed decodes. @@ -526,10 +524,8 @@ pub trait StorageBacking: 'static { /// Sets a value in storage for the given key /// /// TODO: this provides no way to clear (store None) - fn set( - key: &<>::Persistence as StoragePersistence>>::Key, - value: &T, - ) where + fn set(key: &>>::Key, value: &T) + where T: 'static + Clone + Send + Sync, { let encoded = Self::Encoder::serialize(value); diff --git a/packages/storage/src/persistence.rs b/packages/storage/src/persistence.rs index 0fb367c..45fc69f 100644 --- a/packages/storage/src/persistence.rs +++ b/packages/storage/src/persistence.rs @@ -1,10 +1,6 @@ //! Storage utilities which implicitly use [LocalStorage]. //! //! These do not sync: if another session writes to them it will not trigger an update. -//! -//! TODO: -//! Documentation implies this just needs to live across reloads, which for web would be session storage, but for desktop would require local storage. -//! Since docs currently say "local storage" local storage is being used. use crate::LocalStorage; use crate::{new_storage_entry, use_hydrate_storage}; @@ -15,6 +11,11 @@ use serde::de::DeserializeOwned; use super::StorageEntryTrait; +/// What storage to use. +/// +/// TODO: +/// Documentation on the APIs implies this just needs to live across reloads, which for web would be session storage, but for desktop would require local storage. +/// Since docs currently say "local storage" local storage is being used. type Storage = LocalStorage; /// A persistent storage hook that can be used to store data across application reloads. From 372b20c52b1917a2b74a426c10a9c437627260bc Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 14:25:48 -0700 Subject: [PATCH 25/37] More cleanup, remove unneeded #[derive(Clone)] --- packages/storage/src/client_storage/fs.rs | 1 - packages/storage/src/client_storage/memory.rs | 2 -- packages/storage/src/client_storage/web.rs | 2 -- packages/storage/src/default_encoder.rs | 1 - packages/storage/src/lib.rs | 22 +++++++++---------- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/storage/src/client_storage/fs.rs b/packages/storage/src/client_storage/fs.rs index 62f7374..6555654 100644 --- a/packages/storage/src/client_storage/fs.rs +++ b/packages/storage/src/client_storage/fs.rs @@ -62,7 +62,6 @@ fn get(key: &str) -> Option { } /// [StoragePersistence] backed by a file. -#[derive(Clone)] pub struct LocalStorage; /// LocalStorage stores Option. diff --git a/packages/storage/src/client_storage/memory.rs b/packages/storage/src/client_storage/memory.rs index 5dd56d2..181825f 100644 --- a/packages/storage/src/client_storage/memory.rs +++ b/packages/storage/src/client_storage/memory.rs @@ -10,7 +10,6 @@ use crate::{StorageBacking, StorageEncoder, StoragePersistence}; /// [StoragePersistence] backed by the current [SessionStore]. /// /// Skips encoding, and just stores data using `Arc>`. -#[derive(Clone)] pub struct SessionStorage; impl StorageBacking for SessionStorage { @@ -67,7 +66,6 @@ impl StorageEncoder for ArcEncoder { } /// An in-memory session store that is tied to the current Dioxus root context. -#[derive(Clone)] struct SessionStore { /// The underlying map of session data. map: Rc>>>, diff --git a/packages/storage/src/client_storage/web.rs b/packages/storage/src/client_storage/web.rs index 8db301f..a97a6ee 100644 --- a/packages/storage/src/client_storage/web.rs +++ b/packages/storage/src/client_storage/web.rs @@ -26,7 +26,6 @@ impl StorageBac } /// [WebStorageType::Local] backed [StoragePersistence]. -#[derive(Clone)] pub struct LocalStorage; /// LocalStorage stores Option. @@ -109,7 +108,6 @@ static SUBSCRIPTIONS: Lazy>>> = }); /// [WebStorageType::Session] backed [StoragePersistence]. -#[derive(Clone)] pub struct SessionStorage; /// SessionStorage stores Option. diff --git a/packages/storage/src/default_encoder.rs b/packages/storage/src/default_encoder.rs index e3afeb5..6942fbf 100644 --- a/packages/storage/src/default_encoder.rs +++ b/packages/storage/src/default_encoder.rs @@ -8,7 +8,6 @@ use serde::de::DeserializeOwned; /// /// Uses a non-human readable format. /// Format uses Serde, and is compressed and then encoded into a utf8 compatible string. -#[derive(Clone)] pub struct DefaultEncoder; impl StorageEncoder for DefaultEncoder { diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index 34d11f4..4a5a372 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -64,7 +64,7 @@ pub use client_storage::{set_dir_name, set_directory}; /// use dioxus_signals::Signal; /// /// // This hook can be used with any storage backing without multiple versions of the hook -/// fn use_user_id() -> Signal where S: StorageBacking { +/// fn use_user_id() -> Signal where S: StorageBacking { /// use_storage::("user-id", || 123) /// } /// ``` @@ -118,7 +118,7 @@ impl StorageMode { /// use dioxus_signals::Signal; /// /// // This hook can be used with any storage backing without multiple versions of the hook -/// fn user_id() -> Signal where S: StorageBacking { +/// fn user_id() -> Signal where S: StorageBacking { /// new_storage::("user-id", || 123) /// } /// ``` @@ -155,7 +155,7 @@ pub fn use_synced_storage( init: impl FnOnce() -> T, ) -> Signal where - S: Clone + StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { @@ -174,7 +174,7 @@ pub fn new_synced_storage( init: impl FnOnce() -> T, ) -> Signal where - S: Clone + StorageBacking + StorageSubscriber, + S: StorageBacking + StorageSubscriber, >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { @@ -230,13 +230,13 @@ where } /// Returns a StorageEntry with the latest value from storage or the init value if it doesn't exist. -pub fn new_storage_entry( +pub fn new_storage_entry( key: >>::Key, init: impl FnOnce() -> T, ) -> StorageEntry where S: StorageBacking, - T: Send + Sync + 'static, + T: Clone + Send + Sync + 'static, { let data = get_from_storage::(&key, init); StorageEntry::new(key, data) @@ -258,7 +258,7 @@ where } /// Returns a value from storage or the init value if it doesn't exist. -pub fn get_from_storage, T: Send + Sync + 'static + Clone>( +pub fn get_from_storage, T: Send + Sync + Clone + 'static>( key: &>>::Key, init: impl FnOnce() -> T, ) -> T { @@ -270,7 +270,7 @@ pub fn get_from_storage, T: Send + Sync + 'static + Clone>( } /// A trait for common functionality between StorageEntry and SyncedStorageEntry -pub trait StorageEntryTrait, T: 'static>: 'static { +pub trait StorageEntryTrait, T>: 'static { /// Saves the current state to storage fn save(&self); @@ -445,16 +445,17 @@ where } } -impl, T: Clone> StorageEntryTrait for StorageEntry +impl, T> StorageEntryTrait for StorageEntry where S: StorageBacking, - T: PartialEq + Send + Sync + 'static, + T: Clone + PartialEq + Send + Sync + 'static, { fn save(&self) { S::set(&self.key, &*self.data.read()); } fn update(&mut self) { + // TODO: does this need to handle the None case? if let Some(value) = S::get(&self.key) { *self.data.write() = value; } @@ -557,7 +558,6 @@ pub trait StorageEncoder: 'static { /// A way to create a StorageEncoder out of the two layers. /// /// I'm not sure if this is the best way to abstract that. -#[derive(Clone)] pub struct LayeredStorage>, Encoder: StorageEncoder> { persistence: PhantomData, From 9b2bc48a90e88ece303aa72f21a3d316d3c238c9 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 15:35:25 -0700 Subject: [PATCH 26/37] Fix doc tests --- packages/storage/src/lib.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index 4a5a372..3f78abd 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -59,13 +59,17 @@ pub use client_storage::{set_dir_name, set_directory}; /// ## Usage /// /// ```rust -/// use dioxus_storage::{use_storage, StorageBacking}; +/// use dioxus_storage::{use_storage, StorageBacking, StoragePersistence}; /// use dioxus::prelude::*; /// use dioxus_signals::Signal; /// /// // This hook can be used with any storage backing without multiple versions of the hook -/// fn use_user_id() -> Signal where S: StorageBacking { -/// use_storage::("user-id", || 123) +/// fn use_user_id() -> Signal +/// where +/// S: StorageBacking, +/// S::Persistence: StoragePersistence, Key = &'static str>, +/// { +/// use_storage::(&"user-id", || 123) /// } /// ``` pub fn use_storage( @@ -113,13 +117,17 @@ impl StorageMode { /// ## Usage /// /// ```rust -/// use dioxus_storage::{new_storage, StorageBacking}; +/// use dioxus_storage::{new_storage, StorageBacking, StoragePersistence}; /// use dioxus::prelude::*; /// use dioxus_signals::Signal; /// /// // This hook can be used with any storage backing without multiple versions of the hook -/// fn user_id() -> Signal where S: StorageBacking { -/// new_storage::("user-id", || 123) +/// fn user_id() -> Signal +/// where +/// S: StorageBacking, +/// S::Persistence: StoragePersistence, Key = &'static str>, +/// { +/// new_storage::(&"user-id", || 123) /// } /// ``` pub fn new_storage( From bfe02a599c0d2551d060056a88c8248d1bd17530 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 15:35:35 -0700 Subject: [PATCH 27/37] fix desktop build --- packages/storage/src/client_storage/memory.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/storage/src/client_storage/memory.rs b/packages/storage/src/client_storage/memory.rs index 181825f..7421e78 100644 --- a/packages/storage/src/client_storage/memory.rs +++ b/packages/storage/src/client_storage/memory.rs @@ -66,6 +66,7 @@ impl StorageEncoder for ArcEncoder { } /// An in-memory session store that is tied to the current Dioxus root context. +#[derive(Clone)] struct SessionStore { /// The underlying map of session data. map: Rc>>>, From b9e2790d8ade46b4d0d0fe52317895713be23267 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 15:35:55 -0700 Subject: [PATCH 28/37] unit test default encoder --- packages/storage/src/default_encoder.rs | 56 ++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/storage/src/default_encoder.rs b/packages/storage/src/default_encoder.rs index 6942fbf..7a77b47 100644 --- a/packages/storage/src/default_encoder.rs +++ b/packages/storage/src/default_encoder.rs @@ -7,7 +7,7 @@ use serde::de::DeserializeOwned; /// Default [StorageEncoder]. /// /// Uses a non-human readable format. -/// Format uses Serde, and is compressed and then encoded into a utf8 compatible string. +/// Format uses Serde, and is compressed and then encoded into a utf8 compatible string using Hex. pub struct DefaultEncoder; impl StorageEncoder for DefaultEncoder { @@ -73,3 +73,57 @@ fn try_serde_from_string(value: &str) -> Result Result::Err(fail(format!("yazi Error: {err:?}"))), } } + +#[cfg(test)] +mod tests { + use super::*; + use std::fmt::Debug; + + #[test] + fn round_trips() { + round_trip(0); + round_trip(999); + round_trip("Text".to_string()); + round_trip((1, 2, 3)); + } + + fn round_trip(value: T) { + let encoded = DefaultEncoder::serialize(&value); + let decoded: Result<_, FailedDecode> = DefaultEncoder::deserialize(&encoded); + assert_eq!(value, decoded.unwrap()); + } + + #[test] + fn can_decode_existing_data() { + // This test ensures that data produced at the time of writing of this tests remains decomposable. + // This will fail if, for example, the compression library drops support for this format, our the custom decode logic here changes in an incompatible way (like using base64 encoding). + + // Note that it would be possible to change the encode logic without breaking this (for example use base 64, but prefix data with an escape while keeping the old decode logic). + // In the event of such a change, the test below (stable_encoding) will fail. + // In such a case, the test cases here should NOT be modified. + // These cases should be kept, and the new format should be added to these cases to ensure that the new format remains supported long term. + + assert_eq!( + try_serde_from_string::("78da63000000010001").unwrap(), + 0i32 + ); + + assert_eq!( + try_serde_from_string::("78dacb0e492d2e51082e29cacc4b07001da504a3").unwrap(), + "Test String" + ); + } + + #[test] + fn stable_encoding() { + // This tests that the encoder behavior has not changed. + // The encoding changing isn't really breaking for users unless it also breaks decode of existing data or round trips (see other tests for those). + // If this test does need to be updated, see note in `can_decode_existing_data` about adding additional test cases. + + assert_eq!(DefaultEncoder::serialize(&0), "78da63000000010001"); + assert_eq!( + DefaultEncoder::serialize(&"Test String".to_string()), + "78dacb0e492d2e51082e29cacc4b07001da504a3" + ); + } +} From 0c776cde45cfac511f4b656690dfcd28806cf88a Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 15:44:06 -0700 Subject: [PATCH 29/37] Unit tests HumanReadableStorage --- examples/storage/src/main.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 1fdea45..96f9c4c 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -156,3 +156,39 @@ impl StorageEncoder for HumanReadableEncodin serde_json::to_string_pretty(value).unwrap() } } + +#[cfg(test)] +mod tests { + use super::*; + use std::fmt::Debug; + + #[test] + fn round_trips() { + round_trip(0); + round_trip(999); + round_trip("Text".to_string()); + round_trip((1, 2, 3)); + } + + fn round_trip(value: T) { + let encoded = HumanReadableEncoding::serialize(&value); + let decoded = HumanReadableEncoding::deserialize(&encoded); + assert_eq!(value, decoded.unwrap()); + } + + #[test] + fn can_decode_irregular_json_data() { + let decoded: (i32, i32) = + HumanReadableEncoding::deserialize(&" [ 1,2]".to_string()).unwrap(); + assert_eq!(decoded, (1, 2)) + } + + #[test] + fn encodes_json_with_formatting() { + assert_eq!(HumanReadableEncoding::serialize(&1234), "1234"); + assert_eq!( + HumanReadableEncoding::serialize(&(1, "a".to_string())), + "[\n 1,\n \"a\"\n]" + ); + } +} From 5abe4cce3e2396a7e8398d30780d544df8139c2e Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 16:43:13 -0700 Subject: [PATCH 30/37] Remove need for LayeredStorage --- examples/storage/src/main.rs | 19 ++++-- packages/storage/src/client_storage/memory.rs | 10 ++- packages/storage/src/client_storage/web.rs | 1 - packages/storage/src/default_encoder.rs | 2 +- packages/storage/src/lib.rs | 66 +++++-------------- 5 files changed, 34 insertions(+), 64 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 96f9c4c..681ceed 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -91,7 +91,7 @@ fn Storage() -> Element { let mut count_local = use_synced_storage::("local".to_string(), || 0); // Uses LocalStorage with our custom human readable encoder - let mut count_local_human = use_synced_storage::, i32>( + let mut count_local_human = use_synced_storage::, i32>( "local_human".to_string(), || 0, ); @@ -136,10 +136,19 @@ fn Storage() -> Element { ) } -// Define a "human readable" storage format which is pretty printed JSON instead of a compressed binary format. -// -// `Storage` must have `Value=Option` for this to work as that is what the encoder encodes to. -type HumanReadableStorage = LayeredStorage; +/// A [StorageBacking] with [HumanReadableEncoding] and selectable [StoragePersistence]. +struct HumanReadableStorage { + p: std::marker::PhantomData, +} + +impl< + T: Serialize + DeserializeOwned, + P: 'static + StoragePersistence, Value = Option>, + > StorageBacking for HumanReadableStorage

+{ + type Encoder = HumanReadableEncoding; + type Persistence = P; +} #[derive(Clone)] struct HumanReadableEncoding; diff --git a/packages/storage/src/client_storage/memory.rs b/packages/storage/src/client_storage/memory.rs index 7421e78..0da85d5 100644 --- a/packages/storage/src/client_storage/memory.rs +++ b/packages/storage/src/client_storage/memory.rs @@ -47,17 +47,15 @@ impl StoragePersistence for SessionStorage { } } -/// A [StorageEncoder] which encodes Optional data by cloning it's content into `Arc` +/// A [StorageEncoder] which encodes Optional data by cloning it's content into `Arc`. pub struct ArcEncoder; impl StorageEncoder for ArcEncoder { type EncodedValue = Arc; - type DecodeError = (); + type DecodeError = &'static str; - fn deserialize(loaded: &Self::EncodedValue) -> Result { - let v: &Arc = loaded; - // TODO: Better error message - v.downcast_ref::().cloned().ok_or(()) + fn deserialize(loaded: &Self::EncodedValue) -> Result { + loaded.downcast_ref::().cloned().ok_or("Failed Downcast") } fn serialize(value: &T) -> Self::EncodedValue { diff --git a/packages/storage/src/client_storage/web.rs b/packages/storage/src/client_storage/web.rs index a97a6ee..d02e99d 100644 --- a/packages/storage/src/client_storage/web.rs +++ b/packages/storage/src/client_storage/web.rs @@ -110,7 +110,6 @@ static SUBSCRIPTIONS: Lazy>>> = /// [WebStorageType::Session] backed [StoragePersistence]. pub struct SessionStorage; -/// SessionStorage stores Option. impl StoragePersistence for SessionStorage { type Key = String; type Value = Option; diff --git a/packages/storage/src/default_encoder.rs b/packages/storage/src/default_encoder.rs index 7a77b47..3e93b47 100644 --- a/packages/storage/src/default_encoder.rs +++ b/packages/storage/src/default_encoder.rs @@ -7,7 +7,7 @@ use serde::de::DeserializeOwned; /// Default [StorageEncoder]. /// /// Uses a non-human readable format. -/// Format uses Serde, and is compressed and then encoded into a utf8 compatible string using Hex. +/// Format uses Serde, and is compressed and then encoded into a utf8 compatible string using hexadecimal. pub struct DefaultEncoder; impl StorageEncoder for DefaultEncoder { diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index 3f78abd..c427d5c 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -37,7 +37,6 @@ pub use persistence::{ new_persistent, new_singleton_persistent, use_persistent, use_singleton_persistent, }; use std::cell::RefCell; -use std::marker::PhantomData; use std::rc::Rc; use dioxus::prelude::*; @@ -163,7 +162,8 @@ pub fn use_synced_storage( init: impl FnOnce() -> T, ) -> Signal where - S: StorageBacking + StorageSubscriber, + S: StorageBacking, + S::Persistence: StorageSubscriber, >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { @@ -182,7 +182,8 @@ pub fn new_synced_storage( init: impl FnOnce() -> T, ) -> Signal where - S: StorageBacking + StorageSubscriber, + S: StorageBacking, + S::Persistence: StorageSubscriber, >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { @@ -227,7 +228,8 @@ pub fn use_synced_storage_entry( init: impl FnOnce() -> T, ) -> SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking, + S::Persistence: StorageSubscriber, >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { @@ -258,7 +260,8 @@ pub fn new_synced_storage_entry( init: impl FnOnce() -> T, ) -> SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking, + S::Persistence: StorageSubscriber, T: Clone + PartialEq + Send + Sync + 'static, { let data = get_from_storage::(&key, init); @@ -329,7 +332,8 @@ pub struct SyncedStorageEntry, T: 'static> { impl Clone for SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking, + S::Persistence: StorageSubscriber, >>::Key: Clone, T: 'static, { @@ -343,10 +347,11 @@ where impl SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking, + S::Persistence: StorageSubscriber, { pub fn new(key: >>::Key, data: T) -> Self { - let channel = S::subscribe(&key); + let channel = S::Persistence::subscribe(&key); Self { entry: StorageEntry::new(key, data), channel, @@ -387,7 +392,8 @@ where impl StorageEntryTrait for SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, + S: StorageBacking, + S::Persistence: StorageSubscriber, T: Send + Sync + PartialEq + 'static, { fn save(&self) { @@ -563,48 +569,6 @@ pub trait StorageEncoder: 'static { fn serialize(value: &T) -> Self::EncodedValue; } -/// A way to create a StorageEncoder out of the two layers. -/// -/// I'm not sure if this is the best way to abstract that. -pub struct LayeredStorage>, Encoder: StorageEncoder> -{ - persistence: PhantomData, - encoder: PhantomData, - value: PhantomData, -} - -/// StorageBacking for LayeredStorage. -/// T: Use facing type -/// Value: what gets persisted -/// P: Stores a Option -/// E: Translated between T and Value -impl< - T: 'static, - Value, - P: StoragePersistence, Value = Option>, - E: StorageEncoder, -> StorageBacking for LayeredStorage -{ - type Encoder = E; - type Persistence = P; -} - -impl< - T: 'static, - Value, - P: StoragePersistence, Value = Option> + StorageSubscriber, - E: StorageEncoder, -> StorageSubscriber for LayeredStorage -{ - fn subscribe(key: &P::Key) -> Receiver { - P::subscribe(key) - } - - fn unsubscribe(key: &P::Key) { - P::unsubscribe(key) - } -} - /// A trait for a subscriber to events from a storage backing pub trait StorageSubscriber> { /// Subscribes to events from a storage backing for the given key From 196e039751f58bfc5d395888c2ef4869f45137eb Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 16:44:23 -0700 Subject: [PATCH 31/37] Remove unneeded clone --- examples/storage/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 681ceed..911e600 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -150,7 +150,6 @@ impl< type Persistence = P; } -#[derive(Clone)] struct HumanReadableEncoding; impl StorageEncoder for HumanReadableEncoding { From 148f907bbc299c44ebc0d5f29fa20aba48f530b2 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 17:06:06 -0700 Subject: [PATCH 32/37] Clippy --- packages/storage/src/client_storage/fs.rs | 2 +- packages/storage/src/lib.rs | 30 +++++++++++------------ packages/storage/src/persistence.rs | 4 +-- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/packages/storage/src/client_storage/fs.rs b/packages/storage/src/client_storage/fs.rs index 6555654..00aa036 100644 --- a/packages/storage/src/client_storage/fs.rs +++ b/packages/storage/src/client_storage/fs.rs @@ -46,7 +46,7 @@ fn set(key: &str, as_str: &Option) { Ok(_) => {} Err(error) => match error.kind() { std::io::ErrorKind::NotFound => {} - _ => Result::Err(error).unwrap(), + _ => panic!("{:?}", error), }, }, } diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index c427d5c..0cae32a 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -82,7 +82,7 @@ where { let mut init = Some(init); let storage = use_hook(|| new_storage::(key, || init.take().unwrap()())); - use_hydrate_storage::(storage, init); + use_hydrate_storage(storage, init); storage } @@ -169,7 +169,7 @@ where { let mut init = Some(init); let storage = use_hook(|| new_synced_storage::(key, || init.take().unwrap()())); - use_hydrate_storage::(storage, init); + use_hydrate_storage(storage, init); storage } @@ -187,7 +187,7 @@ where >>::Key: Clone, T: Clone + Send + Sync + PartialEq + 'static, { - let signal = { + { let mode = StorageMode::current(); match mode { @@ -202,8 +202,7 @@ where *storage_entry.data() } } - }; - signal + } } /// A hook that creates a StorageEntry with the latest value from storage or the init value if it doesn't exist. @@ -218,7 +217,7 @@ where { let mut init = Some(init); let signal = use_hook(|| new_storage_entry::(key, || init.take().unwrap()())); - use_hydrate_storage::(*StorageEntryTrait::::data(&signal), init); + use_hydrate_storage(*StorageEntryTrait::::data(&signal), init); signal } @@ -235,7 +234,7 @@ where { let mut init = Some(init); let signal = use_hook(|| new_synced_storage_entry::(key, || init.take().unwrap()())); - use_hydrate_storage::(*signal.data(), init); + use_hydrate_storage(*signal.data(), init); signal } @@ -273,7 +272,7 @@ pub fn get_from_storage, T: Send + Sync + Clone + 'static>( key: &>>::Key, init: impl FnOnce() -> T, ) -> T { - S::get(&key).unwrap_or_else(|| { + S::get(key).unwrap_or_else(|| { let data = init(); S::set(key, &data); data @@ -400,10 +399,10 @@ where // We want to save in the following conditions // - The value from the channel is different from the current value // - The value from the channel could not be determined, likely because it hasn't been set yet - if let Some(payload) = self.channel.borrow().data.downcast_ref::() { - if *self.entry.data.read() == *payload { - return; - } + if let Some(payload) = self.channel.borrow().data.downcast_ref::() + && *self.entry.data.read() == *payload + { + return; } StorageEntryTrait::::save(&self.entry); } @@ -438,7 +437,7 @@ where fn clone(&self) -> Self { Self { key: self.key.clone(), - data: self.data.clone(), + data: self.data, } } } @@ -649,13 +648,12 @@ impl FailedDecode { } } -// Take a signal and a storage key and hydrate the value if we are hydrating the client. -pub(crate) fn use_hydrate_storage( +/// Take a signal and a storage key and hydrate the value if we are hydrating the client. +pub(crate) fn use_hydrate_storage( mut signal: Signal, init: Option T>, ) -> Signal where - S: StorageBacking, T: Clone + Send + Sync + PartialEq + 'static, { let mode = StorageMode::current(); diff --git a/packages/storage/src/persistence.rs b/packages/storage/src/persistence.rs index 45fc69f..b765167 100644 --- a/packages/storage/src/persistence.rs +++ b/packages/storage/src/persistence.rs @@ -29,7 +29,7 @@ pub fn use_persistent< ) -> Signal { let mut init = Some(init); let storage = use_hook(|| new_persistent(key.to_string(), || init.take().unwrap()())); - use_hydrate_storage::(storage, init); + use_hydrate_storage(storage, init); storage } @@ -59,7 +59,7 @@ pub fn use_singleton_persistent< ) -> Signal { let mut init = Some(init); let signal = use_hook(|| new_singleton_persistent(|| init.take().unwrap()())); - use_hydrate_storage::(signal, init); + use_hydrate_storage(signal, init); signal } From 4ddf0ff2df759d9a2b4ac74d56c3b8c7276edf81 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 17:08:11 -0700 Subject: [PATCH 33/37] Fix clippy for web --- packages/storage/src/client_storage/web.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/storage/src/client_storage/web.rs b/packages/storage/src/client_storage/web.rs index d02e99d..35b47d2 100644 --- a/packages/storage/src/client_storage/web.rs +++ b/packages/storage/src/client_storage/web.rs @@ -67,11 +67,11 @@ impl< fn unsubscribe(key: &String) { let read_binding = SUBSCRIPTIONS.read().unwrap(); - if let Some(entry) = read_binding.get(key) { - if entry.tx.is_closed() { - drop(read_binding); - SUBSCRIPTIONS.write().unwrap().remove(key); - } + if let Some(entry) = read_binding.get(key) + && entry.tx.is_closed() + { + drop(read_binding); + SUBSCRIPTIONS.write().unwrap().remove(key); } } } @@ -129,7 +129,7 @@ fn store(key: &String, value: &Option, storage_type: WebStorageType) { fn set_or_clear(key: String, value: Option<&str>, storage_type: WebStorageType) { match value { - Some(str) => set(key, &str, storage_type), + Some(str) => set(key, str, storage_type), None => clear(key, storage_type), } } @@ -137,7 +137,7 @@ fn set_or_clear(key: String, value: Option<&str>, storage_type: WebStorageType) fn set(key: String, as_str: &str, storage_type: WebStorageType) { get_storage_by_type(storage_type) .unwrap() - .set_item(&key, &as_str) + .set_item(&key, as_str) .unwrap(); } From 6d392383858679a5bc995b9ed3dcc40537009713 Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Sat, 30 Aug 2025 17:09:41 -0700 Subject: [PATCH 34/37] More more clippy fix --- packages/storage/src/client_storage/web.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/storage/src/client_storage/web.rs b/packages/storage/src/client_storage/web.rs index 35b47d2..1e40ff0 100644 --- a/packages/storage/src/client_storage/web.rs +++ b/packages/storage/src/client_storage/web.rs @@ -123,8 +123,8 @@ impl StoragePersistence for SessionStorage { } } -fn store(key: &String, value: &Option, storage_type: WebStorageType) { - set_or_clear(key.clone(), value.as_deref(), storage_type) +fn store(key: &str, value: &Option, storage_type: WebStorageType) { + set_or_clear(key.to_owned(), value.as_deref(), storage_type) } fn set_or_clear(key: String, value: Option<&str>, storage_type: WebStorageType) { From fa5343d11fbc573048cc288a5a7e7ef39efdbacf Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Mon, 1 Sep 2025 15:13:55 -0700 Subject: [PATCH 35/37] Better comments --- packages/storage/src/lib.rs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index 0cae32a..e58d5f6 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -509,7 +509,7 @@ impl>, T: Debug> Debug for StorageEntry { } } -/// A trait for a storage backing +/// A trait for a storage backing. pub trait StorageBacking: 'static { type Encoder: StorageEncoder; type Persistence: StoragePersistence< @@ -535,9 +535,9 @@ pub trait StorageBacking: 'static { } } } - /// Sets a value in storage for the given key + /// Sets a value in storage for the given key. /// - /// TODO: this provides no way to clear (store None) + /// TODO: this provides no way to clear (store None). fn set(key: &>>::Key, value: &T) where T: 'static + Clone + Send + Sync, @@ -547,19 +547,26 @@ pub trait StorageBacking: 'static { } } -/// A trait for the persistence portion of StorageBacking. +/// A trait for the persistence portion of [StorageBacking]. +/// +/// In addition to implementing this trait, storage may also implement [StorageSubscriber] to enable sync with other editors of the storage. +/// To allow more options for how to implement [StorageSubscriber], [StoragePersistence::store] is provided the `unencoded` `T` value. pub trait StoragePersistence: 'static { - /// The key type used to store data in storage + /// The key type used to store data in storage. type Key: PartialEq + Debug + Send + Sync + 'static; /// The type of value which can be stored. type Value; - /// Gets a value from storage for the given key + /// Gets a value from storage for the given key. fn load(key: &Self::Key) -> Self::Value; - /// Sets a value in storage for the given key + /// Sets a value in storage for the given key. + /// + /// fn store(key: &Self::Key, value: &Self::Value, unencoded: &T); } /// New trait which can be implemented to define a data format for storage. +/// +/// Typically implemented where `T` is an `Option` with `None` being th pub trait StorageEncoder: 'static { /// The type of value which can be stored. type EncodedValue; @@ -568,13 +575,17 @@ pub trait StorageEncoder: 'static { fn serialize(value: &T) -> Self::EncodedValue; } -/// A trait for a subscriber to events from a storage backing +/// A trait for a subscriber to events from a [StorageBacking]. +/// +/// Observes an Option, where None is equivalent to nothing being stored. +/// +/// `T` is the user facing type: already unencoded if needed. pub trait StorageSubscriber> { - /// Subscribes to events from a storage backing for the given key + /// Subscribes to events from a storage backing for the given key. fn subscribe( key: &>>::Key, ) -> Receiver; - /// Unsubscribes from events from a storage backing for the given key + /// Unsubscribes from events from a storage backing for the given key. fn unsubscribe(key: &>>::Key); } From 0de247686fd90ded7265a5a817cc771c09d9c64d Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Mon, 1 Sep 2025 18:24:11 -0700 Subject: [PATCH 36/37] Improve a couple of doc comments --- packages/storage/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index e58d5f6..f5f1ba9 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -547,7 +547,7 @@ pub trait StorageBacking: 'static { } } -/// A trait for the persistence portion of [StorageBacking]. +/// The persistence portion of [StorageBacking]. /// /// In addition to implementing this trait, storage may also implement [StorageSubscriber] to enable sync with other editors of the storage. /// To allow more options for how to implement [StorageSubscriber], [StoragePersistence::store] is provided the `unencoded` `T` value. @@ -564,9 +564,9 @@ pub trait StoragePersistence: 'static { fn store(key: &Self::Key, value: &Self::Value, unencoded: &T); } -/// New trait which can be implemented to define a data format for storage. +/// The Encoder portion of [StorageBacking]. /// -/// Typically implemented where `T` is an `Option` with `None` being th +/// Converts the a `T` into an [StorageEncoder::EncodedValue] which can be stored in the [StoragePersistence]. pub trait StorageEncoder: 'static { /// The type of value which can be stored. type EncodedValue; From 431824db1f6624f106c56ab00a463f102479aebc Mon Sep 17 00:00:00 2001 From: Craig Macomber Date: Mon, 1 Sep 2025 22:15:58 -0700 Subject: [PATCH 37/37] Fix partial comment --- packages/storage/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index f5f1ba9..9cbc543 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -560,7 +560,7 @@ pub trait StoragePersistence: 'static { fn load(key: &Self::Key) -> Self::Value; /// Sets a value in storage for the given key. /// - /// + /// `unencoded` must be the same as `value`, except not having been encoded. fn store(key: &Self::Key, value: &Self::Value, unencoded: &T); }