Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
03f8b19
Working human readable storage example
Craig-Macomber Jun 15, 2025
7bda9ab
Messy failed attempt to decouple from serde
Craig-Macomber Jun 16, 2025
8b1c690
Fix build for desktop (crashes at runtime, web does not compile yet)
Craig-Macomber Jul 20, 2025
3f9cee6
Get storage working without a hard dependency on encoding with serde
Craig-Macomber Aug 24, 2025
89824a4
Dedupe StoragePersistence logic for SessionStorage
Craig-Macomber Aug 29, 2025
a787ec5
cleanup memory
Craig-Macomber Aug 29, 2025
a23ed60
Refactor: Fix build, but still errors
Craig-Macomber Aug 29, 2025
d3a28dd
Fix error
Craig-Macomber Aug 29, 2025
6eb19e4
cleanup example
Craig-Macomber Aug 29, 2025
fcc1331
Fix web build
Craig-Macomber Aug 30, 2025
906b6c6
Split out default_encoder
Craig-Macomber Aug 30, 2025
a122b2b
Generic StoragePersistence
Craig-Macomber Aug 30, 2025
4616336
StorageSubscription references StoragePersistence not StorageBacking
Craig-Macomber Aug 30, 2025
a581bcd
Make use_persistent and related use LocalStorage to match docs
Craig-Macomber Aug 30, 2025
314a4dc
Make StorageEntry use StoragePersistence instead of StorageBacking, a…
Craig-Macomber Aug 30, 2025
5ea76ca
Add todos for issues
Craig-Macomber Aug 30, 2025
74b1cc4
Fix web session storage
Craig-Macomber Aug 30, 2025
6cd6b65
Better document storage type selection in "persistence"
Craig-Macomber Aug 30, 2025
01bf9f6
Fix sync of local storage for web
Craig-Macomber Aug 30, 2025
0c5665c
Add some tracking
Craig-Macomber Aug 30, 2025
f24db9c
Fix sync with custom encoding
Craig-Macomber Aug 30, 2025
bd4b945
use trace
Craig-Macomber Aug 30, 2025
d58fc0d
Use warn!
Craig-Macomber Aug 30, 2025
0ce57e4
Cleanup
Craig-Macomber Aug 30, 2025
372b20c
More cleanup, remove unneeded #[derive(Clone)]
Craig-Macomber Aug 30, 2025
9b2bc48
Fix doc tests
Craig-Macomber Aug 30, 2025
bfe02a5
fix desktop build
Craig-Macomber Aug 30, 2025
b9e2790
unit test default encoder
Craig-Macomber Aug 30, 2025
0c776cd
Unit tests HumanReadableStorage
Craig-Macomber Aug 30, 2025
5abe4cc
Remove need for LayeredStorage
Craig-Macomber Aug 30, 2025
196e039
Remove unneeded clone
Craig-Macomber Aug 30, 2025
148f907
Clippy
Craig-Macomber Aug 31, 2025
4ddf0ff
Fix clippy for web
Craig-Macomber Aug 31, 2025
6d39238
More more clippy fix
Craig-Macomber Aug 31, 2025
fa5343d
Better comments
Craig-Macomber Sep 1, 2025
0de2476
Improve a couple of doc comments
Craig-Macomber Sep 2, 2025
431824d
Fix partial comment
Craig-Macomber Sep 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ edition = "2021"
[dependencies]
dioxus = { workspace = true, features = ["router"] }
dioxus_storage = { workspace = true }
serde = "1.0.219"
serde_json = "1.0.140"

[features]
default = ["desktop"]
Expand Down
124 changes: 112 additions & 12 deletions examples/storage/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use dioxus::prelude::*;
use dioxus_storage::*;

use serde::{de::DeserializeOwned, Serialize};

