From 567ae8241ce3acff0b24fbdc5b77243df4b409fd Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 23 Sep 2023 07:09:26 -0500 Subject: [PATCH 001/108] add storage hook --- Cargo.toml | 11 +- examples/storage/Cargo.toml | 15 ++ examples/storage/README.md | 7 + examples/storage/src/main.rs | 25 +++ src/lib.rs | 6 + src/storage/client_storage.rs | 282 ++++++++++++++++++++++++++++++++++ src/storage/mod.rs | 40 +++++ src/storage/storage.rs | 188 +++++++++++++++++++++++ 8 files changed, 573 insertions(+), 1 deletion(-) create mode 100644 examples/storage/Cargo.toml create mode 100644 examples/storage/README.md create mode 100644 examples/storage/src/main.rs create mode 100644 src/storage/client_storage.rs create mode 100644 src/storage/mod.rs create mode 100644 src/storage/storage.rs diff --git a/Cargo.toml b/Cargo.toml index 5af8bf3..0cf4bb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,8 @@ geolocation = ["dep:windows", "dep:futures-util", "dep:futures", "dep:web-sys", color_scheme = ["dep:web-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] utils = ["dep:async-broadcast", "dep:uuid"] i18n = ["dep:serde", "dep:serde_json", "dep:unic-langid"] -all = ["clipboard", "notifications", "geolocation", "color_scheme", "utils", "i18n"] +storage = ["dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:directories", "dep:yazi"] +all = ["clipboard", "notifications", "geolocation", "color_scheme", "utils", "i18n", "storage"] # CI testing wasm-testing = ["geolocation", "color_scheme", "utils", "i18n"] @@ -46,6 +47,14 @@ futures-util = {version = "0.3.28", optional = true } serde = { version = "1.0.163", optional = true } serde_json = { version = "1.0.96", optional = true } unic-langid = { version = "0.9.1", features = ["serde"], optional = true } +# storage +rustc-hash = "1.1.0" +postcard = { version = "1.0.2", features = ["use-std"], optional = true } +once_cell = { version = "1.17.0", optional = true } +yazi = { version = "0.1.4", optional = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +directories = { version = "4.0.1", optional = true } [target.'cfg(windows)'.dependencies] windows = { version = "0.48.0", features = ["Foundation", "Devices_Geolocation"], optional = true } diff --git a/examples/storage/Cargo.toml b/examples/storage/Cargo.toml new file mode 100644 index 0000000..6ebfd2e --- /dev/null +++ b/examples/storage/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "storage" +version = "0.1.0" +edition = "2021" + +[dependencies] +dioxus-std = { path="../../", features = ["storage"] } +dioxus = "0.4" +dioxus-web = "0.4" + +log = "0.4.6" + +# WebAssembly Debug +wasm-logger = "0.2.0" +console_error_panic_hook = "0.1.7" diff --git a/examples/storage/README.md b/examples/storage/README.md new file mode 100644 index 0000000..2bcc6fb --- /dev/null +++ b/examples/storage/README.md @@ -0,0 +1,7 @@ +# use_persistant + +Learn how to use `use_persistant`. + +Run: + +```dioxus serve``` \ No newline at end of file diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs new file mode 100644 index 0000000..3a95ba0 --- /dev/null +++ b/examples/storage/src/main.rs @@ -0,0 +1,25 @@ +use dioxus::prelude::*; +use dioxus_std::storage::*; +use std::{collections::HashMap, str::FromStr}; + +fn main() { + // init debug tool for WebAssembly + wasm_logger::init(wasm_logger::Config::default()); + console_error_panic_hook::set_once(); + + dioxus_web::launch(app); +} + +fn app(cx: Scope) -> Element { + let count = use_singleton_persistent(cx, || 0); + + render!( + button { + onclick: move |_| { + count.set(count.get() + 1); + }, + "Click me!" + }, + "Clicked {count} times" + ) +} diff --git a/src/lib.rs b/src/lib.rs index 0332a7d..e474d6e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,3 +29,9 @@ cfg_if::cfg_if! { pub mod clipboard; } } + +cfg_if::cfg_if! { + if #[cfg(feature = "storage")] { + pub mod storage; + } +} diff --git a/src/storage/client_storage.rs b/src/storage/client_storage.rs new file mode 100644 index 0000000..fcd481e --- /dev/null +++ b/src/storage/client_storage.rs @@ -0,0 +1,282 @@ +#![allow(unused)] +use dioxus::prelude::*; +use once_cell::sync::OnceCell; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::cell::{Ref, RefMut}; +use std::fmt::Debug; +use std::io::Write; +use std::thread::LocalKey; +use std::{ + fmt::Display, + ops::{Deref, DerefMut}, +}; +use web_sys::{window, Storage}; + +use crate::storage::storage::{ + serde_from_string, serde_to_string, storage_entry, try_serde_from_string, + use_synced_storage_entry, StorageBacking, StorageEntry, StorageEntryMut, +}; + +#[allow(clippy::needless_doctest_main)] +/// Set the directory where the storage files are located on non-wasm targets. +/// +/// ```rust +/// fn main(){ +/// // set the directory to the default location +/// set_dir!(); +/// // set the directory to a custom location +/// set_dir!(PathBuf::from("path/to/dir")); +/// } +/// ``` +#[cfg(not(target_arch = "wasm32"))] +#[macro_export] +macro_rules! set_dir { + () => { + $crate::set_dir_name(env!("CARGO_PKG_NAME")); + }; + ($path: literal) => { + $crate::set_dir(std::path::PathBuf::from($path)); + }; +} + +#[cfg(not(target_arch = "wasm32"))] +#[doc(hidden)] +/// Sets the directory where the storage files are located. +pub fn set_directory(path: std::path::PathBuf) { + LOCATION.set(path).unwrap(); +} + +#[cfg(not(target_arch = "wasm32"))] +#[doc(hidden)] +pub fn set_dir_name(name: &str) { + { + set_directory( + directories::BaseDirs::new() + .unwrap() + .data_local_dir() + .join(name), + ) + } +} + +#[cfg(not(target_arch = "wasm32"))] +static LOCATION: OnceCell = OnceCell::new(); + +#[cfg(target_arch = "wasm32")] +fn local_storage() -> Option { + window()?.local_storage().ok()? +} + +fn set(key: String, value: &T) { + #[cfg(not(feature = "ssr"))] + { + let as_str = serde_to_string(value); + #[cfg(target_arch = "wasm32")] + { + local_storage().unwrap().set_item(&key, &as_str).unwrap(); + } + #[cfg(not(target_arch = "wasm32"))] + { + let path = LOCATION + .get() + .expect("Call the set_dir macro before accessing persistant data"); + std::fs::create_dir_all(path).unwrap(); + let file_path = path.join(key); + let mut file = std::fs::File::create(file_path).unwrap(); + file.write_all(as_str.as_bytes()).unwrap(); + } + } +} + +fn get(key: &str) -> Option { + #[cfg(not(feature = "ssr"))] + { + #[cfg(target_arch = "wasm32")] + { + let s: String = local_storage()?.get_item(key).ok()??; + try_serde_from_string(&s) + } + #[cfg(not(target_arch = "wasm32"))] + { + let path = LOCATION + .get() + .expect("Call the set_dir macro before accessing persistant data") + .join(key); + let s = std::fs::read_to_string(path).ok()?; + try_serde_from_string(&s) + } + } + #[cfg(feature = "ssr")] + None +} + +pub struct ClientStorage; + +impl StorageBacking for ClientStorage { + type Key = String; + + fn set(key: String, value: &T) { + set(key, value); + } + + fn get(key: &String) -> Option { + get(key) + } +} + +/// 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 +#[allow(clippy::needless_return)] +pub fn use_persistent( + cx: &ScopeState, + key: impl ToString, + init: impl FnOnce() -> T, +) -> &UsePersistent { + let mut init = Some(init); + #[cfg(feature = "ssr")] + let state = use_ref(cx, || { + StorageEntry::::new(key.to_string(), init.take().unwrap()()) + }); + // if hydration is not enabled we can just set the storage + #[cfg(all(not(feature = "ssr"), not(feature = "hydrate")))] + let state = use_ref(cx, || { + StorageEntry::new( + key.to_string(), + storage_entry::(key.to_string(), init.take().unwrap()), + ) + }); + // otherwise render the initial value and then hydrate after the first render + #[cfg(all(not(feature = "ssr"), feature = "hydrate"))] + let state = { + let state = use_ref(cx, || { + StorageEntry::::new(key.to_string(), init.take().unwrap()()) + }); + if cx.generation() == 0 { + cx.needs_update(); + } + if cx.generation() == 1 { + state.set(StorageEntry::new( + key.to_string(), + storage_entry::(key.to_string(), init.take().unwrap()), + )); + } + + state + }; + cx.use_hook(|| UsePersistent { + inner: state.clone(), + }) +} + +/// A persistent storage hook that can be used to store data across application reloads. +/// The state will be the same for every call to this hook from the same line of code. +/// +/// Depending on the platform this uses either local storage or a file storage +#[allow(clippy::needless_return)] +#[track_caller] +pub fn use_singleton_persistent( + cx: &ScopeState, + init: impl FnOnce() -> T, +) -> &UsePersistent { + let key = cx.use_hook(|| { + let caller = std::panic::Location::caller(); + format!("{}:{}", caller.file(), caller.line()) + }); + use_persistent(cx, key, init) +} + +pub struct StorageRef<'a, T: Serialize + DeserializeOwned + Default + 'static> { + inner: Ref<'a, StorageEntry>, +} + +impl<'a, T: Serialize + DeserializeOwned + Default + 'static> Deref for StorageRef<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +pub struct StorageRefMut<'a, T: Serialize + DeserializeOwned + 'static> { + inner: RefMut<'a, StorageEntry>, +} + +impl<'a, T: Serialize + DeserializeOwned + 'static> Deref for StorageRefMut<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl<'a, T: Serialize + DeserializeOwned + 'static> DerefMut for StorageRefMut<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner.data + } +} + +impl<'a, T: Serialize + DeserializeOwned + 'static> Drop for StorageRefMut<'a, T> { + fn drop(&mut self) { + self.inner.deref_mut().save(); + } +} + +/// Storage that persists across application reloads +pub struct UsePersistent { + inner: UseRef>, +} + +impl UsePersistent { + /// Returns a reference to the value + pub fn read(&self) -> StorageRef { + StorageRef { + inner: self.inner.read(), + } + } + + /// Returns a mutable reference to the value + pub fn write(&self) -> StorageRefMut { + StorageRefMut { + inner: self.inner.write(), + } + } + + /// Sets the value + pub fn set(&self, value: T) { + *self.write() = value; + } + + /// Modifies the value + pub fn modify(&self, f: F) { + f(&mut self.write()); + } +} + +impl UsePersistent { + /// Returns a clone of the value + pub fn get(&self) -> T { + self.read().clone() + } +} + +impl Display for UsePersistent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + (*self.read()).fmt(f) + } +} + +impl Deref for UsePersistent { + type Target = UseRef>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for UsePersistent { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs new file mode 100644 index 0000000..e28a62c --- /dev/null +++ b/src/storage/mod.rs @@ -0,0 +1,40 @@ +//! # dioxus-storage + +//! A library for handling local storage ergonomically in Dioxus + +//! ## Usage + +//! ```rust +//! use dioxus_storage::use_storage; +//! use dioxus::prelude::*; + +//! fn main() { +//! dioxus_web::launch(app) +//! } + +//! fn app(cx: Scope) -> Element { +//! let num = use_persistent(cx, "count", || 0); + +//! cx.render(rsx! { +//! div { +//! button { +//! onclick: move |_| { +//! num.modify(|num| *num += 1); +//! }, +//! "Increment" +//! } +//! div { +//! "{*num.read()}" +//! } +//! } +//! }) +//! } +//! ``` + +mod client_storage; +mod storage; + +pub use client_storage::{use_persistent, use_singleton_persistent}; + +#[cfg(not(target_arch = "wasm32"))] +pub use client_storage::set_dir; diff --git a/src/storage/storage.rs b/src/storage/storage.rs new file mode 100644 index 0000000..d417b54 --- /dev/null +++ b/src/storage/storage.rs @@ -0,0 +1,188 @@ +use dioxus::prelude::{use_ref, ScopeState, UseRef}; +use postcard::to_allocvec; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::fmt::{Debug, Display}; +use std::ops::{Deref, DerefMut}; + +pub fn serde_to_string(value: &T) -> String { + let serialized = to_allocvec(value).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 +} + +pub fn serde_from_string(value: &str) -> T { + try_serde_from_string(value).unwrap() +} + +pub 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); + } + let (decompressed, _) = yazi::decompress(&bytes, yazi::Format::Zlib).ok()?; + postcard::from_bytes(&decompressed).ok() +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct PersistentStorage { + pub data: Vec>, + pub idx: usize, +} + +pub trait StorageBacking { + type Key; + + fn get(key: &Self::Key) -> Option; + fn set(key: Self::Key, value: &T); +} + +#[derive(Clone, Default)] +pub struct StorageEntry { + key: S::Key, + pub(crate) data: T, +} + +impl Display for StorageEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.data.fmt(f) + } +} + +impl Debug for StorageEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.data.fmt(f) + } +} + +impl StorageEntry +where + S: StorageBacking, + T: Serialize + DeserializeOwned, + S::Key: Clone, +{ + pub fn new(key: S::Key, data: T) -> Self { + Self { key, data } + } + + pub(crate) fn save(&self) { + S::set(self.key.clone(), &self.data); + } + + pub fn write(&mut self) -> StorageEntryMut<'_, S, T> { + StorageEntryMut { + storage_entry: self, + } + } + + pub fn with_mut(&mut self, f: impl FnOnce(&mut T)) { + f(&mut self.data); + self.save(); + } +} + +impl Deref for StorageEntry { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +pub struct StorageEntryMut<'a, S, T> +where + S: StorageBacking, + T: Serialize + DeserializeOwned, + S::Key: Clone, +{ + storage_entry: &'a mut StorageEntry, +} + +impl<'a, S, T> Deref for StorageEntryMut<'a, S, T> +where + S: StorageBacking, + T: Serialize + DeserializeOwned, + S::Key: Clone, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.storage_entry.data + } +} + +impl<'a, S, T> DerefMut for StorageEntryMut<'a, S, T> +where + S: StorageBacking, + T: Serialize + DeserializeOwned, + S::Key: Clone, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.storage_entry.data + } +} + +impl<'a, S, T> Drop for StorageEntryMut<'a, S, T> +where + S: StorageBacking, + T: Serialize + DeserializeOwned, + S::Key: Clone, +{ + fn drop(&mut self) { + self.storage_entry.save(); + } +} + +pub fn storage_entry( + key: S::Key, + init: impl FnOnce() -> T, +) -> T { + S::get(&key).unwrap_or_else(|| { + let data = init(); + S::set(key, &data); + data + }) +} + +pub fn synced_storage_entry(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry +where + S: StorageBacking, + T: Serialize + DeserializeOwned, + S::Key: Clone, +{ + let data = storage_entry::(key.clone(), init); + + StorageEntry::new(key, data) +} + +pub fn use_synced_storage_entry( + cx: &ScopeState, + key: S::Key, + init: impl FnOnce() -> T, +) -> &UseRef> +where + S: StorageBacking + 'static, + T: Serialize + DeserializeOwned + 'static, + S::Key: Clone, +{ + use_ref(cx, || synced_storage_entry(key, init)) +} From 0c3f4bdf6f24ac5d6df9b00c4a0d62b127644843 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 3 Oct 2023 17:23:09 -0700 Subject: [PATCH 002/108] save work --- Cargo.toml | 11 +- examples/storage/Cargo.toml | 4 +- src/storage/client_storage/fs.rs | 92 +++++++++++++ src/storage/client_storage/mod.rs | 9 ++ src/storage/client_storage/web.rs | 84 ++++++++++++ src/storage/mod.rs | 13 +- .../{client_storage.rs => persistence.rs} | 123 +----------------- src/storage/storage.rs | 44 ++++++- 8 files changed, 248 insertions(+), 132 deletions(-) create mode 100644 src/storage/client_storage/fs.rs create mode 100644 src/storage/client_storage/mod.rs create mode 100644 src/storage/client_storage/web.rs rename src/storage/{client_storage.rs => persistence.rs} (60%) diff --git a/Cargo.toml b/Cargo.toml index 0cf4bb5..a0afd1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,15 +22,16 @@ geolocation = ["dep:windows", "dep:futures-util", "dep:futures", "dep:web-sys", color_scheme = ["dep:web-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] utils = ["dep:async-broadcast", "dep:uuid"] i18n = ["dep:serde", "dep:serde_json", "dep:unic-langid"] -storage = ["dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:directories", "dep:yazi"] +storage = ["dep:tokio", "dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:directories", "dep:yazi"] all = ["clipboard", "notifications", "geolocation", "color_scheme", "utils", "i18n", "storage"] +default = ["storage"] # CI testing wasm-testing = ["geolocation", "color_scheme", "utils", "i18n"] desktop-testing = ["clipboard", "notifications", "geolocation", "utils", "i18n"] [dependencies] -dioxus = { version = "0.4" } +dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "1ba6ca39e3f4d4fcd3b4e75e3a739d8c35514e5b" } cfg-if = "1.0.0" # utils @@ -51,7 +52,11 @@ unic-langid = { version = "0.9.1", features = ["serde"], optional = true } rustc-hash = "1.1.0" postcard = { version = "1.0.2", features = ["use-std"], optional = true } once_cell = { version = "1.17.0", optional = true } +dioxus-signals = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "1ba6ca39e3f4d4fcd3b4e75e3a739d8c35514e5b", features = [ + "serialize", +], optional = true } yazi = { version = "0.1.4", optional = true } +tokio = { version = "1.32.0", features = ["sync"], optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] directories = { version = "4.0.1", optional = true } @@ -60,7 +65,7 @@ directories = { version = "4.0.1", optional = true } windows = { version = "0.48.0", features = ["Foundation", "Devices_Geolocation"], optional = true } [target.'cfg(target_family = "wasm")'.dependencies] -web-sys = { version = "0.3.60", features = ["Window", "MediaQueryList", "Navigator", "Geolocation", "PositionOptions"], optional = true } +web-sys = { version = "0.3.60", features = ["Window", "MediaQueryList", "Navigator", "Geolocation", "PositionOptions", "EventTarget"], optional = true } wasm-bindgen = { version = "0.2.87", optional = true } wasm-bindgen-futures = { version = "0.4.35", optional = true} js-sys = "0.3.62" diff --git a/examples/storage/Cargo.toml b/examples/storage/Cargo.toml index 6ebfd2e..ab7ca2d 100644 --- a/examples/storage/Cargo.toml +++ b/examples/storage/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" [dependencies] dioxus-std = { path="../../", features = ["storage"] } -dioxus = "0.4" -dioxus-web = "0.4" +dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "1ba6ca39e3f4d4fcd3b4e75e3a739d8c35514e5b" } +dioxus-web = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "1ba6ca39e3f4d4fcd3b4e75e3a739d8c35514e5b" } log = "0.4.6" diff --git a/src/storage/client_storage/fs.rs b/src/storage/client_storage/fs.rs new file mode 100644 index 0000000..620f123 --- /dev/null +++ b/src/storage/client_storage/fs.rs @@ -0,0 +1,92 @@ +use once_cell::sync::OnceCell; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::io::Write; + +use crate::storage::storage::{ + serde_to_string, try_serde_from_string, StorageBacking, +}; + +#[allow(clippy::needless_doctest_main)] +/// Set the directory where the storage files are located on non-wasm targets. +/// +/// ```rust +/// fn main(){ +/// // set the directory to the default location +/// set_dir!(); +/// // set the directory to a custom location +/// set_dir!(PathBuf::from("path/to/dir")); +/// } +/// ``` +#[macro_export] +macro_rules! set_dir { + () => { + $crate::set_dir_name(env!("CARGO_PKG_NAME")); + }; + ($path: literal) => { + $crate::set_dir(std::path::PathBuf::from($path)); + }; +} +pub use set_dir; + +#[doc(hidden)] +/// Sets the directory where the storage files are located. +pub fn set_directory(path: std::path::PathBuf) { + LOCATION.set(path).unwrap(); +} + +#[doc(hidden)] +pub fn set_dir_name(name: &str) { + { + set_directory( + directories::BaseDirs::new() + .unwrap() + .data_local_dir() + .join(name), + ) + } +} + +static LOCATION: OnceCell = OnceCell::new(); + +fn set(key: String, value: &T) { + #[cfg(not(feature = "ssr"))] + { + let as_str = serde_to_string(value); + let path = LOCATION + .get() + .expect("Call the set_dir macro before accessing persistant data"); + std::fs::create_dir_all(path).unwrap(); + let file_path = path.join(key); + let mut file = std::fs::File::create(file_path).unwrap(); + file.write_all(as_str.as_bytes()).unwrap(); + } +} + +fn get(key: &str) -> Option { + #[cfg(not(feature = "ssr"))] + { + let path = LOCATION + .get() + .expect("Call the set_dir macro before accessing persistant data") + .join(key); + let s = std::fs::read_to_string(path).ok()?; + try_serde_from_string(&s) + } + #[cfg(feature = "ssr")] + None +} + +pub struct ClientStorage; + +impl StorageBacking for ClientStorage { + type Key = String; + + fn set(key: String, value: &T) { + set(key, value); + } + + fn get(key: &String) -> Option { + get(key) + } +} diff --git a/src/storage/client_storage/mod.rs b/src/storage/client_storage/mod.rs new file mode 100644 index 0000000..b053589 --- /dev/null +++ b/src/storage/client_storage/mod.rs @@ -0,0 +1,9 @@ +cfg_if::cfg_if! { + if #[cfg(target_family = "wasm")] { + pub mod web; + pub use web::*; + } else { + pub mod fs; + pub use fs::*; + } +} \ No newline at end of file diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs new file mode 100644 index 0000000..d2ccdfc --- /dev/null +++ b/src/storage/client_storage/web.rs @@ -0,0 +1,84 @@ +#![allow(unused)] +use dioxus::prelude::*; +use once_cell::sync::{Lazy, OnceCell}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::cell::{Ref, RefMut}; +use std::fmt::Debug; +use std::io::Write; +use std::thread::LocalKey; +use std::{ + fmt::Display, + ops::{Deref, DerefMut}, +}; +use web_sys::{window, Storage}; + +use crate::storage::storage::{ + serde_from_string, serde_to_string, storage_entry, try_serde_from_string, + use_synced_storage_entry, StorageBacking, StorageEntry, StorageEntryMut, +}; + +fn local_storage() -> Option { + window()?.local_storage().ok()? +} + +fn set(key: String, value: &T) { + #[cfg(not(feature = "ssr"))] + { + let as_str = serde_to_string(value); + local_storage().unwrap().set_item(&key, &as_str).unwrap(); + } +} + +fn get(key: &str) -> Option { + #[cfg(not(feature = "ssr"))] + { + let s: String = local_storage()?.get_item(key).ok()??; + try_serde_from_string(&s) + } + #[cfg(feature = "ssr")] + None +} + +pub struct ClientStorage; + +impl StorageBacking for ClientStorage { + type Key = String; + + fn get_subscriptions() -> &'static Mutex>> { + &STORAGE_SUBSCRIPTIONS + } + + fn subscribe(key: &Self::Key) -> Option> { + do_storage_backing_subscribe::(key) + } + + fn set(key: String, value: &T) { + set(key, value); + } + + fn get(key: &String) -> Option { + get(key) + } +} + +static STORAGE_SUBSCRIPTIONS: Lazy>>> = Lazy::new(|| { + window() + .unwrap() + .add_event_listener_with_callback("storage", |e: web_sys::StorageEvent| { + process_storage_event(e); + }) + .unwrap(); + Mutex::new(HashMap::new()) +}); + +fn process_storage_event(e: web_sys::StorageEvent) { + let key = e.key().unwrap(); + let s: String = local_storage()?.get_item(&key).ok()??; + let value = try_serde_from_string(&s)?; + for subscription in STORAGE_SUBSCRIPTIONS.iter() { + if subscription.key == key { + subscription.callback(value); + } + } +} \ No newline at end of file diff --git a/src/storage/mod.rs b/src/storage/mod.rs index e28a62c..c5a1286 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,20 +1,15 @@ //! # dioxus-storage - //! A library for handling local storage ergonomically in Dioxus - //! ## Usage - //! ```rust //! use dioxus_storage::use_storage; //! use dioxus::prelude::*; - //! fn main() { //! dioxus_web::launch(app) //! } - +//! //! fn app(cx: Scope) -> Element { //! let num = use_persistent(cx, "count", || 0); - //! cx.render(rsx! { //! div { //! button { @@ -33,8 +28,10 @@ mod client_storage; mod storage; +mod persistence; -pub use client_storage::{use_persistent, use_singleton_persistent}; +pub use persistence::{use_persistent, use_singleton_persistent}; +pub use client_storage::ClientStorage; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(not(target_family = "wasm"))] pub use client_storage::set_dir; diff --git a/src/storage/client_storage.rs b/src/storage/persistence.rs similarity index 60% rename from src/storage/client_storage.rs rename to src/storage/persistence.rs index fcd481e..b591b48 100644 --- a/src/storage/client_storage.rs +++ b/src/storage/persistence.rs @@ -1,130 +1,19 @@ -#![allow(unused)] use dioxus::prelude::*; -use once_cell::sync::OnceCell; use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use std::cell::{Ref, RefMut}; -use std::fmt::Debug; -use std::io::Write; -use std::thread::LocalKey; use std::{ fmt::Display, ops::{Deref, DerefMut}, }; -use web_sys::{window, Storage}; -use crate::storage::storage::{ - serde_from_string, serde_to_string, storage_entry, try_serde_from_string, - use_synced_storage_entry, StorageBacking, StorageEntry, StorageEntryMut, +use crate::storage::{ + ClientStorage, + storage::{ + storage_entry, StorageEntry, + }, }; -#[allow(clippy::needless_doctest_main)] -/// Set the directory where the storage files are located on non-wasm targets. -/// -/// ```rust -/// fn main(){ -/// // set the directory to the default location -/// set_dir!(); -/// // set the directory to a custom location -/// set_dir!(PathBuf::from("path/to/dir")); -/// } -/// ``` -#[cfg(not(target_arch = "wasm32"))] -#[macro_export] -macro_rules! set_dir { - () => { - $crate::set_dir_name(env!("CARGO_PKG_NAME")); - }; - ($path: literal) => { - $crate::set_dir(std::path::PathBuf::from($path)); - }; -} - -#[cfg(not(target_arch = "wasm32"))] -#[doc(hidden)] -/// Sets the directory where the storage files are located. -pub fn set_directory(path: std::path::PathBuf) { - LOCATION.set(path).unwrap(); -} - -#[cfg(not(target_arch = "wasm32"))] -#[doc(hidden)] -pub fn set_dir_name(name: &str) { - { - set_directory( - directories::BaseDirs::new() - .unwrap() - .data_local_dir() - .join(name), - ) - } -} - -#[cfg(not(target_arch = "wasm32"))] -static LOCATION: OnceCell = OnceCell::new(); - -#[cfg(target_arch = "wasm32")] -fn local_storage() -> Option { - window()?.local_storage().ok()? -} - -fn set(key: String, value: &T) { - #[cfg(not(feature = "ssr"))] - { - let as_str = serde_to_string(value); - #[cfg(target_arch = "wasm32")] - { - local_storage().unwrap().set_item(&key, &as_str).unwrap(); - } - #[cfg(not(target_arch = "wasm32"))] - { - let path = LOCATION - .get() - .expect("Call the set_dir macro before accessing persistant data"); - std::fs::create_dir_all(path).unwrap(); - let file_path = path.join(key); - let mut file = std::fs::File::create(file_path).unwrap(); - file.write_all(as_str.as_bytes()).unwrap(); - } - } -} - -fn get(key: &str) -> Option { - #[cfg(not(feature = "ssr"))] - { - #[cfg(target_arch = "wasm32")] - { - let s: String = local_storage()?.get_item(key).ok()??; - try_serde_from_string(&s) - } - #[cfg(not(target_arch = "wasm32"))] - { - let path = LOCATION - .get() - .expect("Call the set_dir macro before accessing persistant data") - .join(key); - let s = std::fs::read_to_string(path).ok()?; - try_serde_from_string(&s) - } - } - #[cfg(feature = "ssr")] - None -} - -pub struct ClientStorage; - -impl StorageBacking for ClientStorage { - type Key = String; - - fn set(key: String, value: &T) { - set(key, value); - } - - fn get(key: &String) -> Option { - get(key) - } -} - /// 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 diff --git a/src/storage/storage.rs b/src/storage/storage.rs index d417b54..4c3183c 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage.rs @@ -1,9 +1,17 @@ +use tokio::sync::broadcast::{channel, Receiver, Sender}; use dioxus::prelude::{use_ref, ScopeState, UseRef}; +use dioxus_signals::Signal; +use once_cell::sync::Lazy; use postcard::to_allocvec; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::collections::HashMap; +use std::error::Error; use std::fmt::{Debug, Display}; use std::ops::{Deref, DerefMut}; +use std::sync::Mutex; +use std::hash::Hash; pub fn serde_to_string(value: &T) -> String { let serialized = to_allocvec(value).unwrap(); @@ -50,12 +58,44 @@ pub struct PersistentStorage { } pub trait StorageBacking { - type Key; - + type Key: Eq + PartialEq + Hash; + // fn subscribe(key: &Self::Key) + // fn get_subscriptions() -> &'static HashMap>; + fn get_subscriptions() -> &'static Mutex>>; + fn subscribe(key: &Self::Key) -> Option> { + do_storage_backing_subscribe::(key) + } fn get(key: &Self::Key) -> Option; fn set(key: Self::Key, value: &T); } +pub(crate) fn do_storage_backing_subscribe(key: &S::Key) -> Option> { + #[cfg(not(feature = "ssr"))] + { + let mut subscriptions = S::get_subscriptions().lock().unwrap(); + if let Some(channel) = subscriptions.get(key) { + if let Some(channel) = channel.downcast_ref::>() { + Some(channel.channel.subscribe()) + } else { + None + } + } else { + let (tx, rx) = channel::(2); + subscriptions.insert( + *key, + Box::new(StorageSender:: { channel: tx }), + ); + return Some(rx) + } + } + #[cfg(feature = "ssr")] + None +} + +struct StorageSender { + channel: Sender, +} + #[derive(Clone, Default)] pub struct StorageEntry { key: S::Key, From 5ae1da263882882af84c75289a2356236262675c Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 3 Oct 2023 17:26:46 -0700 Subject: [PATCH 003/108] swap back to async_broadcast --- Cargo.toml | 3 +-- src/storage/storage.rs | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a0afd1d..04e2223 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ geolocation = ["dep:windows", "dep:futures-util", "dep:futures", "dep:web-sys", color_scheme = ["dep:web-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] utils = ["dep:async-broadcast", "dep:uuid"] i18n = ["dep:serde", "dep:serde_json", "dep:unic-langid"] -storage = ["dep:tokio", "dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:directories", "dep:yazi"] +storage = ["dep:async-broadcast", "dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:directories", "dep:yazi"] all = ["clipboard", "notifications", "geolocation", "color_scheme", "utils", "i18n", "storage"] default = ["storage"] @@ -56,7 +56,6 @@ dioxus-signals = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "1ba6 "serialize", ], optional = true } yazi = { version = "0.1.4", optional = true } -tokio = { version = "1.32.0", features = ["sync"], optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] directories = { version = "4.0.1", optional = true } diff --git a/src/storage/storage.rs b/src/storage/storage.rs index 4c3183c..d75df18 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage.rs @@ -1,4 +1,4 @@ -use tokio::sync::broadcast::{channel, Receiver, Sender}; +use async_broadcast::{broadcast, Receiver, Sender}; use dioxus::prelude::{use_ref, ScopeState, UseRef}; use dioxus_signals::Signal; use once_cell::sync::Lazy; @@ -62,25 +62,25 @@ pub trait StorageBacking { // fn subscribe(key: &Self::Key) // fn get_subscriptions() -> &'static HashMap>; fn get_subscriptions() -> &'static Mutex>>; - fn subscribe(key: &Self::Key) -> Option> { + fn subscribe(key: &Self::Key) -> Option> { do_storage_backing_subscribe::(key) } fn get(key: &Self::Key) -> Option; fn set(key: Self::Key, value: &T); } -pub(crate) fn do_storage_backing_subscribe(key: &S::Key) -> Option> { +pub(crate) fn do_storage_backing_subscribe(key: &S::Key) -> Option> { #[cfg(not(feature = "ssr"))] { let mut subscriptions = S::get_subscriptions().lock().unwrap(); if let Some(channel) = subscriptions.get(key) { if let Some(channel) = channel.downcast_ref::>() { - Some(channel.channel.subscribe()) + Some(channel.channel.new_receiver()) } else { None } } else { - let (tx, rx) = channel::(2); + let (tx, rx) = broadcast::(2); subscriptions.insert( *key, Box::new(StorageSender:: { channel: tx }), From e963c80474109e8c55945092d4d0fd00734c6dfa Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 3 Oct 2023 17:27:32 -0700 Subject: [PATCH 004/108] remove comments --- src/storage/storage.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/storage/storage.rs b/src/storage/storage.rs index d75df18..71bd198 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage.rs @@ -59,8 +59,6 @@ pub struct PersistentStorage { pub trait StorageBacking { type Key: Eq + PartialEq + Hash; - // fn subscribe(key: &Self::Key) - // fn get_subscriptions() -> &'static HashMap>; fn get_subscriptions() -> &'static Mutex>>; fn subscribe(key: &Self::Key) -> Option> { do_storage_backing_subscribe::(key) From 0e69cd5a6d344912b64d82bf1bceb5840a9741a9 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Wed, 4 Oct 2023 21:42:48 -0700 Subject: [PATCH 005/108] save work --- Cargo.toml | 9 +- src/storage/client_storage/web.rs | 43 +++++--- src/storage/persistence.rs | 100 ++++++++++-------- src/storage/storage.rs | 167 +++++++++++++++++++++++------- 4 files changed, 225 insertions(+), 94 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 04e2223..1f60dd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ geolocation = ["dep:windows", "dep:futures-util", "dep:futures", "dep:web-sys", color_scheme = ["dep:web-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] utils = ["dep:async-broadcast", "dep:uuid"] i18n = ["dep:serde", "dep:serde_json", "dep:unic-langid"] -storage = ["dep:async-broadcast", "dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:directories", "dep:yazi"] +storage = ["dep:async-broadcast", "dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:wasm-bindgen", "dep:directories", "dep:yazi"] all = ["clipboard", "notifications", "geolocation", "color_scheme", "utils", "i18n", "storage"] default = ["storage"] @@ -56,6 +56,11 @@ dioxus-signals = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "1ba6 "serialize", ], optional = true } yazi = { version = "0.1.4", optional = true } +log = "0.4.6" + +# WebAssembly Debug +wasm-logger = "0.2.0" +console_error_panic_hook = "0.1.7" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] directories = { version = "4.0.1", optional = true } @@ -64,7 +69,7 @@ directories = { version = "4.0.1", optional = true } windows = { version = "0.48.0", features = ["Foundation", "Devices_Geolocation"], optional = true } [target.'cfg(target_family = "wasm")'.dependencies] -web-sys = { version = "0.3.60", features = ["Window", "MediaQueryList", "Navigator", "Geolocation", "PositionOptions", "EventTarget"], optional = true } +web-sys = { version = "0.3.60", features = ["Window", "MediaQueryList", "Navigator", "Geolocation", "PositionOptions", "EventTarget", "StorageEvent"], optional = true } wasm-bindgen = { version = "0.2.87", optional = true } wasm-bindgen-futures = { version = "0.4.35", optional = true} js-sys = "0.3.62" diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index d2ccdfc..a3c8a02 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -1,11 +1,17 @@ #![allow(unused)] +use async_broadcast::Receiver; use dioxus::prelude::*; use once_cell::sync::{Lazy, OnceCell}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; +use wasm_bindgen::JsCast; +use wasm_bindgen::prelude::Closure; +use std::any::TypeId; use std::cell::{Ref, RefMut}; +use std::collections::HashMap; use std::fmt::Debug; use std::io::Write; +use std::sync::Mutex; use std::thread::LocalKey; use std::{ fmt::Display, @@ -15,7 +21,7 @@ use web_sys::{window, Storage}; use crate::storage::storage::{ serde_from_string, serde_to_string, storage_entry, try_serde_from_string, - use_synced_storage_entry, StorageBacking, StorageEntry, StorageEntryMut, + use_synced_storage_entry, StorageBacking, StorageEntry, StorageEntryMut, StorageSender, do_storage_backing_subscribe, StorageChannelPayload, }; fn local_storage() -> Option { @@ -45,11 +51,11 @@ pub struct ClientStorage; impl StorageBacking for ClientStorage { type Key = String; - fn get_subscriptions() -> &'static Mutex>> { + fn get_subscriptions() -> &'static Mutex> { &STORAGE_SUBSCRIPTIONS } - fn subscribe(key: &Self::Key) -> Option> { + fn subscribe(key: &Self::Key) -> Option> { do_storage_backing_subscribe::(key) } @@ -62,23 +68,36 @@ impl StorageBacking for ClientStorage { } } -static STORAGE_SUBSCRIPTIONS: Lazy>>> = Lazy::new(|| { +static STORAGE_SUBSCRIPTIONS: Lazy>> = Lazy::new(|| { + + log::info!("Initializing storage subscriptions"); + let closure = Closure::::wrap(Box::new(|e: web_sys::StorageEvent| { + process_storage_event(e); + })); window() .unwrap() - .add_event_listener_with_callback("storage", |e: web_sys::StorageEvent| { - process_storage_event(e); - }) + .add_event_listener_with_callback("storage", closure.as_ref().unchecked_ref()) .unwrap(); Mutex::new(HashMap::new()) }); fn process_storage_event(e: web_sys::StorageEvent) { + log::info!("Incoming storage event"); let key = e.key().unwrap(); - let s: String = local_storage()?.get_item(&key).ok()??; - let value = try_serde_from_string(&s)?; - for subscription in STORAGE_SUBSCRIPTIONS.iter() { - if subscription.key == key { - subscription.callback(value); + if let Some(storage) = local_storage() { + let value = storage.get_item(&key).unwrap(); + for subscription in STORAGE_SUBSCRIPTIONS.lock().unwrap().values() { + if subscription.data_type_id == TypeId::of::() { + log::info!("broadcasting"); + subscription.channel.broadcast(StorageChannelPayload::Updated); + } } } + // let s: String = local_storage().map(f)?.get_item(&key).ok()??; + // let value = try_serde_from_string(&s)?; + // for subscription in STORAGE_SUBSCRIPTIONS.iter() { + // if subscription.key == key { + // subscription.callback(value); + // } + // } } \ No newline at end of file diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index b591b48..62027be 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -1,7 +1,8 @@ use dioxus::prelude::*; +use dioxus_signals::{use_signal, Signal, Write}; use serde::de::DeserializeOwned; use serde::Serialize; -use std::cell::{Ref, RefMut}; +use std::cell::Ref; use std::{ fmt::Display, ops::{Deref, DerefMut}, @@ -18,44 +19,55 @@ use crate::storage::{ /// /// Depending on the platform this uses either local storage or a file storage #[allow(clippy::needless_return)] -pub fn use_persistent( +pub fn use_persistent( cx: &ScopeState, key: impl ToString, init: impl FnOnce() -> T, ) -> &UsePersistent { let mut init = Some(init); - #[cfg(feature = "ssr")] - let state = use_ref(cx, || { - StorageEntry::::new(key.to_string(), init.take().unwrap()()) - }); - // if hydration is not enabled we can just set the storage - #[cfg(all(not(feature = "ssr"), not(feature = "hydrate")))] - let state = use_ref(cx, || { - StorageEntry::new( - key.to_string(), - storage_entry::(key.to_string(), init.take().unwrap()), - ) - }); - // otherwise render the initial value and then hydrate after the first render - #[cfg(all(not(feature = "ssr"), feature = "hydrate"))] let state = { - let state = use_ref(cx, || { - StorageEntry::::new(key.to_string(), init.take().unwrap()()) - }); - if cx.generation() == 0 { - cx.needs_update(); + #[cfg(feature = "ssr")] + { + use_ref(cx, || { + StorageEntry::::new(key.to_string(), init.take().unwrap()(), false) + }) } - if cx.generation() == 1 { - state.set(StorageEntry::new( - key.to_string(), - storage_entry::(key.to_string(), init.take().unwrap()), - )); + #[cfg(all(not(feature = "ssr"), not(feature = "hydrate")))] + { + let state = use_signal(cx, || { + StorageEntry::::new( + key.to_string(), + storage_entry::(key.to_string(), init.take().unwrap()), + true + ) + }); + use_future!(cx, || async move { + let mut channel = state.read().channel.clone(); + channel.start_receiver_loop(|_| state.with_mut(|storage_entry| storage_entry.update())).await; + }); + state + } + #[cfg(all(not(feature = "ssr"), feature = "hydrate"))] + { + let state = use_ref(cx, || { + StorageEntry::::new(key.to_string(), init.take().unwrap()(), true) + }); + if cx.generation() == 0 { + cx.needs_update(); + } + if cx.generation() == 1 { + state.set(StorageEntry::new( + key.to_string(), + storage_entry::(key.to_string(), init.take().unwrap()), + )); + } + + state } - state }; cx.use_hook(|| UsePersistent { - inner: state.clone(), + inner: state, }) } @@ -65,7 +77,7 @@ pub fn use_persistent( /// Depending on the platform this uses either local storage or a file storage #[allow(clippy::needless_return)] #[track_caller] -pub fn use_singleton_persistent( +pub fn use_singleton_persistent( cx: &ScopeState, init: impl FnOnce() -> T, ) -> &UsePersistent { @@ -73,14 +85,15 @@ pub fn use_singleton_persistent { +pub struct StorageRef<'a, T: Serialize + DeserializeOwned + Default + Clone + 'static> { inner: Ref<'a, StorageEntry>, } -impl<'a, T: Serialize + DeserializeOwned + Default + 'static> Deref for StorageRef<'a, T> { +impl<'a, T: Serialize + DeserializeOwned + Default + Clone + 'static> Deref for StorageRef<'a, T> { type Target = T; fn deref(&self) -> &Self::Target { @@ -88,11 +101,11 @@ impl<'a, T: Serialize + DeserializeOwned + Default + 'static> Deref for StorageR } } -pub struct StorageRefMut<'a, T: Serialize + DeserializeOwned + 'static> { - inner: RefMut<'a, StorageEntry>, +pub struct StorageRefMut<'a, T: Serialize + DeserializeOwned + Clone + 'static> { + inner: Write<'a, StorageEntry>, } -impl<'a, T: Serialize + DeserializeOwned + 'static> Deref for StorageRefMut<'a, T> { +impl<'a, T: Serialize + DeserializeOwned + Clone + 'static> Deref for StorageRefMut<'a, T> { type Target = T; fn deref(&self) -> &Self::Target { @@ -100,24 +113,24 @@ impl<'a, T: Serialize + DeserializeOwned + 'static> Deref for StorageRefMut<'a, } } -impl<'a, T: Serialize + DeserializeOwned + 'static> DerefMut for StorageRefMut<'a, T> { +impl<'a, T: Serialize + DeserializeOwned + Clone + 'static> DerefMut for StorageRefMut<'a, T> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner.data } } -impl<'a, T: Serialize + DeserializeOwned + 'static> Drop for StorageRefMut<'a, T> { +impl<'a, T: Serialize + DeserializeOwned + Clone + 'static> Drop for StorageRefMut<'a, T> { fn drop(&mut self) { self.inner.deref_mut().save(); } } /// Storage that persists across application reloads -pub struct UsePersistent { - inner: UseRef>, +pub struct UsePersistent { + inner: Signal>, } -impl UsePersistent { +impl UsePersistent { /// Returns a reference to the value pub fn read(&self) -> StorageRef { StorageRef { @@ -135,6 +148,7 @@ impl UsePersistent { /// Sets the value pub fn set(&self, value: T) { *self.write() = value; + self.inner.write().save(); } /// Modifies the value @@ -150,21 +164,21 @@ impl UsePersistent< } } -impl Display for UsePersistent { +impl Display for UsePersistent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { (*self.read()).fmt(f) } } -impl Deref for UsePersistent { - type Target = UseRef>; +impl Deref for UsePersistent { + type Target = Signal>; fn deref(&self) -> &Self::Target { &self.inner } } -impl DerefMut for UsePersistent { +impl DerefMut for UsePersistent { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } diff --git a/src/storage/storage.rs b/src/storage/storage.rs index 71bd198..ae36ef3 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage.rs @@ -1,13 +1,11 @@ -use async_broadcast::{broadcast, Receiver, Sender}; -use dioxus::prelude::{use_ref, ScopeState, UseRef}; -use dioxus_signals::Signal; -use once_cell::sync::Lazy; +use async_broadcast::{broadcast, Receiver, Sender, InactiveReceiver}; +use dioxus::prelude::{ScopeState, use_future}; +use dioxus_signals::{Signal, use_signal}; use postcard::to_allocvec; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -use std::any::Any; +use std::any::TypeId; use std::collections::HashMap; -use std::error::Error; use std::fmt::{Debug, Display}; use std::ops::{Deref, DerefMut}; use std::sync::Mutex; @@ -58,55 +56,71 @@ pub struct PersistentStorage { } pub trait StorageBacking { - type Key: Eq + PartialEq + Hash; - fn get_subscriptions() -> &'static Mutex>>; - fn subscribe(key: &Self::Key) -> Option> { - do_storage_backing_subscribe::(key) - } + type Key: Eq + PartialEq + Hash + Clone + Debug; + fn get_subscriptions() -> &'static Mutex>; + fn subscribe(key: &Self::Key) -> Option>; fn get(key: &Self::Key) -> Option; fn set(key: Self::Key, value: &T); } -pub(crate) fn do_storage_backing_subscribe(key: &S::Key) -> Option> { +pub(crate) fn do_storage_backing_subscribe(key: &S::Key) -> Option> { + log::info!("Subscribing to storage entry: {:?}", key); #[cfg(not(feature = "ssr"))] { let mut subscriptions = S::get_subscriptions().lock().unwrap(); if let Some(channel) = subscriptions.get(key) { - if let Some(channel) = channel.downcast_ref::>() { + if channel.data_type_id == TypeId::of::() { + log::info!("Already subscribed to storage entry: {:?}", key); Some(channel.channel.new_receiver()) } else { + log::info!("Already subscribed to storage entry: {:?} but with a different type", key); None } } else { - let (tx, rx) = broadcast::(2); + let (tx, rx) = broadcast::(2); subscriptions.insert( - *key, - Box::new(StorageSender:: { channel: tx }), + key.clone(), + StorageSender { + channel: tx, + data_type_id: TypeId::of::(), + }, ); - return Some(rx) + log::info!("Subscribed to storage entry: {:?}", key); + Some(rx) } } #[cfg(feature = "ssr")] - None + { + log::info!("Subscription not supported for SSR"); + None + } +} + + +pub struct StorageSender { + pub(crate) channel: Sender, + pub(crate) data_type_id: TypeId, } -struct StorageSender { - channel: Sender, +#[derive(Clone)] +pub enum StorageChannelPayload { + Updated, } #[derive(Clone, Default)] -pub struct StorageEntry { +pub struct StorageEntry { key: S::Key, + pub(crate) channel: StorageEntryChannel, pub(crate) data: T, } -impl Display for StorageEntry { +impl Display for StorageEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.data.fmt(f) } } -impl Debug for StorageEntry { +impl Debug for StorageEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.data.fmt(f) } @@ -115,11 +129,25 @@ impl Debug for Stora impl StorageEntry where S: StorageBacking, - T: Serialize + DeserializeOwned, + T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { - pub fn new(key: S::Key, data: T) -> Self { - Self { key, data } + pub fn new(key: S::Key, data: T, subscribe: bool) -> Self { + let channel = { + #[cfg(feature = "ssr")] + { + StorageEntryChannel::default() + } + #[cfg(not(feature = "ssr"))] + { + let mut channel = StorageEntryChannel::new(S::subscribe::(&key)); + if !subscribe { + channel.deactivate(); + } + channel + } + }; + Self { key, data, channel } } pub(crate) fn save(&self) { @@ -136,9 +164,13 @@ where f(&mut self.data); self.save(); } + + pub fn update(&mut self) { + self.data = S::get(&self.key).unwrap_or(self.data.clone()); + } } -impl Deref for StorageEntry { +impl Deref for StorageEntry { type Target = T; fn deref(&self) -> &Self::Target { @@ -146,10 +178,16 @@ impl Deref for StorageEntry< } } +impl Drop for StorageEntry { + fn drop(&mut self) { + + } +} + pub struct StorageEntryMut<'a, S, T> where S: StorageBacking, - T: Serialize + DeserializeOwned, + T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { storage_entry: &'a mut StorageEntry, @@ -158,7 +196,7 @@ where impl<'a, S, T> Deref for StorageEntryMut<'a, S, T> where S: StorageBacking, - T: Serialize + DeserializeOwned, + T: Serialize + DeserializeOwned + Clone, S::Key: Clone, { type Target = T; @@ -171,7 +209,7 @@ where impl<'a, S, T> DerefMut for StorageEntryMut<'a, S, T> where S: StorageBacking, - T: Serialize + DeserializeOwned, + T: Serialize + DeserializeOwned + Clone, S::Key: Clone, { fn deref_mut(&mut self) -> &mut Self::Target { @@ -182,7 +220,7 @@ where impl<'a, S, T> Drop for StorageEntryMut<'a, S, T> where S: StorageBacking, - T: Serialize + DeserializeOwned, + T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { fn drop(&mut self) { @@ -190,6 +228,54 @@ where } } +#[derive(Clone, Default)] +pub(crate) enum StorageEntryChannel { + #[default] + None, + Active(Receiver), + Inactive(InactiveReceiver), +} + +impl StorageEntryChannel { + fn new(receiver: Option>) -> Self { + match receiver { + Some(receiver) => Self::Active(receiver), + None => Self::None, + } + } + fn activate(&mut self) { + if let Self::Inactive(channel) = self { + *self = Self::Active(channel.activate_cloned()) + } + } + fn deactivate(&mut self) { + if let Self::Active(channel) = self { + *self = Self::Inactive(channel.clone().deactivate()) + } + } + pub async fn start_receiver_loop(&mut self, action:impl Fn(StorageChannelPayload)) { + async fn receiver_loop(receiver: &mut Receiver, action:impl Fn(StorageChannelPayload)) { + loop { + if let Ok(data) = receiver.recv().await { + action(data); + } + } + } + match self { + Self::Active(receiver) => { + receiver_loop(receiver, action).await; + } + Self::Inactive(_) => { + self.activate(); + if let Self::Active(receiver) = self { + receiver_loop(receiver, action).await; + } + } + _ => {} + } + } +} + pub fn storage_entry( key: S::Key, init: impl FnOnce() -> T, @@ -201,26 +287,33 @@ pub fn storage_entry( }) } -pub fn synced_storage_entry(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry +pub fn synced_storage_entry(key: S::Key, init: impl FnOnce() -> T, subscribe: bool) -> StorageEntry where S: StorageBacking, - T: Serialize + DeserializeOwned, + T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { let data = storage_entry::(key.clone(), init); - StorageEntry::new(key, data) + StorageEntry::new(key, data, subscribe) } pub fn use_synced_storage_entry( cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T, -) -> &UseRef> +) -> Signal> where S: StorageBacking + 'static, - T: Serialize + DeserializeOwned + 'static, + T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { - use_ref(cx, || synced_storage_entry(key, init)) + let state = use_signal(cx, || { + synced_storage_entry::(key, init, true) + }); + use_future!(cx, || async move { + let mut channel = state.read().channel.clone(); + channel.start_receiver_loop(|_| state.with_mut(|storage_entry| storage_entry.update())).await; + }); + state } From 061601132f4ff61bb1d73626128d3d47526a628e Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Thu, 5 Oct 2023 17:25:01 -0700 Subject: [PATCH 006/108] save work --- Cargo.toml | 2 +- src/storage/client_storage/web.rs | 54 +++++--------- src/storage/persistence.rs | 12 +-- src/storage/storage.rs | 117 +++++++++++++++--------------- src/utils/channel/use_channel.rs | 20 +++-- 5 files changed, 97 insertions(+), 108 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1f60dd6..6009b5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ directories = { version = "4.0.1", optional = true } windows = { version = "0.48.0", features = ["Foundation", "Devices_Geolocation"], optional = true } [target.'cfg(target_family = "wasm")'.dependencies] -web-sys = { version = "0.3.60", features = ["Window", "MediaQueryList", "Navigator", "Geolocation", "PositionOptions", "EventTarget", "StorageEvent"], optional = true } +web-sys = { version = "0.3.60", features = ["Window", "MediaQueryList", "Navigator", "Geolocation", "PositionOptions", "EventTarget", "EventListener", "StorageEvent"], optional = true } wasm-bindgen = { version = "0.2.87", optional = true } wasm-bindgen-futures = { version = "0.4.35", optional = true} js-sys = "0.3.62" diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index a3c8a02..6533074 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -11,6 +11,7 @@ use std::cell::{Ref, RefMut}; use std::collections::HashMap; use std::fmt::Debug; use std::io::Write; +use std::rc::Rc; use std::sync::Mutex; use std::thread::LocalKey; use std::{ @@ -21,8 +22,9 @@ use web_sys::{window, Storage}; use crate::storage::storage::{ serde_from_string, serde_to_string, storage_entry, try_serde_from_string, - use_synced_storage_entry, StorageBacking, StorageEntry, StorageEntryMut, StorageSender, do_storage_backing_subscribe, StorageChannelPayload, + use_synced_storage_entry, StorageBacking, StorageEntry, StorageEntryMut, StorageSender, do_storage_backing_subscribe, StorageChannelPayload, StorageBackingSubscriptions, }; +use crate::utils::channel::UseChannel; fn local_storage() -> Option { window()?.local_storage().ok()? @@ -46,17 +48,23 @@ fn get(key: &str) -> Option { None } +#[derive(Clone)] pub struct ClientStorage; impl StorageBacking for ClientStorage { type Key = String; - fn get_subscriptions() -> &'static Mutex> { - &STORAGE_SUBSCRIPTIONS - } - - fn subscribe(key: &Self::Key) -> Option> { - do_storage_backing_subscribe::(key) + fn subscribe(cx: &ScopeState, key: &Self::Key) -> Option> { + let channel = do_storage_backing_subscribe::(cx, key); + let subscriptions = cx.consume_context::>().unwrap(); + let closure = cx.provide_root_context::>>(Rc::new(Closure::::new(|e: web_sys::StorageEvent| { + process_storage_event(subscriptions, e); + }))); + window() + .unwrap() + .add_event_listener_with_callback("storage", closure.deref().as_ref().unchecked_ref()) + .unwrap(); + channel } fn set(key: String, value: &T) { @@ -68,36 +76,10 @@ impl StorageBacking for ClientStorage { } } -static STORAGE_SUBSCRIPTIONS: Lazy>> = Lazy::new(|| { - - log::info!("Initializing storage subscriptions"); - let closure = Closure::::wrap(Box::new(|e: web_sys::StorageEvent| { - process_storage_event(e); - })); - window() - .unwrap() - .add_event_listener_with_callback("storage", closure.as_ref().unchecked_ref()) - .unwrap(); - Mutex::new(HashMap::new()) -}); - -fn process_storage_event(e: web_sys::StorageEvent) { +fn process_storage_event(subscriptions: StorageBackingSubscriptions, e: web_sys::StorageEvent) { log::info!("Incoming storage event"); let key = e.key().unwrap(); - if let Some(storage) = local_storage() { - let value = storage.get_item(&key).unwrap(); - for subscription in STORAGE_SUBSCRIPTIONS.lock().unwrap().values() { - if subscription.data_type_id == TypeId::of::() { - log::info!("broadcasting"); - subscription.channel.broadcast(StorageChannelPayload::Updated); - } - } + if let Some(storage_sender) = subscriptions.get(&key) { + storage_sender.channel.send(StorageChannelPayload::Updated); } - // let s: String = local_storage().map(f)?.get_item(&key).ok()??; - // let value = try_serde_from_string(&s)?; - // for subscription in STORAGE_SUBSCRIPTIONS.iter() { - // if subscription.key == key { - // subscription.callback(value); - // } - // } } \ No newline at end of file diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index 62027be..ec87e96 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -7,7 +7,8 @@ use std::{ fmt::Display, ops::{Deref, DerefMut}, }; - +use crate::storage::storage::StorageEntryChannel; +use crate::utils::channel::use_listen_channel; use crate::storage::{ ClientStorage, storage::{ @@ -38,13 +39,12 @@ pub fn use_persistent::new( key.to_string(), storage_entry::(key.to_string(), init.take().unwrap()), - true + Some(cx), ) }); - use_future!(cx, || async move { - let mut channel = state.read().channel.clone(); - channel.start_receiver_loop(|_| state.with_mut(|storage_entry| storage_entry.update())).await; - }); + if let StorageEntryChannel::Active(channel) = &state.read().channel { + use_listen_channel(cx, channel, move |_| async move { state.write().update() }); + } state } #[cfg(all(not(feature = "ssr"), feature = "hydrate"))] diff --git a/src/storage/storage.rs b/src/storage/storage.rs index ae36ef3..4652bfb 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage.rs @@ -1,5 +1,5 @@ use async_broadcast::{broadcast, Receiver, Sender, InactiveReceiver}; -use dioxus::prelude::{ScopeState, use_future}; +use dioxus::prelude::{ScopeState, use_future, use_context}; use dioxus_signals::{Signal, use_signal}; use postcard::to_allocvec; use serde::de::DeserializeOwned; @@ -8,8 +8,11 @@ use std::any::TypeId; use std::collections::HashMap; use std::fmt::{Debug, Display}; use std::ops::{Deref, DerefMut}; -use std::sync::Mutex; +use std::sync::{Mutex, RwLock, Arc}; use std::hash::Hash; +use uuid::Uuid; + +use crate::utils::channel::{UseChannel, use_channel, use_listen_channel}; pub fn serde_to_string(value: &T) -> String { let serialized = to_allocvec(value).unwrap(); @@ -55,38 +58,40 @@ pub struct PersistentStorage { pub idx: usize, } -pub trait StorageBacking { +pub trait StorageBacking: Sized + Clone + 'static { type Key: Eq + PartialEq + Hash + Clone + Debug; - fn get_subscriptions() -> &'static Mutex>; - fn subscribe(key: &Self::Key) -> Option>; + fn subscribe(cx: &ScopeState, key: &Self::Key) -> Option>; fn get(key: &Self::Key) -> Option; fn set(key: Self::Key, value: &T); + fn get_subscriptions(cx: &ScopeState) -> StorageBackingSubscriptions { + cx.consume_context::>().unwrap_or_else(|| cx.provide_root_context(StorageBackingSubscriptions::::new())) + } } -pub(crate) fn do_storage_backing_subscribe(key: &S::Key) -> Option> { +pub(crate) fn do_storage_backing_subscribe(cx: &ScopeState, key: &S::Key) -> Option> { log::info!("Subscribing to storage entry: {:?}", key); #[cfg(not(feature = "ssr"))] { - let mut subscriptions = S::get_subscriptions().lock().unwrap(); - if let Some(channel) = subscriptions.get(key) { - if channel.data_type_id == TypeId::of::() { + let subscriptions = S::get_subscriptions(cx); + if let Some(storage_sender) = subscriptions.get(key) { + if storage_sender.data_type_id == TypeId::of::() { log::info!("Already subscribed to storage entry: {:?}", key); - Some(channel.channel.new_receiver()) + Some(storage_sender.channel.clone()) } else { log::info!("Already subscribed to storage entry: {:?} but with a different type", key); None } } else { - let (tx, rx) = broadcast::(2); + let (tx, rx) = broadcast::(5); subscriptions.insert( key.clone(), StorageSender { - channel: tx, + channel: UseChannel::new(Uuid::new_v4(), tx, rx.deactivate()), data_type_id: TypeId::of::(), }, ); log::info!("Subscribed to storage entry: {:?}", key); - Some(rx) + subscriptions.get(key).map(|storage_sender| storage_sender.channel.clone()) } } #[cfg(feature = "ssr")] @@ -96,9 +101,34 @@ pub(crate) fn do_storage_backing_subscribe { + pub(crate) subscriptions: Arc>>, +} +impl StorageBackingSubscriptions { + pub(crate) fn new() -> Self { + Self { + subscriptions: Arc::new(RwLock::new(HashMap::new())), + } + } + pub(crate) fn get(&self, key: &S::Key) -> Option { + self.subscriptions.read().unwrap().get(key).map_or( None, |storage_sender| Some((*storage_sender).clone())) + } + pub(crate) fn insert(&self, key: S::Key, storage_sender: StorageSender) { + if let Some(existing_sender) = self.get(&key) { + if existing_sender.data_type_id != storage_sender.data_type_id { + panic!("Storage sender type mismatch"); + } + } else { + self.subscriptions.write().unwrap().insert(key, storage_sender); + } + } +} + +#[derive(Clone)] pub struct StorageSender { - pub(crate) channel: Sender, + pub(crate) channel: UseChannel, pub(crate) data_type_id: TypeId, } @@ -132,7 +162,7 @@ where T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { - pub fn new(key: S::Key, data: T, subscribe: bool) -> Self { + pub fn new(key: S::Key, data: T, cx: Option<&ScopeState>) -> Self { let channel = { #[cfg(feature = "ssr")] { @@ -140,11 +170,10 @@ where } #[cfg(not(feature = "ssr"))] { - let mut channel = StorageEntryChannel::new(S::subscribe::(&key)); - if !subscribe { - channel.deactivate(); + match cx { + Some(cx) => StorageEntryChannel::new(S::subscribe::(cx, &key)), + None => StorageEntryChannel::default(), } - channel } }; Self { key, data, channel } @@ -232,48 +261,16 @@ where pub(crate) enum StorageEntryChannel { #[default] None, - Active(Receiver), - Inactive(InactiveReceiver), + Active(UseChannel), } impl StorageEntryChannel { - fn new(receiver: Option>) -> Self { + fn new(receiver: Option>) -> Self { match receiver { Some(receiver) => Self::Active(receiver), None => Self::None, } } - fn activate(&mut self) { - if let Self::Inactive(channel) = self { - *self = Self::Active(channel.activate_cloned()) - } - } - fn deactivate(&mut self) { - if let Self::Active(channel) = self { - *self = Self::Inactive(channel.clone().deactivate()) - } - } - pub async fn start_receiver_loop(&mut self, action:impl Fn(StorageChannelPayload)) { - async fn receiver_loop(receiver: &mut Receiver, action:impl Fn(StorageChannelPayload)) { - loop { - if let Ok(data) = receiver.recv().await { - action(data); - } - } - } - match self { - Self::Active(receiver) => { - receiver_loop(receiver, action).await; - } - Self::Inactive(_) => { - self.activate(); - if let Self::Active(receiver) = self { - receiver_loop(receiver, action).await; - } - } - _ => {} - } - } } pub fn storage_entry( @@ -287,7 +284,7 @@ pub fn storage_entry( }) } -pub fn synced_storage_entry(key: S::Key, init: impl FnOnce() -> T, subscribe: bool) -> StorageEntry +pub fn synced_storage_entry(key: S::Key, init: impl FnOnce() -> T, cx: Option<&ScopeState>) -> StorageEntry where S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static, @@ -295,7 +292,7 @@ where { let data = storage_entry::(key.clone(), init); - StorageEntry::new(key, data, subscribe) + StorageEntry::new(key, data, cx) } pub fn use_synced_storage_entry( @@ -309,11 +306,11 @@ where S::Key: Clone, { let state = use_signal(cx, || { - synced_storage_entry::(key, init, true) - }); - use_future!(cx, || async move { - let mut channel = state.read().channel.clone(); - channel.start_receiver_loop(|_| state.with_mut(|storage_entry| storage_entry.update())).await; + synced_storage_entry::(key, init, Some(cx)) }); + + if let StorageEntryChannel::Active(channel) = &state.read().channel { + use_listen_channel(cx, channel, move |_| async move { state.write().update() }); + } state } diff --git a/src/utils/channel/use_channel.rs b/src/utils/channel/use_channel.rs index 407a891..5071f9e 100644 --- a/src/utils/channel/use_channel.rs +++ b/src/utils/channel/use_channel.rs @@ -10,6 +10,20 @@ pub struct UseChannel { inactive_receiver: InactiveReceiver, } +impl UseChannel { + pub(crate) fn new( + id: Uuid, + sender: Sender, + inactive_receiver: InactiveReceiver, + ) -> Self { + Self { + id, + sender, + inactive_receiver, + } + } +} + impl PartialEq for UseChannel { fn eq(&self, other: &Self) -> bool { self.id == other.id @@ -46,9 +60,5 @@ pub fn use_channel( (sender, receiver.deactivate()) }); - UseChannel { - id: *id, - sender: sender.clone(), - inactive_receiver: inactive_receiver.clone(), - } + UseChannel::new(*id, sender.clone(), inactive_receiver.clone()) } From 856c860731999c21746d676aa60e47cdc2318839 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sat, 7 Oct 2023 12:27:34 -0700 Subject: [PATCH 007/108] it works! --- Cargo.toml | 3 +- examples/storage/src/main.rs | 26 ++++-- src/storage/client_storage/web.rs | 64 +++++++++----- src/storage/mod.rs | 6 +- src/storage/persistence.rs | 51 ++++++----- src/storage/storage.rs | 142 +++++++----------------------- src/utils/channel/use_channel.rs | 8 +- 7 files changed, 127 insertions(+), 173 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6009b5d..18ba10d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ geolocation = ["dep:windows", "dep:futures-util", "dep:futures", "dep:web-sys", color_scheme = ["dep:web-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] utils = ["dep:async-broadcast", "dep:uuid"] i18n = ["dep:serde", "dep:serde_json", "dep:unic-langid"] -storage = ["dep:async-broadcast", "dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:wasm-bindgen", "dep:directories", "dep:yazi"] +storage = ["utils", "dep:async-broadcast", "dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:directories", "dep:yazi"] all = ["clipboard", "notifications", "geolocation", "color_scheme", "utils", "i18n", "storage"] default = ["storage"] @@ -56,6 +56,7 @@ dioxus-signals = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "1ba6 "serialize", ], optional = true } yazi = { version = "0.1.4", optional = true } +tokio = { version = "1.32.0", optional = true, features = ["sync", "time"] } log = "0.4.6" # WebAssembly Debug diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 3a95ba0..986b3fb 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -11,15 +11,27 @@ fn main() { } fn app(cx: Scope) -> Element { - let count = use_singleton_persistent(cx, || 0); + let count1 = use_singleton_persistent(cx, || 0); + let count2 = use_singleton_persistent(cx, || 0); render!( - button { - onclick: move |_| { - count.set(count.get() + 1); + div { + button { + onclick: move |_| { + count1.set(count1.get() + 1); + }, + "Click me!" }, - "Click me!" - }, - "Clicked {count} times" + "Clicked {count1} times" + } + div { + button { + onclick: move |_| { + count2.set(count2.get() + 1); + }, + "Click me!" + }, + "Clicked {count2} times" + } ) } diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 6533074..75f7bb5 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -1,30 +1,32 @@ #![allow(unused)] -use async_broadcast::Receiver; +use async_broadcast::{broadcast, InactiveReceiver, Receiver, Sender}; use dioxus::prelude::*; use once_cell::sync::{Lazy, OnceCell}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -use wasm_bindgen::JsCast; -use wasm_bindgen::prelude::Closure; use std::any::TypeId; use std::cell::{Ref, RefMut}; use std::collections::HashMap; use std::fmt::Debug; +use std::future::IntoFuture; use std::io::Write; use std::rc::Rc; -use std::sync::Mutex; +use std::sync::{Mutex, OnceLock}; use std::thread::LocalKey; use std::{ fmt::Display, ops::{Deref, DerefMut}, }; +use uuid::Uuid; +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; use web_sys::{window, Storage}; use crate::storage::storage::{ serde_from_string, serde_to_string, storage_entry, try_serde_from_string, - use_synced_storage_entry, StorageBacking, StorageEntry, StorageEntryMut, StorageSender, do_storage_backing_subscribe, StorageChannelPayload, StorageBackingSubscriptions, + use_synced_storage_entry, StorageBacking, StorageChannelPayload, StorageEntry, StorageEntryMut, }; -use crate::utils::channel::UseChannel; +use crate::utils::channel::{self, UseChannel}; fn local_storage() -> Option { window()?.local_storage().ok()? @@ -54,17 +56,37 @@ pub struct ClientStorage; impl StorageBacking for ClientStorage { type Key = String; - fn subscribe(cx: &ScopeState, key: &Self::Key) -> Option> { - let channel = do_storage_backing_subscribe::(cx, key); - let subscriptions = cx.consume_context::>().unwrap(); - let closure = cx.provide_root_context::>>(Rc::new(Closure::::new(|e: web_sys::StorageEvent| { - process_storage_event(subscriptions, e); - }))); - window() - .unwrap() - .add_event_listener_with_callback("storage", closure.deref().as_ref().unchecked_ref()) - .unwrap(); - channel + fn subscribe( + cx: &ScopeState, + key: &Self::Key, + ) -> Option>> { + let channel = CHANNEL.get_or_init(|| { + let (tx, rx) = broadcast::>(5); + let channel = UseChannel::new(Uuid::new_v4(), tx, rx.deactivate()); + let channel_clone = channel.clone(); + + let closure = Closure::wrap(Box::new(move |e: web_sys::StorageEvent| { + log::info!("Storage event: {:?}", e); + let key: String = e.key().unwrap(); + let channel_clone_clone = channel_clone.clone(); + wasm_bindgen_futures::spawn_local(async move { + let result = channel_clone_clone + .send(StorageChannelPayload:: { key }) + .await; + match result { + Ok(_) => log::info!("Sent storage event"), + Err(err) => log::info!("Error sending storage event: {:?}", err), + } + }); + }) as Box); + window() + .unwrap() + .add_event_listener_with_callback("storage", closure.as_ref().unchecked_ref()) + .unwrap(); + closure.forget(); + channel + }); + Some(channel.clone()) } fn set(key: String, value: &T) { @@ -76,10 +98,4 @@ impl StorageBacking for ClientStorage { } } -fn process_storage_event(subscriptions: StorageBackingSubscriptions, e: web_sys::StorageEvent) { - log::info!("Incoming storage event"); - let key = e.key().unwrap(); - if let Some(storage_sender) = subscriptions.get(&key) { - storage_sender.channel.send(StorageChannelPayload::Updated); - } -} \ No newline at end of file +static CHANNEL: OnceLock>> = OnceLock::new(); diff --git a/src/storage/mod.rs b/src/storage/mod.rs index c5a1286..dbb6aeb 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -7,7 +7,7 @@ //! fn main() { //! dioxus_web::launch(app) //! } -//! +//! //! fn app(cx: Scope) -> Element { //! let num = use_persistent(cx, "count", || 0); //! cx.render(rsx! { @@ -27,11 +27,11 @@ //! ``` mod client_storage; -mod storage; mod persistence; +mod storage; -pub use persistence::{use_persistent, use_singleton_persistent}; pub use client_storage::ClientStorage; +pub use persistence::{use_persistent, use_singleton_persistent}; #[cfg(not(target_family = "wasm"))] pub use client_storage::set_dir; diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index ec87e96..968429a 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -1,3 +1,8 @@ +use crate::storage::{ + storage::{storage_entry, StorageEntry}, + ClientStorage, +}; +use crate::utils::channel::use_listen_channel; use dioxus::prelude::*; use dioxus_signals::{use_signal, Signal, Write}; use serde::de::DeserializeOwned; @@ -7,14 +12,6 @@ use std::{ fmt::Display, ops::{Deref, DerefMut}, }; -use crate::storage::storage::StorageEntryChannel; -use crate::utils::channel::use_listen_channel; -use crate::storage::{ - ClientStorage, - storage::{ - storage_entry, StorageEntry, - }, -}; /// A persistent storage hook that can be used to store data across application reloads. /// @@ -27,10 +24,10 @@ pub fn use_persistent &UsePersistent { let mut init = Some(init); let state = { - #[cfg(feature = "ssr")] + #[cfg(feature = "ssr")] { use_ref(cx, || { - StorageEntry::::new(key.to_string(), init.take().unwrap()(), false) + StorageEntry::::new(key.to_string(), init.take().unwrap()(), None) }) } #[cfg(all(not(feature = "ssr"), not(feature = "hydrate")))] @@ -42,15 +39,26 @@ pub fn use_persistent { + log::info!("Incoming message: {:?}", value); + if value.key == state.read().key { + state.write().update(); + } + } + Err(err) => log::info!("Error: {err:?}"), + } + }); } state } #[cfg(all(not(feature = "ssr"), feature = "hydrate"))] { let state = use_ref(cx, || { - StorageEntry::::new(key.to_string(), init.take().unwrap()(), true) + StorageEntry::::new(key.to_string(), init.take().unwrap()(), None) }); if cx.generation() == 0 { cx.needs_update(); @@ -61,14 +69,11 @@ pub fn use_persistent(key.to_string(), init.take().unwrap()), )); } - + state } - }; - cx.use_hook(|| UsePersistent { - inner: state, - }) + cx.use_hook(|| UsePersistent { inner: state }) } /// A persistent storage hook that can be used to store data across application reloads. @@ -81,10 +86,8 @@ pub fn use_singleton_persistent T, ) -> &UsePersistent { - let key = cx.use_hook(|| { - let caller = std::panic::Location::caller(); - format!("{}:{}", caller.file(), caller.line()) - }); + let caller = std::panic::Location::caller(); + let key = cx.use_hook(move || format!("{}:{}", caller.file(), caller.line())); log::info!("key: \"{}\"", key); use_persistent(cx, key, init) } @@ -164,7 +167,9 @@ impl UsePersistent< } } -impl Display for UsePersistent { +impl Display + for UsePersistent +{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { (*self.read()).fmt(f) } diff --git a/src/storage/storage.rs b/src/storage/storage.rs index 4652bfb..5dc9c34 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage.rs @@ -1,18 +1,13 @@ -use async_broadcast::{broadcast, Receiver, Sender, InactiveReceiver}; -use dioxus::prelude::{ScopeState, use_future, use_context}; -use dioxus_signals::{Signal, use_signal}; +use dioxus::prelude::ScopeState; +use dioxus_signals::{use_signal, Signal}; use postcard::to_allocvec; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -use std::any::TypeId; -use std::collections::HashMap; use std::fmt::{Debug, Display}; -use std::ops::{Deref, DerefMut}; -use std::sync::{Mutex, RwLock, Arc}; use std::hash::Hash; -use uuid::Uuid; +use std::ops::{Deref, DerefMut}; -use crate::utils::channel::{UseChannel, use_channel, use_listen_channel}; +use crate::utils::channel::{use_listen_channel, UseChannel}; pub fn serde_to_string(value: &T) -> String { let serialized = to_allocvec(value).unwrap(); @@ -60,97 +55,45 @@ pub struct PersistentStorage { pub trait StorageBacking: Sized + Clone + 'static { type Key: Eq + PartialEq + Hash + Clone + Debug; - fn subscribe(cx: &ScopeState, key: &Self::Key) -> Option>; + fn subscribe( + cx: &ScopeState, + key: &Self::Key, + ) -> Option>>; fn get(key: &Self::Key) -> Option; fn set(key: Self::Key, value: &T); - fn get_subscriptions(cx: &ScopeState) -> StorageBackingSubscriptions { - cx.consume_context::>().unwrap_or_else(|| cx.provide_root_context(StorageBackingSubscriptions::::new())) - } -} - -pub(crate) fn do_storage_backing_subscribe(cx: &ScopeState, key: &S::Key) -> Option> { - log::info!("Subscribing to storage entry: {:?}", key); - #[cfg(not(feature = "ssr"))] - { - let subscriptions = S::get_subscriptions(cx); - if let Some(storage_sender) = subscriptions.get(key) { - if storage_sender.data_type_id == TypeId::of::() { - log::info!("Already subscribed to storage entry: {:?}", key); - Some(storage_sender.channel.clone()) - } else { - log::info!("Already subscribed to storage entry: {:?} but with a different type", key); - None - } - } else { - let (tx, rx) = broadcast::(5); - subscriptions.insert( - key.clone(), - StorageSender { - channel: UseChannel::new(Uuid::new_v4(), tx, rx.deactivate()), - data_type_id: TypeId::of::(), - }, - ); - log::info!("Subscribed to storage entry: {:?}", key); - subscriptions.get(key).map(|storage_sender| storage_sender.channel.clone()) - } - } - #[cfg(feature = "ssr")] - { - log::info!("Subscription not supported for SSR"); - None - } } #[derive(Clone)] -pub(crate) struct StorageBackingSubscriptions { - pub(crate) subscriptions: Arc>>, +pub struct StorageChannelPayload { + pub key: S::Key, } -impl StorageBackingSubscriptions { - pub(crate) fn new() -> Self { - Self { - subscriptions: Arc::new(RwLock::new(HashMap::new())), - } - } - pub(crate) fn get(&self, key: &S::Key) -> Option { - self.subscriptions.read().unwrap().get(key).map_or( None, |storage_sender| Some((*storage_sender).clone())) - } - pub(crate) fn insert(&self, key: S::Key, storage_sender: StorageSender) { - if let Some(existing_sender) = self.get(&key) { - if existing_sender.data_type_id != storage_sender.data_type_id { - panic!("Storage sender type mismatch"); - } - } else { - self.subscriptions.write().unwrap().insert(key, storage_sender); - } +impl Debug for StorageChannelPayload { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StorageChannelPayload") + .field("key", &self.key) + .finish() } } -#[derive(Clone)] -pub struct StorageSender { - pub(crate) channel: UseChannel, - pub(crate) data_type_id: TypeId, -} - -#[derive(Clone)] -pub enum StorageChannelPayload { - Updated, -} - #[derive(Clone, Default)] pub struct StorageEntry { - key: S::Key, - pub(crate) channel: StorageEntryChannel, + pub(crate) key: S::Key, + pub(crate) channel: Option>>, pub(crate) data: T, } -impl Display for StorageEntry { +impl Display + for StorageEntry +{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.data.fmt(f) } } -impl Debug for StorageEntry { +impl Debug + for StorageEntry +{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.data.fmt(f) } @@ -166,14 +109,11 @@ where let channel = { #[cfg(feature = "ssr")] { - StorageEntryChannel::default() + None } #[cfg(not(feature = "ssr"))] { - match cx { - Some(cx) => StorageEntryChannel::new(S::subscribe::(cx, &key)), - None => StorageEntryChannel::default(), - } + cx.map_or_else(|| None, |cx| S::subscribe::(cx, &key)) } }; Self { key, data, channel } @@ -208,9 +148,7 @@ impl Deref for Stora } impl Drop for StorageEntry { - fn drop(&mut self) { - - } + fn drop(&mut self) {} } pub struct StorageEntryMut<'a, S, T> @@ -257,22 +195,6 @@ where } } -#[derive(Clone, Default)] -pub(crate) enum StorageEntryChannel { - #[default] - None, - Active(UseChannel), -} - -impl StorageEntryChannel { - fn new(receiver: Option>) -> Self { - match receiver { - Some(receiver) => Self::Active(receiver), - None => Self::None, - } - } -} - pub fn storage_entry( key: S::Key, init: impl FnOnce() -> T, @@ -284,7 +206,11 @@ pub fn storage_entry( }) } -pub fn synced_storage_entry(key: S::Key, init: impl FnOnce() -> T, cx: Option<&ScopeState>) -> StorageEntry +pub fn synced_storage_entry( + key: S::Key, + init: impl FnOnce() -> T, + cx: Option<&ScopeState>, +) -> StorageEntry where S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static, @@ -305,11 +231,9 @@ where T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { - let state = use_signal(cx, || { - synced_storage_entry::(key, init, Some(cx)) - }); + let state = use_signal(cx, || synced_storage_entry::(key, init, Some(cx))); - if let StorageEntryChannel::Active(channel) = &state.read().channel { + if let Some(channel) = &state.read().channel { use_listen_channel(cx, channel, move |_| async move { state.write().update() }); } state diff --git a/src/utils/channel/use_channel.rs b/src/utils/channel/use_channel.rs index 5071f9e..e50fd7c 100644 --- a/src/utils/channel/use_channel.rs +++ b/src/utils/channel/use_channel.rs @@ -11,11 +11,7 @@ pub struct UseChannel { } impl UseChannel { - pub(crate) fn new( - id: Uuid, - sender: Sender, - inactive_receiver: InactiveReceiver, - ) -> Self { + pub(crate) fn new(id: Uuid, sender: Sender, inactive_receiver: InactiveReceiver) -> Self { Self { id, sender, @@ -44,7 +40,7 @@ impl UseChannel { /// Create a receiver for the channel. /// You probably want to use [`super::use_listen_channel()`]. pub fn receiver(&mut self) -> Receiver { - self.inactive_receiver.clone().activate() + self.inactive_receiver.activate_cloned() } } From f0e7fdc266d26f439d4cdcfe4114f32a297f574e Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sat, 7 Oct 2023 12:43:37 -0700 Subject: [PATCH 008/108] remove unnecessary dependency --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 18ba10d..71e69e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,6 @@ dioxus-signals = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "1ba6 "serialize", ], optional = true } yazi = { version = "0.1.4", optional = true } -tokio = { version = "1.32.0", optional = true, features = ["sync", "time"] } log = "0.4.6" # WebAssembly Debug From 48e7b4de53d93c0872db15c67b6f4f9fea5878cb Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sat, 7 Oct 2023 14:45:29 -0700 Subject: [PATCH 009/108] remove unused imports --- src/storage/client_storage/web.rs | 27 ++++++--------------------- src/storage/persistence.rs | 5 ++++- src/storage/storage.rs | 6 ++++-- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 75f7bb5..8902fa5 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -1,32 +1,17 @@ -#![allow(unused)] -use async_broadcast::{broadcast, InactiveReceiver, Receiver, Sender}; +use async_broadcast::broadcast; use dioxus::prelude::*; -use once_cell::sync::{Lazy, OnceCell}; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use std::any::TypeId; -use std::cell::{Ref, RefMut}; -use std::collections::HashMap; -use std::fmt::Debug; -use std::future::IntoFuture; -use std::io::Write; -use std::rc::Rc; -use std::sync::{Mutex, OnceLock}; -use std::thread::LocalKey; -use std::{ - fmt::Display, - ops::{Deref, DerefMut}, -}; +use serde::{de::DeserializeOwned, Serialize}; +use std::sync::OnceLock; use uuid::Uuid; use wasm_bindgen::prelude::Closure; use wasm_bindgen::JsCast; use web_sys::{window, Storage}; use crate::storage::storage::{ - serde_from_string, serde_to_string, storage_entry, try_serde_from_string, - use_synced_storage_entry, StorageBacking, StorageChannelPayload, StorageEntry, StorageEntryMut, + serde_to_string, try_serde_from_string, + StorageBacking, StorageChannelPayload, }; -use crate::utils::channel::{self, UseChannel}; +use crate::utils::channel::UseChannel; fn local_storage() -> Option { window()?.local_storage().ok()? diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index 968429a..f56fc0d 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -151,13 +151,16 @@ impl UsePersistent< /// Sets the value pub fn set(&self, value: T) { *self.write() = value; - self.inner.write().save(); } /// Modifies the value pub fn modify(&self, f: F) { f(&mut self.write()); } + + pub fn save(&self) { + self.inner.write().save(); + } } impl UsePersistent { diff --git a/src/storage/storage.rs b/src/storage/storage.rs index 5dc9c34..cec7ff8 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage.rs @@ -4,7 +4,6 @@ use postcard::to_allocvec; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display}; -use std::hash::Hash; use std::ops::{Deref, DerefMut}; use crate::utils::channel::{use_listen_channel, UseChannel}; @@ -30,6 +29,7 @@ pub fn serde_to_string(value: &T) -> String { as_str } +#[allow(unused)] pub fn serde_from_string(value: &str) -> T { try_serde_from_string(value).unwrap() } @@ -54,7 +54,7 @@ pub struct PersistentStorage { } pub trait StorageBacking: Sized + Clone + 'static { - type Key: Eq + PartialEq + Hash + Clone + Debug; + type Key: Eq + PartialEq + Clone + Debug; fn subscribe( cx: &ScopeState, key: &Self::Key, @@ -206,6 +206,7 @@ pub fn storage_entry( }) } +#[allow(unused)] pub fn synced_storage_entry( key: S::Key, init: impl FnOnce() -> T, @@ -221,6 +222,7 @@ where StorageEntry::new(key, data, cx) } +#[allow(unused)] pub fn use_synced_storage_entry( cx: &ScopeState, key: S::Key, From 0157bd1f7ad64f8748fbf512052ebee5bba9a615 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 9 Oct 2023 10:31:40 -0700 Subject: [PATCH 010/108] add localstorage --- src/storage/storage.rs | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/storage/storage.rs b/src/storage/storage.rs index cec7ff8..e058437 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage.rs @@ -55,12 +55,39 @@ pub struct PersistentStorage { pub trait StorageBacking: Sized + Clone + 'static { type Key: Eq + PartialEq + Clone + Debug; + fn get(key: &Self::Key) -> Option; + fn set(key: Self::Key, value: &T); +} + +pub trait SessionStorageBacking: StorageBacking { + fn new_session(); + fn drop_session(&self); +} + +pub struct StorageSession { + _marker: std::marker::PhantomData, + session_id: uuid::Uuid, +} + +impl StorageSession { + pub fn new() -> Self { + T::new_session(); + Self { + _marker: std::marker::PhantomData, + session_id: uuid::Uuid::new_v4(), + } + } + + pub fn drop(&self) { + T::drop_session(self); + } +} + +pub trait LocalStorageBacking: StorageBacking { fn subscribe( cx: &ScopeState, key: &Self::Key, ) -> Option>>; - fn get(key: &Self::Key) -> Option; - fn set(key: Self::Key, value: &T); } #[derive(Clone)] @@ -77,7 +104,7 @@ impl Debug for StorageChannelPayload { } #[derive(Clone, Default)] -pub struct StorageEntry { +pub struct StorageEntry { pub(crate) key: S::Key, pub(crate) channel: Option>>, pub(crate) data: T, From d957fb3ee451b036770c15f869e6dfff2c127a73 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 9 Oct 2023 15:10:17 -0700 Subject: [PATCH 011/108] add unsubscribe --- src/storage/client_storage/web.rs | 8 ++++++-- src/storage/storage.rs | 25 ++++++++++++++++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 8902fa5..77cf9fd 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -42,8 +42,8 @@ impl StorageBacking for ClientStorage { type Key = String; fn subscribe( - cx: &ScopeState, - key: &Self::Key, + _cx: &ScopeState, + _key: &Self::Key, ) -> Option>> { let channel = CHANNEL.get_or_init(|| { let (tx, rx) = broadcast::>(5); @@ -74,6 +74,10 @@ impl StorageBacking for ClientStorage { Some(channel.clone()) } + fn unsubscribe(_key: &Self::Key) { + // Do nothing for web case, since we don't actually subscribe to specific keys. + } + fn set(key: String, value: &T) { set(key, value); } diff --git a/src/storage/storage.rs b/src/storage/storage.rs index cec7ff8..d3ad3a5 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage.rs @@ -59,6 +59,7 @@ pub trait StorageBacking: Sized + Clone + 'static { cx: &ScopeState, key: &Self::Key, ) -> Option>>; + fn unsubscribe(key: &Self::Key); fn get(key: &Self::Key) -> Option; fn set(key: Self::Key, value: &T); } @@ -106,14 +107,16 @@ where S::Key: Clone, { pub fn new(key: S::Key, data: T, cx: Option<&ScopeState>) -> Self { - let channel = { - #[cfg(feature = "ssr")] - { - None - } - #[cfg(not(feature = "ssr"))] - { - cx.map_or_else(|| None, |cx| S::subscribe::(cx, &key)) + let channel: Option>> = { + cfg_if::cfg_if! { + if #[cfg(feature = "ssr")] + { + None + } + else if #[cfg(not(feature = "ssr"))] + { + cx.map_or_else(|| None, |cx| S::subscribe::(cx, &key)) + } } }; Self { key, data, channel } @@ -148,7 +151,11 @@ impl Deref for Stora } impl Drop for StorageEntry { - fn drop(&mut self) {} + fn drop(&mut self) { + if self.channel.is_some() { + S::unsubscribe(&self.key); + } + } } pub struct StorageEntryMut<'a, S, T> From 0d015b4c9ed9be4d68e7f7d5774b0eedf854528c Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 9 Oct 2023 16:24:31 -0700 Subject: [PATCH 012/108] save work --- src/storage/client_storage/web.rs | 93 ++++++++++++++++++++++--------- src/storage/mod.rs | 2 +- src/storage/persistence.rs | 15 +++-- src/storage/storage.rs | 74 +++++++++++------------- 4 files changed, 105 insertions(+), 79 deletions(-) diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 77cf9fd..cebf38a 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -9,44 +9,39 @@ use web_sys::{window, Storage}; use crate::storage::storage::{ serde_to_string, try_serde_from_string, - StorageBacking, StorageChannelPayload, + StorageBacking, StorageChannelPayload, LocalStorageBacking, StorageType, }; use crate::utils::channel::UseChannel; -fn local_storage() -> Option { - window()?.local_storage().ok()? -} +// Start LocalStorage +#[derive(Clone)] +pub struct LocalStorage; -fn set(key: String, value: &T) { - #[cfg(not(feature = "ssr"))] - { - let as_str = serde_to_string(value); - local_storage().unwrap().set_item(&key, &as_str).unwrap(); +impl StorageBacking for LocalStorage { + type Key = String; + type Local = Self; + + fn set(key: String, value: &T) { + set(key, value, StorageType::Local); } -} -fn get(key: &str) -> Option { - #[cfg(not(feature = "ssr"))] - { - let s: String = local_storage()?.get_item(key).ok()??; - try_serde_from_string(&s) + fn get(key: &String) -> Option { + get(key, StorageType::Local) } - #[cfg(feature = "ssr")] - None -} -#[derive(Clone)] -pub struct ClientStorage; + fn as_local_storage() -> bool { + true + } +} -impl StorageBacking for ClientStorage { - type Key = String; +impl LocalStorageBacking for LocalStorage { fn subscribe( _cx: &ScopeState, _key: &Self::Key, ) -> Option>> { let channel = CHANNEL.get_or_init(|| { - let (tx, rx) = broadcast::>(5); + let (tx, rx) = broadcast::>(5); let channel = UseChannel::new(Uuid::new_v4(), tx, rx.deactivate()); let channel_clone = channel.clone(); @@ -56,7 +51,7 @@ impl StorageBacking for ClientStorage { let channel_clone_clone = channel_clone.clone(); wasm_bindgen_futures::spawn_local(async move { let result = channel_clone_clone - .send(StorageChannelPayload:: { key }) + .send(StorageChannelPayload:: { key }) .await; match result { Ok(_) => log::info!("Sent storage event"), @@ -77,14 +72,58 @@ impl StorageBacking for ClientStorage { fn unsubscribe(_key: &Self::Key) { // Do nothing for web case, since we don't actually subscribe to specific keys. } +} + +static CHANNEL: OnceLock>> = OnceLock::new(); +// End LocalStorage + +// Start SessionStorage +#[derive(Clone)] +pub struct SessionStorage; + +impl StorageBacking for SessionStorage { + type Key = String; + type Local = LocalStorage; fn set(key: String, value: &T) { - set(key, value); + set(key, value, StorageType::Session); } fn get(key: &String) -> Option { - get(key) + get(key, StorageType::Session) + } + + fn as_local_storage() -> bool { + false } } +// End SessionStorage -static CHANNEL: OnceLock>> = OnceLock::new(); +// Start common +fn set(key: String, value: &T, storage_type: StorageType) { + #[cfg(not(feature = "ssr"))] + { + let as_str = serde_to_string(value); + get_storage_by_type(storage_type).unwrap().set_item(&key, &as_str).unwrap(); + } +} + +fn get(key: &str, storage_type: StorageType) -> Option { + #[cfg(not(feature = "ssr"))] + { + let s: String = get_storage_by_type(storage_type)?.get_item(key).ok()??; + try_serde_from_string(&s) + } + #[cfg(feature = "ssr")] + None +} + +fn get_storage_by_type(storage_type: StorageType) -> Option { + window().map_or_else(|| None, |window| { + match storage_type { + StorageType::Local => window.local_storage().ok()?, + StorageType::Session => window.session_storage().ok()?, + } + }) +} +// End common diff --git a/src/storage/mod.rs b/src/storage/mod.rs index dbb6aeb..00491c9 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -30,7 +30,7 @@ mod client_storage; mod persistence; mod storage; -pub use client_storage::ClientStorage; +pub use client_storage::{LocalStorage, SessionStorage}; pub use persistence::{use_persistent, use_singleton_persistent}; #[cfg(not(target_family = "wasm"))] diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index f56fc0d..db05ffd 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -1,6 +1,6 @@ use crate::storage::{ storage::{storage_entry, StorageEntry}, - ClientStorage, + SessionStorage, }; use crate::utils::channel::use_listen_channel; use dioxus::prelude::*; @@ -33,10 +33,9 @@ pub fn use_persistent::new( + StorageEntry::::new( key.to_string(), - storage_entry::(key.to_string(), init.take().unwrap()), - Some(cx), + storage_entry::(key.to_string(), init.take().unwrap()), ) }); if let Some(channel) = &state.read().channel { @@ -93,7 +92,7 @@ pub fn use_singleton_persistent { - inner: Ref<'a, StorageEntry>, + inner: Ref<'a, StorageEntry>, } impl<'a, T: Serialize + DeserializeOwned + Default + Clone + 'static> Deref for StorageRef<'a, T> { @@ -105,7 +104,7 @@ impl<'a, T: Serialize + DeserializeOwned + Default + Clone + 'static> Deref for } pub struct StorageRefMut<'a, T: Serialize + DeserializeOwned + Clone + 'static> { - inner: Write<'a, StorageEntry>, + inner: Write<'a, StorageEntry>, } impl<'a, T: Serialize + DeserializeOwned + Clone + 'static> Deref for StorageRefMut<'a, T> { @@ -130,7 +129,7 @@ impl<'a, T: Serialize + DeserializeOwned + Clone + 'static> Drop for StorageRefM /// Storage that persists across application reloads pub struct UsePersistent { - inner: Signal>, + inner: Signal>, } impl UsePersistent { @@ -179,7 +178,7 @@ impl Disp } impl Deref for UsePersistent { - type Target = Signal>; + type Target = Signal>; fn deref(&self) -> &Self::Target { &self.inner diff --git a/src/storage/storage.rs b/src/storage/storage.rs index a1dccb3..3968713 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage.rs @@ -55,32 +55,10 @@ pub struct PersistentStorage { pub trait StorageBacking: Sized + Clone + 'static { type Key: Eq + PartialEq + Clone + Debug; + type Local: LocalStorageBacking; fn get(key: &Self::Key) -> Option; fn set(key: Self::Key, value: &T); -} - -pub trait SessionStorageBacking: StorageBacking { - fn new_session(); - fn drop_session(&self); -} - -pub struct StorageSession { - _marker: std::marker::PhantomData, - session_id: uuid::Uuid, -} - -impl StorageSession { - pub fn new() -> Self { - T::new_session(); - Self { - _marker: std::marker::PhantomData, - session_id: uuid::Uuid::new_v4(), - } - } - - pub fn drop(&self) { - T::drop_session(self); - } + fn as_local_storage() -> bool; } pub trait LocalStorageBacking: StorageBacking { @@ -89,7 +67,11 @@ pub trait LocalStorageBacking: StorageBacking { key: &Self::Key, ) -> Option>>; fn unsubscribe(key: &Self::Key); +} +pub enum StorageType { + Local, + Session, } #[derive(Clone)] @@ -134,20 +116,8 @@ where T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { - pub fn new(key: S::Key, data: T, cx: Option<&ScopeState>) -> Self { - let channel: Option>> = { - cfg_if::cfg_if! { - if #[cfg(feature = "ssr")] - { - None - } - else if #[cfg(not(feature = "ssr"))] - { - cx.map_or_else(|| None, |cx| S::subscribe::(cx, &key)) - } - } - }; - Self { key, data, channel } + pub fn new(key: S::Key, data: T) -> Self { + Self { key, data, channel: None } } pub(crate) fn save(&self) { @@ -170,6 +140,24 @@ where } } +impl StorageEntry where S: LocalStorageBacking, T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone { + pub fn new_local(key: S::Key, data: T, cx: Option<&ScopeState>) -> Self { + let channel: Option>> = { + cfg_if::cfg_if! { + if #[cfg(feature = "ssr")] + { + None + } + else if #[cfg(not(feature = "ssr"))] + { + cx.map_or_else(|| None, |cx| S::subscribe::(cx, &key)) + } + } + }; + Self { key, data, channel } + } +} + impl Deref for StorageEntry { type Target = T; @@ -180,8 +168,8 @@ impl Deref for Stora impl Drop for StorageEntry { fn drop(&mut self) { - if self.channel.is_some() { - S::unsubscribe(&self.key); + if self.channel.is_some() && (S::as_local_storage()) { + S::Local::unsubscribe(&self.key); } } } @@ -248,13 +236,13 @@ pub fn synced_storage_entry( cx: Option<&ScopeState>, ) -> StorageEntry where - S: StorageBacking, + S: LocalStorageBacking, T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { let data = storage_entry::(key.clone(), init); - StorageEntry::new(key, data, cx) + StorageEntry::new_local(key, data, cx) } #[allow(unused)] @@ -264,7 +252,7 @@ pub fn use_synced_storage_entry( init: impl FnOnce() -> T, ) -> Signal> where - S: StorageBacking + 'static, + S: LocalStorageBacking, T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { From f6d7e299781a741b1bee3fa9bc230b430d5bc53c Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 9 Oct 2023 16:27:30 -0700 Subject: [PATCH 013/108] change name --- src/storage/client_storage/web.rs | 5 +++-- src/storage/storage.rs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index cebf38a..fb7dbdf 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -29,7 +29,7 @@ impl StorageBacking for LocalStorage { get(key, StorageType::Local) } - fn as_local_storage() -> bool { + fn is_local_storage() -> bool { true } } @@ -83,6 +83,7 @@ pub struct SessionStorage; impl StorageBacking for SessionStorage { type Key = String; + // Ideally the following should be the experimental !(never) type, but that's not stable yet. type Local = LocalStorage; fn set(key: String, value: &T) { @@ -93,7 +94,7 @@ impl StorageBacking for SessionStorage { get(key, StorageType::Session) } - fn as_local_storage() -> bool { + fn is_local_storage() -> bool { false } } diff --git a/src/storage/storage.rs b/src/storage/storage.rs index 3968713..0dc4998 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage.rs @@ -58,7 +58,7 @@ pub trait StorageBacking: Sized + Clone + 'static { type Local: LocalStorageBacking; fn get(key: &Self::Key) -> Option; fn set(key: Self::Key, value: &T); - fn as_local_storage() -> bool; + fn is_local_storage() -> bool; } pub trait LocalStorageBacking: StorageBacking { @@ -168,7 +168,7 @@ impl Deref for Stora impl Drop for StorageEntry { fn drop(&mut self) { - if self.channel.is_some() && (S::as_local_storage()) { + if self.channel.is_some() && (S::is_local_storage()) { S::Local::unsubscribe(&self.key); } } From 68dcbfb7b73a536f978bdfb66b6c876e69c718c7 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 10 Oct 2023 00:12:07 -0700 Subject: [PATCH 014/108] Break subscriber out --- src/storage/client_storage/web.rs | 19 ++------ src/storage/persistence.rs | 2 +- src/storage/storage.rs | 78 ++++++++++++------------------- 3 files changed, 35 insertions(+), 64 deletions(-) diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index fb7dbdf..f82dde0 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -9,7 +9,7 @@ use web_sys::{window, Storage}; use crate::storage::storage::{ serde_to_string, try_serde_from_string, - StorageBacking, StorageChannelPayload, LocalStorageBacking, StorageType, + StorageBacking, StorageChannelPayload, StorageSubscriber, StorageType, }; use crate::utils::channel::UseChannel; @@ -19,7 +19,6 @@ pub struct LocalStorage; impl StorageBacking for LocalStorage { type Key = String; - type Local = Self; fn set(key: String, value: &T) { set(key, value, StorageType::Local); @@ -28,17 +27,13 @@ impl StorageBacking for LocalStorage { fn get(key: &String) -> Option { get(key, StorageType::Local) } - - fn is_local_storage() -> bool { - true - } } -impl LocalStorageBacking for LocalStorage { +impl StorageSubscriber for LocalStorage { fn subscribe( _cx: &ScopeState, - _key: &Self::Key, + _key: &String, ) -> Option>> { let channel = CHANNEL.get_or_init(|| { let (tx, rx) = broadcast::>(5); @@ -69,7 +64,7 @@ impl LocalStorageBacking for LocalStorage { Some(channel.clone()) } - fn unsubscribe(_key: &Self::Key) { + fn unsubscribe(_key: &String) { // Do nothing for web case, since we don't actually subscribe to specific keys. } } @@ -83,8 +78,6 @@ pub struct SessionStorage; impl StorageBacking for SessionStorage { type Key = String; - // Ideally the following should be the experimental !(never) type, but that's not stable yet. - type Local = LocalStorage; fn set(key: String, value: &T) { set(key, value, StorageType::Session); @@ -93,10 +86,6 @@ impl StorageBacking for SessionStorage { fn get(key: &String) -> Option { get(key, StorageType::Session) } - - fn is_local_storage() -> bool { - false - } } // End SessionStorage diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index db05ffd..2654f5c 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -27,7 +27,7 @@ pub fn use_persistent::new(key.to_string(), init.take().unwrap()(), None) + StorageEntry::::new(key.to_string(), init.take().unwrap()(), None) }) } #[cfg(all(not(feature = "ssr"), not(feature = "hydrate")))] diff --git a/src/storage/storage.rs b/src/storage/storage.rs index 0dc4998..d0f4db4 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage.rs @@ -1,8 +1,7 @@ use dioxus::prelude::ScopeState; use dioxus_signals::{use_signal, Signal}; use postcard::to_allocvec; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Serialize}; use std::fmt::{Debug, Display}; use std::ops::{Deref, DerefMut}; @@ -47,28 +46,21 @@ pub fn try_serde_from_string(value: &str) -> Option { postcard::from_bytes(&decompressed).ok() } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct PersistentStorage { - pub data: Vec>, - pub idx: usize, -} - pub trait StorageBacking: Sized + Clone + 'static { type Key: Eq + PartialEq + Clone + Debug; - type Local: LocalStorageBacking; fn get(key: &Self::Key) -> Option; fn set(key: Self::Key, value: &T); - fn is_local_storage() -> bool; } -pub trait LocalStorageBacking: StorageBacking { +pub trait StorageSubscriber { fn subscribe( cx: &ScopeState, - key: &Self::Key, - ) -> Option>>; - fn unsubscribe(key: &Self::Key); + key: &S::Key, + ) -> Option>>; + fn unsubscribe(key: &S::Key); } +#[derive(Clone, Copy, Debug, PartialEq)] pub enum StorageType { Local, Session, @@ -88,10 +80,10 @@ impl Debug for StorageChannelPayload { } #[derive(Clone, Default)] -pub struct StorageEntry { +pub struct StorageEntry { pub(crate) key: S::Key, - pub(crate) channel: Option>>, pub(crate) data: T, + pub(crate) channel: Option>>, } impl Display @@ -110,6 +102,18 @@ impl Debug } } +impl StorageEntry +where + S: StorageBacking + StorageSubscriber, + T: Serialize + DeserializeOwned + Clone + 'static, + S::Key: Clone, +{ + fn new_synced(key: S::Key, data: T, cx: &ScopeState) -> Self { + let channel = S::subscribe::(cx, &key); + Self { key, data, channel } + } +} + impl StorageEntry where S: StorageBacking, @@ -117,7 +121,11 @@ where S::Key: Clone, { pub fn new(key: S::Key, data: T) -> Self { - Self { key, data, channel: None } + Self { + key, + data, + channel: None, + } } pub(crate) fn save(&self) { @@ -140,24 +148,6 @@ where } } -impl StorageEntry where S: LocalStorageBacking, T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone { - pub fn new_local(key: S::Key, data: T, cx: Option<&ScopeState>) -> Self { - let channel: Option>> = { - cfg_if::cfg_if! { - if #[cfg(feature = "ssr")] - { - None - } - else if #[cfg(not(feature = "ssr"))] - { - cx.map_or_else(|| None, |cx| S::subscribe::(cx, &key)) - } - } - }; - Self { key, data, channel } - } -} - impl Deref for StorageEntry { type Target = T; @@ -166,14 +156,6 @@ impl Deref for Stora } } -impl Drop for StorageEntry { - fn drop(&mut self) { - if self.channel.is_some() && (S::is_local_storage()) { - S::Local::unsubscribe(&self.key); - } - } -} - pub struct StorageEntryMut<'a, S, T> where S: StorageBacking, @@ -233,16 +215,16 @@ pub fn storage_entry( pub fn synced_storage_entry( key: S::Key, init: impl FnOnce() -> T, - cx: Option<&ScopeState>, + cx: &ScopeState, ) -> StorageEntry where - S: LocalStorageBacking, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { let data = storage_entry::(key.clone(), init); - StorageEntry::new_local(key, data, cx) + StorageEntry::new_synced(key, data, cx) } #[allow(unused)] @@ -252,11 +234,11 @@ pub fn use_synced_storage_entry( init: impl FnOnce() -> T, ) -> Signal> where - S: LocalStorageBacking, + S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { - let state = use_signal(cx, || synced_storage_entry::(key, init, Some(cx))); + let state = use_signal(cx, || synced_storage_entry::(key, init, cx)); if let Some(channel) = &state.read().channel { use_listen_channel(cx, channel, move |_| async move { state.write().update() }); From 321ff7be28278194c6383fac8f9f1799e405991c Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 10 Oct 2023 00:23:21 -0700 Subject: [PATCH 015/108] break out subscribe --- src/storage/persistence.rs | 20 ++------------------ src/storage/storage.rs | 8 +++++++- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index 2654f5c..e2f6306 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -2,7 +2,6 @@ use crate::storage::{ storage::{storage_entry, StorageEntry}, SessionStorage, }; -use crate::utils::channel::use_listen_channel; use dioxus::prelude::*; use dioxus_signals::{use_signal, Signal, Write}; use serde::de::DeserializeOwned; @@ -32,27 +31,12 @@ pub fn use_persistent::new( key.to_string(), storage_entry::(key.to_string(), init.take().unwrap()), ) - }); - if let Some(channel) = &state.read().channel { - log::info!("Subscribing to storage entry"); - use_listen_channel(cx, channel, move |message| async move { - match message { - Ok(value) => { - log::info!("Incoming message: {:?}", value); - if value.key == state.read().key { - state.write().update(); - } - } - Err(err) => log::info!("Error: {err:?}"), - } - }); - } - state + }) } #[cfg(all(not(feature = "ssr"), feature = "hydrate"))] { diff --git a/src/storage/storage.rs b/src/storage/storage.rs index d0f4db4..05cb0ba 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage.rs @@ -241,7 +241,13 @@ where let state = use_signal(cx, || synced_storage_entry::(key, init, cx)); if let Some(channel) = &state.read().channel { - use_listen_channel(cx, channel, move |_| async move { state.write().update() }); + use_listen_channel(cx, channel, move |message| async move { + if let Ok(payload) = message { + if payload.key == state.read().key { + state.write().update(); + } + } + }); } state } From 06d1576ae9ffa0f81b3629ba38c8e4f3c68e2df5 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 10 Oct 2023 00:54:28 -0700 Subject: [PATCH 016/108] update example --- examples/storage/src/main.rs | 12 ++++++------ src/storage/mod.rs | 1 + src/storage/storage.rs | 16 +++++++++++++++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 986b3fb..bba2b6b 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -11,27 +11,27 @@ fn main() { } fn app(cx: Scope) -> Element { - let count1 = use_singleton_persistent(cx, || 0); - let count2 = use_singleton_persistent(cx, || 0); + let count_session = use_singleton_persistent(cx, || 0); + let count_local = use_synced_storage_entry::(cx, "synced".to_string(), || 0); render!( div { button { onclick: move |_| { - count1.set(count1.get() + 1); + count_session.set(count_session.get() + 1); }, "Click me!" }, - "Clicked {count1} times" + "I persist for the current session. Clicked {count_session} times" } div { button { onclick: move |_| { - count2.set(count2.get() + 1); + count_local.with_mut(|count| *count.write() += 1) }, "Click me!" }, - "Clicked {count2} times" + "I persist across all sessions. {count_local} times" } ) } diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 00491c9..a0dfc3d 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -30,6 +30,7 @@ mod client_storage; mod persistence; mod storage; +pub use storage::use_synced_storage_entry; pub use client_storage::{LocalStorage, SessionStorage}; pub use persistence::{use_persistent, use_singleton_persistent}; diff --git a/src/storage/storage.rs b/src/storage/storage.rs index 05cb0ba..ed144fd 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage.rs @@ -244,10 +244,24 @@ where use_listen_channel(cx, channel, move |message| async move { if let Ok(payload) = message { if payload.key == state.read().key { - state.write().update(); + state.write().update() } } }); } state } + +// state.with(move |state| { +// if let Some(channel) = &state.channel { + +// use_listen_channel(cx, channel, move |message: Result, async_broadcast::RecvError>| async move { +// if let Ok(payload) = message { +// if state.key == payload.key { +// state.update(); +// } +// } +// }); +// } +// }); +// state From 577a41aa1846b51ba0c716d4c6c412543a160db5 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 10 Oct 2023 11:15:28 -0700 Subject: [PATCH 017/108] small refactoring --- src/storage/client_storage/web.rs | 27 ++++++++++++-------- src/storage/mod.rs | 4 +-- src/storage/persistence.rs | 2 +- src/storage/{storage.rs => storage_entry.rs} | 6 ----- 4 files changed, 19 insertions(+), 20 deletions(-) rename src/storage/{storage.rs => storage_entry.rs} (98%) diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index f82dde0..2adfb21 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -7,9 +7,9 @@ use wasm_bindgen::prelude::Closure; use wasm_bindgen::JsCast; use web_sys::{window, Storage}; -use crate::storage::storage::{ +use crate::storage::storage_entry::{ serde_to_string, try_serde_from_string, - StorageBacking, StorageChannelPayload, StorageSubscriber, StorageType, + StorageBacking, StorageChannelPayload, StorageSubscriber, }; use crate::utils::channel::UseChannel; @@ -21,11 +21,11 @@ impl StorageBacking for LocalStorage { type Key = String; fn set(key: String, value: &T) { - set(key, value, StorageType::Local); + set(key, value, WebStorageType::Local); } fn get(key: &String) -> Option { - get(key, StorageType::Local) + get(key, WebStorageType::Local) } } @@ -80,17 +80,17 @@ impl StorageBacking for SessionStorage { type Key = String; fn set(key: String, value: &T) { - set(key, value, StorageType::Session); + set(key, value, WebStorageType::Session); } fn get(key: &String) -> Option { - get(key, StorageType::Session) + get(key, WebStorageType::Session) } } // End SessionStorage // Start common -fn set(key: String, value: &T, storage_type: StorageType) { +fn set(key: String, value: &T, storage_type: WebStorageType) { #[cfg(not(feature = "ssr"))] { let as_str = serde_to_string(value); @@ -98,7 +98,7 @@ fn set(key: String, value: &T, storage_type: StorageType) { } } -fn get(key: &str, storage_type: StorageType) -> Option { +fn get(key: &str, storage_type: WebStorageType) -> Option { #[cfg(not(feature = "ssr"))] { let s: String = get_storage_by_type(storage_type)?.get_item(key).ok()??; @@ -108,12 +108,17 @@ fn get(key: &str, storage_type: StorageType) -> Option { None } -fn get_storage_by_type(storage_type: StorageType) -> Option { +fn get_storage_by_type(storage_type: WebStorageType) -> Option { window().map_or_else(|| None, |window| { match storage_type { - StorageType::Local => window.local_storage().ok()?, - StorageType::Session => window.session_storage().ok()?, + WebStorageType::Local => window.local_storage().ok()?, + WebStorageType::Session => window.session_storage().ok()?, } }) } + +enum WebStorageType { + Local, + Session, +} // End common diff --git a/src/storage/mod.rs b/src/storage/mod.rs index a0dfc3d..21f6323 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -28,9 +28,9 @@ mod client_storage; mod persistence; -mod storage; +mod storage_entry; -pub use storage::use_synced_storage_entry; +pub use storage_entry::use_synced_storage_entry; pub use client_storage::{LocalStorage, SessionStorage}; pub use persistence::{use_persistent, use_singleton_persistent}; diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index e2f6306..e621a99 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -1,5 +1,5 @@ use crate::storage::{ - storage::{storage_entry, StorageEntry}, + storage_entry::{storage_entry, StorageEntry}, SessionStorage, }; use dioxus::prelude::*; diff --git a/src/storage/storage.rs b/src/storage/storage_entry.rs similarity index 98% rename from src/storage/storage.rs rename to src/storage/storage_entry.rs index ed144fd..581057f 100644 --- a/src/storage/storage.rs +++ b/src/storage/storage_entry.rs @@ -60,12 +60,6 @@ pub trait StorageSubscriber { fn unsubscribe(key: &S::Key); } -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum StorageType { - Local, - Session, -} - #[derive(Clone)] pub struct StorageChannelPayload { pub key: S::Key, From 59df7633339f898983670d07ec56348f69ab37dd Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 10 Oct 2023 11:16:35 -0700 Subject: [PATCH 018/108] update sample --- examples/storage/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index bba2b6b..c4bb4bf 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -31,7 +31,7 @@ fn app(cx: Scope) -> Element { }, "Click me!" }, - "I persist across all sessions. {count_local} times" + "I persist across all sessions. Clicked {count_local} times" } ) } From 3bb89379c6ea4d75c801e22d108154c779ddea76 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 10 Oct 2023 11:35:51 -0700 Subject: [PATCH 019/108] change UsePersistent to UseStorageEntry, use everywhere --- examples/storage/src/main.rs | 2 +- src/storage/client_storage/web.rs | 2 +- src/storage/persistence.rs | 115 ++---------------------------- src/storage/storage_entry.rs | 115 +++++++++++++++++++++++++++++- 4 files changed, 120 insertions(+), 114 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index c4bb4bf..692efcc 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -27,7 +27,7 @@ fn app(cx: Scope) -> Element { div { button { onclick: move |_| { - count_local.with_mut(|count| *count.write() += 1) + count_local.set(count_local.get() + 1); }, "Click me!" }, diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 2adfb21..e9f39ee 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -7,7 +7,7 @@ use wasm_bindgen::prelude::Closure; use wasm_bindgen::JsCast; use web_sys::{window, Storage}; -use crate::storage::storage_entry::{ +use crate::storage::storage::{ serde_to_string, try_serde_from_string, StorageBacking, StorageChannelPayload, StorageSubscriber, }; diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index e621a99..5102a79 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -3,14 +3,11 @@ use crate::storage::{ SessionStorage, }; use dioxus::prelude::*; -use dioxus_signals::{use_signal, Signal, Write}; +use dioxus_signals::use_signal; use serde::de::DeserializeOwned; use serde::Serialize; -use std::cell::Ref; -use std::{ - fmt::Display, - ops::{Deref, DerefMut}, -}; + +use super::storage_entry::UseStorageEntry; /// A persistent storage hook that can be used to store data across application reloads. /// @@ -20,7 +17,7 @@ pub fn use_persistent T, -) -> &UsePersistent { +) -> &UseStorageEntry { let mut init = Some(init); let state = { #[cfg(feature = "ssr")] @@ -56,7 +53,7 @@ pub fn use_persistent( cx: &ScopeState, init: impl FnOnce() -> T, -) -> &UsePersistent { +) -> &UseStorageEntry { let caller = std::panic::Location::caller(); let key = cx.use_hook(move || format!("{}:{}", caller.file(), caller.line())); log::info!("key: \"{}\"", key); use_persistent(cx, key, init) } - -pub struct StorageRef<'a, T: Serialize + DeserializeOwned + Default + Clone + 'static> { - inner: Ref<'a, StorageEntry>, -} - -impl<'a, T: Serialize + DeserializeOwned + Default + Clone + 'static> Deref for StorageRef<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -pub struct StorageRefMut<'a, T: Serialize + DeserializeOwned + Clone + 'static> { - inner: Write<'a, StorageEntry>, -} - -impl<'a, T: Serialize + DeserializeOwned + Clone + 'static> Deref for StorageRefMut<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'a, T: Serialize + DeserializeOwned + Clone + 'static> DerefMut for StorageRefMut<'a, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner.data - } -} - -impl<'a, T: Serialize + DeserializeOwned + Clone + 'static> Drop for StorageRefMut<'a, T> { - fn drop(&mut self) { - self.inner.deref_mut().save(); - } -} - -/// Storage that persists across application reloads -pub struct UsePersistent { - inner: Signal>, -} - -impl UsePersistent { - /// Returns a reference to the value - pub fn read(&self) -> StorageRef { - StorageRef { - inner: self.inner.read(), - } - } - - /// Returns a mutable reference to the value - pub fn write(&self) -> StorageRefMut { - StorageRefMut { - inner: self.inner.write(), - } - } - - /// Sets the value - pub fn set(&self, value: T) { - *self.write() = value; - } - - /// Modifies the value - pub fn modify(&self, f: F) { - f(&mut self.write()); - } - - pub fn save(&self) { - self.inner.write().save(); - } -} - -impl UsePersistent { - /// Returns a clone of the value - pub fn get(&self) -> T { - self.read().clone() - } -} - -impl Display - for UsePersistent -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - (*self.read()).fmt(f) - } -} - -impl Deref for UsePersistent { - type Target = Signal>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl DerefMut for UsePersistent { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} diff --git a/src/storage/storage_entry.rs b/src/storage/storage_entry.rs index 581057f..3f13ae2 100644 --- a/src/storage/storage_entry.rs +++ b/src/storage/storage_entry.rs @@ -1,7 +1,8 @@ use dioxus::prelude::ScopeState; -use dioxus_signals::{use_signal, Signal}; +use dioxus_signals::{use_signal, Signal, Write}; use postcard::to_allocvec; use serde::{de::DeserializeOwned, Serialize}; +use std::cell::Ref; use std::fmt::{Debug, Display}; use std::ops::{Deref, DerefMut}; @@ -226,7 +227,7 @@ pub fn use_synced_storage_entry( cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T, -) -> Signal> +) -> UseStorageEntry where S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + 'static, @@ -243,9 +244,117 @@ where } }); } - state + UseStorageEntry::new(state) } +// Start UseStorageEntry +pub struct StorageRef<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> { + inner: Ref<'a, StorageEntry>, +} + +impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> Deref for StorageRef<'a, S, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +pub struct StorageRefMut<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> { + inner: Write<'a, StorageEntry>, +} + +impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> Deref for StorageRefMut<'a, S, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> DerefMut for StorageRefMut<'a, S, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner.data + } +} + +impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> Drop for StorageRefMut<'a, S, T> { + fn drop(&mut self) { + self.inner.deref_mut().save(); + } +} + +/// Storage that persists across application reloads +pub struct UseStorageEntry { + inner: Signal>, +} + +#[allow(unused)] +impl UseStorageEntry { + pub fn new(signal: Signal>) -> Self { + Self { inner: signal } + } + + /// Returns a reference to the value + pub fn read(&self) -> StorageRef { + StorageRef { + inner: self.inner.read(), + } + } + + /// Returns a mutable reference to the value + pub fn write(&self) -> StorageRefMut { + StorageRefMut { + inner: self.inner.write(), + } + } + + /// Sets the value + pub fn set(&self, value: T) { + *self.write() = value; + } + + /// Modifies the value + pub fn modify(&self, f: F) { + f(&mut self.write()); + } + + pub fn save(&self) { + self.inner.write().save(); + } +} + +#[allow(unused)] +impl UseStorageEntry { + /// Returns a clone of the value + pub fn get(&self) -> T { + self.read().clone() + } +} + +impl Display + for UseStorageEntry +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + (*self.read()).fmt(f) + } +} + +impl Deref for UseStorageEntry { + type Target = Signal>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for UseStorageEntry { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} +// End UseStorageEntry + // state.with(move |state| { // if let Some(channel) = &state.channel { From d966cefdf7768fd92b22f053d0ad2d9b2b306a4e Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 10 Oct 2023 11:39:28 -0700 Subject: [PATCH 020/108] more refactoring --- src/storage/client_storage/web.rs | 2 +- src/storage/storage_entry.rs | 73 ++++++++++++++++--------------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index e9f39ee..2adfb21 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -7,7 +7,7 @@ use wasm_bindgen::prelude::Closure; use wasm_bindgen::JsCast; use web_sys::{window, Storage}; -use crate::storage::storage::{ +use crate::storage::storage_entry::{ serde_to_string, try_serde_from_string, StorageBacking, StorageChannelPayload, StorageSubscriber, }; diff --git a/src/storage/storage_entry.rs b/src/storage/storage_entry.rs index 3f13ae2..d06d00d 100644 --- a/src/storage/storage_entry.rs +++ b/src/storage/storage_entry.rs @@ -248,42 +248,6 @@ where } // Start UseStorageEntry -pub struct StorageRef<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> { - inner: Ref<'a, StorageEntry>, -} - -impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> Deref for StorageRef<'a, S, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -pub struct StorageRefMut<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> { - inner: Write<'a, StorageEntry>, -} - -impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> Deref for StorageRefMut<'a, S, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> DerefMut for StorageRefMut<'a, S, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner.data - } -} - -impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> Drop for StorageRefMut<'a, S, T> { - fn drop(&mut self) { - self.inner.deref_mut().save(); - } -} - /// Storage that persists across application reloads pub struct UseStorageEntry { inner: Signal>, @@ -353,6 +317,43 @@ impl { + inner: Ref<'a, StorageEntry>, +} + +impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> Deref for StorageRef<'a, S, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +pub struct StorageRefMut<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> { + inner: Write<'a, StorageEntry>, +} + +impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> Deref for StorageRefMut<'a, S, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> DerefMut for StorageRefMut<'a, S, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner.data + } +} + +impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> Drop for StorageRefMut<'a, S, T> { + fn drop(&mut self) { + self.inner.deref_mut().save(); + } +} + // End UseStorageEntry // state.with(move |state| { From 84e4c2fee6d79032b01e1f778cb20b10d34ee670 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Wed, 11 Oct 2023 20:41:22 -0700 Subject: [PATCH 021/108] Remove custom UseStorageEntryRef --- src/storage/client_storage/web.rs | 29 +++++-- src/storage/persistence.rs | 6 +- src/storage/storage_entry.rs | 122 ++++++++++++------------------ 3 files changed, 74 insertions(+), 83 deletions(-) diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 2adfb21..01c60c7 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -8,8 +8,8 @@ use wasm_bindgen::JsCast; use web_sys::{window, Storage}; use crate::storage::storage_entry::{ - serde_to_string, try_serde_from_string, - StorageBacking, StorageChannelPayload, StorageSubscriber, + serde_to_string, try_serde_from_string, StorageBacking, StorageChannelPayload, + StorageSubscriber, }; use crate::utils::channel::UseChannel; @@ -27,10 +27,13 @@ impl StorageBacking for LocalStorage { fn get(key: &String) -> Option { get(key, WebStorageType::Local) } + + fn save_on_write() -> bool { + true + } } impl StorageSubscriber for LocalStorage { - fn subscribe( _cx: &ScopeState, _key: &String, @@ -86,6 +89,12 @@ impl StorageBacking for SessionStorage { fn get(key: &String) -> Option { get(key, WebStorageType::Session) } + + fn save_on_write() -> bool { + // Session storage for web targets is not persisted across sessions, but it can be cloned if a tab is duplicated. + // This means we can't rely on the drop handler to save the data, so we need to save on write. + true + } } // End SessionStorage @@ -94,7 +103,10 @@ fn set(key: String, value: &T, storage_type: WebStorageType) { #[cfg(not(feature = "ssr"))] { let as_str = serde_to_string(value); - get_storage_by_type(storage_type).unwrap().set_item(&key, &as_str).unwrap(); + get_storage_by_type(storage_type) + .unwrap() + .set_item(&key, &as_str) + .unwrap(); } } @@ -109,12 +121,13 @@ fn get(key: &str, storage_type: WebStorageType) -> Option Option { - window().map_or_else(|| None, |window| { - match storage_type { + window().map_or_else( + || None, + |window| match storage_type { WebStorageType::Local => window.local_storage().ok()?, WebStorageType::Session => window.session_storage().ok()?, - } - }) + }, + ) } enum WebStorageType { diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index 5102a79..b53fafa 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -23,7 +23,11 @@ pub fn use_persistent::new(key.to_string(), init.take().unwrap()(), None) + StorageEntry::::new( + key.to_string(), + init.take().unwrap()(), + None, + ) }) } #[cfg(all(not(feature = "ssr"), not(feature = "hydrate")))] diff --git a/src/storage/storage_entry.rs b/src/storage/storage_entry.rs index d06d00d..585db77 100644 --- a/src/storage/storage_entry.rs +++ b/src/storage/storage_entry.rs @@ -1,5 +1,5 @@ use dioxus::prelude::ScopeState; -use dioxus_signals::{use_signal, Signal, Write}; +use dioxus_signals::{use_signal, Signal}; use postcard::to_allocvec; use serde::{de::DeserializeOwned, Serialize}; use std::cell::Ref; @@ -51,6 +51,10 @@ pub trait StorageBacking: Sized + Clone + 'static { type Key: Eq + PartialEq + Clone + Debug; fn get(key: &Self::Key) -> Option; fn set(key: Self::Key, value: &T); + /// Whether the storage should be saved on every write + fn save_on_write() -> bool { + false + } } pub trait StorageSubscriber { @@ -81,22 +85,6 @@ pub struct StorageEntry>>, } -impl Display - for StorageEntry -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.data.fmt(f) - } -} - -impl Debug - for StorageEntry -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.data.fmt(f) - } -} - impl StorageEntry where S: StorageBacking + StorageSubscriber, @@ -135,7 +123,6 @@ where pub fn with_mut(&mut self, f: impl FnOnce(&mut T)) { f(&mut self.data); - self.save(); } pub fn update(&mut self) { @@ -151,6 +138,22 @@ impl Deref for Stora } } +impl Display + for StorageEntry +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.data.fmt(f) + } +} + +impl Debug + for StorageEntry +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.data.fmt(f) + } +} + pub struct StorageEntryMut<'a, S, T> where S: StorageBacking, @@ -236,7 +239,7 @@ where let state = use_signal(cx, || synced_storage_entry::(key, init, cx)); if let Some(channel) = &state.read().channel { - use_listen_channel(cx, channel, move |message| async move { + use_listen_channel(cx, channel, move |message| async move { if let Ok(payload) = message { if payload.key == state.read().key { state.write().update() @@ -260,51 +263,56 @@ impl UseSt } /// Returns a reference to the value - pub fn read(&self) -> StorageRef { - StorageRef { - inner: self.inner.read(), - } - } - - /// Returns a mutable reference to the value - pub fn write(&self) -> StorageRefMut { - StorageRefMut { - inner: self.inner.write(), - } + pub fn read(&self) -> Ref { + Ref::map(self.inner.read(), |entry| &entry.data) } /// Sets the value pub fn set(&self, value: T) { - *self.write() = value; + self.inner.write().data = value; + self.try_save_on_write(); } /// Modifies the value pub fn modify(&self, f: F) { - f(&mut self.write()); + let writer = &mut *self.inner.write(); + writer.with_mut(f); + self.try_save_on_write(); } + fn try_save_on_write(&self) { + if S::save_on_write() { + self.save(); + } + } + + /// Saves the value to the backing storage pub fn save(&self) { self.inner.write().save(); } } #[allow(unused)] -impl UseStorageEntry { +impl + UseStorageEntry +{ /// Returns a clone of the value pub fn get(&self) -> T { self.read().clone() } } -impl Display - for UseStorageEntry +impl + Display for UseStorageEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { (*self.read()).fmt(f) } } -impl Deref for UseStorageEntry { +impl Deref + for UseStorageEntry +{ type Target = Signal>; fn deref(&self) -> &Self::Target { @@ -312,53 +320,19 @@ impl DerefMut for UseStorageEntry { +impl DerefMut + for UseStorageEntry +{ fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } } -pub struct StorageRef<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> { - inner: Ref<'a, StorageEntry>, -} - -impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> Deref for StorageRef<'a, S, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -pub struct StorageRefMut<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> { - inner: Write<'a, StorageEntry>, -} - -impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> Deref for StorageRefMut<'a, S, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> DerefMut for StorageRefMut<'a, S, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner.data - } -} - -impl<'a, S: StorageBacking, T: Serialize + DeserializeOwned + Clone + 'static> Drop for StorageRefMut<'a, S, T> { - fn drop(&mut self) { - self.inner.deref_mut().save(); - } -} - // End UseStorageEntry // state.with(move |state| { // if let Some(channel) = &state.channel { - + // use_listen_channel(cx, channel, move |message: Result, async_broadcast::RecvError>| async move { // if let Ok(payload) = message { // if state.key == payload.key { From 2e338534cb199f68a27e9ef89aceb75568c7fc6d Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Wed, 11 Oct 2023 20:57:48 -0700 Subject: [PATCH 022/108] remove StorageEntryMut in favor of with_mut --- src/storage/client_storage/web.rs | 10 ----- src/storage/storage_entry.rs | 70 +------------------------------ 2 files changed, 2 insertions(+), 78 deletions(-) diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 01c60c7..b764467 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -27,10 +27,6 @@ impl StorageBacking for LocalStorage { fn get(key: &String) -> Option { get(key, WebStorageType::Local) } - - fn save_on_write() -> bool { - true - } } impl StorageSubscriber for LocalStorage { @@ -89,12 +85,6 @@ impl StorageBacking for SessionStorage { fn get(key: &String) -> Option { get(key, WebStorageType::Session) } - - fn save_on_write() -> bool { - // Session storage for web targets is not persisted across sessions, but it can be cloned if a tab is duplicated. - // This means we can't rely on the drop handler to save the data, so we need to save on write. - true - } } // End SessionStorage diff --git a/src/storage/storage_entry.rs b/src/storage/storage_entry.rs index 585db77..7f275bd 100644 --- a/src/storage/storage_entry.rs +++ b/src/storage/storage_entry.rs @@ -51,10 +51,6 @@ pub trait StorageBacking: Sized + Clone + 'static { type Key: Eq + PartialEq + Clone + Debug; fn get(key: &Self::Key) -> Option; fn set(key: Self::Key, value: &T); - /// Whether the storage should be saved on every write - fn save_on_write() -> bool { - false - } } pub trait StorageSubscriber { @@ -115,14 +111,9 @@ where S::set(self.key.clone(), &self.data); } - pub fn write(&mut self) -> StorageEntryMut<'_, S, T> { - StorageEntryMut { - storage_entry: self, - } - } - pub fn with_mut(&mut self, f: impl FnOnce(&mut T)) { f(&mut self.data); + self.save() } pub fn update(&mut self) { @@ -154,50 +145,6 @@ impl Debug } } -pub struct StorageEntryMut<'a, S, T> -where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + 'static, - S::Key: Clone, -{ - storage_entry: &'a mut StorageEntry, -} - -impl<'a, S, T> Deref for StorageEntryMut<'a, S, T> -where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone, - S::Key: Clone, -{ - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.storage_entry.data - } -} - -impl<'a, S, T> DerefMut for StorageEntryMut<'a, S, T> -where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone, - S::Key: Clone, -{ - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.storage_entry.data - } -} - -impl<'a, S, T> Drop for StorageEntryMut<'a, S, T> -where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + 'static, - S::Key: Clone, -{ - fn drop(&mut self) { - self.storage_entry.save(); - } -} - pub fn storage_entry( key: S::Key, init: impl FnOnce() -> T, @@ -269,26 +216,13 @@ impl UseSt /// Sets the value pub fn set(&self, value: T) { - self.inner.write().data = value; - self.try_save_on_write(); + self.inner.write().with_mut(|data| *data = value) } /// Modifies the value pub fn modify(&self, f: F) { let writer = &mut *self.inner.write(); writer.with_mut(f); - self.try_save_on_write(); - } - - fn try_save_on_write(&self) { - if S::save_on_write() { - self.save(); - } - } - - /// Saves the value to the backing storage - pub fn save(&self) { - self.inner.write().save(); } } From 8ba7c460df46d7e42afd46d1ea1a36e8bda9f358 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Wed, 11 Oct 2023 21:00:22 -0700 Subject: [PATCH 023/108] add clone and copy derives --- src/storage/storage_entry.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/storage/storage_entry.rs b/src/storage/storage_entry.rs index 7f275bd..910af22 100644 --- a/src/storage/storage_entry.rs +++ b/src/storage/storage_entry.rs @@ -199,6 +199,7 @@ where // Start UseStorageEntry /// Storage that persists across application reloads +#[derive(Clone, Copy)] pub struct UseStorageEntry { inner: Signal>, } From 4a84603a2390dcd462a270817c46171b15850b14 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Thu, 12 Oct 2023 22:48:22 -0700 Subject: [PATCH 024/108] save work --- examples/storage/src/main.rs | 2 +- src/storage/client_storage/web.rs | 22 ++- src/storage/persistence.rs | 11 +- src/storage/storage_entry.rs | 220 +++++++++++++++++++----------- 4 files changed, 157 insertions(+), 98 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 692efcc..725022a 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -18,7 +18,7 @@ fn app(cx: Scope) -> Element { div { button { onclick: move |_| { - count_session.set(count_session.get() + 1); + *count_session.write() += 1; }, "Click me!" }, diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index b764467..8ef4617 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -1,4 +1,4 @@ -use async_broadcast::broadcast; +use async_broadcast::{broadcast, Receiver, InactiveReceiver, Sender}; use dioxus::prelude::*; use serde::{de::DeserializeOwned, Serialize}; use std::sync::OnceLock; @@ -33,19 +33,17 @@ impl StorageSubscriber for LocalStorage { fn subscribe( _cx: &ScopeState, _key: &String, - ) -> Option>> { - let channel = CHANNEL.get_or_init(|| { + ) -> Option>> { + let (_, rx) = CHANNEL.get_or_init(|| { let (tx, rx) = broadcast::>(5); - let channel = UseChannel::new(Uuid::new_v4(), tx, rx.deactivate()); - let channel_clone = channel.clone(); - + let tx_clone = tx.clone(); let closure = Closure::wrap(Box::new(move |e: web_sys::StorageEvent| { log::info!("Storage event: {:?}", e); let key: String = e.key().unwrap(); - let channel_clone_clone = channel_clone.clone(); + let tx_clone_clone = tx_clone.clone(); wasm_bindgen_futures::spawn_local(async move { - let result = channel_clone_clone - .send(StorageChannelPayload:: { key }) + let result = tx_clone_clone + .broadcast(StorageChannelPayload:: { key }) .await; match result { Ok(_) => log::info!("Sent storage event"), @@ -58,9 +56,9 @@ impl StorageSubscriber for LocalStorage { .add_event_listener_with_callback("storage", closure.as_ref().unchecked_ref()) .unwrap(); closure.forget(); - channel + (tx, rx.deactivate()) }); - Some(channel.clone()) + Some(rx.activate_cloned()) } fn unsubscribe(_key: &String) { @@ -68,7 +66,7 @@ impl StorageSubscriber for LocalStorage { } } -static CHANNEL: OnceLock>> = OnceLock::new(); +static CHANNEL: OnceLock<(Sender>, InactiveReceiver>)> = OnceLock::new(); // End LocalStorage // Start SessionStorage diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index b53fafa..94c8d71 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -7,8 +7,6 @@ use dioxus_signals::use_signal; use serde::de::DeserializeOwned; use serde::Serialize; -use super::storage_entry::UseStorageEntry; - /// 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 @@ -17,7 +15,7 @@ pub fn use_persistent T, -) -> &UseStorageEntry { +) -> &mut StorageEntry { let mut init = Some(init); let state = { #[cfg(feature = "ssr")] @@ -32,10 +30,11 @@ pub fn use_persistent::new( key.to_string(), storage_entry::(key.to_string(), init.take().unwrap()), + cx ) }) } @@ -57,7 +56,7 @@ pub fn use_persistent( cx: &ScopeState, init: impl FnOnce() -> T, -) -> &UseStorageEntry { +) -> &mut StorageEntry { let caller = std::panic::Location::caller(); let key = cx.use_hook(move || format!("{}:{}", caller.file(), caller.line())); log::info!("key: \"{}\"", key); diff --git a/src/storage/storage_entry.rs b/src/storage/storage_entry.rs index 910af22..f095c96 100644 --- a/src/storage/storage_entry.rs +++ b/src/storage/storage_entry.rs @@ -1,5 +1,6 @@ -use dioxus::prelude::ScopeState; -use dioxus_signals::{use_signal, Signal}; +use async_broadcast::Receiver; +use dioxus::prelude::{ScopeState, to_owned}; +use dioxus_signals::{use_signal, Signal, Write}; use postcard::to_allocvec; use serde::{de::DeserializeOwned, Serialize}; use std::cell::Ref; @@ -57,7 +58,7 @@ pub trait StorageSubscriber { fn subscribe( cx: &ScopeState, key: &S::Key, - ) -> Option>>; + ) -> Option>>; fn unsubscribe(key: &S::Key); } @@ -75,10 +76,10 @@ impl Debug for StorageChannelPayload { } #[derive(Clone, Default)] -pub struct StorageEntry { +pub struct StorageEntry { pub(crate) key: S::Key, - pub(crate) data: T, - pub(crate) channel: Option>>, + pub(crate) data: Signal, + pub(crate) channel: Option>>, } impl StorageEntry @@ -89,7 +90,7 @@ where { fn new_synced(key: S::Key, data: T, cx: &ScopeState) -> Self { let channel = S::subscribe::(cx, &key); - Self { key, data, channel } + Self { key, data: Signal::new_in_scope(data, cx.scope_id()), channel } } } @@ -99,10 +100,10 @@ where T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { - pub fn new(key: S::Key, data: T) -> Self { + pub fn new(key: S::Key, data: T, cx: &ScopeState) -> Self { Self { key, - data, + data: Signal::new_in_scope(data, cx.scope_id()), channel: None, } } @@ -112,19 +113,25 @@ where } pub fn with_mut(&mut self, f: impl FnOnce(&mut T)) { - f(&mut self.data); + f(&mut self.data.write()); self.save() } + pub fn write(&mut self) -> StorageEntryMut<'_, S, T> { + StorageEntryMut { + storage_entry: self, + } + } + pub fn update(&mut self) { self.data = S::get(&self.key).unwrap_or(self.data.clone()); } } impl Deref for StorageEntry { - type Target = T; + type Target = Signal; - fn deref(&self) -> &Self::Target { + fn deref(&self) -> &Signal { &self.data } } @@ -145,6 +152,50 @@ impl Debug } } +pub struct StorageEntryMut<'a, S, T> +where + S: StorageBacking, + T: Serialize + DeserializeOwned + Clone + 'static, + S::Key: Clone, +{ + storage_entry: &'a StorageEntry, +} + +impl<'a, S, T> Deref for StorageEntryMut<'a, S, T> +where + S: StorageBacking, + T: Serialize + DeserializeOwned + Clone + 'static, + S::Key: Clone, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.storage_entry.data.read() + } +} + +impl<'a, S, T> DerefMut for StorageEntryMut<'a, S, T> +where + S: StorageBacking, + T: Serialize + DeserializeOwned + Clone + 'static, + S::Key: Clone, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.storage_entry.data.write() + } +} + +impl<'a, S, T> Drop for StorageEntryMut<'a, S, T> +where + S: StorageBacking, + T: Serialize + DeserializeOwned + Clone + 'static, + S::Key: Clone, +{ + fn drop(&mut self) { + self.storage_entry.save(); + } +} + pub fn storage_entry( key: S::Key, init: impl FnOnce() -> T, @@ -177,91 +228,102 @@ pub fn use_synced_storage_entry( cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T, -) -> UseStorageEntry +) -> &mut Signal where S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { - let state = use_signal(cx, || synced_storage_entry::(key, init, cx)); - - if let Some(channel) = &state.read().channel { - use_listen_channel(cx, channel, move |message| async move { - if let Ok(payload) = message { - if payload.key == state.read().key { - state.write().update() + let cx_clone = cx.clone(); + let state = cx.use_hook(|| synced_storage_entry::(key, init, cx_clone)); + + let state_clone = state.clone(); + + if let Some(channel) = &state.channel { + cx.spawn(async move { + let channel_clone = channel.clone(); + loop { + let message = channel_clone.recv().await; + if let Ok(payload) = message { + if state.key == payload.key { + state.update(); + } } } - }); + }) } - UseStorageEntry::new(state) -} - -// Start UseStorageEntry -/// Storage that persists across application reloads -#[derive(Clone, Copy)] -pub struct UseStorageEntry { - inner: Signal>, + &mut state_clone.data } -#[allow(unused)] -impl UseStorageEntry { - pub fn new(signal: Signal>) -> Self { - Self { inner: signal } - } +// // Start UseStorageEntry +// /// Storage that persists across application reloads +// #[derive(Clone, Copy)] +// pub struct UseStorageEntry { +// inner: Signal>, +// } + +// #[allow(unused)] +// impl UseStorageEntry { +// pub fn new(signal: Signal>) -> Self { +// Self { inner: signal } +// } - /// Returns a reference to the value - pub fn read(&self) -> Ref { - Ref::map(self.inner.read(), |entry| &entry.data) - } +// /// Returns a reference to the value +// pub fn read(&self) -> Ref { +// Ref::map(self.inner.read(), |entry| &entry.data) +// } - /// Sets the value - pub fn set(&self, value: T) { - self.inner.write().with_mut(|data| *data = value) - } +// pub fn write(&self) -> Write<'_, T, StorageEntry> { +// Write::map(self.inner.write(), |entry| &mut entry.data) +// } - /// Modifies the value - pub fn modify(&self, f: F) { - let writer = &mut *self.inner.write(); - writer.with_mut(f); - } -} +// /// Sets the value +// pub fn set(&self, value: T) { +// self.inner.write().with_mut(|data| *data = value) +// } -#[allow(unused)] -impl - UseStorageEntry -{ - /// Returns a clone of the value - pub fn get(&self) -> T { - self.read().clone() - } -} +// /// Modifies the value +// pub fn modify(&self, f: F) { +// let writer = &mut *self.inner.write(); +// writer.with_mut(f); +// } +// } + +// #[allow(unused)] +// impl +// UseStorageEntry +// { +// /// Returns a clone of the value +// pub fn get(&self) -> T { +// self.read().clone() +// } +// } -impl - Display for UseStorageEntry -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - (*self.read()).fmt(f) - } -} +// impl +// Display for UseStorageEntry +// { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// (*self.read()).fmt(f) +// } +// } -impl Deref - for UseStorageEntry -{ - type Target = Signal>; +// impl Deref +// for UseStorageEntry +// { +// type Target = Signal>; - fn deref(&self) -> &Self::Target { - &self.inner - } -} +// fn deref(&self) -> &Self::Target { +// &self.inner +// } +// } -impl DerefMut - for UseStorageEntry -{ - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} +// impl DerefMut +// for UseStorageEntry +// { +// fn deref_mut(&mut self) -> &mut Self::Target { +// &mut self.inner +// } +// } // End UseStorageEntry From 84c47f2e33d3a232c4ff42b6d0a2f91cd5023a81 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Thu, 12 Oct 2023 23:46:59 -0700 Subject: [PATCH 025/108] potential signal-centric alternative --- examples/storage/src/main.rs | 2 +- src/storage/storage_entry.rs | 148 +++++++++++++++++++---------------- 2 files changed, 81 insertions(+), 69 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 725022a..4ff3208 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -27,7 +27,7 @@ fn app(cx: Scope) -> Element { div { button { onclick: move |_| { - count_local.set(count_local.get() + 1); + *count_local.write() += 1; }, "Click me!" }, diff --git a/src/storage/storage_entry.rs b/src/storage/storage_entry.rs index f095c96..b2f9104 100644 --- a/src/storage/storage_entry.rs +++ b/src/storage/storage_entry.rs @@ -1,6 +1,6 @@ use async_broadcast::Receiver; -use dioxus::prelude::{ScopeState, to_owned}; -use dioxus_signals::{use_signal, Signal, Write}; +use dioxus::prelude::{ScopeState, to_owned, use_effect}; +use dioxus_signals::{use_signal, use_selector, Signal, Write}; use postcard::to_allocvec; use serde::{de::DeserializeOwned, Serialize}; use std::cell::Ref; @@ -89,8 +89,29 @@ where S::Key: Clone, { fn new_synced(key: S::Key, data: T, cx: &ScopeState) -> Self { + let key_clone = key.clone(); let channel = S::subscribe::(cx, &key); - Self { key, data: Signal::new_in_scope(data, cx.scope_id()), channel } + + let retval = Self { key, data: Signal::new_in_scope(data, cx.scope_id()), channel }; + // let retval_clone = retval.clone(); + + if let Some(mut channel) = retval.channel.clone() { + cx.spawn(async move { + loop { + let message = channel.recv().await; + log::info!("message: {:?}", message); + if let Ok(payload) = message { + if key_clone == payload.key { + *retval.data.write() = S::get(&key_clone).unwrap_or_else(|| { + log::info!("get failed"); + retval.data.read().clone() + }); + } + } + } + }); + } + retval } } @@ -112,19 +133,19 @@ where S::set(self.key.clone(), &self.data); } - pub fn with_mut(&mut self, f: impl FnOnce(&mut T)) { - f(&mut self.data.write()); - self.save() - } + // pub fn with_mut(&mut self, f: impl FnOnce(&mut T)) { + // f(&mut self.data.write()); + // self.save() + // } - pub fn write(&mut self) -> StorageEntryMut<'_, S, T> { - StorageEntryMut { - storage_entry: self, - } - } + // pub fn write(&mut self) -> StorageEntryMut<'_, S, T> { + // StorageEntryMut { + // storage_entry: self, + // } + // } pub fn update(&mut self) { - self.data = S::get(&self.key).unwrap_or(self.data.clone()); + self.data = S::get(&self.key).unwrap_or(self.data); } } @@ -152,49 +173,49 @@ impl Debug } } -pub struct StorageEntryMut<'a, S, T> -where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + 'static, - S::Key: Clone, -{ - storage_entry: &'a StorageEntry, -} +// pub struct StorageEntryMut<'a, S, T> +// where +// S: StorageBacking, +// T: Serialize + DeserializeOwned + Clone + 'static, +// S::Key: Clone, +// { +// storage_entry: &'a StorageEntry, +// } -impl<'a, S, T> Deref for StorageEntryMut<'a, S, T> -where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + 'static, - S::Key: Clone, -{ - type Target = T; +// impl<'a, S, T> Deref for StorageEntryMut<'a, S, T> +// where +// S: StorageBacking, +// T: Serialize + DeserializeOwned + Clone + 'static, +// S::Key: Clone, +// { +// type Target = T; - fn deref(&self) -> &Self::Target { - &self.storage_entry.data.read() - } -} +// fn deref(&self) -> &Self::Target { +// &self.storage_entry.data.read() +// } +// } -impl<'a, S, T> DerefMut for StorageEntryMut<'a, S, T> -where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + 'static, - S::Key: Clone, -{ - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.storage_entry.data.write() - } -} +// impl<'a, S, T> DerefMut for StorageEntryMut<'a, S, T> +// where +// S: StorageBacking, +// T: Serialize + DeserializeOwned + Clone + 'static, +// S::Key: Clone, +// { +// fn deref_mut(&mut self) -> &mut Self::Target { +// &mut self.storage_entry.data.write() +// } +// } -impl<'a, S, T> Drop for StorageEntryMut<'a, S, T> -where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + 'static, - S::Key: Clone, -{ - fn drop(&mut self) { - self.storage_entry.save(); - } -} +// impl<'a, S, T> Drop for StorageEntryMut<'a, S, T> +// where +// S: StorageBacking, +// T: Serialize + DeserializeOwned + Clone + 'static, +// S::Key: Clone, +// { +// fn drop(&mut self) { +// self.storage_entry.save(); +// } +// } pub fn storage_entry( key: S::Key, @@ -236,23 +257,14 @@ where { let cx_clone = cx.clone(); let state = cx.use_hook(|| synced_storage_entry::(key, init, cx_clone)); - let state_clone = state.clone(); - - if let Some(channel) = &state.channel { - cx.spawn(async move { - let channel_clone = channel.clone(); - loop { - let message = channel_clone.recv().await; - if let Ok(payload) = message { - if state.key == payload.key { - state.update(); - } - } - } - }) - } - &mut state_clone.data + let state_signal = state.data; + use_selector(cx_clone, move || { + log::info!("use_synced_storage_entry selector"); + let x = state_signal; + state_clone.save(); + }); + &mut state.data } // // Start UseStorageEntry From e72df4ef3b752fc3be29d38b4c9524b4eb3c71e2 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Thu, 12 Oct 2023 23:58:43 -0700 Subject: [PATCH 026/108] save --- src/storage/storage_entry.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/storage/storage_entry.rs b/src/storage/storage_entry.rs index b2f9104..bcc8a1e 100644 --- a/src/storage/storage_entry.rs +++ b/src/storage/storage_entry.rs @@ -6,6 +6,7 @@ use serde::{de::DeserializeOwned, Serialize}; use std::cell::Ref; use std::fmt::{Debug, Display}; use std::ops::{Deref, DerefMut}; +use std::sync::{Mutex, Arc}; use crate::utils::channel::{use_listen_channel, UseChannel}; @@ -80,6 +81,7 @@ pub struct StorageEntry, pub(crate) channel: Option>>, + pub(crate) lock: Arc>, } impl StorageEntry @@ -92,7 +94,7 @@ where let key_clone = key.clone(); let channel = S::subscribe::(cx, &key); - let retval = Self { key, data: Signal::new_in_scope(data, cx.scope_id()), channel }; + let retval = Self { key, data: Signal::new_in_scope(data, cx.scope_id()), channel, lock: Arc::new(Mutex::new(())) }; // let retval_clone = retval.clone(); if let Some(mut channel) = retval.channel.clone() { @@ -126,11 +128,14 @@ where key, data: Signal::new_in_scope(data, cx.scope_id()), channel: None, + lock: Arc::new(Mutex::new(())), } } pub(crate) fn save(&self) { - S::set(self.key.clone(), &self.data); + let _ = self.lock.try_lock().map(|_| { + S::set(self.key.clone(), &self.data); + }); } // pub fn with_mut(&mut self, f: impl FnOnce(&mut T)) { From e0a929dabadf9c31d06aa38e85464fc0f0413ed2 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Fri, 13 Oct 2023 00:34:59 -0700 Subject: [PATCH 027/108] it works! --- src/storage/client_storage/web.rs | 21 ++-- src/storage/storage_entry.rs | 198 ++++-------------------------- 2 files changed, 36 insertions(+), 183 deletions(-) diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 8ef4617..5fa434a 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -1,4 +1,4 @@ -use async_broadcast::{broadcast, Receiver, InactiveReceiver, Sender}; +use async_broadcast::broadcast; use dioxus::prelude::*; use serde::{de::DeserializeOwned, Serialize}; use std::sync::OnceLock; @@ -33,17 +33,18 @@ impl StorageSubscriber for LocalStorage { fn subscribe( _cx: &ScopeState, _key: &String, - ) -> Option>> { - let (_, rx) = CHANNEL.get_or_init(|| { + ) -> Option>> { + let channel = CHANNEL.get_or_init(|| { let (tx, rx) = broadcast::>(5); - let tx_clone = tx.clone(); + let channel = UseChannel::new(Uuid::new_v4(), tx, rx.deactivate()); + let channel_clone = channel.clone(); let closure = Closure::wrap(Box::new(move |e: web_sys::StorageEvent| { log::info!("Storage event: {:?}", e); let key: String = e.key().unwrap(); - let tx_clone_clone = tx_clone.clone(); + let channel_clone_clone = channel_clone.clone(); wasm_bindgen_futures::spawn_local(async move { - let result = tx_clone_clone - .broadcast(StorageChannelPayload:: { key }) + let result = channel_clone_clone + .send(StorageChannelPayload:: { key }) .await; match result { Ok(_) => log::info!("Sent storage event"), @@ -56,9 +57,9 @@ impl StorageSubscriber for LocalStorage { .add_event_listener_with_callback("storage", closure.as_ref().unchecked_ref()) .unwrap(); closure.forget(); - (tx, rx.deactivate()) + channel }); - Some(rx.activate_cloned()) + Some(channel.clone()) } fn unsubscribe(_key: &String) { @@ -66,7 +67,7 @@ impl StorageSubscriber for LocalStorage { } } -static CHANNEL: OnceLock<(Sender>, InactiveReceiver>)> = OnceLock::new(); +static CHANNEL: OnceLock>> = OnceLock::new(); // End LocalStorage // Start SessionStorage diff --git a/src/storage/storage_entry.rs b/src/storage/storage_entry.rs index bcc8a1e..b3229a5 100644 --- a/src/storage/storage_entry.rs +++ b/src/storage/storage_entry.rs @@ -1,12 +1,10 @@ -use async_broadcast::Receiver; -use dioxus::prelude::{ScopeState, to_owned, use_effect}; -use dioxus_signals::{use_signal, use_selector, Signal, Write}; +use dioxus::prelude::ScopeState; +use dioxus_signals::{use_selector, use_signal, Signal}; use postcard::to_allocvec; use serde::{de::DeserializeOwned, Serialize}; -use std::cell::Ref; use std::fmt::{Debug, Display}; -use std::ops::{Deref, DerefMut}; -use std::sync::{Mutex, Arc}; +use std::ops::Deref; +use std::sync::{Arc, Mutex}; use crate::utils::channel::{use_listen_channel, UseChannel}; @@ -59,7 +57,7 @@ pub trait StorageSubscriber { fn subscribe( cx: &ScopeState, key: &S::Key, - ) -> Option>>; + ) -> Option>>; fn unsubscribe(key: &S::Key); } @@ -80,7 +78,7 @@ impl Debug for StorageChannelPayload { pub struct StorageEntry { pub(crate) key: S::Key, pub(crate) data: Signal, - pub(crate) channel: Option>>, + pub(crate) channel: Option>>, pub(crate) lock: Arc>, } @@ -91,29 +89,14 @@ where S::Key: Clone, { fn new_synced(key: S::Key, data: T, cx: &ScopeState) -> Self { - let key_clone = key.clone(); let channel = S::subscribe::(cx, &key); - let retval = Self { key, data: Signal::new_in_scope(data, cx.scope_id()), channel, lock: Arc::new(Mutex::new(())) }; - // let retval_clone = retval.clone(); - - if let Some(mut channel) = retval.channel.clone() { - cx.spawn(async move { - loop { - let message = channel.recv().await; - log::info!("message: {:?}", message); - if let Ok(payload) = message { - if key_clone == payload.key { - *retval.data.write() = S::get(&key_clone).unwrap_or_else(|| { - log::info!("get failed"); - retval.data.read().clone() - }); - } - } - } - }); + Self { + key, + data: Signal::new_in_scope(data, cx.scope_id()), + channel, + lock: Arc::new(Mutex::new(())), } - retval } } @@ -138,17 +121,6 @@ where }); } - // pub fn with_mut(&mut self, f: impl FnOnce(&mut T)) { - // f(&mut self.data.write()); - // self.save() - // } - - // pub fn write(&mut self) -> StorageEntryMut<'_, S, T> { - // StorageEntryMut { - // storage_entry: self, - // } - // } - pub fn update(&mut self) { self.data = S::get(&self.key).unwrap_or(self.data); } @@ -178,50 +150,6 @@ impl Debug } } -// pub struct StorageEntryMut<'a, S, T> -// where -// S: StorageBacking, -// T: Serialize + DeserializeOwned + Clone + 'static, -// S::Key: Clone, -// { -// storage_entry: &'a StorageEntry, -// } - -// impl<'a, S, T> Deref for StorageEntryMut<'a, S, T> -// where -// S: StorageBacking, -// T: Serialize + DeserializeOwned + Clone + 'static, -// S::Key: Clone, -// { -// type Target = T; - -// fn deref(&self) -> &Self::Target { -// &self.storage_entry.data.read() -// } -// } - -// impl<'a, S, T> DerefMut for StorageEntryMut<'a, S, T> -// where -// S: StorageBacking, -// T: Serialize + DeserializeOwned + Clone + 'static, -// S::Key: Clone, -// { -// fn deref_mut(&mut self) -> &mut Self::Target { -// &mut self.storage_entry.data.write() -// } -// } - -// impl<'a, S, T> Drop for StorageEntryMut<'a, S, T> -// where -// S: StorageBacking, -// T: Serialize + DeserializeOwned + Clone + 'static, -// S::Key: Clone, -// { -// fn drop(&mut self) { -// self.storage_entry.save(); -// } -// } - pub fn storage_entry( key: S::Key, init: impl FnOnce() -> T, @@ -260,100 +188,24 @@ where T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { - let cx_clone = cx.clone(); - let state = cx.use_hook(|| synced_storage_entry::(key, init, cx_clone)); - let state_clone = state.clone(); + let key_signal = use_signal(cx, || key.clone()); + let state = cx.use_hook(|| synced_storage_entry::(key, init, cx)); let state_signal = state.data; - use_selector(cx_clone, move || { + if let Some(channel) = state.channel.clone() { + use_listen_channel(cx, &channel, move |message| async move { + if let Ok(payload) = message { + *state_signal.write() = S::get(&key_signal.read()).unwrap_or_else(|| { + log::info!("get failed"); + state_signal.read().clone() + }); + } + }); + } + let state_clone = state.clone(); + use_selector(cx, move || { log::info!("use_synced_storage_entry selector"); let x = state_signal; state_clone.save(); }); &mut state.data } - -// // Start UseStorageEntry -// /// Storage that persists across application reloads -// #[derive(Clone, Copy)] -// pub struct UseStorageEntry { -// inner: Signal>, -// } - -// #[allow(unused)] -// impl UseStorageEntry { -// pub fn new(signal: Signal>) -> Self { -// Self { inner: signal } -// } - -// /// Returns a reference to the value -// pub fn read(&self) -> Ref { -// Ref::map(self.inner.read(), |entry| &entry.data) -// } - -// pub fn write(&self) -> Write<'_, T, StorageEntry> { -// Write::map(self.inner.write(), |entry| &mut entry.data) -// } - -// /// Sets the value -// pub fn set(&self, value: T) { -// self.inner.write().with_mut(|data| *data = value) -// } - -// /// Modifies the value -// pub fn modify(&self, f: F) { -// let writer = &mut *self.inner.write(); -// writer.with_mut(f); -// } -// } - -// #[allow(unused)] -// impl -// UseStorageEntry -// { -// /// Returns a clone of the value -// pub fn get(&self) -> T { -// self.read().clone() -// } -// } - -// impl -// Display for UseStorageEntry -// { -// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -// (*self.read()).fmt(f) -// } -// } - -// impl Deref -// for UseStorageEntry -// { -// type Target = Signal>; - -// fn deref(&self) -> &Self::Target { -// &self.inner -// } -// } - -// impl DerefMut -// for UseStorageEntry -// { -// fn deref_mut(&mut self) -> &mut Self::Target { -// &mut self.inner -// } -// } - -// End UseStorageEntry - -// state.with(move |state| { -// if let Some(channel) = &state.channel { - -// use_listen_channel(cx, channel, move |message: Result, async_broadcast::RecvError>| async move { -// if let Ok(payload) = message { -// if state.key == payload.key { -// state.update(); -// } -// } -// }); -// } -// }); -// state From 856a86094bbe7420a6b788505121da5eed6b9921 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Fri, 13 Oct 2023 00:38:11 -0700 Subject: [PATCH 028/108] fix persistence --- src/storage/persistence.rs | 9 ++++++++- src/storage/storage_entry.rs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index 94c8d71..a7a7fbd 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -3,7 +3,7 @@ use crate::storage::{ SessionStorage, }; use dioxus::prelude::*; -use dioxus_signals::use_signal; +use dioxus_signals::use_selector; use serde::de::DeserializeOwned; use serde::Serialize; @@ -56,6 +56,13 @@ pub fn use_persistent Date: Fri, 13 Oct 2023 16:59:03 -0700 Subject: [PATCH 029/108] use_effect instead of use_subscribe --- src/storage/storage_entry.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/storage/storage_entry.rs b/src/storage/storage_entry.rs index ec6cf48..05b1824 100644 --- a/src/storage/storage_entry.rs +++ b/src/storage/storage_entry.rs @@ -1,4 +1,4 @@ -use dioxus::prelude::ScopeState; +use dioxus::prelude::{ScopeState, use_effect}; use dioxus_signals::{use_selector, use_signal, Signal}; use postcard::to_allocvec; use serde::{de::DeserializeOwned, Serialize}; @@ -202,9 +202,8 @@ where }); } let state_clone = state.clone(); - use_selector(cx, move || { + use_effect(cx, (&state_signal,), move |_| async move { log::info!("use_synced_storage_entry selector"); - let _x = state_signal; state_clone.save(); }); &mut state.data From eaa787c79ec425aca0dcf31089a0cfb6054798da Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Fri, 13 Oct 2023 17:05:23 -0700 Subject: [PATCH 030/108] fix use_effect --- src/storage/persistence.rs | 10 ++++------ src/storage/storage_entry.rs | 6 +++--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index a7a7fbd..74a353f 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -2,8 +2,7 @@ use crate::storage::{ storage_entry::{storage_entry, StorageEntry}, SessionStorage, }; -use dioxus::prelude::*; -use dioxus_signals::use_selector; +use dioxus::prelude::{use_effect, ScopeState}; use serde::de::DeserializeOwned; use serde::Serialize; @@ -11,7 +10,7 @@ use serde::Serialize; /// /// Depending on the platform this uses either local storage or a file storage #[allow(clippy::needless_return)] -pub fn use_persistent( +pub fn use_persistent( cx: &ScopeState, key: impl ToString, init: impl FnOnce() -> T, @@ -58,9 +57,8 @@ pub fn use_persistent( ) -> &mut Signal where S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + 'static, + T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, S::Key: Clone, { let key_signal = use_signal(cx, || key.clone()); @@ -202,8 +202,8 @@ where }); } let state_clone = state.clone(); - use_effect(cx, (&state_signal,), move |_| async move { - log::info!("use_synced_storage_entry selector"); + use_effect(cx, (&state_signal.value(),), move |_| async move { + log::info!("state value changed, trying to save"); state_clone.save(); }); &mut state.data From 1e4b0e6bf10602e2a1ae1a3b9efa002c42c32e24 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Fri, 13 Oct 2023 17:24:12 -0700 Subject: [PATCH 031/108] working on ssr --- src/storage/persistence.rs | 10 +++++++--- src/storage/storage_entry.rs | 23 ++++++++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index 74a353f..bfc1ec1 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -39,8 +39,12 @@ pub fn use_persistent::new(key.to_string(), init.take().unwrap()(), None) + let state = cx.use_hook(|| { + StorageEntry::::new( + key.to_string(), + storage_entry::(key.to_string(), init.take().unwrap()), + cx + ) }); if cx.generation() == 0 { cx.needs_update(); @@ -70,7 +74,7 @@ pub fn use_persistent( +pub fn use_singleton_persistent( cx: &ScopeState, init: impl FnOnce() -> T, ) -> &mut StorageEntry { diff --git a/src/storage/storage_entry.rs b/src/storage/storage_entry.rs index d0a346b..4bf3427 100644 --- a/src/storage/storage_entry.rs +++ b/src/storage/storage_entry.rs @@ -1,5 +1,5 @@ -use dioxus::prelude::{ScopeState, use_effect}; -use dioxus_signals::{use_selector, use_signal, Signal}; +use dioxus::prelude::{to_owned, use_effect, use_ref, ScopeState}; +use dioxus_signals::{use_signal, Signal}; use postcard::to_allocvec; use serde::{de::DeserializeOwned, Serialize}; use std::fmt::{Debug, Display}; @@ -188,16 +188,21 @@ where T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, S::Key: Clone, { - let key_signal = use_signal(cx, || key.clone()); + let key_clone = key.clone(); let state = cx.use_hook(|| synced_storage_entry::(key, init, cx)); let state_signal = state.data; if let Some(channel) = state.channel.clone() { - use_listen_channel(cx, &channel, move |message| async move { - if let Ok(payload) = message { - *state_signal.write() = S::get(&key_signal.read()).unwrap_or_else(|| { - log::info!("get failed"); - state_signal.read().clone() - }); + use_listen_channel(cx, &channel, move |message| { + to_owned![key_clone]; + async move { + if let Ok(payload) = message { + if payload.key == key_clone { + *state_signal.write() = S::get(&key_clone).unwrap_or_else(|| { + log::info!("get failed for {:?}", key_clone); + state_signal.read().clone() + }); + } + } } }); } From d6ae6e92e96999ebcbf58e44394a78bc452df901 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sun, 15 Oct 2023 13:20:26 -0700 Subject: [PATCH 032/108] refactoring for devex --- examples/storage/src/main.rs | 2 +- src/storage/client_storage/web.rs | 2 +- src/storage/mod.rs | 275 +++++++++++++++++++++++++++++- src/storage/persistence.rs | 38 ++--- src/storage/storage_entry.rs | 215 ----------------------- 5 files changed, 288 insertions(+), 244 deletions(-) delete mode 100644 src/storage/storage_entry.rs diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 4ff3208..c437851 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -12,7 +12,7 @@ fn main() { fn app(cx: Scope) -> Element { let count_session = use_singleton_persistent(cx, || 0); - let count_local = use_synced_storage_entry::(cx, "synced".to_string(), || 0); + let count_local = use_storage_entry_with_subscription::(cx, "synced".to_string(), || 0); render!( div { diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 5fa434a..a68e364 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -7,7 +7,7 @@ use wasm_bindgen::prelude::Closure; use wasm_bindgen::JsCast; use web_sys::{window, Storage}; -use crate::storage::storage_entry::{ +use crate::storage::{ serde_to_string, try_serde_from_string, StorageBacking, StorageChannelPayload, StorageSubscriber, }; diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 21f6323..ab95eb4 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -28,11 +28,282 @@ mod client_storage; mod persistence; -mod storage_entry; -pub use storage_entry::use_synced_storage_entry; pub use client_storage::{LocalStorage, SessionStorage}; pub use persistence::{use_persistent, use_singleton_persistent}; +use dioxus::prelude::{to_owned, use_effect, ScopeState}; +use dioxus_signals::Signal; +use postcard::to_allocvec; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt::{Debug, Display}; +use std::ops::Deref; +use std::sync::{Arc, Mutex}; + +use crate::utils::channel::{use_listen_channel, UseChannel}; + #[cfg(not(target_family = "wasm"))] pub use client_storage::set_dir; + +// // Start use_storage hooks +// pub fn use_synced_storage( +// cx: &ScopeState, +// key: impl ToString, +// init: impl FnOnce() -> T, +// ) -> &mut Signal +// where +// T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, +// { +// cfg_if::cfg_if! { + +// } +// } + +pub fn use_storage_entry( + cx: &ScopeState, + key: S::Key, + init: impl FnOnce() -> T, +) -> &mut StorageEntry +where + S: StorageBacking, + T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, + S::Key: Clone, +{ + let storage_entry = cx.use_hook(|| { + storage_entry::(key, init, cx) + }); + + let storage_entry_clone: StorageEntry = storage_entry.clone(); + use_effect(cx, (&storage_entry_clone.data.value(),), move |_| async move { + log::info!("state value changed, trying to save"); + storage_entry_clone.save(); + }); + + storage_entry +} + +#[allow(unused)] +pub fn use_storage_entry_with_subscription( + cx: &ScopeState, + key: S::Key, + init: impl FnOnce() -> T, +) -> &mut StorageEntry +where + S: StorageBacking + StorageSubscriber, + T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, + S::Key: Clone, +{ + let key_clone = key.clone(); + let storage_entry = cx.use_hook(|| synced_storage_entry::(key, init, cx)); + let storage_entry_signal = storage_entry.data; + if let Some(channel) = storage_entry.channel.clone() { + use_listen_channel(cx, &channel, move |message| { + to_owned![key_clone]; + async move { + if let Ok(payload) = message { + if payload.key == key_clone { + *storage_entry_signal.write() = + get_from_storage::(key_clone, || storage_entry_signal.value()) + } + } + } + }); + } + + let storage_entry_clone: StorageEntry = storage_entry.clone(); + use_effect(cx, (&storage_entry_clone.data.value(),), move |_| async move { + log::info!("state value changed, trying to save"); + storage_entry_clone.save(); + }); + storage_entry +} + +pub fn storage_entry( + key: S::Key, + init: impl FnOnce() -> T, + cx: &ScopeState, +) -> StorageEntry +where + S: StorageBacking, + T: Serialize + DeserializeOwned + Clone + 'static, + S::Key: Clone, +{ + let data = get_from_storage::(key.clone(), init); + StorageEntry::new(key, data, cx) +} + +pub fn synced_storage_entry( + key: S::Key, + init: impl FnOnce() -> T, + cx: &ScopeState, +) -> StorageEntry +where + S: StorageBacking + StorageSubscriber, + T: Serialize + DeserializeOwned + Clone + 'static, + S::Key: Clone, +{ + let data = get_from_storage::(key.clone(), init); + StorageEntry::new_synced(key, data, cx) +} + +pub fn get_from_storage( + key: S::Key, + init: impl FnOnce() -> T, +) -> T { + S::get(&key).unwrap_or_else(|| { + let data = init(); + S::set(key, &data); + data + }) +} +// End use_storage hooks + +// Start StorageEntry +#[derive(Clone, Default)] +pub struct StorageEntry { + pub(crate) key: S::Key, + pub(crate) data: Signal, + pub(crate) channel: Option>>, + pub(crate) lock: Arc>, +} + +impl StorageEntry +where + S: StorageBacking + StorageSubscriber, + T: Serialize + DeserializeOwned + Clone + 'static, + S::Key: Clone, +{ + fn new_synced(key: S::Key, data: T, cx: &ScopeState) -> Self { + let channel = S::subscribe::(cx, &key); + + Self { + key, + data: Signal::new_in_scope(data, cx.scope_id()), + channel, + lock: Arc::new(Mutex::new(())), + } + } +} + +impl StorageEntry +where + S: StorageBacking, + T: Serialize + DeserializeOwned + Clone + 'static, + S::Key: Clone, +{ + pub fn new(key: S::Key, data: T, cx: &ScopeState) -> Self { + Self { + key, + data: Signal::new_in_scope(data, cx.scope_id()), + channel: None, + lock: Arc::new(Mutex::new(())), + } + } + + pub(crate) fn save(&self) { + let _ = self.lock.try_lock().map(|_| { + S::set(self.key.clone(), &self.data); + }); + } + + pub fn update(&mut self) { + self.data = S::get(&self.key).unwrap_or(self.data); + } +} + +impl Deref for StorageEntry { + type Target = Signal; + + fn deref(&self) -> &Signal { + &self.data + } +} + +impl Display + for StorageEntry +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.data.fmt(f) + } +} + +impl Debug + for StorageEntry +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.data.fmt(f) + } +} +// End StorageEntry + +// Start Storage Backing traits +pub trait StorageBacking: Sized + Clone + 'static { + type Key: Eq + PartialEq + Clone + Debug; + fn get(key: &Self::Key) -> Option; + fn set(key: Self::Key, value: &T); +} + +pub trait StorageSubscriber { + fn subscribe( + cx: &ScopeState, + key: &S::Key, + ) -> Option>>; + fn unsubscribe(key: &S::Key); +} +// End Storage Backing traits + +// Start StorageChannelPayload +#[derive(Clone)] +pub struct StorageChannelPayload { + pub key: S::Key, +} + +impl Debug for StorageChannelPayload { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StorageChannelPayload") + .field("key", &self.key) + .finish() + } +} +// End StorageChannelPayload + +// Start helper functions +pub(crate) fn serde_to_string(value: &T) -> String { + let serialized = to_allocvec(value).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)] +pub(crate) fn serde_from_string(value: &str) -> T { + try_serde_from_string(value).unwrap() +} + +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); + } + let (decompressed, _) = yazi::decompress(&bytes, yazi::Format::Zlib).ok()?; + postcard::from_bytes(&decompressed).ok() +} +// End helper functions diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index bfc1ec1..8a60571 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -1,8 +1,6 @@ -use crate::storage::{ - storage_entry::{storage_entry, StorageEntry}, - SessionStorage, -}; -use dioxus::prelude::{use_effect, ScopeState}; +use crate::storage::{SessionStorage, use_storage_entry}; +use dioxus::prelude::ScopeState; +use dioxus_signals::Signal; use serde::de::DeserializeOwned; use serde::Serialize; @@ -14,9 +12,9 @@ pub fn use_persistent T, -) -> &mut StorageEntry { +) -> &mut Signal { let mut init = Some(init); - let state = { + let storage_entry = { #[cfg(feature = "ssr")] { use_ref(cx, || { @@ -29,13 +27,7 @@ pub fn use_persistent::new( - key.to_string(), - storage_entry::(key.to_string(), init.take().unwrap()), - cx - ) - }) + use_storage_entry::(cx, key.to_string(), init.take().unwrap()) } #[cfg(all(not(feature = "ssr"), feature = "hydrate"))] { @@ -43,7 +35,7 @@ pub fn use_persistent::new( key.to_string(), storage_entry::(key.to_string(), init.take().unwrap()), - cx + cx, ) }); if cx.generation() == 0 { @@ -59,13 +51,7 @@ pub fn use_persistent( +pub fn use_singleton_persistent< + T: Serialize + DeserializeOwned + Default + Clone + PartialEq + 'static, +>( cx: &ScopeState, init: impl FnOnce() -> T, -) -> &mut StorageEntry { +) -> &mut Signal { let caller = std::panic::Location::caller(); let key = cx.use_hook(move || format!("{}:{}", caller.file(), caller.line())); - log::info!("key: \"{}\"", key); + log::info!("use_singleton_persistent key: \"{}\"", key); use_persistent(cx, key, init) } diff --git a/src/storage/storage_entry.rs b/src/storage/storage_entry.rs deleted file mode 100644 index 4bf3427..0000000 --- a/src/storage/storage_entry.rs +++ /dev/null @@ -1,215 +0,0 @@ -use dioxus::prelude::{to_owned, use_effect, use_ref, ScopeState}; -use dioxus_signals::{use_signal, Signal}; -use postcard::to_allocvec; -use serde::{de::DeserializeOwned, Serialize}; -use std::fmt::{Debug, Display}; -use std::ops::Deref; -use std::sync::{Arc, Mutex}; - -use crate::utils::channel::{use_listen_channel, UseChannel}; - -pub fn serde_to_string(value: &T) -> String { - let serialized = to_allocvec(value).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)] -pub fn serde_from_string(value: &str) -> T { - try_serde_from_string(value).unwrap() -} - -pub 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); - } - let (decompressed, _) = yazi::decompress(&bytes, yazi::Format::Zlib).ok()?; - postcard::from_bytes(&decompressed).ok() -} - -pub trait StorageBacking: Sized + Clone + 'static { - type Key: Eq + PartialEq + Clone + Debug; - fn get(key: &Self::Key) -> Option; - fn set(key: Self::Key, value: &T); -} - -pub trait StorageSubscriber { - fn subscribe( - cx: &ScopeState, - key: &S::Key, - ) -> Option>>; - fn unsubscribe(key: &S::Key); -} - -#[derive(Clone)] -pub struct StorageChannelPayload { - pub key: S::Key, -} - -impl Debug for StorageChannelPayload { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("StorageChannelPayload") - .field("key", &self.key) - .finish() - } -} - -#[derive(Clone, Default)] -pub struct StorageEntry { - pub(crate) key: S::Key, - pub(crate) data: Signal, - pub(crate) channel: Option>>, - pub(crate) lock: Arc>, -} - -impl StorageEntry -where - S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + 'static, - S::Key: Clone, -{ - fn new_synced(key: S::Key, data: T, cx: &ScopeState) -> Self { - let channel = S::subscribe::(cx, &key); - - Self { - key, - data: Signal::new_in_scope(data, cx.scope_id()), - channel, - lock: Arc::new(Mutex::new(())), - } - } -} - -impl StorageEntry -where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + 'static, - S::Key: Clone, -{ - pub fn new(key: S::Key, data: T, cx: &ScopeState) -> Self { - Self { - key, - data: Signal::new_in_scope(data, cx.scope_id()), - channel: None, - lock: Arc::new(Mutex::new(())), - } - } - - pub(crate) fn save(&self) { - let _ = self.lock.try_lock().map(|_| { - S::set(self.key.clone(), &self.data); - }); - } - - pub fn update(&mut self) { - self.data = S::get(&self.key).unwrap_or(self.data); - } -} - -impl Deref for StorageEntry { - type Target = Signal; - - fn deref(&self) -> &Signal { - &self.data - } -} - -impl Display - for StorageEntry -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.data.fmt(f) - } -} - -impl Debug - for StorageEntry -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.data.fmt(f) - } -} - -pub fn storage_entry( - key: S::Key, - init: impl FnOnce() -> T, -) -> T { - S::get(&key).unwrap_or_else(|| { - let data = init(); - S::set(key, &data); - data - }) -} - -#[allow(unused)] -pub fn synced_storage_entry( - key: S::Key, - init: impl FnOnce() -> T, - cx: &ScopeState, -) -> StorageEntry -where - S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + 'static, - S::Key: Clone, -{ - let data = storage_entry::(key.clone(), init); - - StorageEntry::new_synced(key, data, cx) -} - -#[allow(unused)] -pub fn use_synced_storage_entry( - cx: &ScopeState, - key: S::Key, - init: impl FnOnce() -> T, -) -> &mut Signal -where - S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, - S::Key: Clone, -{ - let key_clone = key.clone(); - let state = cx.use_hook(|| synced_storage_entry::(key, init, cx)); - let state_signal = state.data; - if let Some(channel) = state.channel.clone() { - use_listen_channel(cx, &channel, move |message| { - to_owned![key_clone]; - async move { - if let Ok(payload) = message { - if payload.key == key_clone { - *state_signal.write() = S::get(&key_clone).unwrap_or_else(|| { - log::info!("get failed for {:?}", key_clone); - state_signal.read().clone() - }); - } - } - } - }); - } - let state_clone = state.clone(); - use_effect(cx, (&state_signal.value(),), move |_| async move { - log::info!("state value changed, trying to save"); - state_clone.save(); - }); - &mut state.data -} From f4299c935ef124205e5de9b72bbc2956d2221bec Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sun, 15 Oct 2023 13:34:56 -0700 Subject: [PATCH 033/108] add comments --- src/storage/mod.rs | 46 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index ab95eb4..8381208 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -59,6 +59,10 @@ pub use client_storage::set_dir; // } // } +/// A storage hook that can be used to store data across application reloads. +/// +/// It returns a Signal that can be used to read and modify the state. +/// The changes to the state will be persisted across reloads. pub fn use_storage_entry( cx: &ScopeState, key: S::Key, @@ -82,7 +86,10 @@ where storage_entry } -#[allow(unused)] +/// A storage hook that can be used to store data that will persist across application reloads and be synced across all app sessions for a given installation or browser. +/// +/// 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_storage_entry_with_subscription( cx: &ScopeState, key: S::Key, @@ -94,7 +101,7 @@ where S::Key: Clone, { let key_clone = key.clone(); - let storage_entry = cx.use_hook(|| synced_storage_entry::(key, init, cx)); + let storage_entry = cx.use_hook(|| storage_entry_with_channel::(key, init, cx)); let storage_entry_signal = storage_entry.data; if let Some(channel) = storage_entry.channel.clone() { use_listen_channel(cx, &channel, move |message| { @@ -118,6 +125,7 @@ where storage_entry } +/// Returns a StorageEntry with the latest value from storage or the init value if it doesn't exist. pub fn storage_entry( key: S::Key, init: impl FnOnce() -> T, @@ -132,7 +140,10 @@ where StorageEntry::new(key, data, cx) } -pub fn synced_storage_entry( +/// Returns a synced StorageEntry with the latest value from storage or the init value if it doesn't exist. +/// +/// This differs from `storage_entry` in that this one will return a channel to subscribe to updates to the underlying storage. +pub fn storage_entry_with_channel( key: S::Key, init: impl FnOnce() -> T, cx: &ScopeState, @@ -143,9 +154,10 @@ where S::Key: Clone, { let data = get_from_storage::(key.clone(), init); - StorageEntry::new_synced(key, data, cx) + StorageEntry::new_with_channel(key, data, cx) } +/// Returns a value from storage or the init value if it doesn't exist. pub fn get_from_storage( key: S::Key, init: impl FnOnce() -> T, @@ -159,12 +171,18 @@ pub fn get_from_storage( // End use_storage hooks // Start StorageEntry + +/// 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, Default)] pub struct StorageEntry { + /// 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, + /// An optional channel to subscribe to updates to the underlying storage pub(crate) channel: Option>>, - pub(crate) lock: Arc>, + /// A lock to prevent multiple saves from happening at the same time + pub(crate) storage_save_lock: Arc>, } impl StorageEntry @@ -173,14 +191,15 @@ where T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { - fn new_synced(key: S::Key, data: T, cx: &ScopeState) -> Self { + /// Creates a new StorageEntry with a channel to subscribe to updates to the underlying storage + fn new_with_channel(key: S::Key, data: T, cx: &ScopeState) -> Self { let channel = S::subscribe::(cx, &key); Self { key, data: Signal::new_in_scope(data, cx.scope_id()), channel, - lock: Arc::new(Mutex::new(())), + storage_save_lock: Arc::new(Mutex::new(())), } } } @@ -191,21 +210,24 @@ where T: Serialize + DeserializeOwned + Clone + 'static, S::Key: Clone, { + /// Creates a new StorageEntry pub fn new(key: S::Key, data: T, cx: &ScopeState) -> Self { Self { key, data: Signal::new_in_scope(data, cx.scope_id()), channel: None, - lock: Arc::new(Mutex::new(())), + storage_save_lock: Arc::new(Mutex::new(())), } } + /// Saves the current state to storage. Only one save can happen at a time. pub(crate) fn save(&self) { - let _ = self.lock.try_lock().map(|_| { + let _ = self.storage_save_lock.try_lock().map(|_| { S::set(self.key.clone(), &self.data); }); } + /// Updates the state from storage pub fn update(&mut self) { self.data = S::get(&self.key).unwrap_or(self.data); } @@ -237,12 +259,15 @@ impl Debug // End StorageEntry // Start Storage Backing traits + +/// A trait for a storage backing pub trait StorageBacking: Sized + Clone + 'static { type Key: Eq + PartialEq + Clone + Debug; fn get(key: &Self::Key) -> Option; fn set(key: Self::Key, value: &T); } +/// A trait for a subscriber to events from a storage backing pub trait StorageSubscriber { fn subscribe( cx: &ScopeState, @@ -253,8 +278,11 @@ pub trait StorageSubscriber { // End Storage Backing traits // Start StorageChannelPayload + +/// A payload for a storage channel that contains the key that was updated #[derive(Clone)] pub struct StorageChannelPayload { + /// The key that was updated in storage pub key: S::Key, } From cb703afd56b21090cacb688aab8cc90e2ed294f9 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sun, 15 Oct 2023 13:37:35 -0700 Subject: [PATCH 034/108] add more comments --- src/storage/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 8381208..c5a36e5 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -262,17 +262,22 @@ impl Debug /// A trait for a storage backing pub trait StorageBacking: Sized + Clone + 'static { + /// The key type used to store data in storage type Key: Eq + PartialEq + Clone + Debug; + /// 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); } /// 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( cx: &ScopeState, key: &S::Key, ) -> Option>>; + /// Unsubscribes from events from a storage backing for the given key fn unsubscribe(key: &S::Key); } // End Storage Backing traits From 32eeac7265782368c1a5a7bc8c3e4b79bebd9add Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sun, 15 Oct 2023 13:38:46 -0700 Subject: [PATCH 035/108] format --- src/storage/client_storage/mod.rs | 2 +- src/storage/client_storage/web.rs | 2 +- src/storage/mod.rs | 40 ++++++++++++++++++------------- src/storage/persistence.rs | 4 ++-- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/storage/client_storage/mod.rs b/src/storage/client_storage/mod.rs index b053589..fadad2a 100644 --- a/src/storage/client_storage/mod.rs +++ b/src/storage/client_storage/mod.rs @@ -6,4 +6,4 @@ cfg_if::cfg_if! { pub mod fs; pub use fs::*; } -} \ No newline at end of file +} diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index a68e364..a6cc8a1 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -41,7 +41,7 @@ impl StorageSubscriber for LocalStorage { let closure = Closure::wrap(Box::new(move |e: web_sys::StorageEvent| { log::info!("Storage event: {:?}", e); let key: String = e.key().unwrap(); - let channel_clone_clone = channel_clone.clone(); + let channel_clone_clone = channel_clone.clone(); wasm_bindgen_futures::spawn_local(async move { let result = channel_clone_clone .send(StorageChannelPayload:: { key }) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index c5a36e5..797eb6b 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -60,41 +60,43 @@ pub use client_storage::set_dir; // } /// A storage hook that can be used to store data across application reloads. -/// +/// /// It returns a Signal that can be used to read and modify the state. /// The changes to the state will be persisted across reloads. pub fn use_storage_entry( cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T, -) -> &mut StorageEntry +) -> &mut StorageEntry where S: StorageBacking, T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, S::Key: Clone, { - let storage_entry = cx.use_hook(|| { - storage_entry::(key, init, cx) - }); + let storage_entry = cx.use_hook(|| storage_entry::(key, init, cx)); let storage_entry_clone: StorageEntry = storage_entry.clone(); - use_effect(cx, (&storage_entry_clone.data.value(),), move |_| async move { - log::info!("state value changed, trying to save"); - storage_entry_clone.save(); - }); + use_effect( + cx, + (&storage_entry_clone.data.value(),), + move |_| async move { + log::info!("state value changed, trying to save"); + storage_entry_clone.save(); + }, + ); storage_entry } /// A storage hook that can be used to store data that will persist across application reloads and be synced across all app sessions for a given installation or browser. -/// +/// /// 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_storage_entry_with_subscription( cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T, -) -> &mut StorageEntry +) -> &mut StorageEntry where S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, @@ -118,10 +120,14 @@ where } let storage_entry_clone: StorageEntry = storage_entry.clone(); - use_effect(cx, (&storage_entry_clone.data.value(),), move |_| async move { - log::info!("state value changed, trying to save"); - storage_entry_clone.save(); - }); + use_effect( + cx, + (&storage_entry_clone.data.value(),), + move |_| async move { + log::info!("state value changed, trying to save"); + storage_entry_clone.save(); + }, + ); storage_entry } @@ -141,8 +147,8 @@ where } /// Returns a synced StorageEntry with the latest value from storage or the init value if it doesn't exist. -/// -/// This differs from `storage_entry` in that this one will return a channel to subscribe to updates to the underlying storage. +/// +/// This differs from `storage_entry` in that this one will return a channel to subscribe to updates to the underlying storage. pub fn storage_entry_with_channel( key: S::Key, init: impl FnOnce() -> T, diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index 8a60571..f5d7ce0 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -1,4 +1,4 @@ -use crate::storage::{SessionStorage, use_storage_entry}; +use crate::storage::{use_storage_entry, SessionStorage}; use dioxus::prelude::ScopeState; use dioxus_signals::Signal; use serde::de::DeserializeOwned; @@ -27,7 +27,7 @@ pub fn use_persistent(cx, key.to_string(), init.take().unwrap()) + use_storage_entry::(cx, key.to_string(), init.take().unwrap()) } #[cfg(all(not(feature = "ssr"), feature = "hydrate"))] { From d40b47c1deb8f0da34bbdce4ad5abc706839f69f Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sun, 15 Oct 2023 13:45:57 -0700 Subject: [PATCH 036/108] remove unnecessary constraints --- src/storage/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 797eb6b..9c297f6 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -267,9 +267,9 @@ impl Debug // Start Storage Backing traits /// A trait for a storage backing -pub trait StorageBacking: Sized + Clone + 'static { +pub trait StorageBacking: Clone + 'static { /// The key type used to store data in storage - type Key: Eq + PartialEq + Clone + Debug; + type Key: PartialEq + Clone + Debug; /// Gets a value from storage for the given key fn get(key: &Self::Key) -> Option; /// Sets a value in storage for the given key From 640d45aa651cbd7fc12fb4e1b8ad65c8a54f0b42 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sun, 15 Oct 2023 14:24:31 -0700 Subject: [PATCH 037/108] change name --- examples/storage/src/main.rs | 2 +- src/storage/mod.rs | 29 +++++++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index c437851..4ff3208 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -12,7 +12,7 @@ fn main() { fn app(cx: Scope) -> Element { let count_session = use_singleton_persistent(cx, || 0); - let count_local = use_storage_entry_with_subscription::(cx, "synced".to_string(), || 0); + let count_local = use_synced_storage_entry::(cx, "synced".to_string(), || 0); render!( div { diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 9c297f6..4ff1a57 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -75,15 +75,7 @@ where { let storage_entry = cx.use_hook(|| storage_entry::(key, init, cx)); - let storage_entry_clone: StorageEntry = storage_entry.clone(); - use_effect( - cx, - (&storage_entry_clone.data.value(),), - move |_| async move { - log::info!("state value changed, trying to save"); - storage_entry_clone.save(); - }, - ); + use_save_to_storage_on_change(storage_entry, cx); storage_entry } @@ -92,7 +84,7 @@ 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_storage_entry_with_subscription( +pub fn use_synced_storage_entry( cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T, @@ -119,7 +111,21 @@ where }); } - let storage_entry_clone: StorageEntry = storage_entry.clone(); + use_save_to_storage_on_change(storage_entry, cx); + + storage_entry +} + +/// A hook that will update the state in storage when the StorageEntry state changes. +pub(crate) fn use_save_to_storage_on_change( + storage_entry: &StorageEntry, + cx: &ScopeState, +) +where + S: StorageBacking, + T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, +{ + let storage_entry_clone = storage_entry.clone(); use_effect( cx, (&storage_entry_clone.data.value(),), @@ -128,7 +134,6 @@ where storage_entry_clone.save(); }, ); - storage_entry } /// Returns a StorageEntry with the latest value from storage or the init value if it doesn't exist. From 5579de2371820e0924db65bc42c10887f814924d Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sun, 15 Oct 2023 22:05:26 -0700 Subject: [PATCH 038/108] fix ssr --- examples/storage/src/main.rs | 2 +- src/storage/client_storage/web.rs | 22 ++--- src/storage/mod.rs | 148 +++++++++++++++++++++++------- src/storage/persistence.rs | 45 +-------- 4 files changed, 126 insertions(+), 91 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 4ff3208..6cfbe33 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -12,7 +12,7 @@ fn main() { fn app(cx: Scope) -> Element { let count_session = use_singleton_persistent(cx, || 0); - let count_local = use_synced_storage_entry::(cx, "synced".to_string(), || 0); + let count_local = use_synced_storage::(cx, "synced".to_string(), || 0); render!( div { diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index a6cc8a1..3118e36 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -89,24 +89,16 @@ impl StorageBacking for SessionStorage { // Start common fn set(key: String, value: &T, storage_type: WebStorageType) { - #[cfg(not(feature = "ssr"))] - { - let as_str = serde_to_string(value); - get_storage_by_type(storage_type) - .unwrap() - .set_item(&key, &as_str) - .unwrap(); - } + let as_str = serde_to_string(value); + get_storage_by_type(storage_type) + .unwrap() + .set_item(&key, &as_str) + .unwrap(); } fn get(key: &str, storage_type: WebStorageType) -> Option { - #[cfg(not(feature = "ssr"))] - { - let s: String = get_storage_by_type(storage_type)?.get_item(key).ok()??; - try_serde_from_string(&s) - } - #[cfg(feature = "ssr")] - None + let s: String = get_storage_by_type(storage_type)?.get_item(key).ok()??; + try_serde_from_string(&s) } fn get_storage_by_type(storage_type: WebStorageType) -> Option { diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 4ff1a57..ef80e04 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -33,7 +33,7 @@ pub use client_storage::{LocalStorage, SessionStorage}; pub use persistence::{use_persistent, use_singleton_persistent}; use dioxus::prelude::{to_owned, use_effect, ScopeState}; -use dioxus_signals::Signal; +use dioxus_signals::{use_signal, Signal}; use postcard::to_allocvec; use serde::{de::DeserializeOwned, Serialize}; use std::fmt::{Debug, Display}; @@ -59,31 +59,103 @@ pub use client_storage::set_dir; // } // } -/// A storage hook that can be used to store data across application reloads. -/// -/// It returns a Signal that can be used to read and modify the state. -/// The changes to the state will be persisted across reloads. -pub fn use_storage_entry( +/// A storage hook that can be used to store data that will persist across application reloads. +/// +/// This hook returns a Signal that can be used to read and modify the state. +pub fn use_storage( cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T, -) -> &mut StorageEntry +) -> Signal where S: StorageBacking, T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, S::Key: Clone, -{ - let storage_entry = cx.use_hook(|| storage_entry::(key, init, cx)); - - use_save_to_storage_on_change(storage_entry, cx); - - storage_entry +{ + let mut init = Some(init); + let signal = { + if cfg!(feature = "ssr") { + use_signal(cx, init.take().unwrap()) + } else if cfg!(feature = "hydrate") { + let key_clone = key.clone(); + let storage_entry = cx.use_hook(|| { + storage_entry::(key, init.take().unwrap(), cx) + }); + if cx.generation() == 0 { + cx.needs_update(); + } + if cx.generation() == 1 { + storage_entry.set(get_from_storage::(key_clone, init.take().unwrap())); + use_save_to_storage_on_change(cx, storage_entry); + } + storage_entry.data + } else { + let storage_entry = use_storage_entry::(cx, key, init.take().unwrap()); + use_save_to_storage_on_change(cx, storage_entry); + storage_entry.data + } + }; + signal } /// A storage hook that can be used to store data that will persist across application reloads and be synced across all app sessions for a given installation or browser. /// /// 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( + cx: &ScopeState, + key: S::Key, + init: impl FnOnce() -> T, +) -> Signal +where + S: StorageBacking + StorageSubscriber, + T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, + S::Key: Clone, +{ + let mut init = Some(init); + let signal = { + if cfg!(feature = "ssr") { + use_signal(cx, init.take().unwrap()) + } else if cfg!(feature = "hydrate") { + let key_clone = key.clone(); + let storage_entry = cx.use_hook(|| { + storage_entry_with_channel::(key, init.take().unwrap(), cx) + }); + if cx.generation() == 0 { + cx.needs_update(); + } + if cx.generation() == 1 { + storage_entry.set(get_from_storage::(key_clone, init.take().unwrap())); + use_save_to_storage_on_change(cx, storage_entry); + use_subscribe_to_storage(cx, storage_entry); + } + storage_entry.data + } else { + let storage_entry = use_synced_storage_entry::(cx, key, init.take().unwrap()); + use_save_to_storage_on_change(cx, storage_entry); + use_subscribe_to_storage(cx, storage_entry); + 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( + cx: &ScopeState, + key: S::Key, + init: impl FnOnce() -> T, +) -> &mut StorageEntry +where + S: StorageBacking, + T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, + S::Key: Clone, +{ + cx.use_hook(|| storage_entry::(key, init, cx)) +} + +/// 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( cx: &ScopeState, key: S::Key, @@ -94,32 +166,13 @@ where T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, S::Key: Clone, { - let key_clone = key.clone(); - let storage_entry = cx.use_hook(|| storage_entry_with_channel::(key, init, cx)); - let storage_entry_signal = storage_entry.data; - if let Some(channel) = storage_entry.channel.clone() { - use_listen_channel(cx, &channel, move |message| { - to_owned![key_clone]; - async move { - if let Ok(payload) = message { - if payload.key == key_clone { - *storage_entry_signal.write() = - get_from_storage::(key_clone, || storage_entry_signal.value()) - } - } - } - }); - } - - use_save_to_storage_on_change(storage_entry, cx); - - storage_entry + cx.use_hook(|| storage_entry_with_channel::(key, init, cx)) } /// A hook that will update the state in storage when the StorageEntry state changes. pub(crate) fn use_save_to_storage_on_change( - storage_entry: &StorageEntry, cx: &ScopeState, + storage_entry: &StorageEntry, ) where S: StorageBacking, @@ -136,6 +189,33 @@ where ); } +/// A hook that will update the state from storage when the StorageEntry channel receives an update. +pub(crate) fn use_subscribe_to_storage( + cx: &ScopeState, + storage_entry: &StorageEntry, +) +where + S: StorageBacking + StorageSubscriber, + T: Serialize + DeserializeOwned + Clone + 'static, + S::Key: Clone, +{ + let key = storage_entry.key.clone(); + let storage_entry_signal = storage_entry.data; + if let Some(channel) = storage_entry.channel.clone() { + use_listen_channel(cx, &channel, move |message| { + to_owned![key]; + async move { + if let Ok(payload) = message { + if payload.key == key { + *storage_entry_signal.write() = + get_from_storage::(key, || storage_entry_signal.value()) + } + } + } + }); + } +} + /// Returns a StorageEntry with the latest value from storage or the init value if it doesn't exist. pub fn storage_entry( key: S::Key, diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index f5d7ce0..a4a7bc7 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -12,46 +12,9 @@ pub fn use_persistent T, -) -> &mut Signal { - let mut init = Some(init); - let storage_entry = { - #[cfg(feature = "ssr")] - { - use_ref(cx, || { - StorageEntry::::new( - key.to_string(), - init.take().unwrap()(), - None, - ) - }) - } - #[cfg(all(not(feature = "ssr"), not(feature = "hydrate")))] - { - use_storage_entry::(cx, key.to_string(), init.take().unwrap()) - } - #[cfg(all(not(feature = "ssr"), feature = "hydrate"))] - { - let state = cx.use_hook(|| { - StorageEntry::::new( - key.to_string(), - storage_entry::(key.to_string(), init.take().unwrap()), - cx, - ) - }); - if cx.generation() == 0 { - cx.needs_update(); - } - if cx.generation() == 1 { - state.set(StorageEntry::new( - key.to_string(), - storage_entry::(key.to_string(), init.take().unwrap()), - )); - } - - state - } - }; - &mut storage_entry.data +) -> Signal { + let storage_entry = use_storage_entry::(cx, key.to_string(), init); + storage_entry.data } /// A persistent storage hook that can be used to store data across application reloads. @@ -65,7 +28,7 @@ pub fn use_singleton_persistent< >( cx: &ScopeState, init: impl FnOnce() -> T, -) -> &mut Signal { +) -> Signal { let caller = std::panic::Location::caller(); let key = cx.use_hook(move || format!("{}:{}", caller.file(), caller.line())); log::info!("use_singleton_persistent key: \"{}\"", key); From 7658e0a44dda8e1dbdc491a7296f3c66ff3a8ef6 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sun, 15 Oct 2023 22:14:09 -0700 Subject: [PATCH 039/108] expose getters for channel and key in storageentry --- src/storage/mod.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index ef80e04..a61fa68 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -273,7 +273,7 @@ pub struct StorageEntry>>, /// A lock to prevent multiple saves from happening at the same time - pub(crate) storage_save_lock: Arc>, + storage_save_lock: Arc>, } impl StorageEntry @@ -322,6 +322,16 @@ where pub fn update(&mut self) { self.data = S::get(&self.key).unwrap_or(self.data); } + + /// Gets the channel to subscribe to updates to the underlying storage + pub fn channel(&self) -> Option>> { + self.channel.clone() + } + + /// Gets the key used to store the data in storage + pub fn key(&self) -> &S::Key { + &self.key + } } impl Deref for StorageEntry { From b4f0c562f2ca7c32b9b4325dbb55ac5b538a81ea Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sun, 15 Oct 2023 22:16:31 -0700 Subject: [PATCH 040/108] format --- src/storage/mod.rs | 43 +++++++++++++------------------------- src/storage/persistence.rs | 2 +- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index a61fa68..9ff1f50 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -60,32 +60,27 @@ pub use client_storage::set_dir; // } /// A storage hook that can be used to store data that will persist across application reloads. -/// +/// /// This hook returns a Signal that can be used to read and modify the state. -pub fn use_storage( - cx: &ScopeState, - key: S::Key, - init: impl FnOnce() -> T, -) -> Signal +pub fn use_storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal where S: StorageBacking, T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, S::Key: Clone, -{ +{ let mut init = Some(init); let signal = { if cfg!(feature = "ssr") { use_signal(cx, init.take().unwrap()) } else if cfg!(feature = "hydrate") { let key_clone = key.clone(); - let storage_entry = cx.use_hook(|| { - storage_entry::(key, init.take().unwrap(), cx) - }); + let storage_entry = + cx.use_hook(|| storage_entry::(key, init.take().unwrap(), cx)); if cx.generation() == 0 { cx.needs_update(); } if cx.generation() == 1 { - storage_entry.set(get_from_storage::(key_clone, init.take().unwrap())); + storage_entry.set(get_from_storage::(key_clone, init.take().unwrap())); use_save_to_storage_on_change(cx, storage_entry); } storage_entry.data @@ -102,30 +97,25 @@ 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( - cx: &ScopeState, - key: S::Key, - init: impl FnOnce() -> T, -) -> Signal +pub fn use_synced_storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal where S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, S::Key: Clone, -{ +{ let mut init = Some(init); let signal = { if cfg!(feature = "ssr") { use_signal(cx, init.take().unwrap()) } else if cfg!(feature = "hydrate") { let key_clone = key.clone(); - let storage_entry = cx.use_hook(|| { - storage_entry_with_channel::(key, init.take().unwrap(), cx) - }); + let storage_entry = + cx.use_hook(|| storage_entry_with_channel::(key, init.take().unwrap(), cx)); if cx.generation() == 0 { cx.needs_update(); } if cx.generation() == 1 { - storage_entry.set(get_from_storage::(key_clone, init.take().unwrap())); + storage_entry.set(get_from_storage::(key_clone, init.take().unwrap())); use_save_to_storage_on_change(cx, storage_entry); use_subscribe_to_storage(cx, storage_entry); } @@ -140,7 +130,6 @@ where 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( cx: &ScopeState, @@ -170,11 +159,10 @@ where } /// A hook that will update the state in storage when the StorageEntry state changes. -pub(crate) fn use_save_to_storage_on_change( +pub(crate) fn use_save_to_storage_on_change( cx: &ScopeState, storage_entry: &StorageEntry, -) -where +) where S: StorageBacking, T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, { @@ -190,10 +178,7 @@ where } /// A hook that will update the state from storage when the StorageEntry channel receives an update. -pub(crate) fn use_subscribe_to_storage( - cx: &ScopeState, - storage_entry: &StorageEntry, -) +pub(crate) fn use_subscribe_to_storage(cx: &ScopeState, storage_entry: &StorageEntry) where S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + 'static, diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index a4a7bc7..8bcad8f 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -13,7 +13,7 @@ pub fn use_persistent T, ) -> Signal { - let storage_entry = use_storage_entry::(cx, key.to_string(), init); + let storage_entry = use_storage_entry::(cx, key.to_string(), init); storage_entry.data } From 65d3c4a1285f085c0cd6daeb9ea72f7d448e8093 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 23 Oct 2023 20:52:36 -0700 Subject: [PATCH 041/108] update to use watch channels --- Cargo.toml | 8 +- examples/storage/Cargo.toml | 6 +- examples/storage/src/main.rs | 26 ++- src/storage/client_storage/fs.rs | 52 ++--- src/storage/client_storage/web.rs | 80 ++++---- src/storage/mod.rs | 311 ++++++++++++++++++++---------- src/storage/persistence.rs | 9 +- 7 files changed, 316 insertions(+), 176 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 71e69e0..1652cf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ geolocation = ["dep:windows", "dep:futures-util", "dep:futures", "dep:web-sys", color_scheme = ["dep:web-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] utils = ["dep:async-broadcast", "dep:uuid"] i18n = ["dep:serde", "dep:serde_json", "dep:unic-langid"] -storage = ["utils", "dep:async-broadcast", "dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:directories", "dep:yazi"] +storage = ["dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:directories", "dep:yazi", "dep:tokio"] all = ["clipboard", "notifications", "geolocation", "color_scheme", "utils", "i18n", "storage"] default = ["storage"] @@ -31,7 +31,7 @@ wasm-testing = ["geolocation", "color_scheme", "utils", "i18n"] desktop-testing = ["clipboard", "notifications", "geolocation", "utils", "i18n"] [dependencies] -dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "1ba6ca39e3f4d4fcd3b4e75e3a739d8c35514e5b" } +dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } cfg-if = "1.0.0" # utils @@ -52,15 +52,17 @@ unic-langid = { version = "0.9.1", features = ["serde"], optional = true } rustc-hash = "1.1.0" postcard = { version = "1.0.2", features = ["use-std"], optional = true } once_cell = { version = "1.17.0", optional = true } -dioxus-signals = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "1ba6ca39e3f4d4fcd3b4e75e3a739d8c35514e5b", features = [ +dioxus-signals = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39", features = [ "serialize", ], optional = true } +tokio = { version = "1.33.0", features = ["sync"], optional = true } yazi = { version = "0.1.4", optional = true } log = "0.4.6" # WebAssembly Debug wasm-logger = "0.2.0" console_error_panic_hook = "0.1.7" +dashmap = "5.5.3" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] directories = { version = "4.0.1", optional = true } diff --git a/examples/storage/Cargo.toml b/examples/storage/Cargo.toml index ab7ca2d..fd58e25 100644 --- a/examples/storage/Cargo.toml +++ b/examples/storage/Cargo.toml @@ -5,9 +5,9 @@ edition = "2021" [dependencies] dioxus-std = { path="../../", features = ["storage"] } -dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "1ba6ca39e3f4d4fcd3b4e75e3a739d8c35514e5b" } -dioxus-web = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "1ba6ca39e3f4d4fcd3b4e75e3a739d8c35514e5b" } - +dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } +dioxus-web = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } +# dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } log = "0.4.6" # WebAssembly Debug diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index 6cfbe33..c0cff84 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -6,8 +6,12 @@ fn main() { // init debug tool for WebAssembly wasm_logger::init(wasm_logger::Config::default()); console_error_panic_hook::set_once(); - dioxus_web::launch(app); + // match log::set_boxed_logger(Box::new(simple_logger::SimpleLogger)) { + // Ok(_) => log::set_max_level(level.to_level_filter()), + // Err(e) => panic!("Failed to initialize logger: {}", e), + // } + // dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { @@ -35,3 +39,23 @@ fn app(cx: Scope) -> Element { } ) } + +mod simple_logger { + use log::{Record, Metadata}; + + pub struct SimpleLogger; + + impl log::Log for SimpleLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= log::max_level() + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + println!("{} - {}", record.level(), record.args()); + } + } + + fn flush(&self) {} + } +} \ No newline at end of file diff --git a/src/storage/client_storage/fs.rs b/src/storage/client_storage/fs.rs index 620f123..67b7b35 100644 --- a/src/storage/client_storage/fs.rs +++ b/src/storage/client_storage/fs.rs @@ -3,7 +3,7 @@ use serde::de::DeserializeOwned; use serde::Serialize; use std::io::Write; -use crate::storage::storage::{ +use crate::storage::{ serde_to_string, try_serde_from_string, StorageBacking, }; @@ -37,44 +37,34 @@ pub fn set_directory(path: std::path::PathBuf) { #[doc(hidden)] pub fn set_dir_name(name: &str) { - { - set_directory( - directories::BaseDirs::new() - .unwrap() - .data_local_dir() - .join(name), - ) - } + set_directory( + directories::BaseDirs::new() + .unwrap() + .data_local_dir() + .join(name), + ) } static LOCATION: OnceCell = OnceCell::new(); fn set(key: String, value: &T) { - #[cfg(not(feature = "ssr"))] - { - let as_str = serde_to_string(value); - let path = LOCATION - .get() - .expect("Call the set_dir macro before accessing persistant data"); - std::fs::create_dir_all(path).unwrap(); - let file_path = path.join(key); - let mut file = std::fs::File::create(file_path).unwrap(); - file.write_all(as_str.as_bytes()).unwrap(); - } + let as_str = serde_to_string(value); + let path = LOCATION + .get() + .expect("Call the set_dir macro before accessing persistant data"); + std::fs::create_dir_all(path).unwrap(); + let file_path = path.join(key); + let mut file = std::fs::File::create(file_path).unwrap(); + file.write_all(as_str.as_bytes()).unwrap(); } fn get(key: &str) -> Option { - #[cfg(not(feature = "ssr"))] - { - let path = LOCATION - .get() - .expect("Call the set_dir macro before accessing persistant data") - .join(key); - let s = std::fs::read_to_string(path).ok()?; - try_serde_from_string(&s) - } - #[cfg(feature = "ssr")] - None + let path = LOCATION + .get() + .expect("Call the set_dir macro before accessing persistant data") + .join(key); + let s = std::fs::read_to_string(path).ok()?; + try_serde_from_string(&s) } pub struct ClientStorage; diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 3118e36..16626f8 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -1,17 +1,16 @@ -use async_broadcast::broadcast; +use dashmap::DashMap; use dioxus::prelude::*; +use once_cell::sync::Lazy; use serde::{de::DeserializeOwned, Serialize}; -use std::sync::OnceLock; -use uuid::Uuid; +use tokio::sync::watch::{channel, Receiver}; use wasm_bindgen::prelude::Closure; use wasm_bindgen::JsCast; use web_sys::{window, Storage}; use crate::storage::{ serde_to_string, try_serde_from_string, StorageBacking, StorageChannelPayload, - StorageSubscriber, + StorageSenderEntry, StorageSubscriber, }; -use crate::utils::channel::UseChannel; // Start LocalStorage #[derive(Clone)] @@ -30,44 +29,51 @@ impl StorageBacking for LocalStorage { } impl StorageSubscriber for LocalStorage { - fn subscribe( + fn subscribe( _cx: &ScopeState, - _key: &String, - ) -> Option>> { - let channel = CHANNEL.get_or_init(|| { - let (tx, rx) = broadcast::>(5); - let channel = UseChannel::new(Uuid::new_v4(), tx, rx.deactivate()); - let channel_clone = channel.clone(); - let closure = Closure::wrap(Box::new(move |e: web_sys::StorageEvent| { - log::info!("Storage event: {:?}", e); - let key: String = e.key().unwrap(); - let channel_clone_clone = channel_clone.clone(); - wasm_bindgen_futures::spawn_local(async move { - let result = channel_clone_clone - .send(StorageChannelPayload:: { key }) - .await; - match result { - Ok(_) => log::info!("Sent storage event"), - Err(err) => log::info!("Error sending storage event: {:?}", err), - } - }); - }) as Box); - window() - .unwrap() - .add_event_listener_with_callback("storage", closure.as_ref().unchecked_ref()) - .unwrap(); - closure.forget(); - channel - }); - Some(channel.clone()) + key: &String, + ) -> Receiver { + let rx = CHANNELS.get(key).map_or_else( + || { + let (tx, rx) = channel::(StorageChannelPayload::default()); + let entry = StorageSenderEntry::new::(tx, key.clone()); + CHANNELS.insert(key.clone(), entry); + rx + }, + |entry| entry.tx.subscribe(), + ); + rx } - fn unsubscribe(_key: &String) { - // Do nothing for web case, since we don't actually subscribe to specific keys. + fn unsubscribe(key: &String) { + if let Some(entry) = CHANNELS.get(key) { + if entry.tx.is_closed() { + CHANNELS.remove(key); + } + } } } -static CHANNEL: OnceLock>> = OnceLock::new(); +static CHANNELS: Lazy> = Lazy::new(|| { + let closure = Closure::wrap(Box::new(move |e: web_sys::StorageEvent| { + log::info!("Storage event: {:?}", e); + let key: String = e.key().unwrap(); + if let Some(entry) = CHANNELS.get(&key) { + let result = entry.tx.send((entry.getter)()); + match result { + Ok(_) => log::info!("Sent storage event"), + Err(err) => log::info!("Error sending storage event: {:?}", err), + } + } + }) as Box); + window() + .unwrap() + .add_event_listener_with_callback("storage", closure.as_ref().unchecked_ref()) + .unwrap(); + closure.forget(); + DashMap::new() +}); + // End LocalStorage // Start SessionStorage diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 9ff1f50..df45255 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -36,11 +36,11 @@ use dioxus::prelude::{to_owned, use_effect, ScopeState}; use dioxus_signals::{use_signal, Signal}; use postcard::to_allocvec; use serde::{de::DeserializeOwned, Serialize}; +use std::any::Any; use std::fmt::{Debug, Display}; use std::ops::Deref; use std::sync::{Arc, Mutex}; - -use crate::utils::channel::{use_listen_channel, UseChannel}; +use tokio::sync::watch::{Receiver, Sender}; #[cfg(not(target_family = "wasm"))] pub use client_storage::set_dir; @@ -65,7 +65,7 @@ pub use client_storage::set_dir; pub fn use_storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal where S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, + T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { let mut init = Some(init); @@ -81,12 +81,12 @@ where } if cx.generation() == 1 { storage_entry.set(get_from_storage::(key_clone, init.take().unwrap())); - use_save_to_storage_on_change(cx, storage_entry); + storage_entry.use_save_to_storage_on_change(cx); } storage_entry.data } else { let storage_entry = use_storage_entry::(cx, key, init.take().unwrap()); - use_save_to_storage_on_change(cx, storage_entry); + storage_entry.use_save_to_storage_on_change(cx); storage_entry.data } }; @@ -100,7 +100,7 @@ where pub fn use_synced_storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal where S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, + T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { let mut init = Some(init); @@ -110,21 +110,23 @@ where } else if cfg!(feature = "hydrate") { let key_clone = key.clone(); let storage_entry = - cx.use_hook(|| storage_entry_with_channel::(key, init.take().unwrap(), cx)); + cx.use_hook(|| synced_storage_entry::(key, init.take().unwrap(), cx)); if cx.generation() == 0 { cx.needs_update(); } if cx.generation() == 1 { - storage_entry.set(get_from_storage::(key_clone, init.take().unwrap())); - use_save_to_storage_on_change(cx, storage_entry); + storage_entry + .entry + .set(get_from_storage::(key_clone, init.take().unwrap())); + storage_entry.use_save_to_storage_on_change(cx); use_subscribe_to_storage(cx, storage_entry); } - storage_entry.data + *storage_entry.data() } else { let storage_entry = use_synced_storage_entry::(cx, key, init.take().unwrap()); - use_save_to_storage_on_change(cx, storage_entry); + storage_entry.use_save_to_storage_on_change(cx); use_subscribe_to_storage(cx, storage_entry); - storage_entry.data + *storage_entry.data() } }; signal @@ -138,7 +140,7 @@ pub fn use_storage_entry( ) -> &mut StorageEntry where S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, + T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { cx.use_hook(|| storage_entry::(key, init, cx)) @@ -149,56 +151,39 @@ pub fn use_synced_storage_entry( cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T, -) -> &mut StorageEntry +) -> &mut SyncedStorageEntry where S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, + T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { - cx.use_hook(|| storage_entry_with_channel::(key, init, cx)) + cx.use_hook(|| synced_storage_entry::(key, init, cx)) } -/// A hook that will update the state in storage when the StorageEntry state changes. -pub(crate) fn use_save_to_storage_on_change( +/// A hook that will update the state from storage when the StorageEntry channel receives an update. +pub(crate) fn use_subscribe_to_storage( cx: &ScopeState, - storage_entry: &StorageEntry, + storage_entry: &SyncedStorageEntry, ) where - S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, -{ - let storage_entry_clone = storage_entry.clone(); - use_effect( - cx, - (&storage_entry_clone.data.value(),), - move |_| async move { - log::info!("state value changed, trying to save"); - storage_entry_clone.save(); - }, - ); -} - -/// A hook that will update the state from storage when the StorageEntry channel receives an update. -pub(crate) fn use_subscribe_to_storage(cx: &ScopeState, storage_entry: &StorageEntry) -where S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + 'static, + T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, S::Key: Clone, { - let key = storage_entry.key.clone(); - let storage_entry_signal = storage_entry.data; - if let Some(channel) = storage_entry.channel.clone() { - use_listen_channel(cx, &channel, move |message| { - to_owned![key]; - async move { - if let Ok(payload) = message { - if payload.key == key { - *storage_entry_signal.write() = - get_from_storage::(key, || storage_entry_signal.value()) - } - } + let storage_entry_signal = storage_entry.entry.data; + let channel = storage_entry.channel.clone(); + use_effect(cx, (), move |_| async move { + to_owned![channel]; + loop { + if channel.changed().await.is_ok() { + let payload = channel.borrow_and_update(); + *storage_entry_signal.write() = payload + .data + .downcast_ref::() + .expect("Type mismatch with storage entry") + .clone(); } - }); - } + } + }); } /// Returns a StorageEntry with the latest value from storage or the init value if it doesn't exist. @@ -219,18 +204,18 @@ where /// Returns a synced StorageEntry with the latest value from storage or the init value if it doesn't exist. /// /// This differs from `storage_entry` in that this one will return a channel to subscribe to updates to the underlying storage. -pub fn storage_entry_with_channel( +pub fn synced_storage_entry( key: S::Key, init: impl FnOnce() -> T, cx: &ScopeState, -) -> StorageEntry +) -> SyncedStorageEntry where S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + 'static, + T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, S::Key: Clone, { let data = get_from_storage::(key.clone(), init); - StorageEntry::new_with_channel(key, data, cx) + SyncedStorageEntry::new(key, data, cx) } /// Returns a value from storage or the init value if it doesn't exist. @@ -246,38 +231,130 @@ pub fn get_from_storage( } // End use_storage hooks -// Start StorageEntry +pub trait StorageEntryTrait: + Clone + 'static +{ + /// Saves the current state to storage. Only one save can happen at a time. + fn save(&self); -/// 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, Default)] -pub struct StorageEntry { - /// 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, - /// An optional channel to subscribe to updates to the underlying storage - pub(crate) channel: Option>>, - /// A lock to prevent multiple saves from happening at the same time - storage_save_lock: Arc>, + /// Updates the state from storage + fn update(&mut self); + + /// Gets the key used to store the data in storage + fn key(&self) -> &S::Key; + + /// Gets the signal that can be used to read and modify the state + fn data(&self) -> &Signal; + + fn use_save_to_storage_on_change(&self, cx: &ScopeState) + where + S: StorageBacking, + T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, + { + let entry_clone = self.clone(); + use_effect(cx, (&self.data().value(),), move |_| async move { + log::info!("state value changed, trying to save"); + entry_clone.save(); + }); + } } -impl StorageEntry +// Start SyncedStorageEntry + +#[derive(Clone)] +pub struct SyncedStorageEntry< + S: StorageBacking + StorageSubscriber, + T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, +> { + pub(crate) entry: StorageEntry, + pub(crate) channel: Receiver, +} + +impl SyncedStorageEntry where S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + 'static, - S::Key: Clone, + T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, { - /// Creates a new StorageEntry with a channel to subscribe to updates to the underlying storage - fn new_with_channel(key: S::Key, data: T, cx: &ScopeState) -> Self { + pub fn new(key: S::Key, data: T, cx: &ScopeState) -> Self { let channel = S::subscribe::(cx, &key); - Self { - key, - data: Signal::new_in_scope(data, cx.scope_id()), + entry: StorageEntry::new(key, data, cx), channel, - storage_save_lock: Arc::new(Mutex::new(())), } } + + /// Gets the channel to subscribe to updates to the underlying storage + pub fn channel(&self) -> Receiver { + self.channel.clone() + } + + pub fn use_subscribe_to_storage(&self, cx: &ScopeState) { + let storage_entry_signal = *self.data(); + let channel = self.channel.clone(); + use_effect(cx, (), move |_| async move { + to_owned![channel, storage_entry_signal]; + loop { + if channel.changed().await.is_ok() { + let payload = channel.borrow_and_update(); + *storage_entry_signal.write() = payload + .data + .downcast_ref::() + .expect("Type mismatch with storage entry") + .clone(); + } + } + }); + } +} + +impl StorageEntryTrait for SyncedStorageEntry +where + S: StorageBacking + StorageSubscriber, + T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, +{ + fn save(&self) { + let mut should_save = true; + if let Some(payload) = self.channel.borrow().data.downcast_ref::() { + if *self.entry.data.read() == *payload { + log::info!("value is the same, not saving"); + should_save = false; + } + } + if should_save { + // This should be true in the following conditions: + // - The channel is None + // - 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 + log::info!("saving"); + self.entry.save(); + } + } + + fn update(&mut self) { + self.entry.update(); + } + + fn key(&self) -> &S::Key { + self.entry.key() + } + + fn data(&self) -> &Signal { + &self.entry.data + } +} + +// Start StorageEntry + +/// 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 { + /// 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, + /// An optional channel to subscribe to updates to the underlying storage + /// A lock to prevent multiple saves from happening at the same time + storage_save_lock: Arc>, // TODO: probably unnecessary } impl StorageEntry @@ -291,31 +368,32 @@ where Self { key, data: Signal::new_in_scope(data, cx.scope_id()), - channel: None, storage_save_lock: Arc::new(Mutex::new(())), } } +} - /// Saves the current state to storage. Only one save can happen at a time. - pub(crate) fn save(&self) { +impl StorageEntryTrait for StorageEntry +where + S: StorageBacking, + T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, +{ + fn save(&self) { let _ = self.storage_save_lock.try_lock().map(|_| { S::set(self.key.clone(), &self.data); }); } - /// Updates the state from storage - pub fn update(&mut self) { + fn update(&mut self) { self.data = S::get(&self.key).unwrap_or(self.data); } - /// Gets the channel to subscribe to updates to the underlying storage - pub fn channel(&self) -> Option>> { - self.channel.clone() + fn key(&self) -> &S::Key { + &self.key } - /// Gets the key used to store the data in storage - pub fn key(&self) -> &S::Key { - &self.key + fn data(&self) -> &Signal { + &self.data } } @@ -349,7 +427,7 @@ impl Debug /// 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; + type Key: PartialEq + Clone + 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 @@ -359,10 +437,10 @@ pub trait StorageBacking: Clone + 'static { /// 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( + fn subscribe( cx: &ScopeState, key: &S::Key, - ) -> Option>>; + ) -> Receiver; /// Unsubscribes from events from a storage backing for the given key fn unsubscribe(key: &S::Key); } @@ -371,21 +449,56 @@ pub trait StorageSubscriber { // Start StorageChannelPayload /// A payload for a storage channel that contains the key that was updated -#[derive(Clone)] -pub struct StorageChannelPayload { - /// The key that was updated in storage - pub key: S::Key, +#[derive(Clone, Debug)] +pub struct StorageChannelPayload { + data: Arc, } -impl Debug for StorageChannelPayload { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("StorageChannelPayload") - .field("key", &self.key) - .finish() +impl StorageChannelPayload { + /// Creates a new StorageChannelPayload + pub fn new(data: T) -> Self { + Self { + data: Arc::new(data), + } + } + + /// Gets the data from the payload + pub fn data(&self) -> Option<&T> { + self.data.downcast_ref::() + } +} + +impl Default for StorageChannelPayload { + fn default() -> Self { + Self { data: Arc::new(()) } } } // End StorageChannelPayload +pub struct StorageSenderEntry { + pub(crate) getter: Box StorageChannelPayload + 'static + Send + Sync>, + pub(crate) tx: Sender, +} + +impl StorageSenderEntry { + pub fn new< + S: StorageBacking + StorageSubscriber, + T: DeserializeOwned + Send + Sync + 'static, + >( + tx: Sender, + key: S::Key, + ) -> Self { + let getter = move || { + let data = S::get::(&key).unwrap(); + StorageChannelPayload::new(data) + }; + Self { + getter: Box::new(getter), + tx, + } + } +} + // Start helper functions pub(crate) fn serde_to_string(value: &T) -> String { let serialized = to_allocvec(value).unwrap(); diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index 8bcad8f..bfb8937 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -4,16 +4,21 @@ use dioxus_signals::Signal; use serde::de::DeserializeOwned; use serde::Serialize; +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 #[allow(clippy::needless_return)] -pub fn use_persistent( +pub fn use_persistent< + T: Serialize + DeserializeOwned + Default + Clone + Send + Sync + PartialEq + 'static, +>( cx: &ScopeState, key: impl ToString, init: impl FnOnce() -> T, ) -> Signal { let storage_entry = use_storage_entry::(cx, key.to_string(), init); + storage_entry.use_save_to_storage_on_change(cx); storage_entry.data } @@ -24,7 +29,7 @@ pub fn use_persistent( cx: &ScopeState, init: impl FnOnce() -> T, From 483ec120face1a7a53675004c064ad7c398951a5 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 23 Oct 2023 20:54:51 -0700 Subject: [PATCH 042/108] fix cargo --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1652cf6..b5a8c2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ geolocation = ["dep:windows", "dep:futures-util", "dep:futures", "dep:web-sys", color_scheme = ["dep:web-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] utils = ["dep:async-broadcast", "dep:uuid"] i18n = ["dep:serde", "dep:serde_json", "dep:unic-langid"] -storage = ["dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:directories", "dep:yazi", "dep:tokio"] +storage = ["dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:directories", "dep:yazi", "dep:tokio", "dep:dashmap"] all = ["clipboard", "notifications", "geolocation", "color_scheme", "utils", "i18n", "storage"] default = ["storage"] @@ -56,13 +56,13 @@ dioxus-signals = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "6478 "serialize", ], optional = true } tokio = { version = "1.33.0", features = ["sync"], optional = true } +dashmap = { version = "5.5.3", optional = true } yazi = { version = "0.1.4", optional = true } log = "0.4.6" # WebAssembly Debug wasm-logger = "0.2.0" console_error_panic_hook = "0.1.7" -dashmap = "5.5.3" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] directories = { version = "4.0.1", optional = true } From f7b5fb02cb05536bf1e04d79511f29caef111a3d Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 23 Oct 2023 21:00:38 -0700 Subject: [PATCH 043/108] simplify synced save --- src/storage/mod.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index df45255..434d240 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -234,7 +234,7 @@ pub fn get_from_storage( pub trait StorageEntryTrait: Clone + 'static { - /// Saves the current state to storage. Only one save can happen at a time. + /// Saves the current state to storage fn save(&self); /// Updates the state from storage @@ -313,21 +313,17 @@ where T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, { fn save(&self) { - let mut should_save = true; + // 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 { log::info!("value is the same, not saving"); - should_save = false; + return } } - if should_save { - // This should be true in the following conditions: - // - The channel is None - // - 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 - log::info!("saving"); - self.entry.save(); - } + log::info!("saving"); + self.entry.save(); } fn update(&mut self) { From 07d2c932aa397534c88201a185db19c0dde10a06 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 23 Oct 2023 21:24:18 -0700 Subject: [PATCH 044/108] add more comments, fix unsubscribe --- src/storage/client_storage/web.rs | 15 +++- src/storage/mod.rs | 141 ++++++++++++++++-------------- 2 files changed, 84 insertions(+), 72 deletions(-) diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 16626f8..0297455 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -9,7 +9,7 @@ use web_sys::{window, Storage}; use crate::storage::{ serde_to_string, try_serde_from_string, StorageBacking, StorageChannelPayload, - StorageSenderEntry, StorageSubscriber, + StorageEventChannel, StorageSubscriber, }; // Start LocalStorage @@ -36,7 +36,7 @@ impl StorageSubscriber for LocalStorage { let rx = CHANNELS.get(key).map_or_else( || { let (tx, rx) = channel::(StorageChannelPayload::default()); - let entry = StorageSenderEntry::new::(tx, key.clone()); + let entry = StorageEventChannel::new::(tx, key.clone()); CHANNELS.insert(key.clone(), entry); rx }, @@ -46,30 +46,37 @@ impl StorageSubscriber for LocalStorage { } fn unsubscribe(key: &String) { + log::info!("Unsubscribing from {}", key); if let Some(entry) = CHANNELS.get(key) { if entry.tx.is_closed() { + log::info!("Channel is closed, removing entry"); CHANNELS.remove(key); } } } } -static CHANNELS: Lazy> = Lazy::new(|| { +/// A map of all the channels that are currently subscribed to. This gets initialized lazily and will set up a listener for storage events. +static CHANNELS: Lazy> = Lazy::new(|| { + // Create a closure that will be called when a storage event occurs. let closure = Closure::wrap(Box::new(move |e: web_sys::StorageEvent| { log::info!("Storage event: {:?}", e); let key: String = e.key().unwrap(); if let Some(entry) = CHANNELS.get(&key) { - let result = entry.tx.send((entry.getter)()); + // Call the getter for the given entry and send the value to said entry's channel. + let result = entry.get_and_send(); match result { Ok(_) => log::info!("Sent storage event"), Err(err) => log::info!("Error sending storage event: {:?}", err), } } }) as Box); + // Register the closure to be called when a storage event occurs. window() .unwrap() .add_event_listener_with_callback("storage", closure.as_ref().unchecked_ref()) .unwrap(); + // Relinquish ownership of the closure to the JS runtime so that it can be called later. closure.forget(); DashMap::new() }); diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 434d240..2e1621c 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -40,25 +40,12 @@ use std::any::Any; use std::fmt::{Debug, Display}; use std::ops::Deref; use std::sync::{Arc, Mutex}; +use tokio::sync::watch::error::SendError; use tokio::sync::watch::{Receiver, Sender}; #[cfg(not(target_family = "wasm"))] pub use client_storage::set_dir; -// // Start use_storage hooks -// pub fn use_synced_storage( -// cx: &ScopeState, -// key: impl ToString, -// init: impl FnOnce() -> T, -// ) -> &mut Signal -// where -// T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, -// { -// cfg_if::cfg_if! { - -// } -// } - /// A storage hook that can be used to store data that will persist across application reloads. /// /// This hook returns a Signal that can be used to read and modify the state. @@ -71,20 +58,25 @@ where let mut init = Some(init); let signal = { if cfg!(feature = "ssr") { + // SSR does not support storage on the backend. We will just use a normal Signal to represent the initial state. + // The client will hydrate this with a correct StorageEntry and maintain state. use_signal(cx, init.take().unwrap()) } else if cfg!(feature = "hydrate") { let key_clone = key.clone(); let storage_entry = cx.use_hook(|| storage_entry::(key, init.take().unwrap(), cx)); if cx.generation() == 0 { + // The first generation is rendered on the server side and so must be hydrated. cx.needs_update(); } if cx.generation() == 1 { + // The first time the vdom is hydrated, we set the correct value from storage and set up the subscription to storage events. storage_entry.set(get_from_storage::(key_clone, init.take().unwrap())); storage_entry.use_save_to_storage_on_change(cx); } storage_entry.data } else { + // The client is rendered normally, so we can just use the storage entry. let storage_entry = use_storage_entry::(cx, key, init.take().unwrap()); storage_entry.use_save_to_storage_on_change(cx); storage_entry.data @@ -106,26 +98,31 @@ where let mut init = Some(init); let signal = { if cfg!(feature = "ssr") { + // SSR does not support synced storage on the backend. We will just use a normal Signal to represent the initial state. + // The client will hydrate this with a correct SyncedStorageEntry and maintain state. use_signal(cx, init.take().unwrap()) } else if cfg!(feature = "hydrate") { let key_clone = key.clone(); let storage_entry = cx.use_hook(|| synced_storage_entry::(key, init.take().unwrap(), cx)); if cx.generation() == 0 { + // The first generation is rendered on the server side and so must be hydrated. cx.needs_update(); } if cx.generation() == 1 { + // The first time the vdom is hydrated, we set the correct value from storage and set up the subscription to storage events. storage_entry .entry .set(get_from_storage::(key_clone, init.take().unwrap())); storage_entry.use_save_to_storage_on_change(cx); - use_subscribe_to_storage(cx, storage_entry); + storage_entry.use_subscribe_to_storage(cx); } *storage_entry.data() } else { + // The client is rendered normally, so we can just use the synced storage entry. let storage_entry = use_synced_storage_entry::(cx, key, init.take().unwrap()); storage_entry.use_save_to_storage_on_change(cx); - use_subscribe_to_storage(cx, storage_entry); + storage_entry.use_subscribe_to_storage(cx); *storage_entry.data() } }; @@ -160,32 +157,6 @@ where cx.use_hook(|| synced_storage_entry::(key, init, cx)) } -/// A hook that will update the state from storage when the StorageEntry channel receives an update. -pub(crate) fn use_subscribe_to_storage( - cx: &ScopeState, - storage_entry: &SyncedStorageEntry, -) where - S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, - S::Key: Clone, -{ - let storage_entry_signal = storage_entry.entry.data; - let channel = storage_entry.channel.clone(); - use_effect(cx, (), move |_| async move { - to_owned![channel]; - loop { - if channel.changed().await.is_ok() { - let payload = channel.borrow_and_update(); - *storage_entry_signal.write() = payload - .data - .downcast_ref::() - .expect("Type mismatch with storage entry") - .clone(); - } - } - }); -} - /// Returns a StorageEntry with the latest value from storage or the init value if it doesn't exist. pub fn storage_entry( key: S::Key, @@ -231,6 +202,7 @@ pub fn get_from_storage( } // End use_storage hooks +/// A trait for common functionality between StorageEntry and SyncedStorageEntry pub trait StorageEntryTrait: Clone + 'static { @@ -246,6 +218,7 @@ pub trait StorageEntryTrait: /// Gets the signal that can be used to read and modify the state fn data(&self) -> &Signal; + /// Creates a hook that will save the state to storage when the state changes fn use_save_to_storage_on_change(&self, cx: &ScopeState) where S: StorageBacking, @@ -261,12 +234,15 @@ pub trait StorageEntryTrait: // Start SyncedStorageEntry +/// 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, > { + /// 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, } @@ -288,13 +264,16 @@ where self.channel.clone() } + /// Creates a hool that will update the state when the underlying storage changes pub fn use_subscribe_to_storage(&self, cx: &ScopeState) { let storage_entry_signal = *self.data(); let channel = self.channel.clone(); use_effect(cx, (), move |_| async move { to_owned![channel, storage_entry_signal]; loop { + // Wait for an update to the channel if channel.changed().await.is_ok() { + // Retrieve the latest value from the channel, mark it as read, and update the state let payload = channel.borrow_and_update(); *storage_entry_signal.write() = payload .data @@ -319,7 +298,7 @@ where if let Some(payload) = self.channel.borrow().data.downcast_ref::() { if *self.entry.data.read() == *payload { log::info!("value is the same, not saving"); - return + return; } } log::info!("saving"); @@ -339,6 +318,18 @@ where } } +impl Drop for SyncedStorageEntry +where + S: StorageBacking + StorageSubscriber, + T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, +{ + fn drop(&mut self) { + S::unsubscribe(self.key()); + } +} + +// End SyncedStorageEntry + // Start StorageEntry /// 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. @@ -442,9 +433,47 @@ pub trait StorageSubscriber { } // End Storage Backing traits +// Start StorageSenderEntry + +/// A struct to hold information about processing a storage event. +pub struct StorageEventChannel { + /// A getter function that will get the data from storage and return it as a StorageChannelPayload. + pub(crate) getter: Box StorageChannelPayload + 'static + Send + Sync>, + + /// The channel to send the data to. + pub(crate) tx: Sender, +} + +impl StorageEventChannel { + pub fn new< + S: StorageBacking + StorageSubscriber, + T: DeserializeOwned + Send + Sync + 'static, + >( + tx: Sender, + key: S::Key, + ) -> Self { + let getter = move || { + let data = S::get::(&key).unwrap(); + StorageChannelPayload::new(data) + }; + Self { + getter: Box::new(getter), + tx, + } + } + + /// Gets the latest data from storage and sends it to the channel. + pub fn get_and_send(&self) -> Result<(), SendError> { + let payload = (self.getter)(); + self.tx.send(payload) + } +} + +// End StorageSenderEntry + // Start StorageChannelPayload -/// A payload for a storage channel that contains the key that was updated +/// A payload for a storage channel that contains the latest value from storage. #[derive(Clone, Debug)] pub struct StorageChannelPayload { data: Arc, @@ -471,30 +500,6 @@ impl Default for StorageChannelPayload { } // End StorageChannelPayload -pub struct StorageSenderEntry { - pub(crate) getter: Box StorageChannelPayload + 'static + Send + Sync>, - pub(crate) tx: Sender, -} - -impl StorageSenderEntry { - pub fn new< - S: StorageBacking + StorageSubscriber, - T: DeserializeOwned + Send + Sync + 'static, - >( - tx: Sender, - key: S::Key, - ) -> Self { - let getter = move || { - let data = S::get::(&key).unwrap(); - StorageChannelPayload::new(data) - }; - Self { - getter: Box::new(getter), - tx, - } - } -} - // Start helper functions pub(crate) fn serde_to_string(value: &T) -> String { let serialized = to_allocvec(value).unwrap(); From 4b4c0f25740bd1262c58eb3c2e478b1ba728278a Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 23 Oct 2023 21:28:10 -0700 Subject: [PATCH 045/108] split storage desktop and web examples --- examples/storage_desktop/Cargo.toml | 11 ++++ .../{storage => storage_desktop}/README.md | 0 examples/storage_desktop/src/main.rs | 57 +++++++++++++++++++ examples/{storage => storage_web}/Cargo.toml | 3 +- examples/storage_web/README.md | 7 +++ examples/{storage => storage_web}/src/main.rs | 0 6 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 examples/storage_desktop/Cargo.toml rename examples/{storage => storage_desktop}/README.md (100%) create mode 100644 examples/storage_desktop/src/main.rs rename examples/{storage => storage_web}/Cargo.toml (75%) create mode 100644 examples/storage_web/README.md rename examples/{storage => storage_web}/src/main.rs (100%) diff --git a/examples/storage_desktop/Cargo.toml b/examples/storage_desktop/Cargo.toml new file mode 100644 index 0000000..75098d9 --- /dev/null +++ b/examples/storage_desktop/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "storage-desktop" +version = "0.1.0" +edition = "2021" + +[dependencies] +dioxus-std = { path="../../", features = ["storage"] } +dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } +dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } +log = "0.4.6" + diff --git a/examples/storage/README.md b/examples/storage_desktop/README.md similarity index 100% rename from examples/storage/README.md rename to examples/storage_desktop/README.md diff --git a/examples/storage_desktop/src/main.rs b/examples/storage_desktop/src/main.rs new file mode 100644 index 0000000..3ccc749 --- /dev/null +++ b/examples/storage_desktop/src/main.rs @@ -0,0 +1,57 @@ +use dioxus::prelude::*; +use dioxus_std::storage::*; +use std::{collections::HashMap, str::FromStr}; + +fn main() { + match log::set_boxed_logger(Box::new(simple_logger::SimpleLogger)) { + Ok(_) => log::set_max_level(level.to_level_filter()), + Err(e) => panic!("Failed to initialize logger: {}", e), + } + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let count_session = use_singleton_persistent(cx, || 0); + let count_local = use_synced_storage::(cx, "synced".to_string(), || 0); + + render!( + div { + button { + onclick: move |_| { + *count_session.write() += 1; + }, + "Click me!" + }, + "I persist for the current session. Clicked {count_session} times" + } + div { + button { + onclick: move |_| { + *count_local.write() += 1; + }, + "Click me!" + }, + "I persist across all sessions. Clicked {count_local} times" + } + ) +} + +mod simple_logger { + use log::{Record, Metadata}; + + pub struct SimpleLogger; + + impl log::Log for SimpleLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= log::max_level() + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + println!("{} - {}", record.level(), record.args()); + } + } + + fn flush(&self) {} + } +} \ No newline at end of file diff --git a/examples/storage/Cargo.toml b/examples/storage_web/Cargo.toml similarity index 75% rename from examples/storage/Cargo.toml rename to examples/storage_web/Cargo.toml index fd58e25..ecaf897 100644 --- a/examples/storage/Cargo.toml +++ b/examples/storage_web/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "storage" +name = "storage-web" version = "0.1.0" edition = "2021" @@ -7,7 +7,6 @@ edition = "2021" dioxus-std = { path="../../", features = ["storage"] } dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } dioxus-web = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } -# dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } log = "0.4.6" # WebAssembly Debug diff --git a/examples/storage_web/README.md b/examples/storage_web/README.md new file mode 100644 index 0000000..2bcc6fb --- /dev/null +++ b/examples/storage_web/README.md @@ -0,0 +1,7 @@ +# use_persistant + +Learn how to use `use_persistant`. + +Run: + +```dioxus serve``` \ No newline at end of file diff --git a/examples/storage/src/main.rs b/examples/storage_web/src/main.rs similarity index 100% rename from examples/storage/src/main.rs rename to examples/storage_web/src/main.rs From 9917de47f5302bff58419dc2152c620b55ecc6f1 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 23 Oct 2023 21:44:35 -0700 Subject: [PATCH 046/108] remove semicolon --- src/storage/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 2e1621c..1af5509 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -298,7 +298,7 @@ where if let Some(payload) = self.channel.borrow().data.downcast_ref::() { if *self.entry.data.read() == *payload { log::info!("value is the same, not saving"); - return; + return } } log::info!("saving"); From 97d84e678b4372b638c5741ed6ebcdc966a12f77 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 23 Oct 2023 21:48:20 -0700 Subject: [PATCH 047/108] typo --- src/storage/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 1af5509..5f929de 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -264,7 +264,7 @@ where self.channel.clone() } - /// Creates a hool that will update the state when the underlying storage changes + /// Creates a hook that will update the state when the underlying storage changes pub fn use_subscribe_to_storage(&self, cx: &ScopeState) { let storage_entry_signal = *self.data(); let channel = self.channel.clone(); From 25300cb8fd9087d5a5b03fad97e15338cf471dcb Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 24 Oct 2023 16:02:57 -0700 Subject: [PATCH 048/108] remove dashmap due to wasm compatibility issues --- Cargo.toml | 3 +- examples/storage_web/Cargo.toml | 1 + examples/storage_web/src/main.rs | 73 +++++++++++++++++++------------ src/storage/client_storage/web.rs | 50 +++++++++++++-------- src/storage/mod.rs | 19 +++----- 5 files changed, 85 insertions(+), 61 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b5a8c2e..b95c609 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ geolocation = ["dep:windows", "dep:futures-util", "dep:futures", "dep:web-sys", color_scheme = ["dep:web-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] utils = ["dep:async-broadcast", "dep:uuid"] i18n = ["dep:serde", "dep:serde_json", "dep:unic-langid"] -storage = ["dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:directories", "dep:yazi", "dep:tokio", "dep:dashmap"] +storage = ["dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:directories", "dep:yazi", "dep:tokio"] all = ["clipboard", "notifications", "geolocation", "color_scheme", "utils", "i18n", "storage"] default = ["storage"] @@ -56,7 +56,6 @@ dioxus-signals = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "6478 "serialize", ], optional = true } tokio = { version = "1.33.0", features = ["sync"], optional = true } -dashmap = { version = "5.5.3", optional = true } yazi = { version = "0.1.4", optional = true } log = "0.4.6" diff --git a/examples/storage_web/Cargo.toml b/examples/storage_web/Cargo.toml index ecaf897..6ae8412 100644 --- a/examples/storage_web/Cargo.toml +++ b/examples/storage_web/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" dioxus-std = { path="../../", features = ["storage"] } dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } dioxus-web = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } +dioxus-router = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } log = "0.4.6" # WebAssembly Debug diff --git a/examples/storage_web/src/main.rs b/examples/storage_web/src/main.rs index c0cff84..df80c36 100644 --- a/examples/storage_web/src/main.rs +++ b/examples/storage_web/src/main.rs @@ -1,20 +1,59 @@ use dioxus::prelude::*; use dioxus_std::storage::*; +use dioxus_router::prelude::*; use std::{collections::HashMap, str::FromStr}; fn main() { // init debug tool for WebAssembly wasm_logger::init(wasm_logger::Config::default()); console_error_panic_hook::set_once(); - dioxus_web::launch(app); - // match log::set_boxed_logger(Box::new(simple_logger::SimpleLogger)) { - // Ok(_) => log::set_max_level(level.to_level_filter()), - // Err(e) => panic!("Failed to initialize logger: {}", e), - // } - // dioxus_desktop::launch(app); + dioxus_web::launch(App); } -fn app(cx: Scope) -> Element { +#[component] +fn App(cx: Scope) -> Element { + render! { + Router:: {} + } +} + +#[derive(Routable, Clone)] +#[rustfmt::skip] +enum Route { + #[layout(Footer)] + #[route("/")] + Page1 {}, + #[route("/page2")] + Page2 {}, +} + +#[component] +fn Footer(cx: Scope) -> Element { + render! { + div { + Outlet:: { } + + p { + "----" + } + + nav { + ul { + li { Link { to: Route::Page1 {}, "Page1" } } + li { Link { to: Route::Page2 {}, "Page2" } } + } + } + } + } +} + +#[component] +fn Page1(cx: Scope) -> Element { + render!("Home") +} + +#[component] +fn Page2(cx: Scope) -> Element { let count_session = use_singleton_persistent(cx, || 0); let count_local = use_synced_storage::(cx, "synced".to_string(), || 0); @@ -39,23 +78,3 @@ fn app(cx: Scope) -> Element { } ) } - -mod simple_logger { - use log::{Record, Metadata}; - - pub struct SimpleLogger; - - impl log::Log for SimpleLogger { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.level() <= log::max_level() - } - - fn log(&self, record: &Record) { - if self.enabled(record.metadata()) { - println!("{} - {}", record.level(), record.args()); - } - } - - fn flush(&self) {} - } -} \ No newline at end of file diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 0297455..30924e0 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -1,4 +1,8 @@ -use dashmap::DashMap; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; + use dioxus::prelude::*; use once_cell::sync::Lazy; use serde::{de::DeserializeOwned, Serialize}; @@ -33,41 +37,51 @@ impl StorageSubscriber for LocalStorage { _cx: &ScopeState, key: &String, ) -> Receiver { - let rx = CHANNELS.get(key).map_or_else( - || { + let read_binding = CHANNELS.read().unwrap(); + match read_binding.get(key) { + Some(entry) => entry.tx.subscribe(), + None => { + drop(read_binding); let (tx, rx) = channel::(StorageChannelPayload::default()); let entry = StorageEventChannel::new::(tx, key.clone()); - CHANNELS.insert(key.clone(), entry); + CHANNELS.write().unwrap().insert(key.clone(), entry); rx - }, - |entry| entry.tx.subscribe(), - ); - rx + } + } } fn unsubscribe(key: &String) { - log::info!("Unsubscribing from {}", key); - if let Some(entry) = CHANNELS.get(key) { + log::info!("Unsubscribing from \"{}\"", key); + let read_binding = CHANNELS.read().unwrap(); + if let Some(entry) = read_binding.get(key) { + log::info!("Found entry for \"{}\"", key); if entry.tx.is_closed() { - log::info!("Channel is closed, removing entry"); - CHANNELS.remove(key); + log::info!("Channel is closed, removing entry for \"{}\"", key); + drop(read_binding); + CHANNELS.write().unwrap().remove(key); } } } } /// A map of all the channels that are currently subscribed to. This gets initialized lazily and will set up a listener for storage events. -static CHANNELS: Lazy> = Lazy::new(|| { +static CHANNELS: Lazy>>> = Lazy::new(|| { // Create a closure that will be called when a storage event occurs. let closure = Closure::wrap(Box::new(move |e: web_sys::StorageEvent| { log::info!("Storage event: {:?}", e); let key: String = e.key().unwrap(); - if let Some(entry) = CHANNELS.get(&key) { + let read_binding = CHANNELS.read().unwrap(); + if let Some(entry) = read_binding.get(&key) { + if entry.tx.is_closed() { + log::info!("Channel is closed, removing entry for \"{}\"", key); + drop(read_binding); + CHANNELS.write().unwrap().remove(&key); + return; + } // Call the getter for the given entry and send the value to said entry's channel. - let result = entry.get_and_send(); - match result { + match entry.get_and_send() { Ok(_) => log::info!("Sent storage event"), - Err(err) => log::info!("Error sending storage event: {:?}", err), + Err(err) => log::error!("Error sending storage event: {:?}", err.to_string()), } } }) as Box); @@ -78,7 +92,7 @@ static CHANNELS: Lazy> = Lazy::new(|| { .unwrap(); // Relinquish ownership of the closure to the JS runtime so that it can be called later. closure.forget(); - DashMap::new() + Arc::new(RwLock::new(HashMap::new())) }); // End LocalStorage diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 5f929de..86f32c7 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -260,8 +260,8 @@ where } /// Gets the channel to subscribe to updates to the underlying storage - pub fn channel(&self) -> Receiver { - self.channel.clone() + pub fn channel(&self) -> &Receiver { + &self.channel } /// Creates a hook that will update the state when the underlying storage changes @@ -273,6 +273,7 @@ where loop { // Wait for an update to the channel if channel.changed().await.is_ok() { + log::info!("channel changed"); // Retrieve the latest value from the channel, mark it as read, and update the state let payload = channel.borrow_and_update(); *storage_entry_signal.write() = payload @@ -318,16 +319,6 @@ where } } -impl Drop for SyncedStorageEntry -where - S: StorageBacking + StorageSubscriber, - T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, -{ - fn drop(&mut self) { - S::unsubscribe(self.key()); - } -} - // End SyncedStorageEntry // Start StorageEntry @@ -441,7 +432,7 @@ pub struct StorageEventChannel { pub(crate) getter: Box StorageChannelPayload + 'static + Send + Sync>, /// The channel to send the data to. - pub(crate) tx: Sender, + pub(crate) tx: Arc>, } impl StorageEventChannel { @@ -458,7 +449,7 @@ impl StorageEventChannel { }; Self { getter: Box::new(getter), - tx, + tx: Arc::new(tx), } } From 0470435b1552acfd69673a1dc0fc7f656041967c Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 24 Oct 2023 16:33:50 -0700 Subject: [PATCH 049/108] add comments --- src/storage/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 86f32c7..fda24b1 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -492,6 +492,8 @@ impl Default for StorageChannelPayload { // End StorageChannelPayload // Start helper functions + +/// Serializes a value to a string and compresses it. pub(crate) fn serde_to_string(value: &T) -> String { let serialized = to_allocvec(value).unwrap(); let compressed = yazi::compress( @@ -514,10 +516,12 @@ pub(crate) fn serde_to_string(value: &T) -> 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(); From 8b74f37e3c52d048305c42908c42567684a21336 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Wed, 25 Oct 2023 23:40:20 -0700 Subject: [PATCH 050/108] adding filesystem and memory stores --- Cargo.toml | 3 +- examples/storage_desktop/src/main.rs | 3 +- src/storage/client_storage/fs.rs | 156 +++++++++++++++++++++++++-- src/storage/client_storage/memory.rs | 30 ++++++ src/storage/client_storage/mod.rs | 2 + src/storage/client_storage/web.rs | 33 +++--- src/storage/mod.rs | 6 +- 7 files changed, 204 insertions(+), 29 deletions(-) create mode 100644 src/storage/client_storage/memory.rs diff --git a/Cargo.toml b/Cargo.toml index b95c609..f804f96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ geolocation = ["dep:windows", "dep:futures-util", "dep:futures", "dep:web-sys", color_scheme = ["dep:web-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] utils = ["dep:async-broadcast", "dep:uuid"] i18n = ["dep:serde", "dep:serde_json", "dep:unic-langid"] -storage = ["dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:directories", "dep:yazi", "dep:tokio"] +storage = ["dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:directories", "dep:yazi", "dep:tokio", "dep:notify"] all = ["clipboard", "notifications", "geolocation", "color_scheme", "utils", "i18n", "storage"] default = ["storage"] @@ -56,6 +56,7 @@ dioxus-signals = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "6478 "serialize", ], optional = true } tokio = { version = "1.33.0", features = ["sync"], optional = true } +notify = { version = "6.1.1", default-features = false, features = ["macos_kqueue"], optional = true } yazi = { version = "0.1.4", optional = true } log = "0.4.6" diff --git a/examples/storage_desktop/src/main.rs b/examples/storage_desktop/src/main.rs index 3ccc749..a7ecdf4 100644 --- a/examples/storage_desktop/src/main.rs +++ b/examples/storage_desktop/src/main.rs @@ -4,9 +4,10 @@ use std::{collections::HashMap, str::FromStr}; fn main() { match log::set_boxed_logger(Box::new(simple_logger::SimpleLogger)) { - Ok(_) => log::set_max_level(level.to_level_filter()), + Ok(_) => log::set_max_level(log::LevelFilter::Info), Err(e) => panic!("Failed to initialize logger: {}", e), } + dioxus_std::storage::set_dir!(); dioxus_desktop::launch(app); } diff --git a/src/storage/client_storage/fs.rs b/src/storage/client_storage/fs.rs index 67b7b35..9afecc6 100644 --- a/src/storage/client_storage/fs.rs +++ b/src/storage/client_storage/fs.rs @@ -1,11 +1,14 @@ -use once_cell::sync::OnceCell; +use crate::storage::{StorageChannelPayload, StorageSubscription}; +use dioxus::prelude::*; +use notify::Watcher; use serde::de::DeserializeOwned; use serde::Serialize; +use std::collections::HashMap; use std::io::Write; +use std::sync::{OnceLock, RwLock}; +use tokio::sync::{mpsc, watch}; -use crate::storage::{ - serde_to_string, try_serde_from_string, StorageBacking, -}; +use crate::storage::{serde_to_string, try_serde_from_string, StorageBacking, StorageSubscriber}; #[allow(clippy::needless_doctest_main)] /// Set the directory where the storage files are located on non-wasm targets. @@ -21,10 +24,12 @@ use crate::storage::{ #[macro_export] macro_rules! set_dir { () => { - $crate::set_dir_name(env!("CARGO_PKG_NAME")); + extern crate self as storage; + storage::set_dir_name(env!("CARGO_PKG_NAME")); }; ($path: literal) => { - $crate::set_dir(std::path::PathBuf::from($path)); + extern crate self as storage; + storage::set_directory(std::path::PathBuf::from($path)); }; } pub use set_dir; @@ -45,7 +50,7 @@ pub fn set_dir_name(name: &str) { ) } -static LOCATION: OnceCell = OnceCell::new(); +static LOCATION: OnceLock = OnceLock::new(); fn set(key: String, value: &T) { let as_str = serde_to_string(value); @@ -67,9 +72,10 @@ fn get(key: &str) -> Option { try_serde_from_string(&s) } -pub struct ClientStorage; +#[derive(Clone)] +pub struct LocalStorage; -impl StorageBacking for ClientStorage { +impl StorageBacking for LocalStorage { type Key = String; fn set(key: String, value: &T) { @@ -80,3 +86,135 @@ impl StorageBacking for ClientStorage { get(key) } } + +impl StorageSubscriber for LocalStorage { + fn subscribe( + cx: &ScopeState, + key: &::Key, + ) -> watch::Receiver { + let watcher_helper = WATCHER_HELPER.get_or_init(|| { + let (tx, mut rx) = mpsc::channel::(10); + + cx.spawn_forever(async move { + let mut watcher = + notify::recommended_watcher(|result: Result| { + match result { + Ok(event) => { + let path = event.paths.first().unwrap(); + let key = path.file_name().unwrap().to_str().unwrap().to_string(); + let read_binding = + WATCHER_HELPER.get().unwrap().subscriptions.read().unwrap(); + if let Some(subscription) = read_binding.get(&key) { + if subscription.tx.is_closed() { + log::info!( + "Channel is closed, removing subscription for \"{}\"", + key + ); + drop(read_binding); + WATCHER_HELPER + .get() + .unwrap() + .subscriptions + .write() + .unwrap() + .remove(&key); + return; + } + // Call the getter for the given entry and send the value to said entry's channel. + match subscription.get_and_send() { + Ok(_) => log::info!("Sent storage event"), + Err(err) => log::error!( + "Error sending storage event: {:?}", + err.to_string() + ), + } + } + } + Err(e) => { + log::error!("Error watching file: {}", e); + } + } + }) + .unwrap(); + while let Some(message) = rx.recv().await { + match message { + WatcherAction::Subscribe(key) => { + let path = LOCATION + .get() + .expect("Call the set_dir macro before accessing persistant data") + .join(key); + watcher + .watch(&path, notify::RecursiveMode::NonRecursive) + .unwrap(); + } + WatcherAction::Unsubscribe(key) => { + let path = LOCATION + .get() + .expect("Call the set_dir macro before accessing persistant data") + .join(key); + watcher.unwatch(&path).unwrap(); + } + } + } + }); + WatcherHelper { + channel: tx, + subscriptions: RwLock::new(HashMap::new()), + } + }); + let read_binding = watcher_helper.subscriptions.read().unwrap(); + match read_binding.get(key) { + Some(subscription) => subscription.tx.subscribe(), + None => { + drop(read_binding); + let (tx, rx) = + watch::channel::(StorageChannelPayload::default()); + let subscription = StorageSubscription::new::(tx, key.clone()); + watcher_helper + .subscriptions + .write() + .unwrap() + .insert(key.clone(), subscription); + watcher_helper + .channel + .try_send(WatcherAction::Subscribe(key.clone())) + .unwrap(); + rx + } + } + } + + fn unsubscribe(key: &::Key) { + log::info!("Unsubscribing from \"{}\"", key); + if let Some(watcher_helper) = WATCHER_HELPER.get() { + let read_binding = watcher_helper.subscriptions.read().unwrap(); + if let Some(entry) = read_binding.get(key) { + log::info!("Found entry for \"{}\"", key); + drop(read_binding); + watcher_helper + .channel + .try_send(WatcherAction::Unsubscribe(key.clone())) + .unwrap(); + watcher_helper.subscriptions.write().unwrap().remove(key); + } + } + } +} + +static WATCHER_HELPER: OnceLock = OnceLock::new(); + +struct WatcherHelper { + channel: mpsc::Sender, + subscriptions: RwLock>, +} + +enum WatcherAction { + Subscribe(String), + Unsubscribe(String), +} + +enum WatcherResult { + Changed(String), + Error(String), +} +// TODO: add single thread to manage watcher diff --git a/src/storage/client_storage/memory.rs b/src/storage/client_storage/memory.rs new file mode 100644 index 0000000..f97c8b1 --- /dev/null +++ b/src/storage/client_storage/memory.rs @@ -0,0 +1,30 @@ +use once_cell::sync::Lazy; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::sync::Arc; +use std::{collections::HashMap, sync::RwLock}; + +use crate::storage::{serde_to_string, try_serde_from_string, StorageBacking}; + +#[derive(Clone)] +pub struct SessionStorage; + +impl StorageBacking for SessionStorage { + type Key = String; + + fn set(key: String, value: &T) { + SESSION_STORE + .write() + .unwrap() + .insert(key, serde_to_string(value)); + } + + fn get(key: &String) -> Option { + let read_binding = SESSION_STORE.read().unwrap(); + let string = read_binding.get(key)?; + try_serde_from_string(string) + } +} + +static SESSION_STORE: Lazy>>> = + Lazy::new(|| Arc::new(RwLock::new(HashMap::new()))); diff --git a/src/storage/client_storage/mod.rs b/src/storage/client_storage/mod.rs index fadad2a..3642cef 100644 --- a/src/storage/client_storage/mod.rs +++ b/src/storage/client_storage/mod.rs @@ -5,5 +5,7 @@ cfg_if::cfg_if! { } else { pub mod fs; pub use fs::*; + pub mod memory; + pub use memory::SessionStorage; } } diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 30924e0..92af33e 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -13,7 +13,7 @@ use web_sys::{window, Storage}; use crate::storage::{ serde_to_string, try_serde_from_string, StorageBacking, StorageChannelPayload, - StorageEventChannel, StorageSubscriber, + StorageSubscriber, StorageSubscription, }; // Start LocalStorage @@ -37,14 +37,17 @@ impl StorageSubscriber for LocalStorage { _cx: &ScopeState, key: &String, ) -> Receiver { - let read_binding = CHANNELS.read().unwrap(); + let read_binding = SUBSCRIPTIONS.read().unwrap(); match read_binding.get(key) { - Some(entry) => entry.tx.subscribe(), + Some(subscription) => subscription.tx.subscribe(), None => { drop(read_binding); let (tx, rx) = channel::(StorageChannelPayload::default()); - let entry = StorageEventChannel::new::(tx, key.clone()); - CHANNELS.write().unwrap().insert(key.clone(), entry); + let subscription = StorageSubscription::new::(tx, key.clone()); + SUBSCRIPTIONS + .write() + .unwrap() + .insert(key.clone(), subscription); rx } } @@ -52,34 +55,34 @@ impl StorageSubscriber for LocalStorage { fn unsubscribe(key: &String) { log::info!("Unsubscribing from \"{}\"", key); - let read_binding = CHANNELS.read().unwrap(); + let read_binding = SUBSCRIPTIONS.read().unwrap(); if let Some(entry) = read_binding.get(key) { log::info!("Found entry for \"{}\"", key); if entry.tx.is_closed() { log::info!("Channel is closed, removing entry for \"{}\"", key); drop(read_binding); - CHANNELS.write().unwrap().remove(key); + SUBSCRIPTIONS.write().unwrap().remove(key); } } } } -/// A map of all the channels that are currently subscribed to. This gets initialized lazily and will set up a listener for storage events. -static CHANNELS: Lazy>>> = Lazy::new(|| { +/// A map of all the channels that are currently subscribed to and the getters for the corresponding storage entry. This gets initialized lazily and will set up a listener for storage events. +static SUBSCRIPTIONS: Lazy>>> = Lazy::new(|| { // Create a closure that will be called when a storage event occurs. let closure = Closure::wrap(Box::new(move |e: web_sys::StorageEvent| { log::info!("Storage event: {:?}", e); let key: String = e.key().unwrap(); - let read_binding = CHANNELS.read().unwrap(); - if let Some(entry) = read_binding.get(&key) { - if entry.tx.is_closed() { - log::info!("Channel is closed, removing entry for \"{}\"", key); + let read_binding = SUBSCRIPTIONS.read().unwrap(); + if let Some(subscription) = read_binding.get(&key) { + if subscription.tx.is_closed() { + log::info!("Channel is closed, removing subscription for \"{}\"", key); drop(read_binding); - CHANNELS.write().unwrap().remove(&key); + SUBSCRIPTIONS.write().unwrap().remove(&key); return; } // Call the getter for the given entry and send the value to said entry's channel. - match entry.get_and_send() { + match subscription.get_and_send() { Ok(_) => log::info!("Sent storage event"), Err(err) => log::error!("Error sending storage event: {:?}", err.to_string()), } diff --git a/src/storage/mod.rs b/src/storage/mod.rs index fda24b1..101d268 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -44,7 +44,7 @@ use tokio::sync::watch::error::SendError; use tokio::sync::watch::{Receiver, Sender}; #[cfg(not(target_family = "wasm"))] -pub use client_storage::set_dir; +pub use client_storage::{set_dir, set_directory, set_dir_name}; /// A storage hook that can be used to store data that will persist across application reloads. /// @@ -427,7 +427,7 @@ pub trait StorageSubscriber { // Start StorageSenderEntry /// A struct to hold information about processing a storage event. -pub struct StorageEventChannel { +pub struct StorageSubscription { /// A getter function that will get the data from storage and return it as a StorageChannelPayload. pub(crate) getter: Box StorageChannelPayload + 'static + Send + Sync>, @@ -435,7 +435,7 @@ pub struct StorageEventChannel { pub(crate) tx: Arc>, } -impl StorageEventChannel { +impl StorageSubscription { pub fn new< S: StorageBacking + StorageSubscriber, T: DeserializeOwned + Send + Sync + 'static, From 5de9137525eb0e9d9f2925de8290ca56dbcb6629 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Wed, 25 Oct 2023 23:54:02 -0700 Subject: [PATCH 051/108] more comments --- src/storage/client_storage/fs.rs | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/storage/client_storage/fs.rs b/src/storage/client_storage/fs.rs index 9afecc6..0a30a98 100644 --- a/src/storage/client_storage/fs.rs +++ b/src/storage/client_storage/fs.rs @@ -50,8 +50,10 @@ pub fn set_dir_name(name: &str) { ) } +/// The location where the storage files are located. 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); let path = LOCATION @@ -63,6 +65,7 @@ fn set(key: String, value: &T) { file.write_all(as_str.as_bytes()).unwrap(); } +/// Get a value from the configured storage location using the key as the file name. fn get(key: &str) -> Option { let path = LOCATION .get() @@ -92,19 +95,24 @@ impl StorageSubscriber for LocalStorage { cx: &ScopeState, key: &::Key, ) -> watch::Receiver { + // Initialize the watcher helper if it hasn't been initialized yet. let watcher_helper = WATCHER_HELPER.get_or_init(|| { let (tx, mut rx) = mpsc::channel::(10); + // The watcher is spawned and managed in a separate thread because it is not thread-safe. cx.spawn_forever(async move { + // Create a watcher and set up a listener for storage events that will notify the correct subscribers. let mut watcher = notify::recommended_watcher(|result: Result| { match result { Ok(event) => { + // Get the path of the file that was changed and use it as the key. let path = event.paths.first().unwrap(); let key = path.file_name().unwrap().to_str().unwrap().to_string(); let read_binding = WATCHER_HELPER.get().unwrap().subscriptions.read().unwrap(); if let Some(subscription) = read_binding.get(&key) { + // If the subscription channel is closed, remove it from the subscriptions map the watcher. if subscription.tx.is_closed() { log::info!( "Channel is closed, removing subscription for \"{}\"", @@ -136,6 +144,7 @@ impl StorageSubscriber for LocalStorage { } }) .unwrap(); + // Create an infinite loop that will listen for watcher actions and subscribe/unsubscribe from the watcher accordingly. while let Some(message) = rx.recv().await { match message { WatcherAction::Subscribe(key) => { @@ -162,6 +171,9 @@ impl StorageSubscriber for LocalStorage { subscriptions: RwLock::new(HashMap::new()), } }); + + // Check if the subscription already exists. If it does, return the existing subscription's channel. + // If it doesn't, create a new subscription, register it with the watcher, and return its channel. let read_binding = watcher_helper.subscriptions.read().unwrap(); match read_binding.get(key) { Some(subscription) => subscription.tx.subscribe(), @@ -170,6 +182,7 @@ impl StorageSubscriber for LocalStorage { let (tx, rx) = watch::channel::(StorageChannelPayload::default()); let subscription = StorageSubscription::new::(tx, key.clone()); + watcher_helper .subscriptions .write() @@ -186,8 +199,12 @@ impl StorageSubscriber for LocalStorage { fn unsubscribe(key: &::Key) { log::info!("Unsubscribing from \"{}\"", key); + + // Fail silently if unsubscribe is called but the watcher isn't initialized yet. if let Some(watcher_helper) = WATCHER_HELPER.get() { let read_binding = watcher_helper.subscriptions.read().unwrap(); + + // If the subscription exists, remove it from the subscriptions map and send an unsubscribe message to the watcher. if let Some(entry) = read_binding.get(key) { log::info!("Found entry for \"{}\"", key); drop(read_binding); @@ -201,20 +218,22 @@ impl StorageSubscriber for LocalStorage { } } +/// A map of all the channels that are currently subscribed to and the getters for the corresponding storage entry. +/// This gets initialized lazily and will set up a listener for storage events. static WATCHER_HELPER: OnceLock = OnceLock::new(); +/// A helper struct that manages the watcher channel and subscriptions. struct WatcherHelper { + /// The channel that the application uses to communicate with the watcher thread. channel: mpsc::Sender, + /// A map of all the subscriptions that are currently active. subscriptions: RwLock>, } +/// The actions that can be sent to the watcher thread. enum WatcherAction { + /// Subscribe to a key. Subscribe(String), + /// Unsubscribe from a key. Unsubscribe(String), } - -enum WatcherResult { - Changed(String), - Error(String), -} -// TODO: add single thread to manage watcher From 270e5d578d1697fb35e17d7af6eac85bb8f0306e Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Thu, 26 Oct 2023 11:41:43 -0700 Subject: [PATCH 052/108] Updating storage desktop example to add new window --- examples/storage_desktop/Cargo.toml | 1 + examples/storage_desktop/src/main.rs | 60 +++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/examples/storage_desktop/Cargo.toml b/examples/storage_desktop/Cargo.toml index 75098d9..ac1f781 100644 --- a/examples/storage_desktop/Cargo.toml +++ b/examples/storage_desktop/Cargo.toml @@ -7,5 +7,6 @@ edition = "2021" dioxus-std = { path="../../", features = ["storage"] } dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } +dioxus-router = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } log = "0.4.6" diff --git a/examples/storage_desktop/src/main.rs b/examples/storage_desktop/src/main.rs index a7ecdf4..4dd6695 100644 --- a/examples/storage_desktop/src/main.rs +++ b/examples/storage_desktop/src/main.rs @@ -1,4 +1,5 @@ use dioxus::prelude::*; +use dioxus_router::prelude::*; use dioxus_std::storage::*; use std::{collections::HashMap, str::FromStr}; @@ -8,10 +9,65 @@ fn main() { Err(e) => panic!("Failed to initialize logger: {}", e), } dioxus_std::storage::set_dir!(); - dioxus_desktop::launch(app); + dioxus_desktop::launch(App); } -fn app(cx: Scope) -> Element { +#[component] +fn App(cx: Scope) -> Element { + render! { + Router:: {} + } +} + +#[derive(Routable, Clone)] +#[rustfmt::skip] +enum Route { + #[layout(Footer)] + #[route("/")] + Page1 {}, + #[route("/page2")] + Page2 {}, +} + +#[component] +fn Footer(cx: Scope) -> Element { + let window = dioxus_desktop::use_window(cx); + + render! { + div { + Outlet:: { } + + p { + "----" + } + + div { + button { + onclick: move |_| { + let dom = VirtualDom::new(App); + window.new_window(dom, Default::default()); + }, + "New Window" + } + } + + nav { + ul { + li { Link { to: Route::Page1 {}, "Page1" } } + li { Link { to: Route::Page2 {}, "Page2" } } + } + } + } + } +} + +#[component] +fn Page1(cx: Scope) -> Element { + render!("Home") +} + +#[component] +fn Page2(cx: Scope) -> Element { let count_session = use_singleton_persistent(cx, || 0); let count_local = use_synced_storage::(cx, "synced".to_string(), || 0); From ab764a8795a705cbe17cfa96ddb60d9d743f633f Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Thu, 26 Oct 2023 12:09:56 -0700 Subject: [PATCH 053/108] Remove notify crate --- Cargo.toml | 3 +- src/storage/client_storage/fs.rs | 125 +++++------------------------- src/storage/client_storage/web.rs | 4 +- src/storage/mod.rs | 20 ++--- 4 files changed, 31 insertions(+), 121 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f804f96..b95c609 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ geolocation = ["dep:windows", "dep:futures-util", "dep:futures", "dep:web-sys", color_scheme = ["dep:web-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] utils = ["dep:async-broadcast", "dep:uuid"] i18n = ["dep:serde", "dep:serde_json", "dep:unic-langid"] -storage = ["dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:directories", "dep:yazi", "dep:tokio", "dep:notify"] +storage = ["dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:directories", "dep:yazi", "dep:tokio"] all = ["clipboard", "notifications", "geolocation", "color_scheme", "utils", "i18n", "storage"] default = ["storage"] @@ -56,7 +56,6 @@ dioxus-signals = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "6478 "serialize", ], optional = true } tokio = { version = "1.33.0", features = ["sync"], optional = true } -notify = { version = "6.1.1", default-features = false, features = ["macos_kqueue"], optional = true } yazi = { version = "0.1.4", optional = true } log = "0.4.6" diff --git a/src/storage/client_storage/fs.rs b/src/storage/client_storage/fs.rs index 0a30a98..d3f6a68 100644 --- a/src/storage/client_storage/fs.rs +++ b/src/storage/client_storage/fs.rs @@ -5,7 +5,7 @@ use serde::de::DeserializeOwned; use serde::Serialize; use std::collections::HashMap; use std::io::Write; -use std::sync::{OnceLock, RwLock}; +use std::sync::{OnceLock, RwLock, Arc}; use tokio::sync::{mpsc, watch}; use crate::storage::{serde_to_string, try_serde_from_string, StorageBacking, StorageSubscriber}; @@ -81,8 +81,16 @@ pub struct LocalStorage; impl StorageBacking for LocalStorage { type Key = String; - fn set(key: String, value: &T) { + fn set(key: String, value: &T) { + let key_clone = key.clone(); + let value_clone = (*value).clone(); set(key, value); + if let Some(subscriptions) = SUBSCRIPTIONS.get() { + let read_binding = subscriptions.read().unwrap(); + if let Some(subscription) = read_binding.get(&key_clone) { + subscription.tx.send(StorageChannelPayload::new(value_clone)).unwrap(); + } + } } fn get(key: &String) -> Option { @@ -96,85 +104,13 @@ impl StorageSubscriber for LocalStorage { key: &::Key, ) -> watch::Receiver { // Initialize the watcher helper if it hasn't been initialized yet. - let watcher_helper = WATCHER_HELPER.get_or_init(|| { - let (tx, mut rx) = mpsc::channel::(10); - - // The watcher is spawned and managed in a separate thread because it is not thread-safe. - cx.spawn_forever(async move { - // Create a watcher and set up a listener for storage events that will notify the correct subscribers. - let mut watcher = - notify::recommended_watcher(|result: Result| { - match result { - Ok(event) => { - // Get the path of the file that was changed and use it as the key. - let path = event.paths.first().unwrap(); - let key = path.file_name().unwrap().to_str().unwrap().to_string(); - let read_binding = - WATCHER_HELPER.get().unwrap().subscriptions.read().unwrap(); - if let Some(subscription) = read_binding.get(&key) { - // If the subscription channel is closed, remove it from the subscriptions map the watcher. - if subscription.tx.is_closed() { - log::info!( - "Channel is closed, removing subscription for \"{}\"", - key - ); - drop(read_binding); - WATCHER_HELPER - .get() - .unwrap() - .subscriptions - .write() - .unwrap() - .remove(&key); - return; - } - // Call the getter for the given entry and send the value to said entry's channel. - match subscription.get_and_send() { - Ok(_) => log::info!("Sent storage event"), - Err(err) => log::error!( - "Error sending storage event: {:?}", - err.to_string() - ), - } - } - } - Err(e) => { - log::error!("Error watching file: {}", e); - } - } - }) - .unwrap(); - // Create an infinite loop that will listen for watcher actions and subscribe/unsubscribe from the watcher accordingly. - while let Some(message) = rx.recv().await { - match message { - WatcherAction::Subscribe(key) => { - let path = LOCATION - .get() - .expect("Call the set_dir macro before accessing persistant data") - .join(key); - watcher - .watch(&path, notify::RecursiveMode::NonRecursive) - .unwrap(); - } - WatcherAction::Unsubscribe(key) => { - let path = LOCATION - .get() - .expect("Call the set_dir macro before accessing persistant data") - .join(key); - watcher.unwatch(&path).unwrap(); - } - } - } - }); - WatcherHelper { - channel: tx, - subscriptions: RwLock::new(HashMap::new()), - } + let subscriptions = SUBSCRIPTIONS.get_or_init(|| { + RwLock::new(HashMap::new()) }); // Check if the subscription already exists. If it does, return the existing subscription's channel. // If it doesn't, create a new subscription, register it with the watcher, and return its channel. - let read_binding = watcher_helper.subscriptions.read().unwrap(); + let read_binding = subscriptions.read().unwrap(); match read_binding.get(key) { Some(subscription) => subscription.tx.subscribe(), None => { @@ -183,15 +119,10 @@ impl StorageSubscriber for LocalStorage { watch::channel::(StorageChannelPayload::default()); let subscription = StorageSubscription::new::(tx, key.clone()); - watcher_helper - .subscriptions + subscriptions .write() .unwrap() .insert(key.clone(), subscription); - watcher_helper - .channel - .try_send(WatcherAction::Subscribe(key.clone())) - .unwrap(); rx } } @@ -201,18 +132,14 @@ impl StorageSubscriber for LocalStorage { log::info!("Unsubscribing from \"{}\"", key); // Fail silently if unsubscribe is called but the watcher isn't initialized yet. - if let Some(watcher_helper) = WATCHER_HELPER.get() { - let read_binding = watcher_helper.subscriptions.read().unwrap(); + if let Some(subscriptions) = SUBSCRIPTIONS.get() { + let read_binding = subscriptions.read().unwrap(); // If the subscription exists, remove it from the subscriptions map and send an unsubscribe message to the watcher. if let Some(entry) = read_binding.get(key) { log::info!("Found entry for \"{}\"", key); drop(read_binding); - watcher_helper - .channel - .try_send(WatcherAction::Unsubscribe(key.clone())) - .unwrap(); - watcher_helper.subscriptions.write().unwrap().remove(key); + subscriptions.write().unwrap().remove(key); } } } @@ -220,20 +147,4 @@ impl StorageSubscriber for LocalStorage { /// A map of all the channels that are currently subscribed to and the getters for the corresponding storage entry. /// This gets initialized lazily and will set up a listener for storage events. -static WATCHER_HELPER: OnceLock = OnceLock::new(); - -/// A helper struct that manages the watcher channel and subscriptions. -struct WatcherHelper { - /// The channel that the application uses to communicate with the watcher thread. - channel: mpsc::Sender, - /// A map of all the subscriptions that are currently active. - subscriptions: RwLock>, -} - -/// The actions that can be sent to the watcher thread. -enum WatcherAction { - /// Subscribe to a key. - Subscribe(String), - /// Unsubscribe from a key. - Unsubscribe(String), -} +static SUBSCRIPTIONS: OnceLock>> = OnceLock::new(); diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 92af33e..3e912e5 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -23,7 +23,7 @@ pub struct LocalStorage; impl StorageBacking for LocalStorage { type Key = String; - fn set(key: String, value: &T) { + fn set(key: String, value: &T) { set(key, value, WebStorageType::Local); } @@ -107,7 +107,7 @@ pub struct SessionStorage; impl StorageBacking for SessionStorage { type Key = String; - fn set(key: String, value: &T) { + fn set(key: String, value: &T) { set(key, value, WebStorageType::Session); } diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 101d268..f5de9fd 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -165,7 +165,7 @@ pub fn storage_entry( ) -> StorageEntry where S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + 'static, + T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static, S::Key: Clone, { let data = get_from_storage::(key.clone(), init); @@ -190,7 +190,7 @@ where } /// Returns a value from storage or the init value if it doesn't exist. -pub fn get_from_storage( +pub fn get_from_storage( key: S::Key, init: impl FnOnce() -> T, ) -> T { @@ -325,7 +325,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 { +pub struct StorageEntry { /// 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 @@ -338,7 +338,7 @@ pub struct StorageEntry StorageEntry where S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + 'static, + T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static, S::Key: Clone, { /// Creates a new StorageEntry @@ -354,11 +354,11 @@ where impl StorageEntryTrait for StorageEntry where S: StorageBacking, - T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, + T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, { fn save(&self) { let _ = self.storage_save_lock.try_lock().map(|_| { - S::set(self.key.clone(), &self.data); + S::set(self.key.clone(), &self.data.value()); }); } @@ -375,7 +375,7 @@ where } } -impl Deref for StorageEntry { +impl Deref for StorageEntry { type Target = Signal; fn deref(&self) -> &Signal { @@ -383,7 +383,7 @@ impl Deref for Stora } } -impl Display +impl Display for StorageEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -391,7 +391,7 @@ impl Displ } } -impl Debug +impl Debug for StorageEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -409,7 +409,7 @@ pub trait StorageBacking: Clone + '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 a subscriber to events from a storage backing From e5aaf730bbf4713c42bb17f266209631cb6a002e Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Thu, 26 Oct 2023 12:10:46 -0700 Subject: [PATCH 054/108] fix typo --- src/storage/client_storage/fs.rs | 14 +++++++------- src/storage/mod.rs | 18 +++++++++++++----- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/storage/client_storage/fs.rs b/src/storage/client_storage/fs.rs index d3f6a68..f2324ed 100644 --- a/src/storage/client_storage/fs.rs +++ b/src/storage/client_storage/fs.rs @@ -1,12 +1,11 @@ use crate::storage::{StorageChannelPayload, StorageSubscription}; use dioxus::prelude::*; -use notify::Watcher; use serde::de::DeserializeOwned; use serde::Serialize; use std::collections::HashMap; use std::io::Write; -use std::sync::{OnceLock, RwLock, Arc}; -use tokio::sync::{mpsc, watch}; +use std::sync::{OnceLock, RwLock}; +use tokio::sync::watch; use crate::storage::{serde_to_string, try_serde_from_string, StorageBacking, StorageSubscriber}; @@ -88,7 +87,10 @@ impl StorageBacking for LocalStorage { if let Some(subscriptions) = SUBSCRIPTIONS.get() { let read_binding = subscriptions.read().unwrap(); if let Some(subscription) = read_binding.get(&key_clone) { - subscription.tx.send(StorageChannelPayload::new(value_clone)).unwrap(); + subscription + .tx + .send(StorageChannelPayload::new(value_clone)) + .unwrap(); } } } @@ -104,9 +106,7 @@ impl StorageSubscriber for LocalStorage { key: &::Key, ) -> watch::Receiver { // Initialize the watcher helper if it hasn't been initialized yet. - let subscriptions = SUBSCRIPTIONS.get_or_init(|| { - RwLock::new(HashMap::new()) - }); + let subscriptions = SUBSCRIPTIONS.get_or_init(|| RwLock::new(HashMap::new())); // Check if the subscription already exists. If it does, return the existing subscription's channel. // If it doesn't, create a new subscription, register it with the watcher, and return its channel. diff --git a/src/storage/mod.rs b/src/storage/mod.rs index f5de9fd..dc04c3f 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -44,7 +44,7 @@ use tokio::sync::watch::error::SendError; use tokio::sync::watch::{Receiver, Sender}; #[cfg(not(target_family = "wasm"))] -pub use client_storage::{set_dir, set_directory, set_dir_name}; +pub use client_storage::{set_dir, set_dir_name, set_directory}; /// A storage hook that can be used to store data that will persist across application reloads. /// @@ -190,7 +190,10 @@ where } /// Returns a value from storage or the init value if it doesn't exist. -pub fn get_from_storage( +pub fn get_from_storage< + S: StorageBacking, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, +>( key: S::Key, init: impl FnOnce() -> T, ) -> T { @@ -299,7 +302,7 @@ where if let Some(payload) = self.channel.borrow().data.downcast_ref::() { if *self.entry.data.read() == *payload { log::info!("value is the same, not saving"); - return + return; } } log::info!("saving"); @@ -325,7 +328,10 @@ 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 { +pub struct StorageEntry< + S: StorageBacking, + T: Serialize + DeserializeOwned + Clone + Send + Sync + '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 @@ -375,7 +381,9 @@ where } } -impl Deref for StorageEntry { +impl Deref + for StorageEntry +{ type Target = Signal; fn deref(&self) -> &Signal { From 1c7e1f6e1feed48d028a25932099e04e5a090a73 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Thu, 26 Oct 2023 12:16:51 -0700 Subject: [PATCH 055/108] add more comments --- src/storage/client_storage/fs.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/storage/client_storage/fs.rs b/src/storage/client_storage/fs.rs index f2324ed..246be43 100644 --- a/src/storage/client_storage/fs.rs +++ b/src/storage/client_storage/fs.rs @@ -5,7 +5,7 @@ use serde::Serialize; use std::collections::HashMap; use std::io::Write; use std::sync::{OnceLock, RwLock}; -use tokio::sync::watch; +use tokio::sync::watch::{channel, Receiver}; use crate::storage::{serde_to_string, try_serde_from_string, StorageBacking, StorageSubscriber}; @@ -84,6 +84,8 @@ impl StorageBacking for LocalStorage { let key_clone = key.clone(); let value_clone = (*value).clone(); 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) { @@ -100,23 +102,26 @@ impl StorageBacking 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( cx: &ScopeState, key: &::Key, - ) -> watch::Receiver { - // Initialize the watcher helper if it hasn't been initialized yet. + ) -> Receiver { + // Initialize the subscriptions map if it hasn't been initialized yet. let subscriptions = SUBSCRIPTIONS.get_or_init(|| RwLock::new(HashMap::new())); // Check if the subscription already exists. If it does, return the existing subscription's channel. - // If it doesn't, create a new subscription, register it with the watcher, and return its channel. + // If it doesn't, create a new subscription and return its channel. let read_binding = subscriptions.read().unwrap(); match read_binding.get(key) { Some(subscription) => subscription.tx.subscribe(), None => { drop(read_binding); let (tx, rx) = - watch::channel::(StorageChannelPayload::default()); + channel::(StorageChannelPayload::default()); let subscription = StorageSubscription::new::(tx, key.clone()); subscriptions @@ -131,11 +136,11 @@ impl StorageSubscriber for LocalStorage { fn unsubscribe(key: &::Key) { log::info!("Unsubscribing from \"{}\"", key); - // Fail silently if unsubscribe is called but the watcher isn't initialized yet. + // Fail silently if unsubscribe is called but the subscriptions map isn't initialized yet. if let Some(subscriptions) = SUBSCRIPTIONS.get() { let read_binding = subscriptions.read().unwrap(); - // If the subscription exists, remove it from the subscriptions map and send an unsubscribe message to the watcher. + // If the subscription exists, remove it from the subscriptions map. if let Some(entry) = read_binding.get(key) { log::info!("Found entry for \"{}\"", key); drop(read_binding); @@ -146,5 +151,5 @@ impl StorageSubscriber for LocalStorage { } /// A map of all the channels that are currently subscribed to and the getters for the corresponding storage entry. -/// This gets initialized lazily and will set up a listener for storage events. +/// This gets initialized lazily. static SUBSCRIPTIONS: OnceLock>> = OnceLock::new(); From fcd4abcac68561d1f8f99f6843250a3f6501d204 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Thu, 26 Oct 2023 12:24:46 -0700 Subject: [PATCH 056/108] make log info into trace --- src/storage/client_storage/fs.rs | 4 ++-- src/storage/client_storage/web.rs | 12 ++++++------ src/storage/mod.rs | 8 ++++---- src/storage/persistence.rs | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/storage/client_storage/fs.rs b/src/storage/client_storage/fs.rs index 246be43..8d4c567 100644 --- a/src/storage/client_storage/fs.rs +++ b/src/storage/client_storage/fs.rs @@ -134,7 +134,7 @@ impl StorageSubscriber for LocalStorage { } fn unsubscribe(key: &::Key) { - log::info!("Unsubscribing from \"{}\"", key); + log::trace!("Unsubscribing from \"{}\"", key); // Fail silently if unsubscribe is called but the subscriptions map isn't initialized yet. if let Some(subscriptions) = SUBSCRIPTIONS.get() { @@ -142,7 +142,7 @@ impl StorageSubscriber for LocalStorage { // If the subscription exists, remove it from the subscriptions map. if let Some(entry) = read_binding.get(key) { - log::info!("Found entry for \"{}\"", key); + log::trace!("Found entry for \"{}\"", key); drop(read_binding); subscriptions.write().unwrap().remove(key); } diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 3e912e5..28091a2 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -54,12 +54,12 @@ impl StorageSubscriber for LocalStorage { } fn unsubscribe(key: &String) { - log::info!("Unsubscribing from \"{}\"", key); + log::trace!("Unsubscribing from \"{}\"", key); let read_binding = SUBSCRIPTIONS.read().unwrap(); if let Some(entry) = read_binding.get(key) { - log::info!("Found entry for \"{}\"", key); + log::trace!("Found entry for \"{}\"", key); if entry.tx.is_closed() { - log::info!("Channel is closed, removing entry for \"{}\"", key); + log::trace!("Channel is closed, removing entry for \"{}\"", key); drop(read_binding); SUBSCRIPTIONS.write().unwrap().remove(key); } @@ -71,19 +71,19 @@ impl StorageSubscriber for LocalStorage { static SUBSCRIPTIONS: Lazy>>> = Lazy::new(|| { // Create a closure that will be called when a storage event occurs. let closure = Closure::wrap(Box::new(move |e: web_sys::StorageEvent| { - log::info!("Storage event: {:?}", e); + log::trace!("Storage event: {:?}", e); let key: String = e.key().unwrap(); let read_binding = SUBSCRIPTIONS.read().unwrap(); if let Some(subscription) = read_binding.get(&key) { if subscription.tx.is_closed() { - log::info!("Channel is closed, removing subscription for \"{}\"", key); + log::trace!("Channel is closed, removing subscription for \"{}\"", key); drop(read_binding); SUBSCRIPTIONS.write().unwrap().remove(&key); return; } // Call the getter for the given entry and send the value to said entry's channel. match subscription.get_and_send() { - Ok(_) => log::info!("Sent storage event"), + Ok(_) => log::trace!("Sent storage event"), Err(err) => log::error!("Error sending storage event: {:?}", err.to_string()), } } diff --git a/src/storage/mod.rs b/src/storage/mod.rs index dc04c3f..4673819 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -229,7 +229,7 @@ pub trait StorageEntryTrait: { let entry_clone = self.clone(); use_effect(cx, (&self.data().value(),), move |_| async move { - log::info!("state value changed, trying to save"); + log::trace!("state value changed, trying to save"); entry_clone.save(); }); } @@ -276,7 +276,7 @@ where loop { // Wait for an update to the channel if channel.changed().await.is_ok() { - log::info!("channel changed"); + log::trace!("channel changed"); // Retrieve the latest value from the channel, mark it as read, and update the state let payload = channel.borrow_and_update(); *storage_entry_signal.write() = payload @@ -301,11 +301,11 @@ where // - 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 { - log::info!("value is the same, not saving"); + log::trace!("value is the same, not saving"); return; } } - log::info!("saving"); + log::trace!("saving"); self.entry.save(); } diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index bfb8937..a9f706b 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -36,6 +36,6 @@ pub fn use_singleton_persistent< ) -> Signal { let caller = std::panic::Location::caller(); let key = cx.use_hook(move || format!("{}:{}", caller.file(), caller.line())); - log::info!("use_singleton_persistent key: \"{}\"", key); + log::trace!("use_singleton_persistent key: \"{}\"", key); use_persistent(cx, key, init) } From 474433c9ca4f9729aabec474c65dd053973dc448 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sat, 28 Oct 2023 10:58:59 -0700 Subject: [PATCH 057/108] fix memory map --- src/storage/client_storage/memory.rs | 43 +++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/storage/client_storage/memory.rs b/src/storage/client_storage/memory.rs index f97c8b1..35bade4 100644 --- a/src/storage/client_storage/memory.rs +++ b/src/storage/client_storage/memory.rs @@ -1,6 +1,7 @@ use once_cell::sync::Lazy; use serde::de::DeserializeOwned; use serde::Serialize; +use std::ops::{DerefMut, Deref}; use std::sync::Arc; use std::{collections::HashMap, sync::RwLock}; @@ -13,18 +14,52 @@ impl StorageBacking for SessionStorage { type Key = String; fn set(key: String, value: &T) { - SESSION_STORE + let session = SessionStore::get_current_session(); + session .write() .unwrap() .insert(key, serde_to_string(value)); } fn get(key: &String) -> Option { - let read_binding = SESSION_STORE.read().unwrap(); + let session = SessionStore::get_current_session(); + let read_binding = session.read().unwrap(); let string = read_binding.get(key)?; try_serde_from_string(string) } } -static SESSION_STORE: Lazy>>> = - Lazy::new(|| Arc::new(RwLock::new(HashMap::new()))); +#[derive(Clone)] +struct SessionStore { + map: Arc>>, +} + +impl SessionStore { + fn new() -> Self { + Self { + map: Arc::new(RwLock::new(HashMap::::new())), + } + } + fn get_current_session() -> Self { + dioxus::prelude::consume_context_from_scope::(dioxus::prelude::ScopeId::ROOT).map_or_else(|| { + let session = Self::new(); + dioxus::prelude::provide_root_context(session.clone()); + session + }, |s| s) + + } +} + +impl Deref for SessionStore { + type Target = Arc>>; + + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl DerefMut for SessionStore { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} \ No newline at end of file From 016c92538d9d84b971e4f2697d55d24180d1ecc9 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sat, 28 Oct 2023 11:10:51 -0700 Subject: [PATCH 058/108] Update in-memory to use Any instead of serialize --- src/storage/client_storage/fs.rs | 5 ++- src/storage/client_storage/memory.rs | 47 +++++++++++++++------------- src/storage/mod.rs | 6 ++-- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/storage/client_storage/fs.rs b/src/storage/client_storage/fs.rs index 8d4c567..9f38489 100644 --- a/src/storage/client_storage/fs.rs +++ b/src/storage/client_storage/fs.rs @@ -106,7 +106,7 @@ impl StorageBacking 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( + fn subscribe( cx: &ScopeState, key: &::Key, ) -> Receiver { @@ -120,8 +120,7 @@ impl StorageSubscriber for LocalStorage { Some(subscription) => subscription.tx.subscribe(), None => { drop(read_binding); - let (tx, rx) = - channel::(StorageChannelPayload::default()); + let (tx, rx) = channel::(StorageChannelPayload::default()); let subscription = StorageSubscription::new::(tx, key.clone()); subscriptions diff --git a/src/storage/client_storage/memory.rs b/src/storage/client_storage/memory.rs index 35bade4..c79cb63 100644 --- a/src/storage/client_storage/memory.rs +++ b/src/storage/client_storage/memory.rs @@ -1,11 +1,9 @@ -use once_cell::sync::Lazy; -use serde::de::DeserializeOwned; -use serde::Serialize; -use std::ops::{DerefMut, Deref}; +use std::any::Any; +use std::ops::{Deref, DerefMut}; use std::sync::Arc; use std::{collections::HashMap, sync::RwLock}; -use crate::storage::{serde_to_string, try_serde_from_string, StorageBacking}; +use crate::storage::StorageBacking; #[derive(Clone)] pub struct SessionStorage; @@ -13,45 +11,52 @@ 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 .write() .unwrap() - .insert(key, serde_to_string(value)); + .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.read().unwrap(); - let string = read_binding.get(key)?; - try_serde_from_string(string) + let value_any = read_binding.get(key)?; + value_any.downcast_ref::().cloned() } } +/// An in-memory session store that is tied to the current Dioxus root context. #[derive(Clone)] struct SessionStore { - map: Arc>>, + /// The underlying map of session data. + map: Arc>>>, } impl SessionStore { fn new() -> Self { Self { - map: Arc::new(RwLock::new(HashMap::::new())), + map: Arc::new(RwLock::new(HashMap::>::new())), } } - fn get_current_session() -> Self { - dioxus::prelude::consume_context_from_scope::(dioxus::prelude::ScopeId::ROOT).map_or_else(|| { - let session = Self::new(); - dioxus::prelude::provide_root_context(session.clone()); - session - }, |s| s) + /// Get the current session store from the root context, or create a new one if it doesn't exist. + fn get_current_session() -> Self { + dioxus::prelude::consume_context_from_scope::(dioxus::prelude::ScopeId::ROOT) + .map_or_else( + || { + let session = Self::new(); + dioxus::prelude::provide_root_context(session.clone()); + session + }, + |s| s, + ) } } impl Deref for SessionStore { - type Target = Arc>>; + type Target = Arc>>>; fn deref(&self) -> &Self::Target { &self.map @@ -60,6 +65,6 @@ impl Deref for SessionStore { impl DerefMut for SessionStore { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.map + &mut self.map } -} \ No newline at end of file +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 4673819..9f2b06d 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -415,7 +415,7 @@ pub trait StorageBacking: 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); } @@ -423,7 +423,7 @@ pub trait StorageBacking: Clone + 'static { /// 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( + fn subscribe( cx: &ScopeState, key: &S::Key, ) -> Receiver; @@ -446,7 +446,7 @@ pub struct StorageSubscription { impl StorageSubscription { pub fn new< S: StorageBacking + StorageSubscriber, - T: DeserializeOwned + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + Clone + 'static, >( tx: Sender, key: S::Key, From a35bdc1811a0de461c9a5606df36a3cba6113b52 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 28 Oct 2023 14:42:10 -0500 Subject: [PATCH 059/108] add clone bound to the web storage subscribe method --- examples/storage_web/src/main.rs | 2 +- src/storage/client_storage/web.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/storage_web/src/main.rs b/examples/storage_web/src/main.rs index df80c36..38be592 100644 --- a/examples/storage_web/src/main.rs +++ b/examples/storage_web/src/main.rs @@ -1,6 +1,6 @@ use dioxus::prelude::*; -use dioxus_std::storage::*; use dioxus_router::prelude::*; +use dioxus_std::storage::*; use std::{collections::HashMap, str::FromStr}; fn main() { diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 28091a2..6da63c6 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -33,7 +33,7 @@ impl StorageBacking for LocalStorage { } impl StorageSubscriber for LocalStorage { - fn subscribe( + fn subscribe( _cx: &ScopeState, key: &String, ) -> Receiver { From 48f5737b8e84e51314ceac6ddf6363095681c47c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 28 Oct 2023 14:45:39 -0500 Subject: [PATCH 060/108] fix clippy --- examples/storage_desktop/src/main.rs | 5 ++--- examples/storage_web/src/main.rs | 1 - src/storage/client_storage/fs.rs | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/storage_desktop/src/main.rs b/examples/storage_desktop/src/main.rs index 4dd6695..dd1fd73 100644 --- a/examples/storage_desktop/src/main.rs +++ b/examples/storage_desktop/src/main.rs @@ -1,7 +1,6 @@ use dioxus::prelude::*; use dioxus_router::prelude::*; use dioxus_std::storage::*; -use std::{collections::HashMap, str::FromStr}; fn main() { match log::set_boxed_logger(Box::new(simple_logger::SimpleLogger)) { @@ -94,7 +93,7 @@ fn Page2(cx: Scope) -> Element { } mod simple_logger { - use log::{Record, Metadata}; + use log::{Metadata, Record}; pub struct SimpleLogger; @@ -111,4 +110,4 @@ mod simple_logger { fn flush(&self) {} } -} \ No newline at end of file +} diff --git a/examples/storage_web/src/main.rs b/examples/storage_web/src/main.rs index 38be592..30c9db8 100644 --- a/examples/storage_web/src/main.rs +++ b/examples/storage_web/src/main.rs @@ -1,7 +1,6 @@ use dioxus::prelude::*; use dioxus_router::prelude::*; use dioxus_std::storage::*; -use std::{collections::HashMap, str::FromStr}; fn main() { // init debug tool for WebAssembly diff --git a/src/storage/client_storage/fs.rs b/src/storage/client_storage/fs.rs index 9f38489..d9aa0ea 100644 --- a/src/storage/client_storage/fs.rs +++ b/src/storage/client_storage/fs.rs @@ -107,7 +107,7 @@ impl StorageBacking for LocalStorage { // storage event listener. impl StorageSubscriber for LocalStorage { fn subscribe( - cx: &ScopeState, + _cx: &ScopeState, key: &::Key, ) -> Receiver { // Initialize the subscriptions map if it hasn't been initialized yet. @@ -140,7 +140,7 @@ impl StorageSubscriber for LocalStorage { let read_binding = subscriptions.read().unwrap(); // If the subscription exists, remove it from the subscriptions map. - if let Some(entry) = read_binding.get(key) { + if read_binding.contains_key(key) { log::trace!("Found entry for \"{}\"", key); drop(read_binding); subscriptions.write().unwrap().remove(key); From 69c6c0ac465c26dd8613f18bcb9df0bbe9e213dd Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 1 Nov 2023 15:21:31 -0500 Subject: [PATCH 061/108] add non-hook versions of storage --- src/storage/client_storage/fs.rs | 2 - src/storage/client_storage/web.rs | 1 - src/storage/mod.rs | 105 ++++++++++++++++++------------ src/storage/persistence.rs | 40 ++++++++++-- 4 files changed, 97 insertions(+), 51 deletions(-) diff --git a/src/storage/client_storage/fs.rs b/src/storage/client_storage/fs.rs index d9aa0ea..0046fef 100644 --- a/src/storage/client_storage/fs.rs +++ b/src/storage/client_storage/fs.rs @@ -1,5 +1,4 @@ use crate::storage::{StorageChannelPayload, StorageSubscription}; -use dioxus::prelude::*; use serde::de::DeserializeOwned; use serde::Serialize; use std::collections::HashMap; @@ -107,7 +106,6 @@ impl StorageBacking for LocalStorage { // storage event listener. impl StorageSubscriber for LocalStorage { fn subscribe( - _cx: &ScopeState, key: &::Key, ) -> Receiver { // Initialize the subscriptions map if it hasn't been initialized yet. diff --git a/src/storage/client_storage/web.rs b/src/storage/client_storage/web.rs index 6da63c6..bc48d6f 100644 --- a/src/storage/client_storage/web.rs +++ b/src/storage/client_storage/web.rs @@ -34,7 +34,6 @@ impl StorageBacking for LocalStorage { impl StorageSubscriber for LocalStorage { fn subscribe( - _cx: &ScopeState, key: &String, ) -> Receiver { let read_binding = SUBSCRIPTIONS.read().unwrap(); diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 9f2b06d..403d8e0 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -32,8 +32,8 @@ mod persistence; pub use client_storage::{LocalStorage, SessionStorage}; pub use persistence::{use_persistent, use_singleton_persistent}; -use dioxus::prelude::{to_owned, use_effect, ScopeState}; -use dioxus_signals::{use_signal, Signal}; +use dioxus::prelude::{current_scope_id, to_owned, ScopeState}; +use dioxus_signals::{Effect, Signal}; use postcard::to_allocvec; use serde::{de::DeserializeOwned, Serialize}; use std::any::Any; @@ -50,6 +50,18 @@ pub use client_storage::{set_dir, set_dir_name, set_directory}; /// /// This hook returns a Signal that can be used to read and modify the state. pub fn use_storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal +where + S: StorageBacking, + T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, + S::Key: Clone, +{ + *cx.use_hook(|| storage::(cx, key, init)) +} + +/// Creates a Signal that can be used to store data that will persist across application reloads. +/// +/// This hook returns a Signal that can be used to read and modify the state. +pub fn storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal where S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, @@ -60,11 +72,10 @@ where if cfg!(feature = "ssr") { // SSR does not support storage on the backend. We will just use a normal Signal to represent the initial state. // The client will hydrate this with a correct StorageEntry and maintain state. - use_signal(cx, init.take().unwrap()) + Signal::new(init.take().unwrap()()) } else if cfg!(feature = "hydrate") { let key_clone = key.clone(); - let storage_entry = - cx.use_hook(|| storage_entry::(key, init.take().unwrap(), cx)); + let storage_entry = storage_entry::(key, init.take().unwrap()); if cx.generation() == 0 { // The first generation is rendered on the server side and so must be hydrated. cx.needs_update(); @@ -72,13 +83,13 @@ where if cx.generation() == 1 { // The first time the vdom is hydrated, we set the correct value from storage and set up the subscription to storage events. storage_entry.set(get_from_storage::(key_clone, init.take().unwrap())); - storage_entry.use_save_to_storage_on_change(cx); + storage_entry.save_to_storage_on_change(); } storage_entry.data } else { // The client is rendered normally, so we can just use the storage entry. - let storage_entry = use_storage_entry::(cx, key, init.take().unwrap()); - storage_entry.use_save_to_storage_on_change(cx); + let storage_entry = storage_entry::( key, init.take().unwrap()); + storage_entry.save_to_storage_on_change(); storage_entry.data } }; @@ -90,6 +101,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(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal +where + S: StorageBacking + StorageSubscriber, + T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, + S::Key: Clone, +{ + *cx.use_hook(|| synced_storage::(cx, key, init)) +} + +/// Create a signal that can be used to store data that will persist across application reloads and be synced across all app sessions for a given installation or browser. +/// +/// 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 synced_storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal where S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, @@ -100,11 +124,10 @@ where if cfg!(feature = "ssr") { // SSR does not support synced storage on the backend. We will just use a normal Signal to represent the initial state. // The client will hydrate this with a correct SyncedStorageEntry and maintain state. - use_signal(cx, init.take().unwrap()) + Signal::new(init.take().unwrap()()) } else if cfg!(feature = "hydrate") { let key_clone = key.clone(); - let storage_entry = - cx.use_hook(|| synced_storage_entry::(key, init.take().unwrap(), cx)); + let storage_entry = synced_storage_entry::(key, init.take().unwrap()); if cx.generation() == 0 { // The first generation is rendered on the server side and so must be hydrated. cx.needs_update(); @@ -114,15 +137,15 @@ where storage_entry .entry .set(get_from_storage::(key_clone, init.take().unwrap())); - storage_entry.use_save_to_storage_on_change(cx); - storage_entry.use_subscribe_to_storage(cx); + storage_entry.save_to_storage_on_change(); + storage_entry.subscribe_to_storage(cx); } *storage_entry.data() } else { // The client is rendered normally, so we can just use the synced storage entry. - let storage_entry = use_synced_storage_entry::(cx, key, init.take().unwrap()); - storage_entry.use_save_to_storage_on_change(cx); - storage_entry.use_subscribe_to_storage(cx); + let storage_entry = synced_storage_entry::(key, init.take().unwrap()); + storage_entry.save_to_storage_on_change(); + storage_entry.subscribe_to_storage(cx); *storage_entry.data() } }; @@ -140,7 +163,7 @@ where T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { - cx.use_hook(|| storage_entry::(key, init, cx)) + cx.use_hook(|| storage_entry::(key, init)) } /// 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. @@ -154,39 +177,31 @@ where T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { - cx.use_hook(|| synced_storage_entry::(key, init, cx)) + cx.use_hook(|| synced_storage_entry::(key, init)) } /// Returns a StorageEntry with the latest value from storage or the init value if it doesn't exist. -pub fn storage_entry( - key: S::Key, - init: impl FnOnce() -> T, - cx: &ScopeState, -) -> StorageEntry +pub fn storage_entry(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry where S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static, S::Key: Clone, { let data = get_from_storage::(key.clone(), init); - StorageEntry::new(key, data, cx) + StorageEntry::new(key, data) } /// Returns a synced StorageEntry with the latest value from storage or the init value if it doesn't exist. /// /// This differs from `storage_entry` in that this one will return a channel to subscribe to updates to the underlying storage. -pub fn synced_storage_entry( - key: S::Key, - init: impl FnOnce() -> T, - cx: &ScopeState, -) -> SyncedStorageEntry +pub fn synced_storage_entry(key: S::Key, init: impl FnOnce() -> T) -> SyncedStorageEntry where S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, S::Key: Clone, { let data = get_from_storage::(key.clone(), init); - SyncedStorageEntry::new(key, data, cx) + SyncedStorageEntry::new(key, data) } /// Returns a value from storage or the init value if it doesn't exist. @@ -222,15 +237,19 @@ pub trait StorageEntryTrait: fn data(&self) -> &Signal; /// Creates a hook that will save the state to storage when the state changes - fn use_save_to_storage_on_change(&self, cx: &ScopeState) + fn save_to_storage_on_change(&self) where S: StorageBacking, T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, { let entry_clone = self.clone(); - use_effect(cx, (&self.data().value(),), move |_| async move { - log::trace!("state value changed, trying to save"); - entry_clone.save(); + let old = Signal::new(self.data().value()); + let data = *self.data(); + Effect::new(move || { + if &*old() != &*data() { + log::trace!("state value changed, trying to save"); + entry_clone.save(); + } }); } } @@ -254,10 +273,10 @@ where S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, { - pub fn new(key: S::Key, data: T, cx: &ScopeState) -> Self { - let channel = S::subscribe::(cx, &key); + pub fn new(key: S::Key, data: T) -> Self { + let channel = S::subscribe::(&key); Self { - entry: StorageEntry::new(key, data, cx), + entry: StorageEntry::new(key, data), channel, } } @@ -268,10 +287,10 @@ where } /// Creates a hook that will update the state when the underlying storage changes - pub fn use_subscribe_to_storage(&self, cx: &ScopeState) { + pub fn subscribe_to_storage(&self, cx: &ScopeState) { let storage_entry_signal = *self.data(); let channel = self.channel.clone(); - use_effect(cx, (), move |_| async move { + cx.spawn(async move { to_owned![channel, storage_entry_signal]; loop { // Wait for an update to the channel @@ -348,10 +367,13 @@ where S::Key: Clone, { /// Creates a new StorageEntry - pub fn new(key: S::Key, data: T, cx: &ScopeState) -> Self { + pub fn new(key: S::Key, data: T) -> Self { Self { key, - data: Signal::new_in_scope(data, cx.scope_id()), + data: Signal::new_in_scope( + data, + current_scope_id().expect("must be called from inside of the dioxus context"), + ), storage_save_lock: Arc::new(Mutex::new(())), } } @@ -424,7 +446,6 @@ pub trait StorageBacking: Clone + 'static { pub trait StorageSubscriber { /// Subscribes to events from a storage backing for the given key fn subscribe( - cx: &ScopeState, key: &S::Key, ) -> Receiver; /// Unsubscribes from events from a storage backing for the given key diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index a9f706b..0b031e9 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -1,4 +1,5 @@ -use crate::storage::{use_storage_entry, SessionStorage}; +use crate::storage::storage_entry; +use crate::storage::SessionStorage; use dioxus::prelude::ScopeState; use dioxus_signals::Signal; use serde::de::DeserializeOwned; @@ -17,8 +18,21 @@ pub fn use_persistent< key: impl ToString, init: impl FnOnce() -> T, ) -> Signal { - let storage_entry = use_storage_entry::(cx, key.to_string(), init); - storage_entry.use_save_to_storage_on_change(cx); + *cx.use_hook(|| persistent(key, init)) +} + +/// 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 +#[allow(clippy::needless_return)] +pub fn persistent< + T: Serialize + DeserializeOwned + Default + Clone + Send + Sync + PartialEq + 'static, +>( + key: impl ToString, + init: impl FnOnce() -> T, +) -> Signal { + let storage_entry = storage_entry::(key.to_string(), init); + storage_entry.save_to_storage_on_change(); storage_entry.data } @@ -33,9 +47,23 @@ pub fn use_singleton_persistent< >( cx: &ScopeState, init: impl FnOnce() -> T, +) -> Signal { + *cx.use_hook(|| singleton_persistent(init)) +} + +/// Create a persistent storage signal that can be used to store data across application reloads. +/// The state will be the same for every call to this hook from the same line of code. +/// +/// Depending on the platform this uses either local storage or a file storage +#[allow(clippy::needless_return)] +#[track_caller] +pub fn singleton_persistent< + T: Serialize + DeserializeOwned + Default + Clone + Send + Sync + PartialEq + 'static, +>( + init: impl FnOnce() -> T, ) -> Signal { let caller = std::panic::Location::caller(); - let key = cx.use_hook(move || format!("{}:{}", caller.file(), caller.line())); - log::trace!("use_singleton_persistent key: \"{}\"", key); - use_persistent(cx, key, init) + let key = format!("{}:{}", caller.file(), caller.line()); + log::trace!("singleton_persistent key: \"{}\"", key); + persistent(key, init) } From cb1eecf029aa96b31170790df97568d14d53663d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 2 Nov 2023 13:27:39 -0500 Subject: [PATCH 062/108] add new_ prefix to the non-hook storage functions --- src/storage/mod.rs | 24 ++++++++++++------------ src/storage/persistence.rs | 14 +++++++------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 403d8e0..0618295 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -55,13 +55,13 @@ where T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { - *cx.use_hook(|| storage::(cx, key, init)) + *cx.use_hook(|| new_storage::(cx, key, init)) } /// Creates a Signal that can be used to store data that will persist across application reloads. /// /// This hook returns a Signal that can be used to read and modify the state. -pub fn storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal +pub fn new_storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal where S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, @@ -75,7 +75,7 @@ where Signal::new(init.take().unwrap()()) } else if cfg!(feature = "hydrate") { let key_clone = key.clone(); - let storage_entry = storage_entry::(key, init.take().unwrap()); + let storage_entry = new_storage_entry::(key, init.take().unwrap()); if cx.generation() == 0 { // The first generation is rendered on the server side and so must be hydrated. cx.needs_update(); @@ -88,7 +88,7 @@ where storage_entry.data } else { // The client is rendered normally, so we can just use the storage entry. - let storage_entry = storage_entry::( key, init.take().unwrap()); + let storage_entry = new_storage_entry::( key, init.take().unwrap()); storage_entry.save_to_storage_on_change(); storage_entry.data } @@ -106,14 +106,14 @@ where T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { - *cx.use_hook(|| synced_storage::(cx, key, init)) + *cx.use_hook(|| new_synced_storage::(cx, key, init)) } /// Create a signal that can be used to store data that will persist across application reloads and be synced across all app sessions for a given installation or browser. /// /// 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 synced_storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal +pub fn new_synced_storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal where S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, @@ -127,7 +127,7 @@ where Signal::new(init.take().unwrap()()) } else if cfg!(feature = "hydrate") { let key_clone = key.clone(); - let storage_entry = synced_storage_entry::(key, init.take().unwrap()); + let storage_entry = new_synced_storage_entry::(key, init.take().unwrap()); if cx.generation() == 0 { // The first generation is rendered on the server side and so must be hydrated. cx.needs_update(); @@ -143,7 +143,7 @@ where *storage_entry.data() } else { // The client is rendered normally, so we can just use the synced storage entry. - let storage_entry = synced_storage_entry::(key, init.take().unwrap()); + let storage_entry = new_synced_storage_entry::(key, init.take().unwrap()); storage_entry.save_to_storage_on_change(); storage_entry.subscribe_to_storage(cx); *storage_entry.data() @@ -163,7 +163,7 @@ where T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { - cx.use_hook(|| storage_entry::(key, init)) + cx.use_hook(|| new_storage_entry::(key, init)) } /// 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. @@ -177,11 +177,11 @@ where T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { - cx.use_hook(|| synced_storage_entry::(key, init)) + cx.use_hook(|| new_synced_storage_entry::(key, init)) } /// Returns a StorageEntry with the latest value from storage or the init value if it doesn't exist. -pub fn storage_entry(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry +pub fn new_storage_entry(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry where S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static, @@ -194,7 +194,7 @@ where /// Returns a synced StorageEntry with the latest value from storage or the init value if it doesn't exist. /// /// This differs from `storage_entry` in that this one will return a channel to subscribe to updates to the underlying storage. -pub fn synced_storage_entry(key: S::Key, init: impl FnOnce() -> T) -> SyncedStorageEntry +pub fn new_synced_storage_entry(key: S::Key, init: impl FnOnce() -> T) -> SyncedStorageEntry where S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, diff --git a/src/storage/persistence.rs b/src/storage/persistence.rs index 0b031e9..3832ff5 100644 --- a/src/storage/persistence.rs +++ b/src/storage/persistence.rs @@ -1,4 +1,4 @@ -use crate::storage::storage_entry; +use crate::storage::new_storage_entry; use crate::storage::SessionStorage; use dioxus::prelude::ScopeState; use dioxus_signals::Signal; @@ -18,20 +18,20 @@ pub fn use_persistent< key: impl ToString, init: impl FnOnce() -> T, ) -> Signal { - *cx.use_hook(|| persistent(key, init)) + *cx.use_hook(|| new_persistent(key, init)) } /// 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 #[allow(clippy::needless_return)] -pub fn persistent< +pub fn new_persistent< T: Serialize + DeserializeOwned + Default + Clone + Send + Sync + PartialEq + 'static, >( key: impl ToString, init: impl FnOnce() -> T, ) -> Signal { - let storage_entry = 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 +48,7 @@ pub fn use_singleton_persistent< cx: &ScopeState, init: impl FnOnce() -> T, ) -> Signal { - *cx.use_hook(|| singleton_persistent(init)) + *cx.use_hook(|| new_singleton_persistent(init)) } /// Create a persistent storage signal that can be used to store data across application reloads. @@ -57,7 +57,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 singleton_persistent< +pub fn new_singleton_persistent< T: Serialize + DeserializeOwned + Default + Clone + Send + Sync + PartialEq + 'static, >( init: impl FnOnce() -> T, @@ -65,5 +65,5 @@ pub fn singleton_persistent< let caller = std::panic::Location::caller(); let key = format!("{}:{}", caller.file(), caller.line()); log::trace!("singleton_persistent key: \"{}\"", key); - persistent(key, init) + new_persistent(key, init) } From 2b2e9a99276ea86c45bf278534558f6a4881c1e8 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 6 Nov 2023 13:44:46 -0600 Subject: [PATCH 063/108] fix storage example import --- src/storage/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 0618295..a64f650 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -2,7 +2,7 @@ //! A library for handling local storage ergonomically in Dioxus //! ## Usage //! ```rust -//! use dioxus_storage::use_storage; +//! use dioxus_std::storage::use_storage; //! use dioxus::prelude::*; //! fn main() { //! dioxus_web::launch(app) From c29a1c303768c8be3124e137cabccafe329184ff Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 6 Nov 2023 13:52:50 -0600 Subject: [PATCH 064/108] add use_storage example --- src/storage/mod.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index a64f650..ea61588 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -46,9 +46,22 @@ use tokio::sync::watch::{Receiver, Sender}; #[cfg(not(target_family = "wasm"))] pub use client_storage::{set_dir, set_dir_name, set_directory}; -/// A storage hook that can be used to store data that will persist across application reloads. +/// A storage hook that can be used to store data that will persist across application reloads. This hook is generic over the storage location which can be useful for other hooks. /// /// This hook returns a Signal that can be used to read and modify the state. +/// +/// ## Usage +/// +/// ```rust +/// use dioxus_std::storage::{use_storage, StorageBacking}; +/// 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(cx: &ScopeState) -> Signal where S: StorageBacking { +/// use_storage::(cx, "user-id", || 123) +/// } +/// ``` pub fn use_storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal where S: StorageBacking, From f0ae9757d6b0667aef9b72b15dfbf41873d4bd43 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 6 Nov 2023 13:55:06 -0600 Subject: [PATCH 065/108] add example to new_storage --- src/storage/mod.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index ea61588..4bed140 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -74,6 +74,19 @@ where /// Creates a Signal that can be used to store data that will persist across application reloads. /// /// This hook returns a Signal that can be used to read and modify the state. +/// +/// ## Usage +/// +/// ```rust +/// use dioxus_std::storage::{use_storage, StorageBacking}; +/// 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(cx: &ScopeState) -> Signal where S: StorageBacking { +/// new_storage::(cx, "user-id", || 123) +/// } +/// ``` pub fn new_storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal where S: StorageBacking, From 1fb405cc5b188034e2db0f2abacc3d036920fd8c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 6 Nov 2023 13:57:35 -0600 Subject: [PATCH 066/108] fix some lints --- src/storage/client_storage/memory.rs | 18 ++++---- src/storage/mod.rs | 65 ++++++++++++++-------------- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/src/storage/client_storage/memory.rs b/src/storage/client_storage/memory.rs index c79cb63..91b4b02 100644 --- a/src/storage/client_storage/memory.rs +++ b/src/storage/client_storage/memory.rs @@ -1,7 +1,10 @@ use std::any::Any; +use std::collections::HashMap; use std::ops::{Deref, DerefMut}; +use std::rc::Rc; use std::sync::Arc; -use std::{collections::HashMap, sync::RwLock}; + +use dioxus::prelude::RefCell; use crate::storage::StorageBacking; @@ -13,15 +16,12 @@ impl StorageBacking for SessionStorage { fn set(key: String, value: &T) { let session = SessionStore::get_current_session(); - session - .write() - .unwrap() - .insert(key, Arc::new(value.clone())); + session.borrow_mut().insert(key, Arc::new(value.clone())); } fn get(key: &String) -> Option { let session = SessionStore::get_current_session(); - let read_binding = session.read().unwrap(); + let read_binding = session.borrow(); let value_any = read_binding.get(key)?; value_any.downcast_ref::().cloned() } @@ -31,13 +31,13 @@ impl StorageBacking for SessionStorage { #[derive(Clone)] struct SessionStore { /// The underlying map of session data. - map: Arc>>>, + map: Rc>>>, } impl SessionStore { fn new() -> Self { Self { - map: Arc::new(RwLock::new(HashMap::>::new())), + map: Rc::new(RefCell::new(HashMap::>::new())), } } @@ -56,7 +56,7 @@ impl SessionStore { } impl Deref for SessionStore { - type Target = Arc>>>; + type Target = Rc>>>; fn deref(&self) -> &Self::Target { &self.map diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 4bed140..96c93b9 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -49,14 +49,14 @@ pub use client_storage::{set_dir, set_dir_name, set_directory}; /// A storage hook that can be used to store data that will persist across application reloads. This hook is generic over the storage location which can be useful for other hooks. /// /// This hook returns a Signal that can be used to read and modify the state. -/// +/// /// ## Usage -/// +/// /// ```rust /// use dioxus_std::storage::{use_storage, StorageBacking}; /// 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(cx: &ScopeState) -> Signal where S: StorageBacking { /// use_storage::(cx, "user-id", || 123) @@ -74,14 +74,14 @@ where /// Creates a Signal that can be used to store data that will persist across application reloads. /// /// This hook returns a Signal that can be used to read and modify the state. -/// +/// /// ## Usage -/// +/// /// ```rust /// use dioxus_std::storage::{use_storage, StorageBacking}; /// 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(cx: &ScopeState) -> Signal where S: StorageBacking { /// new_storage::(cx, "user-id", || 123) @@ -94,32 +94,30 @@ where S::Key: Clone, { let mut init = Some(init); - let signal = { - if cfg!(feature = "ssr") { - // SSR does not support storage on the backend. We will just use a normal Signal to represent the initial state. - // The client will hydrate this with a correct StorageEntry and maintain state. - Signal::new(init.take().unwrap()()) - } else if cfg!(feature = "hydrate") { - let key_clone = key.clone(); - let storage_entry = new_storage_entry::(key, init.take().unwrap()); - if cx.generation() == 0 { - // The first generation is rendered on the server side and so must be hydrated. - cx.needs_update(); - } - if cx.generation() == 1 { - // The first time the vdom is hydrated, we set the correct value from storage and set up the subscription to storage events. - storage_entry.set(get_from_storage::(key_clone, init.take().unwrap())); - storage_entry.save_to_storage_on_change(); - } - storage_entry.data - } else { - // The client is rendered normally, so we can just use the storage entry. - let storage_entry = new_storage_entry::( key, init.take().unwrap()); + + if cfg!(feature = "ssr") { + // SSR does not support storage on the backend. We will just use a normal Signal to represent the initial state. + // The client will hydrate this with a correct StorageEntry and maintain state. + Signal::new(init.take().unwrap()()) + } else if cfg!(feature = "hydrate") { + let key_clone = key.clone(); + let storage_entry = new_storage_entry::(key, init.take().unwrap()); + if cx.generation() == 0 { + // The first generation is rendered on the server side and so must be hydrated. + cx.needs_update(); + } + if cx.generation() == 1 { + // The first time the vdom is hydrated, we set the correct value from storage and set up the subscription to storage events. + storage_entry.set(get_from_storage::(key_clone, init.take().unwrap())); storage_entry.save_to_storage_on_change(); - storage_entry.data } - }; - signal + storage_entry.data + } else { + // The client is rendered normally, so we can just use the storage entry. + let storage_entry = new_storage_entry::(key, init.take().unwrap()); + storage_entry.save_to_storage_on_change(); + storage_entry.data + } } /// A storage hook that can be used to store data that will persist across application reloads and be synced across all app sessions for a given installation or browser. @@ -220,7 +218,10 @@ where /// Returns a synced StorageEntry with the latest value from storage or the init value if it doesn't exist. /// /// 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, init: impl FnOnce() -> T) -> SyncedStorageEntry +pub fn new_synced_storage_entry( + key: S::Key, + init: impl FnOnce() -> T, +) -> SyncedStorageEntry where S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, @@ -272,7 +273,7 @@ pub trait StorageEntryTrait: let old = Signal::new(self.data().value()); let data = *self.data(); Effect::new(move || { - if &*old() != &*data() { + if *old() != *data() { log::trace!("state value changed, trying to save"); entry_clone.save(); } From 7a99a3e0f21696c0fbdcd76cbfc34898d616768d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 6 Nov 2023 14:01:34 -0600 Subject: [PATCH 067/108] remove storage from the default features --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b95c609..d7deed9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ utils = ["dep:async-broadcast", "dep:uuid"] i18n = ["dep:serde", "dep:serde_json", "dep:unic-langid"] storage = ["dep:dioxus-signals", "dep:serde", "dep:postcard", "dep:once_cell", "dep:web-sys", "web-sys/Document", "web-sys/Element", "web-sys/HtmlDocument", "web-sys/Storage", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:directories", "dep:yazi", "dep:tokio"] all = ["clipboard", "notifications", "geolocation", "color_scheme", "utils", "i18n", "storage"] -default = ["storage"] +default = [] # CI testing wasm-testing = ["geolocation", "color_scheme", "utils", "i18n"] From 22c03cbd5e31a595e4b205e5cca6080bf38fd571 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 8 Nov 2023 08:18:53 -0600 Subject: [PATCH 068/108] update some out of date readmes --- examples/README.md | 5 ++++- examples/storage_desktop/README.md | 7 ++++--- examples/storage_web/README.md | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/README.md b/examples/README.md index d1673f7..fbad836 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,4 +10,7 @@ Learn how to use the `geolocation` abstraction. Learn how to use the `i18n` abstraction. ### [`channel`](./channel/) -Learn how to use the `channel` abstraction. \ No newline at end of file +Learn how to use the `channel` abstraction. + +### [`storage`](./storage/) +Learn how to use the `storage` abstraction. diff --git a/examples/storage_desktop/README.md b/examples/storage_desktop/README.md index 2bcc6fb..15ffa8a 100644 --- a/examples/storage_desktop/README.md +++ b/examples/storage_desktop/README.md @@ -1,7 +1,8 @@ -# use_persistant +# use_persistent (Desktop) -Learn how to use `use_persistant`. + +Use persistent allows you to create data that will persist across sessions. This example will teach you how to use the `use_persistant` hook. Run: -```dioxus serve``` \ No newline at end of file +```dioxus serve --platform desktop``` diff --git a/examples/storage_web/README.md b/examples/storage_web/README.md index 2bcc6fb..2cea929 100644 --- a/examples/storage_web/README.md +++ b/examples/storage_web/README.md @@ -1,6 +1,6 @@ -# use_persistant +# use_persistent (Web) -Learn how to use `use_persistant`. +Use persistent allows you to create data that will persist across sessions. This example will teach you how to use the `use_persistent` hook. Run: From 178aa78bdc426461023c705f6e1e6cede29036cb Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 8 Nov 2023 08:21:27 -0600 Subject: [PATCH 069/108] export new_ variants of hooks --- src/storage/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 96c93b9..5876508 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -30,7 +30,7 @@ mod client_storage; mod persistence; pub use client_storage::{LocalStorage, SessionStorage}; -pub use persistence::{use_persistent, use_singleton_persistent}; +pub use persistence::{use_persistent, use_singleton_persistent, new_persistent, new_singleton_persistent}; use dioxus::prelude::{current_scope_id, to_owned, ScopeState}; use dioxus_signals::{Effect, Signal}; From 2e2c00d3be7ed073ad4361017afa8334f36fec79 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 8 Nov 2023 08:22:55 -0600 Subject: [PATCH 070/108] log serialization errors --- src/storage/mod.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 5876508..fd76b84 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -587,7 +587,18 @@ pub(crate) fn try_serde_from_string(value: &str) -> Option< let n2 = c2.to_digit(16)?; bytes.push((n1 * 16 + n2) as u8); } - let (decompressed, _) = yazi::decompress(&bytes, yazi::Format::Zlib).ok()?; - postcard::from_bytes(&decompressed).ok() + match yazi::decompress(&bytes, yazi::Format::Zlib) { + Ok((decompressed, _)) => match postcard::from_bytes(&decompressed) { + Ok(v) => Some(v), + Err(err) => { + tracing::error!("Error deserializing value from storage: {:?}", err); + None + } + }, + Err(err) => { + tracing::error!("Error decompressing value from storage: {:?}", err); + None + } + } } // End helper functions From 72d2346e2502ddae12b0a4bf46f37bae3d59aaac Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 8 Nov 2023 08:25:28 -0600 Subject: [PATCH 071/108] fix formatting --- src/storage/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index fd76b84..184c9cb 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -30,7 +30,9 @@ mod client_storage; mod persistence; pub use client_storage::{LocalStorage, SessionStorage}; -pub use persistence::{use_persistent, use_singleton_persistent, new_persistent, new_singleton_persistent}; +pub use persistence::{ + new_persistent, new_singleton_persistent, use_persistent, use_singleton_persistent, +}; use dioxus::prelude::{current_scope_id, to_owned, ScopeState}; use dioxus_signals::{Effect, Signal}; From 9d79bb9a89a46668cd143732ea623fb024e4be5e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 8 Nov 2023 09:00:55 -0600 Subject: [PATCH 072/108] add storage to the tested features --- .github/workflows/rust.yml | 2 +- Cargo.toml | 4 ++-- src/storage/mod.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 821ae47..a32602c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -33,4 +33,4 @@ jobs: - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - run: rustup component add clippy - uses: actions/checkout@v3 - - run: cargo clippy --workspace --examples --tests -- -D warnings + - run: cargo clippy --workspace --examples --tests --all-features -- -D warnings diff --git a/Cargo.toml b/Cargo.toml index d7deed9..146fc7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ all = ["clipboard", "notifications", "geolocation", "color_scheme", "utils", "i1 default = [] # CI testing -wasm-testing = ["geolocation", "color_scheme", "utils", "i18n"] -desktop-testing = ["clipboard", "notifications", "geolocation", "utils", "i18n"] +wasm-testing = ["geolocation", "color_scheme", "utils", "i18n", "storage"] +desktop-testing = ["clipboard", "notifications", "geolocation", "utils", "i18n", "storage"] [dependencies] dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 184c9cb..e69f3c2 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -593,12 +593,12 @@ pub(crate) fn try_serde_from_string(value: &str) -> Option< Ok((decompressed, _)) => match postcard::from_bytes(&decompressed) { Ok(v) => Some(v), Err(err) => { - tracing::error!("Error deserializing value from storage: {:?}", err); + log::error!("Error deserializing value from storage: {:?}", err); None } }, Err(err) => { - tracing::error!("Error decompressing value from storage: {:?}", err); + log::error!("Error decompressing value from storage: {:?}", err); None } } From 0edb1653df778f5dfbb41563790c4089f391025f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 8 Nov 2023 18:14:18 -0600 Subject: [PATCH 073/108] fix dioxus storage tests --- src/geolocation/mod.rs | 8 ++++++++ src/storage/client_storage/fs.rs | 16 +++++++++++----- src/storage/mod.rs | 9 +++------ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/geolocation/mod.rs b/src/geolocation/mod.rs index ce0cc1d..67549e7 100644 --- a/src/geolocation/mod.rs +++ b/src/geolocation/mod.rs @@ -1,6 +1,14 @@ +#[cfg(any(windows, target_family = "wasm"))] mod core; +#[cfg(any(windows, target_family = "wasm"))] mod platform; +#[cfg(any(windows, target_family = "wasm"))] mod use_geolocation; +#[cfg(any(windows, target_family = "wasm"))] pub use self::core::*; +#[cfg(any(windows, target_family = "wasm"))] pub use self::use_geolocation::*; + +#[cfg(not(any(windows, target_family = "wasm")))] +compile_error!("The geolocation module is not supported on this platform."); diff --git a/src/storage/client_storage/fs.rs b/src/storage/client_storage/fs.rs index 0046fef..b418177 100644 --- a/src/storage/client_storage/fs.rs +++ b/src/storage/client_storage/fs.rs @@ -12,22 +12,28 @@ use crate::storage::{serde_to_string, try_serde_from_string, StorageBacking, Sto /// Set the directory where the storage files are located on non-wasm targets. /// /// ```rust +/// use dioxus_std::set_dir; +/// /// fn main(){ /// // set the directory to the default location /// set_dir!(); +/// } +/// ``` +/// ```rust +/// use dioxus_std::set_dir; +/// +/// fn main(){ /// // set the directory to a custom location -/// set_dir!(PathBuf::from("path/to/dir")); +/// set_dir!("path/to/dir"); /// } /// ``` #[macro_export] macro_rules! set_dir { () => { - extern crate self as storage; - storage::set_dir_name(env!("CARGO_PKG_NAME")); + $crate::storage::set_dir_name(env!("CARGO_PKG_NAME")) }; ($path: literal) => { - extern crate self as storage; - storage::set_directory(std::path::PathBuf::from($path)); + $crate::storage::set_directory(std::path::PathBuf::from($path)) }; } pub use set_dir; diff --git a/src/storage/mod.rs b/src/storage/mod.rs index e69f3c2..2f5639c 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -2,11 +2,8 @@ //! A library for handling local storage ergonomically in Dioxus //! ## Usage //! ```rust -//! use dioxus_std::storage::use_storage; +//! use dioxus_std::storage::use_persistent; //! use dioxus::prelude::*; -//! fn main() { -//! dioxus_web::launch(app) -//! } //! //! fn app(cx: Scope) -> Element { //! let num = use_persistent(cx, "count", || 0); @@ -14,7 +11,7 @@ //! div { //! button { //! onclick: move |_| { -//! num.modify(|num| *num += 1); +//! *num.write() += 1; //! }, //! "Increment" //! } @@ -80,7 +77,7 @@ where /// ## Usage /// /// ```rust -/// use dioxus_std::storage::{use_storage, StorageBacking}; +/// use dioxus_std::storage::{new_storage, StorageBacking}; /// use dioxus::prelude::*; /// use dioxus_signals::Signal; /// From 63fe35e4fd0a49b2c695e42ac798f83e60f66587 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 8 Nov 2023 18:15:42 -0600 Subject: [PATCH 074/108] only enable desktop features for clippy --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a32602c..2621eae 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -33,4 +33,4 @@ jobs: - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - run: rustup component add clippy - uses: actions/checkout@v3 - - run: cargo clippy --workspace --examples --tests --all-features -- -D warnings + - run: cargo clippy --workspace --examples --tests --features clipboard,notifications,utils,i18n,storage -- -D warnings From 9e4e53b05a194522e2cac5705fabad6062fb82e6 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 8 Nov 2023 18:17:29 -0600 Subject: [PATCH 075/108] fix formatting --- src/storage/client_storage/fs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/storage/client_storage/fs.rs b/src/storage/client_storage/fs.rs index b418177..3b1119a 100644 --- a/src/storage/client_storage/fs.rs +++ b/src/storage/client_storage/fs.rs @@ -13,7 +13,7 @@ use crate::storage::{serde_to_string, try_serde_from_string, StorageBacking, Sto /// /// ```rust /// use dioxus_std::set_dir; -/// +/// /// fn main(){ /// // set the directory to the default location /// set_dir!(); @@ -21,7 +21,7 @@ use crate::storage::{serde_to_string, try_serde_from_string, StorageBacking, Sto /// ``` /// ```rust /// use dioxus_std::set_dir; -/// +/// /// fn main(){ /// // set the directory to a custom location /// set_dir!("path/to/dir"); From cf79ae71d300eb4148f20d9c98bf9241301e092b Mon Sep 17 00:00:00 2001 From: marc2332 Date: Tue, 23 Jan 2024 22:37:05 +0100 Subject: [PATCH 076/108] feat: Dioxus 0.5 support --- Cargo.toml | 5 +++- README.md | 8 ++--- examples/channel/Cargo.toml | 4 +-- examples/channel/src/main.rs | 12 ++++---- examples/clipboard/Cargo.toml | 4 +-- examples/clipboard/src/main.rs | 14 ++++----- examples/color_scheme/Cargo.toml | 4 +-- examples/color_scheme/src/main.rs | 6 ++-- examples/geolocation/Cargo.toml | 6 ++-- examples/geolocation/src/main.rs | 10 +++---- examples/i18n/Cargo.toml | 4 +-- examples/i18n/src/main.rs | 25 +++++++--------- std/Cargo.toml | 4 +-- std/src/clipboard/use_clipboard.rs | 17 +++++------ .../use_preferred_color_scheme.rs | 8 ++--- std/src/geolocation/use_geolocation.rs | 15 +++++----- std/src/i18n/use_i18n.rs | 26 ++++++++-------- std/src/i18n/use_init_i18n.rs | 10 ++++--- std/src/utils/channel/use_channel.rs | 9 ++---- std/src/utils/channel/use_listen_channel.rs | 30 ++++++++++--------- std/src/utils/rw/use_rw.rs | 17 ++++------- 21 files changed, 113 insertions(+), 125 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8bde861..cec0b65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,7 @@ members = [ ] [workspace.dependencies] -dioxus-std = { path = "./std"} +dioxus-std = { path = "./std" } +dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "c172914b21211a3196c86ebe75a7b63b7a6a23e0" } +dioxus-web = { git = "https://github.com/DioxusLabs/dioxus", rev = "c172914b21211a3196c86ebe75a7b63b7a6a23e0" } +dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus", rev = "c172914b21211a3196c86ebe75a7b63b7a6a23e0" } \ No newline at end of file diff --git a/README.md b/README.md index 1c39168..02e393f 100644 --- a/README.md +++ b/README.md @@ -55,13 +55,13 @@ fn app(cx: Scope) -> Element { match coords { Ok(coords) => { - render! { p { format!("Latitude: {} | Longitude: {}", coords.latitude, coords.longitude) } } + rsx! { p { format!("Latitude: {} | Longitude: {}", coords.latitude, coords.longitude) } } } Err(Error::NotInitialized) => { - render! { p { "Initializing..." }} + rsx! { p { "Initializing..." }} } Err(e) => { - render! { p { "An error occurred {e}" }} + rsx! { p { "An error occurred {e}" }} } } } @@ -79,7 +79,7 @@ sudo apt-get install xorg-dev You can add `dioxus-std` to your application by adding it to your dependencies. ```toml [dependencies] -dioxus-std = { version = "0.4.2", features = [] } +dioxus-std = { version = "0.5", features = [] } ``` ## License diff --git a/examples/channel/Cargo.toml b/examples/channel/Cargo.toml index 5f21512..9cb8bd3 100644 --- a/examples/channel/Cargo.toml +++ b/examples/channel/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" [dependencies] dioxus-std = { workspace = true, features = ["utils"]} -dioxus = "0.4" -dioxus-web = "0.4" +dioxus = { workspace = true } +dioxus-web = { workspace = true } log = "0.4.6" diff --git a/examples/channel/src/main.rs b/examples/channel/src/main.rs index cc9f7d0..ac96c42 100644 --- a/examples/channel/src/main.rs +++ b/examples/channel/src/main.rs @@ -6,27 +6,27 @@ fn main() { wasm_logger::init(wasm_logger::Config::default()); console_error_panic_hook::set_once(); - dioxus_web::launch(app); + launch(app); } -fn app(cx: Scope) -> Element { - let channel = use_channel::(cx, 5); +fn app() -> Element { + let channel = use_channel::(5); - use_listen_channel(cx, &channel, |message| async { + use_listen_channel(&channel, |message| async { match message { Ok(value) => log::info!("Incoming message: {value}"), Err(err) => log::info!("Error: {err:?}"), } }); - let send = |_: MouseEvent| { + let send = move |_: MouseEvent| { to_owned![channel]; async move { channel.send("Hello").await.ok(); } }; - render!( + rsx!( button { onclick: send, "Send hello" diff --git a/examples/clipboard/Cargo.toml b/examples/clipboard/Cargo.toml index 9f29e7e..465b3c0 100644 --- a/examples/clipboard/Cargo.toml +++ b/examples/clipboard/Cargo.toml @@ -5,5 +5,5 @@ edition = "2021" [dependencies] dioxus-std = { workspace = true, features = ["clipboard"] } -dioxus = "0.4" -dioxus-desktop = "0.4" +dioxus = { workspace = true } +dioxus-desktop = { workspace = true } diff --git a/examples/clipboard/src/main.rs b/examples/clipboard/src/main.rs index e3c7d68..0a9a239 100644 --- a/examples/clipboard/src/main.rs +++ b/examples/clipboard/src/main.rs @@ -5,18 +5,18 @@ fn main() { dioxus_desktop::launch(app); } -fn app(cx: Scope) -> Element { - let clipboard = use_clipboard(cx); - let text = use_state(cx, String::new); +fn app() -> Element { + let clipboard = use_clipboard(); + let mut text = use_signal(String::new); - let oninput = |e: FormEvent| { + let oninput = move |e: FormEvent| { text.set(e.data.value.clone()); }; let oncopy = { to_owned![clipboard]; - move |_| match clipboard.set(text.get().clone()) { - Ok(_) => println!("Copied to clipboard: {}", text.get()), + move |_| match clipboard.set(text.read().clone()) { + Ok(_) => println!("Copied to clipboard: {}", text.read()), Err(err) => println!("Error on copy: {err:?}"), } }; @@ -29,7 +29,7 @@ fn app(cx: Scope) -> Element { Err(err) => println!("Error on paste: {err:?}"), }; - render!( + rsx!( input { oninput: oninput, value: "{text}" diff --git a/examples/color_scheme/Cargo.toml b/examples/color_scheme/Cargo.toml index d335619..deda12c 100644 --- a/examples/color_scheme/Cargo.toml +++ b/examples/color_scheme/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" [dependencies] dioxus-std = { workspace = true, features = ["color_scheme"] } -dioxus = "0.4" -dioxus-web = "0.4" +dioxus = { workspace = true } +dioxus-web = { workspace = true } log = "0.4.6" diff --git a/examples/color_scheme/src/main.rs b/examples/color_scheme/src/main.rs index 6e9c59d..4f5fed5 100644 --- a/examples/color_scheme/src/main.rs +++ b/examples/color_scheme/src/main.rs @@ -9,10 +9,10 @@ fn main() { dioxus_web::launch(app); } -fn app(cx: Scope) -> Element { - let color_scheme = use_preferred_color_scheme(cx); +fn app() -> Element { + let color_scheme = use_preferred_color_scheme(); - render!( + rsx!( div { style: "text-align: center;", h1 { "🌗 Dioxus 🚀" } diff --git a/examples/geolocation/Cargo.toml b/examples/geolocation/Cargo.toml index 904d72a..deeb997 100644 --- a/examples/geolocation/Cargo.toml +++ b/examples/geolocation/Cargo.toml @@ -5,6 +5,6 @@ edition = "2021" [dependencies] dioxus-std = { workspace = true, features = ["geolocation"] } -dioxus = "0.4" -dioxus-desktop = "0.4" -#dioxus-web = "0.4" +dioxus = { workspace = true } +dioxus-desktop ={ workspace = true } +# dioxus-web ={ workspace = true } diff --git a/examples/geolocation/src/main.rs b/examples/geolocation/src/main.rs index 8969974..a8f9930 100644 --- a/examples/geolocation/src/main.rs +++ b/examples/geolocation/src/main.rs @@ -6,12 +6,10 @@ fn main() { //dioxus_web::launch(app); } -fn app(cx: Scope) -> Element { - let geolocator = init_geolocator(cx, PowerMode::High).unwrap(); - let initial_coords = use_future(cx, (), |_| async move { - geolocator.get_coordinates().await.unwrap() - }); - let latest_coords = use_geolocation(cx); +fn app() -> Element { + let geolocator = init_geolocator(PowerMode::High).unwrap(); + let initial_coords = use_future(|_| async move { geolocator.get_coordinates().await.unwrap() }); + let latest_coords = use_geolocation(); let latest_coords = match latest_coords { Ok(v) => v, diff --git a/examples/i18n/Cargo.toml b/examples/i18n/Cargo.toml index 71ec68c..b023daa 100644 --- a/examples/i18n/Cargo.toml +++ b/examples/i18n/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" [dependencies] dioxus-std = { workspace = true, features = ["i18n"] } -dioxus = "0.4" -dioxus-web = "0.4" +dioxus = { workspace = true } +dioxus-web = { workspace = true } log = "0.4.6" diff --git a/examples/i18n/src/main.rs b/examples/i18n/src/main.rs index 3970ed6..b729a51 100644 --- a/examples/i18n/src/main.rs +++ b/examples/i18n/src/main.rs @@ -15,13 +15,13 @@ static EN_US: &str = include_str!("./en-US.json"); static ES_ES: &str = include_str!("./es-ES.json"); #[allow(non_snake_case)] -fn Body(cx: Scope) -> Element { - let i18 = use_i18(cx); +fn Body() -> Element { + let i18 = use_i18(); let change_to_english = move |_| i18.set_language("en-US".parse().unwrap()); let change_to_spanish = move |_| i18.set_language("es-ES".parse().unwrap()); - render!( + rsx!( button { onclick: change_to_english, label { @@ -40,16 +40,11 @@ fn Body(cx: Scope) -> Element { } fn app(cx: Scope) -> Element { - use_init_i18n( - cx, - "en-US".parse().unwrap(), - "en-US".parse().unwrap(), - || { - let en_us = Language::from_str(EN_US).unwrap(); - let es_es = Language::from_str(ES_ES).unwrap(); - vec![en_us, es_es] - }, - ); - - render!(Body {}) + use_init_i18n("en-US".parse().unwrap(), "en-US".parse().unwrap(), || { + let en_us = Language::from_str(EN_US).unwrap(); + let es_es = Language::from_str(ES_ES).unwrap(); + vec![en_us, es_es] + }); + + rsx!(Body {}) } diff --git a/std/Cargo.toml b/std/Cargo.toml index f7b3658..12b9866 100644 --- a/std/Cargo.toml +++ b/std/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-std" -version = "0.4.2" +version = "0.5.0" authors = ["Jonathan Kelley", "Dioxus Labs", "ealmloff", "DogeDark", "marc2332"] edition = "2021" description = "Platform agnostic library for supercharging your productivity with Dioxus" @@ -25,7 +25,7 @@ wasm-testing = ["geolocation", "color_scheme", "utils", "i18n"] desktop-testing = ["clipboard", "notifications", "geolocation", "utils", "i18n"] [dependencies] -dioxus = { version = "0.4" } +dioxus = { workspace = true } cfg-if = "1.0.0" # utils diff --git a/std/src/clipboard/use_clipboard.rs b/std/src/clipboard/use_clipboard.rs index e90d631..449ddd8 100644 --- a/std/src/clipboard/use_clipboard.rs +++ b/std/src/clipboard/use_clipboard.rs @@ -1,8 +1,7 @@ //! Provides a clipboard abstraction to access the target system's clipboard. use copypasta::{ClipboardContext, ClipboardProvider}; -use dioxus::prelude::{RefCell, ScopeState}; -use std::rc::Rc; +use dioxus::prelude::*; #[derive(Debug, PartialEq, Clone)] pub enum ClipboardError { @@ -13,14 +12,14 @@ pub enum ClipboardError { /// Handle to access the ClipboardContext. #[derive(Clone)] pub struct UseClipboard { - clipboard: Rc>, + clipboard: Signal, } impl UseClipboard { // Read from the clipboard pub fn get(&self) -> Result { self.clipboard - .borrow_mut() + .read() .get_contents() .map_err(|_| ClipboardError::FailedToRead) } @@ -28,7 +27,7 @@ impl UseClipboard { // Write to the clipboard pub fn set(&self, contents: String) -> Result<(), ClipboardError> { self.clipboard - .borrow_mut() + .write() .set_contents(contents) .map_err(|_| ClipboardError::FailedToSet) } @@ -42,7 +41,7 @@ impl UseClipboard { /// use dioxus_std::clipboard::use_clipboard; /// /// // Get a handle to the clipboard -/// let clipboard = use_clipboard(cx); +/// let clipboard = use_clipboard(); /// /// // Read the clipboard content /// if let Ok(content) = clipboard.get() { @@ -53,12 +52,12 @@ impl UseClipboard { /// clipboard.set("Hello, Dioxus!".to_string());; /// /// ``` -pub fn use_clipboard(cx: &ScopeState) -> UseClipboard { - let clipboard = match cx.consume_context() { +pub fn use_clipboard() -> UseClipboard { + let clipboard = match consume_context() { Some(rt) => rt, None => { let clipboard = ClipboardContext::new().expect("Cannot create Clipboard."); - cx.provide_root_context(Rc::new(RefCell::new(clipboard))) + provide_root_context(Signal::new(clipboard)).unwrap() } }; UseClipboard { clipboard } diff --git a/std/src/color_scheme/use_preferred_color_scheme.rs b/std/src/color_scheme/use_preferred_color_scheme.rs index 23317f2..8dce8c9 100644 --- a/std/src/color_scheme/use_preferred_color_scheme.rs +++ b/std/src/color_scheme/use_preferred_color_scheme.rs @@ -1,5 +1,5 @@ //! Provides a hook to access the user's preferred color scheme. -use dioxus::prelude::ScopeState; +use dioxus::prelude::*; use std::{fmt, sync::Once}; use wasm_bindgen::{prelude::Closure, JsCast}; @@ -13,9 +13,7 @@ pub enum PreferredColorScheme { static INIT: Once = Once::new(); /// Retrieves (as well as listens for changes) to the user's preferred color scheme (dark or light) so your application can adapt accordingly. -pub fn use_preferred_color_scheme( - cx: &ScopeState, -) -> Result { +pub fn use_preferred_color_scheme() -> Result { // This code is kinda messy.. let window = match web_sys::window() { Some(w) => w, @@ -44,7 +42,7 @@ pub fn use_preferred_color_scheme( } }; - let update_callback = cx.schedule_update(); + let update_callback = schedule_update(); // Create closure that listens to the event of matchMedia and calls write to scheme INIT.call_once(|| { diff --git a/std/src/geolocation/use_geolocation.rs b/std/src/geolocation/use_geolocation.rs index 6588690..2741ca0 100644 --- a/std/src/geolocation/use_geolocation.rs +++ b/std/src/geolocation/use_geolocation.rs @@ -1,7 +1,7 @@ //! Provides an initialization and use_geolocation hook. use super::core::{Error, Event, Geocoordinates, Geolocator, PowerMode, Status}; -use dioxus::prelude::{use_coroutine, ScopeState, UnboundedReceiver}; +use dioxus::prelude::{provide_context, try_consume_context, use_coroutine, UnboundedReceiver}; use futures_util::stream::StreamExt; use std::{rc::Rc, sync::Once}; @@ -10,13 +10,12 @@ use crate::utils::rw::{use_rw, UseRw}; static INIT: Once = Once::new(); /// Provides the latest geocoordinates. Good for navigation-type apps. -pub fn use_geolocation(cx: &ScopeState) -> Result { +pub fn use_geolocation() -> Result { // Store the coords - let coords: &mut UseRw> = - use_rw(cx, || Err(Error::NotInitialized)); + let coords: UseRw> = use_rw(|| Err(Error::NotInitialized)); // Get geolocator - let geolocator = match cx.consume_context::>() { + let geolocator = match try_consume_context::>() { Some(v) => v, None => return Err(Error::NotInitialized), }; @@ -24,7 +23,7 @@ pub fn use_geolocation(cx: &ScopeState) -> Result { let coords_cloned = coords.clone(); // Initialize the handler of events - let listener = use_coroutine(cx, |mut rx: UnboundedReceiver| async move { + let listener = use_coroutine(|mut rx: UnboundedReceiver| async move { while let Some(event) = rx.next().await { match event { Event::NewGeocoordinates(new_coords) => { @@ -48,9 +47,9 @@ pub fn use_geolocation(cx: &ScopeState) -> Result { } /// Must be called before any use of the geolocation abstraction. -pub fn init_geolocator(cx: &ScopeState, power_mode: PowerMode) -> Result, Error> { +pub fn init_geolocator(power_mode: PowerMode) -> Result, Error> { let geolocator = Geolocator::new(power_mode)?; let shared_locator = Rc::new(geolocator); - cx.provide_context(shared_locator.clone()); + provide_context(shared_locator.clone()); Ok(shared_locator) } diff --git a/std/src/i18n/use_i18n.rs b/std/src/i18n/use_i18n.rs index 141b609..9d3a8e4 100644 --- a/std/src/i18n/use_i18n.rs +++ b/std/src/i18n/use_i18n.rs @@ -74,13 +74,13 @@ impl Language { } } -#[derive(Clone, Copy)] -pub struct UseI18<'a> { - pub selected_language: &'a UseSharedState, - pub data: &'a UseSharedState, +#[derive(Clone, PartialEq)] +pub struct UseI18 { + pub selected_language: Signal, + pub data: Signal, } -impl<'a> UseI18<'a> { +impl UseI18 { pub fn translate_with_params(&self, id: &str, params: HashMap<&str, String>) -> String { let i18n_data = self.data.read(); @@ -111,12 +111,14 @@ impl<'a> UseI18<'a> { } } -pub fn use_i18(cx: &ScopeState) -> UseI18 { - let selected_language = use_shared_state::(cx).unwrap(); - let data = use_shared_state::(cx).unwrap(); +pub fn use_i18() -> UseI18 { + use_hook(|| { + let selected_language = consume_context::>(); + let data = consume_context::>(); - UseI18 { - selected_language, - data, - } + UseI18 { + selected_language, + data, + } + }) } diff --git a/std/src/i18n/use_init_i18n.rs b/std/src/i18n/use_init_i18n.rs index f4054c7..49b320e 100644 --- a/std/src/i18n/use_init_i18n.rs +++ b/std/src/i18n/use_init_i18n.rs @@ -9,14 +9,16 @@ pub struct UseInitI18Data { } pub fn use_init_i18n( - cx: &ScopeState, selected_language: LanguageIdentifier, fallback_language: LanguageIdentifier, languages: impl FnOnce() -> Vec, ) { - use_shared_state_provider(cx, || selected_language); - use_shared_state_provider(cx, || UseInitI18Data { + let selected_language = use_signal(|| selected_language); + let init_i18_data = use_signal(|| UseInitI18Data { languages: languages(), fallback_language, - }) + }); + + provide_context(selected_language); + provide_context(init_i18_data); } diff --git a/std/src/utils/channel/use_channel.rs b/std/src/utils/channel/use_channel.rs index 6130dc4..642f276 100644 --- a/std/src/utils/channel/use_channel.rs +++ b/std/src/utils/channel/use_channel.rs @@ -1,5 +1,5 @@ use async_broadcast::{broadcast, InactiveReceiver, Receiver, SendError, Sender, TrySendError}; -use dioxus::prelude::ScopeState; +use dioxus::prelude::*; use uuid::Uuid; /// Send and listen for messages between multiple components. @@ -35,11 +35,8 @@ impl UseChannel { } /// Send and listen for messages between multiple components. -pub fn use_channel( - cx: &ScopeState, - size: usize, -) -> &UseChannel { - cx.use_hook(|| { +pub fn use_channel(size: usize) -> UseChannel { + use_hook(|| { let id = Uuid::new_v4(); let (sender, receiver) = broadcast::(size); UseChannel { diff --git a/std/src/utils/channel/use_listen_channel.rs b/std/src/utils/channel/use_listen_channel.rs index 4e2dec7..7c89ad6 100644 --- a/std/src/utils/channel/use_listen_channel.rs +++ b/std/src/utils/channel/use_listen_channel.rs @@ -1,7 +1,7 @@ -use std::future::Future; +use std::{future::Future, rc::Rc}; use async_broadcast::RecvError; -use dioxus::prelude::{use_effect, ScopeState}; +use dioxus::prelude::*; use super::UseChannel; @@ -9,22 +9,24 @@ pub type UseListenChannelError = RecvError; /// Create a messages listener for the given channel. pub fn use_listen_channel( - cx: &ScopeState, channel: &UseChannel, action: impl Fn(Result) -> Handler + 'static, ) where Handler: Future + 'static, { - use_effect(cx, (channel,), move |(mut channel,)| async move { - let mut receiver = channel.receiver(); - - loop { - let message = receiver.recv().await; - let message_err = message.clone().err(); - action(message).await; - if message_err == Some(UseListenChannelError::Closed) { - break; - } - } + let action = use_hook(|| Rc::new(action)); + use_memo_with_dependencies((channel,), move |(mut channel,)| { + to_owned![action]; + spawn(async move { + let mut receiver = channel.receiver(); + loop { + let message = receiver.recv().await; + let message_err = message.clone().err(); + action(message).await; + if message_err == Some(UseListenChannelError::Closed) { + break; + } + } + }) }); } diff --git a/std/src/utils/rw/use_rw.rs b/std/src/utils/rw/use_rw.rs index ec0d8e1..54cced6 100644 --- a/std/src/utils/rw/use_rw.rs +++ b/std/src/utils/rw/use_rw.rs @@ -1,24 +1,17 @@ //! Essentially the use_ref hook except Send + Sync using Arc and RwLock. +use dioxus::prelude::*; use std::sync::{Arc, RwLock, RwLockReadGuard}; -use dioxus::prelude::ScopeState; - -pub fn use_rw( - cx: &ScopeState, - init_rw: impl FnOnce() -> T, -) -> &mut UseRw { - let hook = cx.use_hook(|| UseRw { - update: cx.schedule_update(), +pub fn use_rw(init_rw: impl FnOnce() -> T) -> UseRw { + use_hook(|| UseRw { + update: schedule_update(), value: Arc::new(RwLock::new(init_rw())), - }); - - hook + }) } pub struct UseRw { update: Arc, value: Arc>, - //dirty: Arc>, } impl Clone for UseRw { From 137b4149dc86a648119eef8f331e3a682c2c6b62 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Thu, 25 Jan 2024 16:35:05 +0100 Subject: [PATCH 077/108] updated dioxus --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cec0b65..7121488 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,6 @@ members = [ [workspace.dependencies] dioxus-std = { path = "./std" } -dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "c172914b21211a3196c86ebe75a7b63b7a6a23e0" } -dioxus-web = { git = "https://github.com/DioxusLabs/dioxus", rev = "c172914b21211a3196c86ebe75a7b63b7a6a23e0" } -dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus", rev = "c172914b21211a3196c86ebe75a7b63b7a6a23e0" } \ No newline at end of file +dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be" } +dioxus-web = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be" } +dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be" } \ No newline at end of file From cde6452ce683a3276381a2b5b0c9a8b72d720974 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 26 Jan 2024 09:47:24 +0100 Subject: [PATCH 078/108] fix use_clipboard --- std/src/clipboard/use_clipboard.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/std/src/clipboard/use_clipboard.rs b/std/src/clipboard/use_clipboard.rs index 449ddd8..b75b6e2 100644 --- a/std/src/clipboard/use_clipboard.rs +++ b/std/src/clipboard/use_clipboard.rs @@ -19,7 +19,7 @@ impl UseClipboard { // Read from the clipboard pub fn get(&self) -> Result { self.clipboard - .read() + .write() .get_contents() .map_err(|_| ClipboardError::FailedToRead) } From 191edf0eacc5034388d041fc9b91dc7db6cfd669 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 26 Jan 2024 09:51:26 +0100 Subject: [PATCH 079/108] fix use_clipboard again --- std/src/clipboard/use_clipboard.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/std/src/clipboard/use_clipboard.rs b/std/src/clipboard/use_clipboard.rs index b75b6e2..2796629 100644 --- a/std/src/clipboard/use_clipboard.rs +++ b/std/src/clipboard/use_clipboard.rs @@ -53,7 +53,7 @@ impl UseClipboard { /// /// ``` pub fn use_clipboard() -> UseClipboard { - let clipboard = match consume_context() { + let clipboard = match try_consume_context() { Some(rt) => rt, None => { let clipboard = ClipboardContext::new().expect("Cannot create Clipboard."); From 73e2d0cd58dbeb088a694465a1391720a8985c8e Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 26 Jan 2024 12:13:09 +0100 Subject: [PATCH 080/108] better error handling of use_clipboard --- std/src/clipboard/use_clipboard.rs | 131 +++++++++++++++-------------- 1 file changed, 67 insertions(+), 64 deletions(-) diff --git a/std/src/clipboard/use_clipboard.rs b/std/src/clipboard/use_clipboard.rs index 2796629..ecb7f14 100644 --- a/std/src/clipboard/use_clipboard.rs +++ b/std/src/clipboard/use_clipboard.rs @@ -1,64 +1,67 @@ -//! Provides a clipboard abstraction to access the target system's clipboard. - -use copypasta::{ClipboardContext, ClipboardProvider}; -use dioxus::prelude::*; - -#[derive(Debug, PartialEq, Clone)] -pub enum ClipboardError { - FailedToRead, - FailedToSet, -} - -/// Handle to access the ClipboardContext. -#[derive(Clone)] -pub struct UseClipboard { - clipboard: Signal, -} - -impl UseClipboard { - // Read from the clipboard - pub fn get(&self) -> Result { - self.clipboard - .write() - .get_contents() - .map_err(|_| ClipboardError::FailedToRead) - } - - // Write to the clipboard - pub fn set(&self, contents: String) -> Result<(), ClipboardError> { - self.clipboard - .write() - .set_contents(contents) - .map_err(|_| ClipboardError::FailedToSet) - } -} - -/// Access the clipboard. -/// -/// # Examples -/// -/// ```ignore -/// use dioxus_std::clipboard::use_clipboard; -/// -/// // Get a handle to the clipboard -/// let clipboard = use_clipboard(); -/// -/// // Read the clipboard content -/// if let Ok(content) = clipboard.get() { -/// println!("{}", content); -/// } -/// -/// // Write to the clipboard -/// clipboard.set("Hello, Dioxus!".to_string());; -/// -/// ``` -pub fn use_clipboard() -> UseClipboard { - let clipboard = match try_consume_context() { - Some(rt) => rt, - None => { - let clipboard = ClipboardContext::new().expect("Cannot create Clipboard."); - provide_root_context(Signal::new(clipboard)).unwrap() - } - }; - UseClipboard { clipboard } -} +//! Provides a clipboard abstraction to access the target system's clipboard. + +use copypasta::{ClipboardContext, ClipboardProvider}; +use dioxus::prelude::*; + +#[derive(Debug, PartialEq, Clone)] +pub enum ClipboardError { + FailedToRead, + FailedToSet, + NotAvailable, +} + +/// Handle to access the ClipboardContext. +#[derive(Clone)] +pub struct UseClipboard { + clipboard: Signal>, +} + +impl UseClipboard { + // Read from the clipboard + pub fn get(&self) -> Result { + self.clipboard + .write() + .ok_or_else(|| ClipboardError::NotAvailable)? + .get_contents() + .map_err(|_| ClipboardError::FailedToRead) + } + + // Write to the clipboard + pub fn set(&self, contents: String) -> Result<(), ClipboardError> { + self.clipboard + .write() + .ok_or_else(|| ClipboardError::NotAvailable)? + .set_contents(contents) + .map_err(|_| ClipboardError::FailedToSet) + } +} + +/// Access the clipboard. +/// +/// # Examples +/// +/// ```ignore +/// use dioxus_std::clipboard::use_clipboard; +/// +/// // Get a handle to the clipboard +/// let clipboard = use_clipboard(); +/// +/// // Read the clipboard content +/// if let Ok(content) = clipboard.get() { +/// println!("{}", content); +/// } +/// +/// // Write to the clipboard +/// clipboard.set("Hello, Dioxus!".to_string());; +/// +/// ``` +pub fn use_clipboard() -> UseClipboard { + let clipboard = match try_consume_context() { + Some(rt) => rt, + None => { + let clipboard = ClipboardContext::new().ok(); + provide_root_context(Signal::new(clipboard)).unwrap() + } + }; + UseClipboard { clipboard } +} From d0841290a2c1ed43dc687201d5306d4c0e556c71 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 26 Jan 2024 12:13:23 +0100 Subject: [PATCH 081/108] fmt --- std/src/utils/channel/use_listen_channel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/std/src/utils/channel/use_listen_channel.rs b/std/src/utils/channel/use_listen_channel.rs index 7c89ad6..74d68da 100644 --- a/std/src/utils/channel/use_listen_channel.rs +++ b/std/src/utils/channel/use_listen_channel.rs @@ -26,7 +26,7 @@ pub fn use_listen_channel( if message_err == Some(UseListenChannelError::Closed) { break; } - } + } }) }); } From 7b4dbe3c98ec9cb09c2234f2713b0523ae2ddeba Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 26 Jan 2024 12:26:24 +0100 Subject: [PATCH 082/108] fix --- std/src/clipboard/use_clipboard.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/std/src/clipboard/use_clipboard.rs b/std/src/clipboard/use_clipboard.rs index ecb7f14..ab040db 100644 --- a/std/src/clipboard/use_clipboard.rs +++ b/std/src/clipboard/use_clipboard.rs @@ -21,6 +21,7 @@ impl UseClipboard { pub fn get(&self) -> Result { self.clipboard .write() + .as_ref() .ok_or_else(|| ClipboardError::NotAvailable)? .get_contents() .map_err(|_| ClipboardError::FailedToRead) @@ -30,6 +31,7 @@ impl UseClipboard { pub fn set(&self, contents: String) -> Result<(), ClipboardError> { self.clipboard .write() + .as_ref() .ok_or_else(|| ClipboardError::NotAvailable)? .set_contents(contents) .map_err(|_| ClipboardError::FailedToSet) From 6791b1df228173d37e14b90547174497e11abefe Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 26 Jan 2024 12:31:05 +0100 Subject: [PATCH 083/108] fixing it blindly --- std/src/clipboard/use_clipboard.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/std/src/clipboard/use_clipboard.rs b/std/src/clipboard/use_clipboard.rs index ab040db..e32dfdd 100644 --- a/std/src/clipboard/use_clipboard.rs +++ b/std/src/clipboard/use_clipboard.rs @@ -21,7 +21,7 @@ impl UseClipboard { pub fn get(&self) -> Result { self.clipboard .write() - .as_ref() + .as_mut() .ok_or_else(|| ClipboardError::NotAvailable)? .get_contents() .map_err(|_| ClipboardError::FailedToRead) @@ -31,7 +31,7 @@ impl UseClipboard { pub fn set(&self, contents: String) -> Result<(), ClipboardError> { self.clipboard .write() - .as_ref() + .as_mut() .ok_or_else(|| ClipboardError::NotAvailable)? .set_contents(contents) .map_err(|_| ClipboardError::FailedToSet) From b79c38d7fed25dc1ddbf1f41f6b37a452b99b886 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Tue, 6 Feb 2024 22:34:05 +0100 Subject: [PATCH 084/108] chore: Bump dx --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7121488..4261f52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,6 @@ members = [ [workspace.dependencies] dioxus-std = { path = "./std" } -dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be" } -dioxus-web = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be" } -dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be" } \ No newline at end of file +dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "ef101dd876ee8ecdd702a7e22addfd47f2ebd892" } +dioxus-web = { git = "https://github.com/DioxusLabs/dioxus", rev = "ef101dd876ee8ecdd702a7e22addfd47f2ebd892" } +dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus", rev = "ef101dd876ee8ecdd702a7e22addfd47f2ebd892" } \ No newline at end of file From a5ebe58e5d6a061fe2e747aead052247aa664447 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Tue, 6 Feb 2024 22:41:20 +0100 Subject: [PATCH 085/108] Remove unwrap --- std/src/clipboard/use_clipboard.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/std/src/clipboard/use_clipboard.rs b/std/src/clipboard/use_clipboard.rs index e32dfdd..c799be0 100644 --- a/std/src/clipboard/use_clipboard.rs +++ b/std/src/clipboard/use_clipboard.rs @@ -62,7 +62,7 @@ pub fn use_clipboard() -> UseClipboard { Some(rt) => rt, None => { let clipboard = ClipboardContext::new().ok(); - provide_root_context(Signal::new(clipboard)).unwrap() + provide_root_context(Signal::new(clipboard)) } }; UseClipboard { clipboard } From 791a21f67657b43a66baac241b6431eab6c5fd2f Mon Sep 17 00:00:00 2001 From: marc2332 Date: Tue, 6 Feb 2024 22:46:21 +0100 Subject: [PATCH 086/108] chore: Fixes --- examples/clipboard/src/main.rs | 2 +- std/src/clipboard/use_clipboard.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/clipboard/src/main.rs b/examples/clipboard/src/main.rs index 0a9a239..ebad283 100644 --- a/examples/clipboard/src/main.rs +++ b/examples/clipboard/src/main.rs @@ -6,7 +6,7 @@ fn main() { } fn app() -> Element { - let clipboard = use_clipboard(); + let mut clipboard = use_clipboard(); let mut text = use_signal(String::new); let oninput = move |e: FormEvent| { diff --git a/std/src/clipboard/use_clipboard.rs b/std/src/clipboard/use_clipboard.rs index c799be0..9a2f85e 100644 --- a/std/src/clipboard/use_clipboard.rs +++ b/std/src/clipboard/use_clipboard.rs @@ -18,7 +18,7 @@ pub struct UseClipboard { impl UseClipboard { // Read from the clipboard - pub fn get(&self) -> Result { + pub fn get(&mut self) -> Result { self.clipboard .write() .as_mut() @@ -28,7 +28,7 @@ impl UseClipboard { } // Write to the clipboard - pub fn set(&self, contents: String) -> Result<(), ClipboardError> { + pub fn set(&mut self, contents: String) -> Result<(), ClipboardError> { self.clipboard .write() .as_mut() @@ -46,7 +46,7 @@ impl UseClipboard { /// use dioxus_std::clipboard::use_clipboard; /// /// // Get a handle to the clipboard -/// let clipboard = use_clipboard(); +/// let mut clipboard = use_clipboard(); /// /// // Read the clipboard content /// if let Ok(content) = clipboard.get() { From 074c27ab63d9563572ec909eca4e94e6fa50a10b Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 9 Feb 2024 14:57:28 +0100 Subject: [PATCH 087/108] derive copy and clean up --- README.md | 6 +++--- examples/channel/src/main.rs | 7 ++----- examples/clipboard/src/main.rs | 9 +++------ examples/geolocation/src/main.rs | 8 ++++---- examples/i18n/src/main.rs | 2 +- std/src/clipboard/use_clipboard.rs | 2 +- std/src/i18n/use_i18n.rs | 2 +- std/src/utils/channel/use_channel.rs | 20 ++++++++++---------- std/src/utils/channel/use_listen_channel.rs | 4 +--- std/src/utils/rw/use_rw.rs | 21 +++++++++++++-------- 10 files changed, 39 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 02e393f..568ca06 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,9 @@ use dioxus_std::geolocation::{ init_geolocator, use_geolocation, PowerMode }; -fn app(cx: Scope) -> Element { - let geolocator = init_geolocator(cx, PowerMode::High).unwrap(); - let coords = use_geolocation(cx); +fn app() -> Element { + let geolocator = init_geolocator(, PowerMode::High).unwrap(); + let coords = use_geolocation(); match coords { Ok(coords) => { diff --git a/examples/channel/src/main.rs b/examples/channel/src/main.rs index ac96c42..b9dcc77 100644 --- a/examples/channel/src/main.rs +++ b/examples/channel/src/main.rs @@ -19,11 +19,8 @@ fn app() -> Element { } }); - let send = move |_: MouseEvent| { - to_owned![channel]; - async move { - channel.send("Hello").await.ok(); - } + let send = move |_: MouseEvent| async move { + channel.send("Hello").await.ok(); }; rsx!( diff --git a/examples/clipboard/src/main.rs b/examples/clipboard/src/main.rs index ebad283..bcac658 100644 --- a/examples/clipboard/src/main.rs +++ b/examples/clipboard/src/main.rs @@ -13,12 +13,9 @@ fn app() -> Element { text.set(e.data.value.clone()); }; - let oncopy = { - to_owned![clipboard]; - move |_| match clipboard.set(text.read().clone()) { - Ok(_) => println!("Copied to clipboard: {}", text.read()), - Err(err) => println!("Error on copy: {err:?}"), - } + let oncopy = move |_| match clipboard.set(text.read().clone()) { + Ok(_) => println!("Copied to clipboard: {}", text.read()), + Err(err) => println!("Error on copy: {err:?}"), }; let onpaste = move |_| match clipboard.get() { diff --git a/examples/geolocation/src/main.rs b/examples/geolocation/src/main.rs index a8f9930..3d87e9f 100644 --- a/examples/geolocation/src/main.rs +++ b/examples/geolocation/src/main.rs @@ -15,7 +15,7 @@ fn app() -> Element { Ok(v) => v, Err(e) => { let e = format!("Initializing: {:?}", e); - return cx.render(rsx!(p { "{e}" })); + return rsx!(p { "{e}" }); } }; @@ -24,7 +24,7 @@ fn app() -> Element { let initial_coords = initial_coords.value(); - cx.render(rsx! ( + rsx!( div { style: "text-align: center;", h1 { "🗺️ Dioxus Geolocation Example 🛰️" } @@ -32,7 +32,7 @@ fn app() -> Element { p { if let Some(coords) = initial_coords { - format!("Latitude: {} | Longitude: {}", coords.latitude, coords.longitude) + format!("Latitude: {} | Longitude: {}", coords.latitude, coords.longitude) } else { "Loading...".to_string() } @@ -49,5 +49,5 @@ fn app() -> Element { // src: "https://www.google.com/maps/embed/v1/view?key={key}¢er={latest_coords.latitude},{latest_coords.longitude}&zoom=16", //} } - )) + ) } diff --git a/examples/i18n/src/main.rs b/examples/i18n/src/main.rs index b729a51..7420d26 100644 --- a/examples/i18n/src/main.rs +++ b/examples/i18n/src/main.rs @@ -39,7 +39,7 @@ fn Body() -> Element { ) } -fn app(cx: Scope) -> Element { +fn app() -> Element { use_init_i18n("en-US".parse().unwrap(), "en-US".parse().unwrap(), || { let en_us = Language::from_str(EN_US).unwrap(); let es_es = Language::from_str(ES_ES).unwrap(); diff --git a/std/src/clipboard/use_clipboard.rs b/std/src/clipboard/use_clipboard.rs index 9a2f85e..5e13a9e 100644 --- a/std/src/clipboard/use_clipboard.rs +++ b/std/src/clipboard/use_clipboard.rs @@ -11,7 +11,7 @@ pub enum ClipboardError { } /// Handle to access the ClipboardContext. -#[derive(Clone)] +#[derive(Clone, Copy, PartialEq)] pub struct UseClipboard { clipboard: Signal>, } diff --git a/std/src/i18n/use_i18n.rs b/std/src/i18n/use_i18n.rs index 9d3a8e4..8e05ea5 100644 --- a/std/src/i18n/use_i18n.rs +++ b/std/src/i18n/use_i18n.rs @@ -74,7 +74,7 @@ impl Language { } } -#[derive(Clone, PartialEq)] +#[derive(Clone, Copy, PartialEq)] pub struct UseI18 { pub selected_language: Signal, pub data: Signal, diff --git a/std/src/utils/channel/use_channel.rs b/std/src/utils/channel/use_channel.rs index 642f276..6b8677b 100644 --- a/std/src/utils/channel/use_channel.rs +++ b/std/src/utils/channel/use_channel.rs @@ -3,11 +3,11 @@ use dioxus::prelude::*; use uuid::Uuid; /// Send and listen for messages between multiple components. -#[derive(Debug, Clone)] -pub struct UseChannel { +#[derive(Clone, Copy)] +pub struct UseChannel { id: Uuid, - sender: Sender, - inactive_receiver: InactiveReceiver, + sender: Signal>, + inactive_receiver: Signal>, } impl PartialEq for UseChannel { @@ -16,21 +16,21 @@ impl PartialEq for UseChannel { } } -impl UseChannel { +impl UseChannel { /// Tries to send a message to all listeners of the channel. pub fn try_send(&self, msg: impl Into) -> Result<(), TrySendError> { - self.sender.try_broadcast(msg.into()).map(|_| ()) + self.sender.peek().try_broadcast(msg.into()).map(|_| ()) } /// Sends a message to all listeners of the channel. pub async fn send(&self, msg: impl Into) -> Result<(), SendError> { - self.sender.broadcast(msg.into()).await.map(|_| ()) + self.sender.peek().broadcast(msg.into()).await.map(|_| ()) } /// Create a receiver for the channel. /// You probably want to use [`super::use_listen_channel()`]. pub fn receiver(&mut self) -> Receiver { - self.inactive_receiver.clone().activate() + self.inactive_receiver.peek().clone().activate() } } @@ -41,8 +41,8 @@ pub fn use_channel(size: usize) -> UseChannel(size); UseChannel { id, - sender, - inactive_receiver: receiver.deactivate(), + sender: Signal::new(sender), + inactive_receiver: Signal::new(receiver.deactivate()), } }) } diff --git a/std/src/utils/channel/use_listen_channel.rs b/std/src/utils/channel/use_listen_channel.rs index 74d68da..98f24f4 100644 --- a/std/src/utils/channel/use_listen_channel.rs +++ b/std/src/utils/channel/use_listen_channel.rs @@ -1,4 +1,4 @@ -use std::{future::Future, rc::Rc}; +use std::future::Future; use async_broadcast::RecvError; use dioxus::prelude::*; @@ -14,9 +14,7 @@ pub fn use_listen_channel( ) where Handler: Future + 'static, { - let action = use_hook(|| Rc::new(action)); use_memo_with_dependencies((channel,), move |(mut channel,)| { - to_owned![action]; spawn(async move { let mut receiver = channel.receiver(); loop { diff --git a/std/src/utils/rw/use_rw.rs b/std/src/utils/rw/use_rw.rs index 54cced6..c7886fc 100644 --- a/std/src/utils/rw/use_rw.rs +++ b/std/src/utils/rw/use_rw.rs @@ -4,14 +4,15 @@ use std::sync::{Arc, RwLock, RwLockReadGuard}; pub fn use_rw(init_rw: impl FnOnce() -> T) -> UseRw { use_hook(|| UseRw { - update: schedule_update(), - value: Arc::new(RwLock::new(init_rw())), + update: Signal::new(schedule_update()), + value: Signal::new(Arc::new(RwLock::new(init_rw()))), }) } -pub struct UseRw { - update: Arc, - value: Arc>, +#[derive(Copy)] +pub struct UseRw { + update: Signal>, + value: Signal>>, } impl Clone for UseRw { @@ -25,18 +26,22 @@ impl Clone for UseRw { impl UseRw { pub fn read(&self) -> Result, UseRwError> { - self.value.read().map_err(|_| UseRwError::Poisoned) + self.value.read().read().map_err(|_| UseRwError::Poisoned) } pub fn write(&self, new_value: T) -> Result<(), UseRwError> { - let mut lock = self.value.write().map_err(|_| UseRwError::Poisoned)?; + let mut lock = self + .value + .read() + .write() + .map_err(|_| UseRwError::Poisoned)?; *lock = new_value; self.needs_update(); Ok(()) } pub fn needs_update(&self) { - (self.update)() + (self.update.read())() } } From ce5ea5545865b5c35ff9cadb1a305ca7ea77bb4a Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 9 Feb 2024 15:12:16 +0100 Subject: [PATCH 088/108] fix --- std/src/i18n/use_i18n.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/std/src/i18n/use_i18n.rs b/std/src/i18n/use_i18n.rs index 8e05ea5..36eb79e 100644 --- a/std/src/i18n/use_i18n.rs +++ b/std/src/i18n/use_i18n.rs @@ -74,7 +74,7 @@ impl Language { } } -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, PartialEq)] pub struct UseI18 { pub selected_language: Signal, pub data: Signal, @@ -106,7 +106,7 @@ impl UseI18 { self.translate_with_params(id, HashMap::default()) } - pub fn set_language(&self, id: LanguageIdentifier) { + pub fn set_language(&mut self, id: LanguageIdentifier) { *self.selected_language.write() = id; } } From 4424071a6f545d560e1a69134eefbf24fd991ace Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 9 Feb 2024 16:40:04 +0100 Subject: [PATCH 089/108] fix --- std/src/i18n/use_i18n.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/std/src/i18n/use_i18n.rs b/std/src/i18n/use_i18n.rs index 36eb79e..2b791ee 100644 --- a/std/src/i18n/use_i18n.rs +++ b/std/src/i18n/use_i18n.rs @@ -74,7 +74,7 @@ impl Language { } } -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Copy)] pub struct UseI18 { pub selected_language: Signal, pub data: Signal, From 1b35f9d2a0119d4242c9d90c9b963ecb5c06c0bb Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 9 Feb 2024 16:54:22 +0100 Subject: [PATCH 090/108] empty From 42adeb47b1f7eea82bd75863a819929e53ae0cb2 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 9 Feb 2024 17:03:24 +0100 Subject: [PATCH 091/108] fix use_geolocation --- std/src/geolocation/core.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/std/src/geolocation/core.rs b/std/src/geolocation/core.rs index 6206612..d9c2cf4 100644 --- a/std/src/geolocation/core.rs +++ b/std/src/geolocation/core.rs @@ -75,10 +75,11 @@ impl Geolocator { /// Subscribe a mpsc channel to the events. pub fn listen(&self, listener: Coroutine) -> Result<(), Error> { + let tx = listener.tx(); platform::listen( &self.device_geolocator, Arc::new(move |event: Event| { - listener.send(event); + tx.unbounded_send(event); }), ) } From f330cdbe47f33eedca22d9762417f5206578f540 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 9 Feb 2024 17:23:13 +0100 Subject: [PATCH 092/108] fix --- std/src/utils/channel/use_listen_channel.rs | 4 +++- std/src/utils/rw/use_rw.rs | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/std/src/utils/channel/use_listen_channel.rs b/std/src/utils/channel/use_listen_channel.rs index 98f24f4..74d68da 100644 --- a/std/src/utils/channel/use_listen_channel.rs +++ b/std/src/utils/channel/use_listen_channel.rs @@ -1,4 +1,4 @@ -use std::future::Future; +use std::{future::Future, rc::Rc}; use async_broadcast::RecvError; use dioxus::prelude::*; @@ -14,7 +14,9 @@ pub fn use_listen_channel( ) where Handler: Future + 'static, { + let action = use_hook(|| Rc::new(action)); use_memo_with_dependencies((channel,), move |(mut channel,)| { + to_owned![action]; spawn(async move { let mut receiver = channel.receiver(); loop { diff --git a/std/src/utils/rw/use_rw.rs b/std/src/utils/rw/use_rw.rs index c7886fc..8dedb3c 100644 --- a/std/src/utils/rw/use_rw.rs +++ b/std/src/utils/rw/use_rw.rs @@ -26,13 +26,15 @@ impl Clone for UseRw { impl UseRw { pub fn read(&self) -> Result, UseRwError> { - self.value.read().read().map_err(|_| UseRwError::Poisoned) + let rw_lock = self.value.read(); + rw_lock.read().map_err(|_| UseRwError::Poisoned) } pub fn write(&self, new_value: T) -> Result<(), UseRwError> { - let mut lock = self + let rw_lock = self .value - .read() + .read(); + let mut lock = rw_lock .write() .map_err(|_| UseRwError::Poisoned)?; *lock = new_value; From 2e111cda95f816fe4232bae5e75515a58afa4a1e Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sun, 18 Feb 2024 13:30:50 +0100 Subject: [PATCH 093/108] bump dx --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4261f52..8fe29d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,6 @@ members = [ [workspace.dependencies] dioxus-std = { path = "./std" } -dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "ef101dd876ee8ecdd702a7e22addfd47f2ebd892" } -dioxus-web = { git = "https://github.com/DioxusLabs/dioxus", rev = "ef101dd876ee8ecdd702a7e22addfd47f2ebd892" } -dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus", rev = "ef101dd876ee8ecdd702a7e22addfd47f2ebd892" } \ No newline at end of file +dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1" } +dioxus-web = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1" } +dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1" } \ No newline at end of file From e8d3b670b9f1c3d0c6fe79059e2161539b6a72f0 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sat, 24 Feb 2024 11:16:16 +0100 Subject: [PATCH 094/108] bump dx --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8fe29d9..4840674 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,6 @@ members = [ [workspace.dependencies] dioxus-std = { path = "./std" } -dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1" } -dioxus-web = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1" } -dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1" } \ No newline at end of file +dioxus = "0.5.0-alpha.0" +dioxus-web = "0.5.0-alpha.0" +dioxus-desktop = "0.5.0-alpha.0" \ No newline at end of file From 569e9b2e0f27a956eaf261d0c32ed3a67bb35a19 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sat, 24 Feb 2024 14:04:26 +0100 Subject: [PATCH 095/108] fixes --- std/Cargo.toml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/std/Cargo.toml b/std/Cargo.toml index e4fa29f..ef792f7 100644 --- a/std/Cargo.toml +++ b/std/Cargo.toml @@ -18,11 +18,10 @@ categories = ["multimedia", "os", "wasm"] [features] utils = ["dep:async-broadcast", "uuid/v4"] -clipboard = ["dep:dioxus", "dep:copypasta"] -notifications = ["dep:dioxus", "dep:notify-rust"] +clipboard = ["dep:copypasta"] +notifications = ["dep:notify-rust"] geolocation = [ # Shared - "dep:dioxus", "dep:futures", "dep:futures-util", @@ -37,9 +36,6 @@ geolocation = [ "dep:wasm-bindgen", ] color_scheme = [ - # Shared - "dep:dioxus", - # Wasm "web-sys/Window", "web-sys/MediaQueryList", @@ -67,7 +63,6 @@ desktop-testing = ["clipboard", "notifications", "geolocation", "utils", "i18n"] [dependencies] dioxus = { workspace = true } cfg-if = "1.0.0" -dioxus = { version = "0.4", optional = true } # Used by: clipboard copypasta = { version = "0.8.2", optional = true } From 827923908389108e4bb069e221be4cda7ded0b35 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Wed, 6 Mar 2024 17:34:29 +0100 Subject: [PATCH 096/108] bump dx --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4840674..17ed532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,6 @@ members = [ [workspace.dependencies] dioxus-std = { path = "./std" } -dioxus = "0.5.0-alpha.0" -dioxus-web = "0.5.0-alpha.0" -dioxus-desktop = "0.5.0-alpha.0" \ No newline at end of file +dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "608fec59e5cae78a470eaa6ab509cd166a46085a" } +dioxus-web = { git = "https://github.com/DioxusLabs/dioxus", rev = "608fec59e5cae78a470eaa6ab509cd166a46085a" } +dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus", rev = "608fec59e5cae78a470eaa6ab509cd166a46085a" } \ No newline at end of file From 0fe58a69df21013fc7e57c5a88f0cc2ee0eda857 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sat, 9 Mar 2024 09:56:08 +0100 Subject: [PATCH 097/108] bump dx --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 17ed532..50d19d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,6 @@ members = [ [workspace.dependencies] dioxus-std = { path = "./std" } -dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "608fec59e5cae78a470eaa6ab509cd166a46085a" } -dioxus-web = { git = "https://github.com/DioxusLabs/dioxus", rev = "608fec59e5cae78a470eaa6ab509cd166a46085a" } -dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus", rev = "608fec59e5cae78a470eaa6ab509cd166a46085a" } \ No newline at end of file +dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "2d2e9dc56a59b4f3ad4deb55d15c27f337cf564c" } +dioxus-web = { git = "https://github.com/DioxusLabs/dioxus", rev = "2d2e9dc56a59b4f3ad4deb55d15c27f337cf564c" } +dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus", rev = "2d2e9dc56a59b4f3ad4deb55d15c27f337cf564c" } \ No newline at end of file From 4d8ae33b94537d54471cd924c32f03c1d949430f Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 15 Mar 2024 18:40:58 +0100 Subject: [PATCH 098/108] 0.5 --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 50d19d6..44be3e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,6 @@ members = [ [workspace.dependencies] dioxus-std = { path = "./std" } -dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "2d2e9dc56a59b4f3ad4deb55d15c27f337cf564c" } -dioxus-web = { git = "https://github.com/DioxusLabs/dioxus", rev = "2d2e9dc56a59b4f3ad4deb55d15c27f337cf564c" } -dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus", rev = "2d2e9dc56a59b4f3ad4deb55d15c27f337cf564c" } \ No newline at end of file +dioxus = { version = "0.5.0-alpha.0" } +dioxus-web = { version = "0.5.0-alpha.0" } +dioxus-desktop = { version = "0.5.0-alpha.0" } \ No newline at end of file From 33f6fce1b293864c31bf16355656682482747221 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Tue, 19 Mar 2024 18:29:42 +0100 Subject: [PATCH 099/108] bump dx, fmt, clippy --- Cargo.toml | 6 +++--- std/src/utils/channel/use_listen_channel.rs | 4 ++-- std/src/utils/rw/use_rw.rs | 8 ++------ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 44be3e9..bde3519 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,6 @@ members = [ [workspace.dependencies] dioxus-std = { path = "./std" } -dioxus = { version = "0.5.0-alpha.0" } -dioxus-web = { version = "0.5.0-alpha.0" } -dioxus-desktop = { version = "0.5.0-alpha.0" } \ No newline at end of file +dioxus = { version = "0.5.0-alpha.2" } +dioxus-web = { version = "0.5.0-alpha.2" } +dioxus-desktop = { version = "0.5.0-alpha.2" } \ No newline at end of file diff --git a/std/src/utils/channel/use_listen_channel.rs b/std/src/utils/channel/use_listen_channel.rs index 74d68da..77e31f0 100644 --- a/std/src/utils/channel/use_listen_channel.rs +++ b/std/src/utils/channel/use_listen_channel.rs @@ -15,7 +15,7 @@ pub fn use_listen_channel( Handler: Future + 'static, { let action = use_hook(|| Rc::new(action)); - use_memo_with_dependencies((channel,), move |(mut channel,)| { + use_memo(use_reactive(channel, move |mut channel| { to_owned![action]; spawn(async move { let mut receiver = channel.receiver(); @@ -28,5 +28,5 @@ pub fn use_listen_channel( } } }) - }); + })); } diff --git a/std/src/utils/rw/use_rw.rs b/std/src/utils/rw/use_rw.rs index 8dedb3c..4df8a25 100644 --- a/std/src/utils/rw/use_rw.rs +++ b/std/src/utils/rw/use_rw.rs @@ -31,12 +31,8 @@ impl UseRw { } pub fn write(&self, new_value: T) -> Result<(), UseRwError> { - let rw_lock = self - .value - .read(); - let mut lock = rw_lock - .write() - .map_err(|_| UseRwError::Poisoned)?; + let rw_lock = self.value.read(); + let mut lock = rw_lock.write().map_err(|_| UseRwError::Poisoned)?; *lock = new_value; self.needs_update(); Ok(()) From fd64567c36eec642ff28d408be7f91442cfef6d1 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Tue, 19 Mar 2024 20:16:17 +0100 Subject: [PATCH 100/108] nuke use_rw and fix use_geolocation --- README.md | 1 - std/src/geolocation/use_geolocation.rs | 20 ++++++----- std/src/utils/mod.rs | 1 - std/src/utils/rw/mod.rs | 3 -- std/src/utils/rw/use_rw.rs | 49 -------------------------- 5 files changed, 11 insertions(+), 63 deletions(-) delete mode 100644 std/src/utils/rw/mod.rs delete mode 100644 std/src/utils/rw/use_rw.rs diff --git a/README.md b/README.md index 568ca06..0ae5605 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,6 @@ - [x] Notifications - (Desktop) - [x] Color Scheme - (any) - [x] Utility Hooks - - [x] use_rw - (any) - [x] use_channel - (any) - [ ] use_interval (any) - [x] i18n - (any) diff --git a/std/src/geolocation/use_geolocation.rs b/std/src/geolocation/use_geolocation.rs index 2741ca0..e4e3887 100644 --- a/std/src/geolocation/use_geolocation.rs +++ b/std/src/geolocation/use_geolocation.rs @@ -1,18 +1,22 @@ //! Provides an initialization and use_geolocation hook. use super::core::{Error, Event, Geocoordinates, Geolocator, PowerMode, Status}; -use dioxus::prelude::{provide_context, try_consume_context, use_coroutine, UnboundedReceiver}; +use dioxus::{ + prelude::{ + provide_context, try_consume_context, use_coroutine, use_signal, Signal, UnboundedReceiver, + }, + signals::{Readable, Writable}, +}; use futures_util::stream::StreamExt; use std::{rc::Rc, sync::Once}; -use crate::utils::rw::{use_rw, UseRw}; - static INIT: Once = Once::new(); /// Provides the latest geocoordinates. Good for navigation-type apps. pub fn use_geolocation() -> Result { // Store the coords - let coords: UseRw> = use_rw(|| Err(Error::NotInitialized)); + let mut coords: Signal> = + use_signal(|| Err(Error::NotInitialized)); // Get geolocator let geolocator = match try_consume_context::>() { @@ -20,17 +24,15 @@ pub fn use_geolocation() -> Result { None => return Err(Error::NotInitialized), }; - let coords_cloned = coords.clone(); - // Initialize the handler of events let listener = use_coroutine(|mut rx: UnboundedReceiver| async move { while let Some(event) = rx.next().await { match event { Event::NewGeocoordinates(new_coords) => { - let _ = coords_cloned.write(Ok(new_coords)); + *coords.write() = Ok(new_coords); } Event::StatusChanged(Status::Disabled) => { - let _ = coords_cloned.write(Err(Error::AccessDenied)); + *coords.write() = Err(Error::AccessDenied); } _ => {} } @@ -43,7 +45,7 @@ pub fn use_geolocation() -> Result { }); // Get the result and return a clone - coords.read().map_err(|_| Error::Poisoned)?.clone() + coords.read_unchecked().clone() } /// Must be called before any use of the geolocation abstraction. diff --git a/std/src/utils/mod.rs b/std/src/utils/mod.rs index 68fb600..ff02972 100644 --- a/std/src/utils/mod.rs +++ b/std/src/utils/mod.rs @@ -1,2 +1 @@ pub mod channel; -pub mod rw; diff --git a/std/src/utils/rw/mod.rs b/std/src/utils/rw/mod.rs deleted file mode 100644 index 2e0b8a6..0000000 --- a/std/src/utils/rw/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod use_rw; - -pub use use_rw::*; diff --git a/std/src/utils/rw/use_rw.rs b/std/src/utils/rw/use_rw.rs deleted file mode 100644 index 4df8a25..0000000 --- a/std/src/utils/rw/use_rw.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Essentially the use_ref hook except Send + Sync using Arc and RwLock. -use dioxus::prelude::*; -use std::sync::{Arc, RwLock, RwLockReadGuard}; - -pub fn use_rw(init_rw: impl FnOnce() -> T) -> UseRw { - use_hook(|| UseRw { - update: Signal::new(schedule_update()), - value: Signal::new(Arc::new(RwLock::new(init_rw()))), - }) -} - -#[derive(Copy)] -pub struct UseRw { - update: Signal>, - value: Signal>>, -} - -impl Clone for UseRw { - fn clone(&self) -> Self { - Self { - update: self.update.clone(), - value: self.value.clone(), - } - } -} - -impl UseRw { - pub fn read(&self) -> Result, UseRwError> { - let rw_lock = self.value.read(); - rw_lock.read().map_err(|_| UseRwError::Poisoned) - } - - pub fn write(&self, new_value: T) -> Result<(), UseRwError> { - let rw_lock = self.value.read(); - let mut lock = rw_lock.write().map_err(|_| UseRwError::Poisoned)?; - *lock = new_value; - self.needs_update(); - Ok(()) - } - - pub fn needs_update(&self) { - (self.update.read())() - } -} - -#[derive(Debug)] -pub enum UseRwError { - Poisoned, -} From bc29d7cade3b04cf3d45305dddf9c0347f95ff98 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Tue, 19 Mar 2024 20:20:48 +0100 Subject: [PATCH 101/108] clippy --- std/src/geolocation/core.rs | 2 +- std/src/geolocation/use_geolocation.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/std/src/geolocation/core.rs b/std/src/geolocation/core.rs index d9c2cf4..c2f0e80 100644 --- a/std/src/geolocation/core.rs +++ b/std/src/geolocation/core.rs @@ -79,7 +79,7 @@ impl Geolocator { platform::listen( &self.device_geolocator, Arc::new(move |event: Event| { - tx.unbounded_send(event); + tx.unbounded_send(event).ok(); }), ) } diff --git a/std/src/geolocation/use_geolocation.rs b/std/src/geolocation/use_geolocation.rs index e4e3887..ab4c9bf 100644 --- a/std/src/geolocation/use_geolocation.rs +++ b/std/src/geolocation/use_geolocation.rs @@ -41,7 +41,7 @@ pub fn use_geolocation() -> Result { // Start listening INIT.call_once(|| { - let _ = geolocator.listen(listener.clone()); + geolocator.listen(listener).ok(); }); // Get the result and return a clone From a07270fb9a7fc83ce348ad90c9a044524c70ce03 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Tue, 19 Mar 2024 20:25:38 +0100 Subject: [PATCH 102/108] clippy --- std/src/clipboard/use_clipboard.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/std/src/clipboard/use_clipboard.rs b/std/src/clipboard/use_clipboard.rs index 5e13a9e..499bd7d 100644 --- a/std/src/clipboard/use_clipboard.rs +++ b/std/src/clipboard/use_clipboard.rs @@ -22,7 +22,7 @@ impl UseClipboard { self.clipboard .write() .as_mut() - .ok_or_else(|| ClipboardError::NotAvailable)? + .ok_or(ClipboardError::NotAvailable)? .get_contents() .map_err(|_| ClipboardError::FailedToRead) } @@ -32,7 +32,7 @@ impl UseClipboard { self.clipboard .write() .as_mut() - .ok_or_else(|| ClipboardError::NotAvailable)? + .ok_or(ClipboardError::NotAvailable)? .set_contents(contents) .map_err(|_| ClipboardError::FailedToSet) } From 0fca567443cd99c0571577171e428b150f9699a5 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 27 Mar 2024 18:57:05 -0500 Subject: [PATCH 103/108] move storage into the workspace --- examples/storage_desktop/Cargo.toml | 2 +- examples/storage_web/Cargo.toml | 2 +- std/Cargo.toml | 20 +++++++++++++++++++ {src => std/src}/storage/client_storage/fs.rs | 0 .../src}/storage/client_storage/memory.rs | 0 .../src}/storage/client_storage/mod.rs | 0 .../src}/storage/client_storage/web.rs | 0 {src => std/src}/storage/mod.rs | 0 {src => std/src}/storage/persistence.rs | 0 9 files changed, 22 insertions(+), 2 deletions(-) rename {src => std/src}/storage/client_storage/fs.rs (100%) rename {src => std/src}/storage/client_storage/memory.rs (100%) rename {src => std/src}/storage/client_storage/mod.rs (100%) rename {src => std/src}/storage/client_storage/web.rs (100%) rename {src => std/src}/storage/mod.rs (100%) rename {src => std/src}/storage/persistence.rs (100%) diff --git a/examples/storage_desktop/Cargo.toml b/examples/storage_desktop/Cargo.toml index ac1f781..7c8a289 100644 --- a/examples/storage_desktop/Cargo.toml +++ b/examples/storage_desktop/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -dioxus-std = { path="../../", features = ["storage"] } +dioxus-std = { workspace = true, features = ["storage"] } dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } dioxus-router = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } diff --git a/examples/storage_web/Cargo.toml b/examples/storage_web/Cargo.toml index 6ae8412..d25c29a 100644 --- a/examples/storage_web/Cargo.toml +++ b/examples/storage_web/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -dioxus-std = { path="../../", features = ["storage"] } +dioxus-std = { workspace = true, features = ["storage"] } dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } dioxus-web = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } dioxus-router = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } diff --git a/std/Cargo.toml b/std/Cargo.toml index ffd8aae..166578d 100644 --- a/std/Cargo.toml +++ b/std/Cargo.toml @@ -54,6 +54,17 @@ i18n = [ # Non Shared "dep:unic-langid", ] +storage = [ + # Shared + "dep:dioxus", + "dep:rustc-hash", + "dep:postcard", + "dep:once_cell", + "dep:dioxus-signals", + "dep:tokio", + "dep:yazi", + "web-sys/StorageEvent", +] # CI testing wasm-testing = ["geolocation", "color_scheme", "utils", "i18n"] @@ -88,6 +99,15 @@ serde = { version = "1.0.163", optional = true } serde_json = { version = "1.0.96", optional = true } unic-langid = { version = "0.9.1", features = ["serde"], optional = true } +# Used by: storage +rustc-hash = { version = "1.1.0", optional = true } +postcard = { version = "1.0.2", features = ["use-std"], optional = true } +once_cell = { version = "1.17.0", optional = true } +dioxus-signals = { version = "0.5.0-alpha.2", features = [ + "serialize", +], optional = true } +tokio = { version = "1.33.0", features = ["sync"], optional = true } +yazi = { version = "0.1.4", optional = true } # # # # # # # # # # Windows Deps. # diff --git a/src/storage/client_storage/fs.rs b/std/src/storage/client_storage/fs.rs similarity index 100% rename from src/storage/client_storage/fs.rs rename to std/src/storage/client_storage/fs.rs diff --git a/src/storage/client_storage/memory.rs b/std/src/storage/client_storage/memory.rs similarity index 100% rename from src/storage/client_storage/memory.rs rename to std/src/storage/client_storage/memory.rs diff --git a/src/storage/client_storage/mod.rs b/std/src/storage/client_storage/mod.rs similarity index 100% rename from src/storage/client_storage/mod.rs rename to std/src/storage/client_storage/mod.rs diff --git a/src/storage/client_storage/web.rs b/std/src/storage/client_storage/web.rs similarity index 100% rename from src/storage/client_storage/web.rs rename to std/src/storage/client_storage/web.rs diff --git a/src/storage/mod.rs b/std/src/storage/mod.rs similarity index 100% rename from src/storage/mod.rs rename to std/src/storage/mod.rs diff --git a/src/storage/persistence.rs b/std/src/storage/persistence.rs similarity index 100% rename from src/storage/persistence.rs rename to std/src/storage/persistence.rs From e3ad85b07c17cec5779e7348c470fc0229326108 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 27 Mar 2024 19:09:25 -0500 Subject: [PATCH 104/108] revert channel changes --- std/src/utils/channel/use_channel.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/std/src/utils/channel/use_channel.rs b/std/src/utils/channel/use_channel.rs index 4d57411..6b8677b 100644 --- a/std/src/utils/channel/use_channel.rs +++ b/std/src/utils/channel/use_channel.rs @@ -10,16 +10,6 @@ pub struct UseChannel { inactive_receiver: Signal>, } -impl UseChannel { - pub(crate) fn new(id: Uuid, sender: Sender, inactive_receiver: InactiveReceiver) -> Self { - Self { - id, - sender, - inactive_receiver, - } - } -} - impl PartialEq for UseChannel { fn eq(&self, other: &Self) -> bool { self.id == other.id @@ -49,7 +39,6 @@ pub fn use_channel(size: usize) -> UseChannel(size); - UseChannel { id, sender: Signal::new(sender), From a8e4f99161328164bb9ad875ea98d77f237d96d8 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 27 Mar 2024 19:32:16 -0500 Subject: [PATCH 105/108] fix dioxus storage --- examples/storage/Cargo.toml | 12 ++ .../{storage_desktop => storage}/README.md | 0 examples/{storage_web => storage}/src/main.rs | 54 +++++-- examples/storage_desktop/Cargo.toml | 12 -- examples/storage_desktop/src/main.rs | 113 --------------- examples/storage_web/Cargo.toml | 15 -- examples/storage_web/README.md | 7 - std/Cargo.toml | 16 ++- std/src/storage/client_storage/fs.rs | 34 +---- std/src/storage/client_storage/memory.rs | 3 +- std/src/storage/client_storage/mod.rs | 32 +++++ std/src/storage/client_storage/web.rs | 18 +-- std/src/storage/mod.rs | 136 ++++++++---------- std/src/storage/persistence.rs | 9 +- 14 files changed, 167 insertions(+), 294 deletions(-) create mode 100644 examples/storage/Cargo.toml rename examples/{storage_desktop => storage}/README.md (100%) rename examples/{storage_web => storage}/src/main.rs (54%) delete mode 100644 examples/storage_desktop/Cargo.toml delete mode 100644 examples/storage_desktop/src/main.rs delete mode 100644 examples/storage_web/Cargo.toml delete mode 100644 examples/storage_web/README.md diff --git a/examples/storage/Cargo.toml b/examples/storage/Cargo.toml new file mode 100644 index 0000000..4d485b2 --- /dev/null +++ b/examples/storage/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "storage-desktop" +version = "0.1.0" +edition = "2021" + +[dependencies] +dioxus-std = { workspace = true, features = ["storage"] } +dioxus = { version = "0.5.0-alpha.2", features = ["router"] } + +[features] +web = ["dioxus/web"] +desktop = ["dioxus/desktop"] diff --git a/examples/storage_desktop/README.md b/examples/storage/README.md similarity index 100% rename from examples/storage_desktop/README.md rename to examples/storage/README.md diff --git a/examples/storage_web/src/main.rs b/examples/storage/src/main.rs similarity index 54% rename from examples/storage_web/src/main.rs rename to examples/storage/src/main.rs index 30c9db8..d83cb83 100644 --- a/examples/storage_web/src/main.rs +++ b/examples/storage/src/main.rs @@ -3,15 +3,12 @@ use dioxus_router::prelude::*; use dioxus_std::storage::*; fn main() { - // init debug tool for WebAssembly - wasm_logger::init(wasm_logger::Config::default()); - console_error_panic_hook::set_once(); - dioxus_web::launch(App); + dioxus_std::storage::set_dir!(); + launch(app); } -#[component] -fn App(cx: Scope) -> Element { - render! { +fn app() -> Element { + rsx! { Router:: {} } } @@ -27,8 +24,33 @@ enum Route { } #[component] -fn Footer(cx: Scope) -> Element { - render! { +fn Footer() -> Element { + + let new_window = { + #[cfg(feature = "desktop")] + { + let window = dioxus::desktop::use_window(); + rsx! { + div { + button { + onclick: move |_| { + let dom = VirtualDom::new(app); + window.new_window(dom, Default::default()); + }, + "New Window" + } + } + } + } + #[cfg(not(feature = "desktop"))] + { + rsx! { + div {} + } + } + }; + + rsx! { div { Outlet:: { } @@ -36,6 +58,8 @@ fn Footer(cx: Scope) -> Element { "----" } + {new_window} + nav { ul { li { Link { to: Route::Page1 {}, "Page1" } } @@ -47,16 +71,16 @@ fn Footer(cx: Scope) -> Element { } #[component] -fn Page1(cx: Scope) -> Element { - render!("Home") +fn Page1() -> Element { + rsx!("Home") } #[component] -fn Page2(cx: Scope) -> Element { - let count_session = use_singleton_persistent(cx, || 0); - let count_local = use_synced_storage::(cx, "synced".to_string(), || 0); +fn Page2() -> Element { + let mut count_session = use_singleton_persistent(|| 0); + let mut count_local = use_synced_storage::("synced".to_string(), || 0); - render!( + rsx!( div { button { onclick: move |_| { diff --git a/examples/storage_desktop/Cargo.toml b/examples/storage_desktop/Cargo.toml deleted file mode 100644 index 7c8a289..0000000 --- a/examples/storage_desktop/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "storage-desktop" -version = "0.1.0" -edition = "2021" - -[dependencies] -dioxus-std = { workspace = true, features = ["storage"] } -dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } -dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } -dioxus-router = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } -log = "0.4.6" - diff --git a/examples/storage_desktop/src/main.rs b/examples/storage_desktop/src/main.rs deleted file mode 100644 index dd1fd73..0000000 --- a/examples/storage_desktop/src/main.rs +++ /dev/null @@ -1,113 +0,0 @@ -use dioxus::prelude::*; -use dioxus_router::prelude::*; -use dioxus_std::storage::*; - -fn main() { - match log::set_boxed_logger(Box::new(simple_logger::SimpleLogger)) { - Ok(_) => log::set_max_level(log::LevelFilter::Info), - Err(e) => panic!("Failed to initialize logger: {}", e), - } - dioxus_std::storage::set_dir!(); - dioxus_desktop::launch(App); -} - -#[component] -fn App(cx: Scope) -> Element { - render! { - Router:: {} - } -} - -#[derive(Routable, Clone)] -#[rustfmt::skip] -enum Route { - #[layout(Footer)] - #[route("/")] - Page1 {}, - #[route("/page2")] - Page2 {}, -} - -#[component] -fn Footer(cx: Scope) -> Element { - let window = dioxus_desktop::use_window(cx); - - render! { - div { - Outlet:: { } - - p { - "----" - } - - div { - button { - onclick: move |_| { - let dom = VirtualDom::new(App); - window.new_window(dom, Default::default()); - }, - "New Window" - } - } - - nav { - ul { - li { Link { to: Route::Page1 {}, "Page1" } } - li { Link { to: Route::Page2 {}, "Page2" } } - } - } - } - } -} - -#[component] -fn Page1(cx: Scope) -> Element { - render!("Home") -} - -#[component] -fn Page2(cx: Scope) -> Element { - let count_session = use_singleton_persistent(cx, || 0); - let count_local = use_synced_storage::(cx, "synced".to_string(), || 0); - - render!( - div { - button { - onclick: move |_| { - *count_session.write() += 1; - }, - "Click me!" - }, - "I persist for the current session. Clicked {count_session} times" - } - div { - button { - onclick: move |_| { - *count_local.write() += 1; - }, - "Click me!" - }, - "I persist across all sessions. Clicked {count_local} times" - } - ) -} - -mod simple_logger { - use log::{Metadata, Record}; - - pub struct SimpleLogger; - - impl log::Log for SimpleLogger { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.level() <= log::max_level() - } - - fn log(&self, record: &Record) { - if self.enabled(record.metadata()) { - println!("{} - {}", record.level(), record.args()); - } - } - - fn flush(&self) {} - } -} diff --git a/examples/storage_web/Cargo.toml b/examples/storage_web/Cargo.toml deleted file mode 100644 index d25c29a..0000000 --- a/examples/storage_web/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "storage-web" -version = "0.1.0" -edition = "2021" - -[dependencies] -dioxus-std = { workspace = true, features = ["storage"] } -dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } -dioxus-web = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } -dioxus-router = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "647815fa6f6db2304cda5bd36c78b4f8b0379f39" } -log = "0.4.6" - -# WebAssembly Debug -wasm-logger = "0.2.0" -console_error_panic_hook = "0.1.7" diff --git a/examples/storage_web/README.md b/examples/storage_web/README.md deleted file mode 100644 index 2cea929..0000000 --- a/examples/storage_web/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# use_persistent (Web) - -Use persistent allows you to create data that will persist across sessions. This example will teach you how to use the `use_persistent` hook. - -Run: - -```dioxus serve``` \ No newline at end of file diff --git a/std/Cargo.toml b/std/Cargo.toml index e339fdf..4de707b 100644 --- a/std/Cargo.toml +++ b/std/Cargo.toml @@ -52,7 +52,6 @@ i18n = [ ] storage = [ # Shared - "dep:dioxus", "dep:rustc-hash", "dep:postcard", "dep:once_cell", @@ -60,6 +59,14 @@ storage = [ "dep:tokio", "dep:yazi", "web-sys/StorageEvent", + "dep:serde", + "dep:futures-util", + + # WASM + "dep:directories", + + # Not WASM + "dep:wasm-bindgen", ] # CI testing @@ -86,7 +93,7 @@ notify-rust = { version = "4.8.0", optional = true } uuid = { version = "1.3.2", optional = true } async-broadcast = { version = "0.5.1", optional = true } -# Used by: geolocation +# Used by: geolocation, storage futures = { version = "0.3.28", features = ["std"], optional = true } futures-util = { version = "0.3.28", optional = true } @@ -104,6 +111,7 @@ dioxus-signals = { version = "0.5.0-alpha.2", features = [ ], optional = true } tokio = { version = "1.33.0", features = ["sync"], optional = true } yazi = { version = "0.1.4", optional = true } +tracing = "0.1.40" # # # # # # # # # # Windows Deps. # @@ -133,6 +141,10 @@ js-sys = "0.3.62" uuid = { version = "1.3.2", features = ["js"] } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +# Used by: storage +directories = { version = "4.0.1", optional = true } + # # # # # # Docs. # # # # # # diff --git a/std/src/storage/client_storage/fs.rs b/std/src/storage/client_storage/fs.rs index 3b1119a..9d2dde5 100644 --- a/std/src/storage/client_storage/fs.rs +++ b/std/src/storage/client_storage/fs.rs @@ -8,36 +8,6 @@ use tokio::sync::watch::{channel, Receiver}; use crate::storage::{serde_to_string, try_serde_from_string, StorageBacking, StorageSubscriber}; -#[allow(clippy::needless_doctest_main)] -/// Set the directory where the storage files are located on non-wasm targets. -/// -/// ```rust -/// use dioxus_std::set_dir; -/// -/// fn main(){ -/// // set the directory to the default location -/// set_dir!(); -/// } -/// ``` -/// ```rust -/// use dioxus_std::set_dir; -/// -/// fn main(){ -/// // set the directory to a custom location -/// set_dir!("path/to/dir"); -/// } -/// ``` -#[macro_export] -macro_rules! set_dir { - () => { - $crate::storage::set_dir_name(env!("CARGO_PKG_NAME")) - }; - ($path: literal) => { - $crate::storage::set_directory(std::path::PathBuf::from($path)) - }; -} -pub use set_dir; - #[doc(hidden)] /// Sets the directory where the storage files are located. pub fn set_directory(path: std::path::PathBuf) { @@ -137,7 +107,7 @@ impl StorageSubscriber for LocalStorage { } fn unsubscribe(key: &::Key) { - log::trace!("Unsubscribing from \"{}\"", key); + tracing::trace!("Unsubscribing from \"{}\"", key); // Fail silently if unsubscribe is called but the subscriptions map isn't initialized yet. if let Some(subscriptions) = SUBSCRIPTIONS.get() { @@ -145,7 +115,7 @@ impl StorageSubscriber for LocalStorage { // If the subscription exists, remove it from the subscriptions map. if read_binding.contains_key(key) { - log::trace!("Found entry for \"{}\"", key); + tracing::trace!("Found entry for \"{}\"", key); drop(read_binding); subscriptions.write().unwrap().remove(key); } diff --git a/std/src/storage/client_storage/memory.rs b/std/src/storage/client_storage/memory.rs index 91b4b02..195007f 100644 --- a/std/src/storage/client_storage/memory.rs +++ b/std/src/storage/client_storage/memory.rs @@ -1,11 +1,10 @@ use std::any::Any; +use std::cell::RefCell; use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::rc::Rc; use std::sync::Arc; -use dioxus::prelude::RefCell; - use crate::storage::StorageBacking; #[derive(Clone)] diff --git a/std/src/storage/client_storage/mod.rs b/std/src/storage/client_storage/mod.rs index 3642cef..6acd450 100644 --- a/std/src/storage/client_storage/mod.rs +++ b/std/src/storage/client_storage/mod.rs @@ -1,3 +1,35 @@ +#[allow(clippy::needless_doctest_main)] +/// Set the directory where the storage files are located on non-wasm targets. +/// +/// ```rust +/// use dioxus_std::set_dir; +/// +/// fn main(){ +/// // set the directory to the default location +/// set_dir!(); +/// } +/// ``` +/// ```rust +/// use dioxus_std::set_dir; +/// +/// fn main(){ +/// // set the directory to a custom location +/// set_dir!("path/to/dir"); +/// } +/// ``` +#[macro_export] +macro_rules! set_dir { + () => { + #[cfg(not(target_family = "wasm"))] + $crate::storage::set_dir_name(env!("CARGO_PKG_NAME")) + }; + ($path: literal) => { + #[cfg(not(target_family = "wasm"))] + $crate::storage::set_directory(std::path::PathBuf::from($path)) + }; +} +pub use set_dir; + cfg_if::cfg_if! { if #[cfg(target_family = "wasm")] { pub mod web; diff --git a/std/src/storage/client_storage/web.rs b/std/src/storage/client_storage/web.rs index bc48d6f..b555a40 100644 --- a/std/src/storage/client_storage/web.rs +++ b/std/src/storage/client_storage/web.rs @@ -16,7 +16,6 @@ use crate::storage::{ StorageSubscriber, StorageSubscription, }; -// Start LocalStorage #[derive(Clone)] pub struct LocalStorage; @@ -53,12 +52,9 @@ impl StorageSubscriber for LocalStorage { } fn unsubscribe(key: &String) { - log::trace!("Unsubscribing from \"{}\"", key); let read_binding = SUBSCRIPTIONS.read().unwrap(); if let Some(entry) = read_binding.get(key) { - log::trace!("Found entry for \"{}\"", key); if entry.tx.is_closed() { - log::trace!("Channel is closed, removing entry for \"{}\"", key); drop(read_binding); SUBSCRIPTIONS.write().unwrap().remove(key); } @@ -70,20 +66,20 @@ impl StorageSubscriber for LocalStorage { static SUBSCRIPTIONS: Lazy>>> = Lazy::new(|| { // Create a closure that will be called when a storage event occurs. let closure = Closure::wrap(Box::new(move |e: web_sys::StorageEvent| { - log::trace!("Storage event: {:?}", e); + tracing::trace!("Storage event: {:?}", e); let key: String = e.key().unwrap(); let read_binding = SUBSCRIPTIONS.read().unwrap(); if let Some(subscription) = read_binding.get(&key) { if subscription.tx.is_closed() { - log::trace!("Channel is closed, removing subscription for \"{}\"", key); + tracing::trace!("Channel is closed, removing subscription for \"{}\"", key); drop(read_binding); SUBSCRIPTIONS.write().unwrap().remove(&key); return; } // Call the getter for the given entry and send the value to said entry's channel. match subscription.get_and_send() { - Ok(_) => log::trace!("Sent storage event"), - Err(err) => log::error!("Error sending storage event: {:?}", err.to_string()), + Ok(_) => tracing::trace!("Sent storage event"), + Err(err) => tracing::error!("Error sending storage event: {:?}", err.to_string()), } } }) as Box); @@ -97,9 +93,6 @@ static SUBSCRIPTIONS: Lazy>>> = Arc::new(RwLock::new(HashMap::new())) }); -// End LocalStorage - -// Start SessionStorage #[derive(Clone)] pub struct SessionStorage; @@ -114,9 +107,7 @@ impl StorageBacking for SessionStorage { get(key, WebStorageType::Session) } } -// End SessionStorage -// Start common fn set(key: String, value: &T, storage_type: WebStorageType) { let as_str = serde_to_string(value); get_storage_by_type(storage_type) @@ -144,4 +135,3 @@ enum WebStorageType { Local, Session, } -// End common diff --git a/std/src/storage/mod.rs b/std/src/storage/mod.rs index 2f5639c..170943d 100644 --- a/std/src/storage/mod.rs +++ b/std/src/storage/mod.rs @@ -5,9 +5,9 @@ //! use dioxus_std::storage::use_persistent; //! use dioxus::prelude::*; //! -//! fn app(cx: Scope) -> Element { -//! let num = use_persistent(cx, "count", || 0); -//! cx.render(rsx! { +//! fn app() -> Element { +//! let num = use_persistent("count", || 0); +//! rsx! { //! div { //! button { //! onclick: move |_| { @@ -19,7 +19,7 @@ //! "{*num.read()}" //! } //! } -//! }) +//! } //! } //! ``` @@ -27,23 +27,24 @@ mod client_storage; mod persistence; pub use client_storage::{LocalStorage, SessionStorage}; +use futures_util::stream::StreamExt; pub use persistence::{ new_persistent, new_singleton_persistent, use_persistent, use_singleton_persistent, }; -use dioxus::prelude::{current_scope_id, to_owned, ScopeState}; -use dioxus_signals::{Effect, Signal}; +use dioxus::prelude::*; use postcard::to_allocvec; use serde::{de::DeserializeOwned, Serialize}; use std::any::Any; use std::fmt::{Debug, Display}; -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; use std::sync::{Arc, Mutex}; use tokio::sync::watch::error::SendError; use tokio::sync::watch::{Receiver, Sender}; #[cfg(not(target_family = "wasm"))] -pub use client_storage::{set_dir, set_dir_name, set_directory}; +pub use client_storage::{set_dir_name, set_directory}; +pub use client_storage::set_dir; /// A storage hook that can be used to store data that will persist across application reloads. This hook is generic over the storage location which can be useful for other hooks. /// @@ -57,17 +58,17 @@ pub use client_storage::{set_dir, 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(cx: &ScopeState) -> Signal where S: StorageBacking { -/// use_storage::(cx, "user-id", || 123) +/// fn use_user_id() -> Signal where S: StorageBacking { +/// use_storage::("user-id", || 123) /// } /// ``` -pub fn use_storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal +pub fn use_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal where S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { - *cx.use_hook(|| new_storage::(cx, key, init)) + use_hook(|| new_storage::(key, init)) } /// Creates a Signal that can be used to store data that will persist across application reloads. @@ -82,11 +83,11 @@ where /// use dioxus_signals::Signal; /// /// // This hook can be used with any storage backing without multiple versions of the hook -/// fn user_id(cx: &ScopeState) -> Signal where S: StorageBacking { -/// new_storage::(cx, "user-id", || 123) +/// fn user_id() -> Signal where S: StorageBacking { +/// new_storage::("user-id", || 123) /// } /// ``` -pub fn new_storage(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal +pub fn new_storage(key: S::Key, init: impl FnOnce() -> T) -> Signal where S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, @@ -100,12 +101,12 @@ where Signal::new(init.take().unwrap()()) } else if cfg!(feature = "hydrate") { let key_clone = key.clone(); - let storage_entry = new_storage_entry::(key, init.take().unwrap()); - if cx.generation() == 0 { + let mut storage_entry = new_storage_entry::(key, init.take().unwrap()); + if generation() == 0 { // The first generation is rendered on the server side and so must be hydrated. - cx.needs_update(); + needs_update(); } - if cx.generation() == 1 { + if generation() == 1 { // The first time the vdom is hydrated, we set the correct value from storage and set up the subscription to storage events. storage_entry.set(get_from_storage::(key_clone, init.take().unwrap())); storage_entry.save_to_storage_on_change(); @@ -123,20 +124,20 @@ 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(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal +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::Key: Clone, { - *cx.use_hook(|| new_synced_storage::(cx, key, init)) + use_hook(|| new_synced_storage::(key, init)) } /// Create a signal that can be used to store data that will persist across application reloads and be synced across all app sessions for a given installation or browser. /// /// 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(cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T) -> Signal +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, @@ -150,25 +151,25 @@ where Signal::new(init.take().unwrap()()) } else if cfg!(feature = "hydrate") { let key_clone = key.clone(); - let storage_entry = new_synced_storage_entry::(key, init.take().unwrap()); - if cx.generation() == 0 { + let mut storage_entry = new_synced_storage_entry::(key, init.take().unwrap()); + if generation() == 0 { // The first generation is rendered on the server side and so must be hydrated. - cx.needs_update(); + needs_update(); } - if cx.generation() == 1 { + if generation() == 1 { // The first time the vdom is hydrated, we set the correct value from storage and set up the subscription to storage events. storage_entry .entry .set(get_from_storage::(key_clone, init.take().unwrap())); storage_entry.save_to_storage_on_change(); - storage_entry.subscribe_to_storage(cx); + storage_entry.subscribe_to_storage(); } *storage_entry.data() } else { // The client is rendered normally, so we can just use the synced storage entry. let storage_entry = new_synced_storage_entry::(key, init.take().unwrap()); storage_entry.save_to_storage_on_change(); - storage_entry.subscribe_to_storage(cx); + storage_entry.subscribe_to_storage(); *storage_entry.data() } }; @@ -176,31 +177,26 @@ 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( - cx: &ScopeState, - key: S::Key, - init: impl FnOnce() -> T, -) -> &mut StorageEntry +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, { - cx.use_hook(|| new_storage_entry::(key, init)) + use_hook(|| new_storage_entry::(key, init)) } /// 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( - cx: &ScopeState, key: S::Key, init: impl FnOnce() -> T, -) -> &mut SyncedStorageEntry +) -> SyncedStorageEntry where S: StorageBacking + StorageSubscriber, T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static, S::Key: Clone, { - cx.use_hook(|| new_synced_storage_entry::(key, init)) + use_hook(|| new_synced_storage_entry::(key, init)) } /// Returns a StorageEntry with the latest value from storage or the init value if it doesn't exist. @@ -244,7 +240,6 @@ pub fn get_from_storage< data }) } -// End use_storage hooks /// A trait for common functionality between StorageEntry and SyncedStorageEntry pub trait StorageEntryTrait: @@ -269,19 +264,25 @@ pub trait StorageEntryTrait: T: Serialize + DeserializeOwned + Clone + PartialEq + 'static, { let entry_clone = self.clone(); - let old = Signal::new(self.data().value()); + let old = Signal::new(self.data().cloned()); let data = *self.data(); - Effect::new(move || { - if *old() != *data() { - log::trace!("state value changed, trying to save"); - entry_clone.save(); + spawn(async move { + loop { + let (rc, mut reactive_context) = ReactiveContext::new(); + rc.run_in(|| { + if *old.read() != *data.read() { + tracing::trace!("Saving to storage"); + entry_clone.save(); + } + }); + if reactive_context.next().await.is_none() { + break; + } } }); } } -// Start SyncedStorageEntry - /// A wrapper around StorageEntry that provides a channel to subscribe to updates to the underlying storage. #[derive(Clone)] pub struct SyncedStorageEntry< @@ -313,15 +314,14 @@ where } /// Creates a hook that will update the state when the underlying storage changes - pub fn subscribe_to_storage(&self, cx: &ScopeState) { + pub fn subscribe_to_storage(&self) { let storage_entry_signal = *self.data(); let channel = self.channel.clone(); - cx.spawn(async move { + spawn(async move { to_owned![channel, storage_entry_signal]; loop { // Wait for an update to the channel if channel.changed().await.is_ok() { - log::trace!("channel changed"); // Retrieve the latest value from the channel, mark it as read, and update the state let payload = channel.borrow_and_update(); *storage_entry_signal.write() = payload @@ -346,11 +346,9 @@ where // - 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 { - log::trace!("value is the same, not saving"); return; } } - log::trace!("saving"); self.entry.save(); } @@ -367,10 +365,6 @@ where } } -// End SyncedStorageEntry - -// Start StorageEntry - /// 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< @@ -412,7 +406,7 @@ where { fn save(&self) { let _ = self.storage_save_lock.try_lock().map(|_| { - S::set(self.key.clone(), &self.data.value()); + S::set(self.key.clone(), &*self.data.read()); }); } @@ -439,6 +433,14 @@ impl D } } +impl DerefMut + for StorageEntry +{ + fn deref_mut(&mut self) -> &mut Signal { + &mut self.data + } +} + impl Display for StorageEntry { @@ -454,9 +456,6 @@ impl { /// Unsubscribes from events from a storage backing for the given key fn unsubscribe(key: &S::Key); } -// End Storage Backing traits - -// Start StorageSenderEntry /// A struct to hold information about processing a storage event. pub struct StorageSubscription { @@ -515,10 +511,6 @@ impl StorageSubscription { } } -// End StorageSenderEntry - -// Start StorageChannelPayload - /// A payload for a storage channel that contains the latest value from storage. #[derive(Clone, Debug)] pub struct StorageChannelPayload { @@ -544,9 +536,8 @@ impl Default for StorageChannelPayload { Self { data: Arc::new(()) } } } -// End StorageChannelPayload -// Start helper functions +// Helper functions /// Serializes a value to a string and compresses it. pub(crate) fn serde_to_string(value: &T) -> String { @@ -589,15 +580,8 @@ pub(crate) fn try_serde_from_string(value: &str) -> Option< match yazi::decompress(&bytes, yazi::Format::Zlib) { Ok((decompressed, _)) => match postcard::from_bytes(&decompressed) { Ok(v) => Some(v), - Err(err) => { - log::error!("Error deserializing value from storage: {:?}", err); - None - } + Err(err) => None, }, - Err(err) => { - log::error!("Error decompressing value from storage: {:?}", err); - None - } + Err(err) => None, } } -// End helper functions diff --git a/std/src/storage/persistence.rs b/std/src/storage/persistence.rs index 3832ff5..e7eb76e 100644 --- a/std/src/storage/persistence.rs +++ b/std/src/storage/persistence.rs @@ -1,6 +1,6 @@ use crate::storage::new_storage_entry; use crate::storage::SessionStorage; -use dioxus::prelude::ScopeState; +use dioxus::prelude::*; use dioxus_signals::Signal; use serde::de::DeserializeOwned; use serde::Serialize; @@ -14,11 +14,10 @@ use super::StorageEntryTrait; pub fn use_persistent< T: Serialize + DeserializeOwned + Default + Clone + Send + Sync + PartialEq + 'static, >( - cx: &ScopeState, key: impl ToString, init: impl FnOnce() -> T, ) -> Signal { - *cx.use_hook(|| new_persistent(key, init)) + use_hook(|| new_persistent(key, init)) } /// Creates a persistent storage signal that can be used to store data across application reloads. @@ -45,10 +44,9 @@ pub fn new_persistent< pub fn use_singleton_persistent< T: Serialize + DeserializeOwned + Default + Clone + Send + Sync + PartialEq + 'static, >( - cx: &ScopeState, init: impl FnOnce() -> T, ) -> Signal { - *cx.use_hook(|| new_singleton_persistent(init)) + use_hook(|| new_singleton_persistent(init)) } /// Create a persistent storage signal that can be used to store data across application reloads. @@ -64,6 +62,5 @@ pub fn new_singleton_persistent< ) -> Signal { let caller = std::panic::Location::caller(); let key = format!("{}:{}", caller.file(), caller.line()); - log::trace!("singleton_persistent key: \"{}\"", key); new_persistent(key, init) } From 3a27bdf6a0c903fc7fb2bad5255401a6630f72b8 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 28 Mar 2024 15:21:23 -0500 Subject: [PATCH 106/108] fix formatting --- examples/storage/src/main.rs | 1 - std/src/storage/mod.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/storage/src/main.rs b/examples/storage/src/main.rs index d83cb83..a488508 100644 --- a/examples/storage/src/main.rs +++ b/examples/storage/src/main.rs @@ -25,7 +25,6 @@ enum Route { #[component] fn Footer() -> Element { - let new_window = { #[cfg(feature = "desktop")] { diff --git a/std/src/storage/mod.rs b/std/src/storage/mod.rs index 170943d..43f5bfa 100644 --- a/std/src/storage/mod.rs +++ b/std/src/storage/mod.rs @@ -42,9 +42,9 @@ use std::sync::{Arc, Mutex}; use tokio::sync::watch::error::SendError; use tokio::sync::watch::{Receiver, Sender}; +pub use client_storage::set_dir; #[cfg(not(target_family = "wasm"))] pub use client_storage::{set_dir_name, set_directory}; -pub use client_storage::set_dir; /// A storage hook that can be used to store data that will persist across application reloads. This hook is generic over the storage location which can be useful for other hooks. /// From 25e6b2a9766327a621860c37e0e1c96e39ebcd23 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 28 Mar 2024 21:27:57 -0500 Subject: [PATCH 107/108] address comments --- examples/storage/Cargo.toml | 2 +- std/Cargo.toml | 4 ++-- std/src/geolocation/mod.rs | 26 ++++++++++++-------------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/examples/storage/Cargo.toml b/examples/storage/Cargo.toml index 4d485b2..f4d790f 100644 --- a/examples/storage/Cargo.toml +++ b/examples/storage/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] dioxus-std = { workspace = true, features = ["storage"] } -dioxus = { version = "0.5.0-alpha.2", features = ["router"] } +dioxus = { workspace = true, features = ["router"] } [features] web = ["dioxus/web"] diff --git a/std/Cargo.toml b/std/Cargo.toml index 4de707b..e6e143c 100644 --- a/std/Cargo.toml +++ b/std/Cargo.toml @@ -63,10 +63,10 @@ storage = [ "dep:futures-util", # WASM - "dep:directories", + "dep:wasm-bindgen", # Not WASM - "dep:wasm-bindgen", + "dep:directories", ] # CI testing diff --git a/std/src/geolocation/mod.rs b/std/src/geolocation/mod.rs index 67549e7..79a3ec8 100644 --- a/std/src/geolocation/mod.rs +++ b/std/src/geolocation/mod.rs @@ -1,14 +1,12 @@ -#[cfg(any(windows, target_family = "wasm"))] -mod core; -#[cfg(any(windows, target_family = "wasm"))] -mod platform; -#[cfg(any(windows, target_family = "wasm"))] -mod use_geolocation; - -#[cfg(any(windows, target_family = "wasm"))] -pub use self::core::*; -#[cfg(any(windows, target_family = "wasm"))] -pub use self::use_geolocation::*; - -#[cfg(not(any(windows, target_family = "wasm")))] -compile_error!("The geolocation module is not supported on this platform."); +cfg_if::cfg_if! { + if #[cfg(any(windows, target_family = "wasm"))] { + pub mod core; + pub mod platform; + pub mod use_geolocation; + pub use self::core::*; + pub use self::use_geolocation::*; + } + else { + compile_error!("The geolocation module is not supported on this platform."); + } +} From fa7871c0564bb118db8b693e1925cc18732a4499 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 1 Apr 2024 08:50:34 -0500 Subject: [PATCH 108/108] remove unnessicary lock --- std/src/storage/mod.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/std/src/storage/mod.rs b/std/src/storage/mod.rs index 43f5bfa..884995b 100644 --- a/std/src/storage/mod.rs +++ b/std/src/storage/mod.rs @@ -38,7 +38,7 @@ use serde::{de::DeserializeOwned, Serialize}; use std::any::Any; use std::fmt::{Debug, Display}; use std::ops::{Deref, DerefMut}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use tokio::sync::watch::error::SendError; use tokio::sync::watch::{Receiver, Sender}; @@ -375,9 +375,6 @@ pub struct StorageEntry< pub(crate) key: S::Key, /// A signal that can be used to read and modify the state pub(crate) data: Signal, - /// An optional channel to subscribe to updates to the underlying storage - /// A lock to prevent multiple saves from happening at the same time - storage_save_lock: Arc>, // TODO: probably unnecessary } impl StorageEntry @@ -394,7 +391,6 @@ where data, current_scope_id().expect("must be called from inside of the dioxus context"), ), - storage_save_lock: Arc::new(Mutex::new(())), } } } @@ -405,9 +401,7 @@ where T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static, { fn save(&self) { - let _ = self.storage_save_lock.try_lock().map(|_| { - S::set(self.key.clone(), &*self.data.read()); - }); + S::set(self.key.clone(), &*self.data.read()); } fn update(&mut self) {