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..911e600 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" } + } } } } @@ -76,18 +80,40 @@ 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); + // 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); + + // 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>( + "local_human".to_string(), + || 0, + ); rsx!( + div { + button { + onclick: move |_| { + *count_persistent.write() += 1; + }, + "Click me!" + } + "Persisted to local storage (but not synced): Clicked {count_persistent} times." + } div { button { onclick: move |_| { *count_session.write() += 1; }, "Click me!" - }, - "I persist for the current session. Clicked {count_session} times" + } + "Session: Clicked {count_session} times." } div { button { @@ -95,8 +121,82 @@ fn Storage() -> Element { *count_local.write() += 1; }, "Click me!" - }, - "I persist across all sessions. Clicked {count_local} times" + } + "Local: Clicked {count_local} times." + } + div { + button { + onclick: move |_| { + *count_local_human.write() += 1; + }, + "Click me!" + } + "Human readable local: Clicked {count_local_human} times." } ) } + +/// 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; +} + +struct HumanReadableEncoding; + +impl StorageEncoder for HumanReadableEncoding { + type EncodedValue = String; + type DecodeError = serde_json::Error; + + fn deserialize(loaded: &Self::EncodedValue) -> Result { + serde_json::from_str(loaded) + } + + fn serialize(value: &T) -> Self::EncodedValue { + 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]" + ); + } +} diff --git a/packages/storage/src/client_storage/fs.rs b/packages/storage/src/client_storage/fs.rs index f7fd110..00aa036 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; @@ -7,7 +7,7 @@ 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::{StoragePersistence, StorageSubscriber}; #[doc(hidden)] /// Sets the directory where the storage files are located. @@ -29,62 +29,77 @@ 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 => {} + _ => panic!("{:?}", error), + }, + }, + } } /// 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)] +/// [StoragePersistence] backed by a file. 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(); + 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_clone) { + 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(); } } } - - 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 // 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, - ) -> Receiver { +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. let subscriptions = SUBSCRIPTIONS.get_or_init(|| RwLock::new(HashMap::new())); @@ -96,7 +111,7 @@ impl StorageSubscriber for LocalStorage { 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() @@ -107,7 +122,7 @@ impl StorageSubscriber for LocalStorage { } } - 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 d0433ba..0da85d5 100644 --- a/packages/storage/src/client_storage/memory.rs +++ b/packages/storage/src/client_storage/memory.rs @@ -5,24 +5,61 @@ use std::ops::{Deref, DerefMut}; use std::rc::Rc; use std::sync::Arc; -use crate::StorageBacking; +use crate::{StorageBacking, StorageEncoder, StoragePersistence}; -#[derive(Clone)] +/// [StoragePersistence] backed by the current [SessionStore]. +/// +/// Skips encoding, and just stores data using `Arc>`. pub struct SessionStorage; -impl StorageBacking for SessionStorage { - type Key = String; +impl StorageBacking for SessionStorage { + type Encoder = ArcEncoder; + type Persistence = SessionStorage; +} - fn set(key: String, value: &T) { - let session = SessionStore::get_current_session(); - session.borrow_mut().insert(key, Arc::new(value.clone())); +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); + } } +} - fn get(key: &String) -> Option { +impl StoragePersistence for SessionStorage { + type Key = Key; + type Value = Value; + + fn load(key: &Self::Key) -> Self::Value { let session = SessionStore::get_current_session(); let read_binding = session.borrow(); - let value_any = read_binding.get(key)?; - value_any.downcast_ref::().cloned() + read_binding.get(key).cloned() + } + + fn store(key: &Self::Key, value: &Self::Value, unencoded: &T) { + store(key, value, unencoded); + } +} + +/// 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 = &'static str; + + fn deserialize(loaded: &Self::EncodedValue) -> Result { + loaded.downcast_ref::().cloned().ok_or("Failed Downcast") + } + + fn serialize(value: &T) -> Self::EncodedValue { + Arc::new(value.clone()) } } diff --git a/packages/storage/src/client_storage/web.rs b/packages/storage/src/client_storage/web.rs index b0b224b..1e40ff0 100644 --- a/packages/storage/src/client_storage/web.rs +++ b/packages/storage/src/client_storage/web.rs @@ -5,43 +5,57 @@ use std::{ use dioxus::logger::tracing::{error, trace}; use once_cell::sync::Lazy; -use serde::{Serialize, de::DeserializeOwned}; +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::{ - StorageBacking, StorageChannelPayload, StorageSubscriber, StorageSubscription, serde_to_string, - try_serde_from_string, + StorageBacking, StorageChannelPayload, StorageEncoder, StoragePersistence, StorageSubscriber, + StorageSubscription, default_encoder::DefaultEncoder, }; -#[derive(Clone)] +/// StorageBacking using default encoder +impl StorageBacking + for SessionStorage +{ + type Encoder = DefaultEncoder; + type Persistence = SessionStorage; +} + +/// [WebStorageType::Local] backed [StoragePersistence]. 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, _unencoded: &T) { + store(key, value, WebStorageType::Local) } } -impl StorageSubscriber for LocalStorage { - fn subscribe( - key: &String, - ) -> Receiver { +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(); match read_binding.get(key) { Some(subscription) => subscription.tx.subscribe(), 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() @@ -53,11 +67,11 @@ impl StorageSubscriber for LocalStorage { 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); } } } @@ -93,32 +107,49 @@ static SUBSCRIPTIONS: Lazy>>> = Arc::new(RwLock::new(HashMap::new())) }); -#[derive(Clone)] +/// [WebStorageType::Session] backed [StoragePersistence]. pub struct SessionStorage; -impl StorageBacking for SessionStorage { +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, _unencoded: &T) { + store(key, value, WebStorageType::Session) + } +} + +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) { + match value { + Some(str) => set(key, str, storage_type), + None => clear(key, storage_type), } } -fn set(key: String, value: &T, storage_type: WebStorageType) { - let as_str = serde_to_string(value); +fn set(key: String, as_str: &str, storage_type: WebStorageType) { + get_storage_by_type(storage_type) + .unwrap() + .set_item(&key, as_str) + .unwrap(); +} + +fn clear(key: String, storage_type: WebStorageType) { get_storage_by_type(storage_type) .unwrap() - .set_item(&key, &as_str) + .delete(&key) .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 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/default_encoder.rs b/packages/storage/src/default_encoder.rs new file mode 100644 index 0000000..3e93b47 --- /dev/null +++ b/packages/storage/src/default_encoder.rs @@ -0,0 +1,129 @@ +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 using hexadecimal. +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:?}"))), + } +} + +#[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" + ); + } +} diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index 89881f0..9cbc543 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -27,10 +27,11 @@ //! ``` mod client_storage; +mod default_encoder; mod persistence; pub use client_storage::{LocalStorage, SessionStorage}; -use dioxus::logger::tracing::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, @@ -57,24 +58,31 @@ 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(key: S::Key, init: impl FnOnce() -> T) -> Signal +pub fn use_storage( + key: >>::Key, + init: impl FnOnce() -> T, +) -> Signal where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, - S::Key: Clone, + S: StorageBacking, + >>::Key: Clone, + T: Clone + Send + Sync + PartialEq + 'static, { 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 } @@ -108,20 +116,27 @@ 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(key: S::Key, init: impl FnOnce() -> T) -> Signal +pub fn new_storage( + key: >>::Key, + init: impl FnOnce() -> T, +) -> Signal where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, - S::Key: Clone, + S: StorageBacking, + >>::Key: Clone, + T: Clone + Send + Sync + PartialEq + 'static, { let mode = StorageMode::current(); @@ -132,7 +147,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 } } @@ -142,15 +157,19 @@ 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: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, - S::Key: Clone, + S: StorageBacking, + S::Persistence: StorageSubscriber, + >>::Key: Clone, + T: Clone + Send + Sync + PartialEq + 'static, { 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 } @@ -158,13 +177,17 @@ 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: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, - S::Key: Clone, + S: StorageBacking, + S::Persistence: StorageSubscriber, + >>::Key: Clone, + T: Clone + Send + Sync + PartialEq + 'static, { - let signal = { + { let mode = StorageMode::current(); match mode { @@ -179,47 +202,52 @@ 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. -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, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, - S::Key: Clone, + S: StorageBacking, + >>::Key: Clone, + T: Clone + Send + Sync + PartialEq + 'static, { 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 } /// 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, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, - S::Key: Clone, + S: StorageBacking, + S::Persistence: StorageSubscriber, + >>::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()())); - use_hydrate_storage::(*signal.data(), init); + use_hydrate_storage(*signal.data(), init); signal } /// 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: Serialize + DeserializeOwned + Clone + Send + Sync + 'static, - S::Key: Clone, + S: StorageBacking, + T: Clone + Send + Sync + 'static, { - let data = get_from_storage::(key.clone(), init); + let data = get_from_storage::(&key, init); StorageEntry::new(key, data) } @@ -227,27 +255,24 @@ 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 - S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, - S::Key: Clone, + S: StorageBacking, + S::Persistence: 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 + 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 @@ -255,9 +280,7 @@ pub fn get_from_storage< } /// A trait for common functionality between StorageEntry and SyncedStorageEntry -pub trait StorageEntryTrait: - Clone + 'static -{ +pub trait StorageEntryTrait, T>: 'static { /// Saves the current state to storage fn save(&self); @@ -265,7 +288,7 @@ pub trait StorageEntryTrait: 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; @@ -273,8 +296,9 @@ 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, - T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, + Self: Clone, + S: StorageBacking, + T: Clone + PartialEq + 'static, { let entry_clone = self.clone(); let old = RefCell::new(None); @@ -298,24 +322,35 @@ 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, - 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, + 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, + S::Persistence: StorageSubscriber, + >>::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, + S::Persistence: StorageSubscriber, { - pub fn new(key: S::Key, data: T) -> Self { - let channel = S::subscribe::(&key); + pub fn new(key: >>::Key, data: T) -> Self { + let channel = S::Persistence::subscribe(&key); Self { entry: StorageEntry::new(key, data), channel, @@ -328,7 +363,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 { @@ -340,38 +378,41 @@ 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() + // Currently there is no API exposed to clear storage, so it should never be changed to None + .expect("Expected storage entry to be Some"); } } }); } } -impl StorageEntryTrait for SyncedStorageEntry +impl StorageEntryTrait for SyncedStorageEntry where - S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, + S: StorageBacking, + S::Persistence: StorageSubscriber, + T: Send + Sync + PartialEq + 'static, { fn save(&self) { // 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; } - self.entry.save(); + StorageEntryTrait::::save(&self.entry); } fn update(&mut self) { - self.entry.update(); + StorageEntryTrait::::update(&mut self.entry); } - fn key(&self) -> &S::Key { - self.entry.key() + fn key(&self) -> &>>::Key { + StorageEntryTrait::::key(&self.entry) } fn data(&self) -> &Signal { @@ -380,25 +421,33 @@ 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, + pub(crate) key: P::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, - S::Key: Clone, + P: StoragePersistence>, + T: 'static, + P::Key: Clone, +{ + fn clone(&self) -> Self { + Self { + key: self.key.clone(), + data: self.data, + } + } +} + +impl StorageEntry +where + P: StoragePersistence>, { /// Creates a new StorageEntry - pub fn new(key: S::Key, data: T) -> Self { + pub fn new(key: P::Key, data: T) -> Self { Self { key, data: Signal::new_in_scope( @@ -409,20 +458,23 @@ where } } -impl StorageEntryTrait for StorageEntry +impl, T> StorageEntryTrait for StorageEntry where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, + S: StorageBacking, + T: Clone + 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) { - self.data = S::get(&self.key).unwrap_or(self.data); + // TODO: does this need to handle the None case? + if let Some(value) = S::get(&self.key) { + *self.data.write() = value; + } } - fn key(&self) -> &S::Key { + fn key(&self) -> &>>::Key { &self.key } @@ -431,9 +483,7 @@ where } } -impl Deref - for StorageEntry -{ +impl>, T: Send + Sync> Deref for StorageEntry { type Target = Signal; fn deref(&self) -> &Signal { @@ -441,48 +491,102 @@ impl D } } -impl DerefMut - for StorageEntry -{ +impl>, T: Send + Sync> DerefMut for StorageEntry { fn deref_mut(&mut self) -> &mut Signal { &mut self.data } } -impl 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 Debug - for StorageEntry -{ +impl>, T: Debug> Debug for StorageEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.data.fmt(f) } } -/// A trait for a storage backing -pub trait StorageBacking: Clone + 'static { - /// The key type used to store data in storage - type Key: PartialEq + Clone + Debug + Send + Sync + 'static; +/// A trait for a storage backing. +pub trait StorageBacking: 'static { + type Encoder: StorageEncoder; + type Persistence: StoragePersistence< + Option, + Value = Option<>::EncodedValue>, + >; + /// 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 get(key: &>>::Key) -> Option { + let loaded = Self::Persistence::load(key); + match loaded { + // TODO: this treats None the same as failed decodes. + Some(x) => { + let deserialized = Self::Encoder::deserialize(&x); + if let Err(err) = &deserialized { + warn!("Deserialization error: {err:?}"); + } + deserialized.ok() + } + None => { + warn!("Got None for key {key:?}"); + None + } + } + } + /// Sets a value in storage for the given key. + /// + /// TODO: this provides no way to clear (store None). + fn set(key: &>>::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 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, +/// 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. + 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. + /// + /// `unencoded` must be the same as `value`, except not having been encoded. + fn store(key: &Self::Key, value: &Self::Value, unencoded: &T); +} + +/// The Encoder portion of [StorageBacking]. +/// +/// 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; + type DecodeError: Debug; + fn deserialize(loaded: &Self::EncodedValue) -> Result; + fn serialize(value: &T) -> Self::EncodedValue; +} + +/// 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. + fn subscribe( + key: &>>::Key, ) -> Receiver; - /// Unsubscribes from events from a storage backing for the given key - fn unsubscribe(key: &S::Key); + /// Unsubscribes from events from a storage backing for the given key. + fn unsubscribe(key: &>>::Key); } /// A struct to hold information about processing a storage event. @@ -494,16 +598,14 @@ 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, - T: DeserializeOwned + Send + Sync + Clone + 'static, - >( + pub fn new, T: Send + Sync + 'static>( tx: Sender, - key: S::Key, + key: >>::Key, ) -> Self { let getter = move || { - let data = S::get::(&key).unwrap(); + let data = S::get(&key); StorageChannelPayload::new(data) }; Self { @@ -519,17 +621,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), } } @@ -541,67 +643,29 @@ impl StorageChannelPayload { impl Default for StorageChannelPayload { fn default() -> Self { - Self { data: Arc::new(()) } + Self { data: Box::new(()) } } } -// 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 +#[derive(Debug)] +pub struct FailedDecode { + pub from: From, + pub description: String, } -#[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) -> Option { - 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)?; - 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, +impl FailedDecode { + fn from(from: T, description: String) -> FailedDecode { + FailedDecode { from, description } } } -// 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: 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. @@ -630,3 +694,11 @@ where } signal } + +/// StorageBacking using default encoder +impl StorageBacking + for LocalStorage +{ + type Encoder = default_encoder::DefaultEncoder; + type Persistence = LocalStorage; +} diff --git a/packages/storage/src/persistence.rs b/packages/storage/src/persistence.rs index 64ee5f2..b765167 100644 --- a/packages/storage/src/persistence.rs +++ b/packages/storage/src/persistence.rs @@ -1,4 +1,8 @@ -use crate::SessionStorage; +//! 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}; use dioxus::prelude::*; use dioxus_signals::Signal; @@ -7,6 +11,13 @@ 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. /// /// Depending on the platform this uses either local storage or a file storage @@ -18,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 } @@ -31,8 +42,8 @@ pub fn new_persistent< key: impl ToString, init: impl FnOnce() -> T, ) -> Signal { - let storage_entry = new_storage_entry::(key.to_string(), init); - storage_entry.save_to_storage_on_change(); + let storage_entry = new_storage_entry::(key.to_string(), init); + StorageEntryTrait::::save_to_storage_on_change(&storage_entry); storage_entry.data } @@ -48,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 }