fn main() {
dioxus_storage::set_dir!();
launch(App);
Expand Down Expand Up @@ -51,18 +53,20 @@ fn Footer() -> Element {

rsx! {
div {
Outlet::<Route> { }
Outlet::<Route> {}

p {
"----"
}
p { "----" }

{new_window}

nav {
ul {
li { Link { to: Route::Home {}, "Home" } }
li { Link { to: Route::Storage {}, "Storage" } }
li {
Link { to: Route::Home {}, "Home" }
}
li {
Link { to: Route::Storage {}, "Storage" }
}
}
}
}
Expand All @@ -76,27 +80,123 @@ fn Home() -> Element {

#[component]
fn Storage() -> Element {
let mut count_session = use_singleton_persistent(|| 0);
let mut count_local = use_synced_storage::<LocalStorage, i32>("synced".to_string(), || 0);
// TODO: maybe this should sync? (It does not currently)
// Uses default encoder and LocalStorage implicitly.
let mut count_persistent = use_persistent("persistent".to_string(), || 0);

// Uses session storage with the default encoder.
let mut count_session = use_storage::<SessionStorage, i32>("session".to_string(), || 0);

// Uses local storage with the default encoder.
let mut count_local = use_synced_storage::<LocalStorage, i32>("local".to_string(), || 0);

// Uses LocalStorage with our custom human readable encoder
let mut count_local_human = use_synced_storage::<HumanReadableStorage<LocalStorage>, i32>(
"local_human".to_string(),
|| 0,
);

rsx!(
div {
button {
onclick: move |_| {
*count_persistent.write() += 1;
},
"Click me!"
}
"Persisted to local storage (but not synced): Clicked {count_persistent} times."
}
div {
button {
onclick: move |_| {
*count_session.write() += 1;
},
"Click me!"
},
"I persist for the current session. Clicked {count_session} times"
}
"Session: Clicked {count_session} times."
}
div {
button {
onclick: move |_| {
*count_local.write() += 1;
},
"Click me!"
},
"I persist across all sessions. Clicked {count_local} times"
}
"Local: Clicked {count_local} times."
}
div {
button {
onclick: move |_| {
*count_local_human.write() += 1;
},
"Click me!"
}
"Human readable local: Clicked {count_local_human} times."
}
)
}

/// A [StorageBacking] with [HumanReadableEncoding] and selectable [StoragePersistence].
struct HumanReadableStorage<Persistence> {
p: std::marker::PhantomData<Persistence>,
}

impl<
T: Serialize + DeserializeOwned,
P: 'static + StoragePersistence<std::option::Option<T>, Value = Option<String>>,
> StorageBacking<T> for HumanReadableStorage<P>
{
type Encoder = HumanReadableEncoding;
type Persistence = P;
}

struct HumanReadableEncoding;

impl<T: Serialize + DeserializeOwned> StorageEncoder<T> for HumanReadableEncoding {
type EncodedValue = String;
type DecodeError = serde_json::Error;

fn deserialize(loaded: &Self::EncodedValue) -> Result<T, Self::DecodeError> {
serde_json::from_str(loaded)
}

fn serialize(value: &T) -> Self::EncodedValue {
serde_json::to_string_pretty(value).unwrap()
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::fmt::Debug;

#[test]
fn round_trips() {
round_trip(0);
round_trip(999);
round_trip("Text".to_string());
round_trip((1, 2, 3));
}

fn round_trip<T: Serialize + DeserializeOwned + PartialEq + Debug>(value: T) {
let encoded = HumanReadableEncoding::serialize(&value);
let decoded = HumanReadableEncoding::deserialize(&encoded);
assert_eq!(value, decoded.unwrap());
}

#[test]
fn can_decode_irregular_json_data() {
let decoded: (i32, i32) =
HumanReadableEncoding::deserialize(&" [ 1,2]".to_string()).unwrap();
assert_eq!(decoded, (1, 2))
}

#[test]
fn encodes_json_with_formatting() {
assert_eq!(HumanReadableEncoding::serialize(&1234), "1234");
assert_eq!(
HumanReadableEncoding::serialize(&(1, "a".to_string())),
"[\n 1,\n \"a\"\n]"
);
}
}
73 changes: 44 additions & 29 deletions packages/storage/src/client_storage/fs.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{StorageChannelPayload, StorageSubscription};
use crate::{StorageBacking, StorageChannelPayload, StorageEncoder, StorageSubscription};
use dioxus::logger::tracing::trace;
use serde::Serialize;
use serde::de::DeserializeOwned;
Expand All @@ -7,7 +7,7 @@ use std::io::Write;
use std::sync::{OnceLock, RwLock};
use tokio::sync::watch::{Receiver, channel};

use crate::{StorageBacking, StorageSubscriber, serde_to_string, try_serde_from_string};
use crate::{StoragePersistence, StorageSubscriber};

#[doc(hidden)]
/// Sets the directory where the storage files are located.
Expand All @@ -29,62 +29,77 @@ pub fn set_dir_name(name: &str) {
static LOCATION: OnceLock<std::path::PathBuf> = OnceLock::new();

/// Set a value in the configured storage location using the key as the file name.
fn set<T: Serialize>(key: String, value: &T) {
let as_str = serde_to_string(value);
fn set(key: &str, as_str: &Option<String>) {
let path = LOCATION
.get()
.expect("Call the set_dir macro before accessing persistant data");
std::fs::create_dir_all(path).unwrap();
.expect("Call the set_dir macro before accessing persistent data");

let file_path = path.join(key);
let mut file = std::fs::File::create(file_path).unwrap();
file.write_all(as_str.as_bytes()).unwrap();

match as_str {
Some(as_str) => {
std::fs::create_dir_all(path).unwrap();
let mut file = std::fs::File::create(file_path).unwrap();
file.write_all(as_str.as_bytes()).unwrap()
}
None => match std::fs::remove_file(file_path) {
Ok(_) => {}
Err(error) => match error.kind() {
std::io::ErrorKind::NotFound => {}
_ => panic!("{:?}", error),
},
},
}
}

/// Get a value from the configured storage location using the key as the file name.
fn get<T: DeserializeOwned>(key: &str) -> Option<T> {
fn get(key: &str) -> Option<String> {
let path = LOCATION
.get()
.expect("Call the set_dir macro before accessing persistant data")
.expect("Call the set_dir macro before accessing persistent data")
.join(key);
let s = std::fs::read_to_string(path).ok()?;
try_serde_from_string(&s)
std::fs::read_to_string(path).ok()
}

#[derive(Clone)]
/// [StoragePersistence] backed by a file.
pub struct LocalStorage;

impl StorageBacking for LocalStorage {
/// LocalStorage stores Option<String>.
impl<T: Clone + Send + Sync + 'static> StoragePersistence<T> for LocalStorage {
type Key = String;
type Value = Option<String>;

fn load(key: &Self::Key) -> Self::Value {
get(key)
}

fn set<T: Serialize + Send + Sync + Clone + 'static>(key: String, value: &T) {
let key_clone = key.clone();
let value_clone = (*value).clone();
fn store(key: &Self::Key, value: &Self::Value, unencoded: &T) {
set(key, value);

// If the subscriptions map is not initialized, we don't need to notify any subscribers.
if let Some(subscriptions) = SUBSCRIPTIONS.get() {
let read_binding = subscriptions.read().unwrap();
if let Some(subscription) = read_binding.get(&key_clone) {
if let Some(subscription) = read_binding.get(key) {
subscription
.tx
.send(StorageChannelPayload::new(value_clone))
// .send(StorageChannelPayload::new(value.clone()))
.send(StorageChannelPayload::new(unencoded.clone()))
.unwrap();
}
}
}

fn get<T: DeserializeOwned>(key: &String) -> Option<T> {
get(key)
}
}

// Note that this module contains an optimization that differs from the web version. Dioxus Desktop runs all windows in
// the same thread, meaning that we can just directly notify the subscribers via the same channels, rather than using the
// storage event listener.
impl StorageSubscriber<LocalStorage> for LocalStorage {
fn subscribe<T: DeserializeOwned + Send + Sync + Clone + 'static>(
key: &<LocalStorage as StorageBacking>::Key,
) -> Receiver<StorageChannelPayload> {
impl<
T: Send + Sync + Serialize + DeserializeOwned + Clone + 'static,
E: StorageEncoder<T, EncodedValue = String>,
S: StorageBacking<T, Encoder = E, Persistence = LocalStorage>,
> StorageSubscriber<T, S> for LocalStorage
{
fn subscribe(key: &String) -> Receiver<StorageChannelPayload> {
// Initialize the subscriptions map if it hasn't been initialized yet.
let subscriptions = SUBSCRIPTIONS.get_or_init(|| RwLock::new(HashMap::new()));

Expand All @@ -96,7 +111,7 @@ impl StorageSubscriber<LocalStorage> for LocalStorage {
None => {
drop(read_binding);
let (tx, rx) = channel::<StorageChannelPayload>(StorageChannelPayload::default());
let subscription = StorageSubscription::new::<LocalStorage, T>(tx, key.clone());
let subscription = StorageSubscription::new::<S, T>(tx, key.clone());

subscriptions
.write()
Expand All @@ -107,7 +122,7 @@ impl StorageSubscriber<LocalStorage> for LocalStorage {
}
}

fn unsubscribe(key: &<LocalStorage as StorageBacking>::Key) {
fn unsubscribe(key: &String) {
trace!("Unsubscribing from \"{}\"", key);

// Fail silently if unsubscribe is called but the subscriptions map isn't initialized yet.
Expand Down
57 changes: 47 additions & 10 deletions packages/storage/src/client_storage/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,61 @@ use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use std::sync::Arc;

use crate::StorageBacking;
use crate::{StorageBacking, StorageEncoder, StoragePersistence};

#[derive(Clone)]
/// [StoragePersistence] backed by the current [SessionStore].
///
/// Skips encoding, and just stores data using `Arc<dyn Any>>`.
pub struct SessionStorage;

impl StorageBacking for SessionStorage {
type Key = String;
impl<T: Clone + Any + Send + Sync> StorageBacking<T> for SessionStorage {
type Encoder = ArcEncoder;
type Persistence = SessionStorage;
}

fn set<T: Clone + 'static>(key: String, value: &T) {
let session = SessionStore::get_current_session();
session.borrow_mut().insert(key, Arc::new(value.clone()));
type Key = String;
type Value = Option<Arc<dyn Any>>;

fn store<T>(key: &Key, value: &Value, _unencoded: &T) {
let session = SessionStore::get_current_session();
match value {
Some(value) => {
session.borrow_mut().insert(key.clone(), value.clone());
}
None => {
session.borrow_mut().remove(key);
}
}
}

fn get<T: Clone + 'static>(key: &String) -> Option<T> {
impl<T> StoragePersistence<T> for SessionStorage {
type Key = Key;
type Value = Value;

fn load(key: &Self::Key) -> Self::Value {
let session = SessionStore::get_current_session();
let read_binding = session.borrow();
let value_any = read_binding.get(key)?;
value_any.downcast_ref::<T>().cloned()
read_binding.get(key).cloned()
}

fn store(key: &Self::Key, value: &Self::Value, unencoded: &T) {
store(key, value, unencoded);
}
}

/// A [StorageEncoder] which encodes Optional data by cloning it's content into `Arc<dyn Any>`.
pub struct ArcEncoder;

impl<T: Clone + Any> StorageEncoder<T> for ArcEncoder {
type EncodedValue = Arc<dyn Any>;
type DecodeError = &'static str;

fn deserialize(loaded: &Self::EncodedValue) -> Result<T, &'static str> {
loaded.downcast_ref::<T>().cloned().ok_or("Failed Downcast")
}

fn serialize(value: &T) -> Self::EncodedValue {
Arc::new(value.clone())
}
}

Expand Down
Loading
Loading