dioxus_sdk_storage/
lib.rs

1#![allow(clippy::collapsible_if)]
2
3//! Local and persistent storage.
4//!
5//! Handle local storage ergonomically.
6//!
7//! ## Usage
8//! ```rust
9//! use dioxus_sdk_storage::use_persistent;
10//! use dioxus::prelude::*;
11//!
12//! #[component]
13//! fn App() -> Element {
14//!     let mut num = use_persistent("count", || 0);
15//!     rsx! {
16//!         div {
17//!             button {
18//!                 onclick: move |_| {
19//!                     *num.write() += 1;
20//!                 },
21//!                 "Increment"
22//!             }
23//!             div {
24//!                 "{*num.read()}"
25//!             }
26//!         }
27//!     }
28//! }
29//! ```
30
31mod client_storage;
32mod persistence;
33
34pub use client_storage::{LocalStorage, SessionStorage};
35use dioxus::core::{ReactiveContext, current_scope_id, generation, needs_update};
36use dioxus::logger::tracing::trace;
37use futures_util::stream::StreamExt;
38pub use persistence::{
39    new_persistent, new_singleton_persistent, use_persistent, use_singleton_persistent,
40};
41use std::cell::RefCell;
42use std::rc::Rc;
43
44use dioxus::prelude::*;
45use serde::{Serialize, de::DeserializeOwned};
46use std::any::Any;
47use std::fmt::{Debug, Display};
48use std::ops::{Deref, DerefMut};
49use std::sync::Arc;
50use tokio::sync::watch::error::SendError;
51use tokio::sync::watch::{Receiver, Sender};
52
53#[cfg(not(target_family = "wasm"))]
54pub use client_storage::{set_dir_name, set_directory};
55
56/// 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///
58/// This hook returns a Signal that can be used to read and modify the state.
59///
60/// ## Usage
61///
62/// ```rust
63/// use dioxus_sdk_storage::{use_storage, StorageBacking};
64/// use dioxus::prelude::*;
65/// use dioxus_signals::Signal;
66///
67/// // This hook can be used with any storage backing without multiple versions of the hook
68/// fn use_user_id<S>() -> Signal<usize> where S: StorageBacking<Key=&'static str> {
69///     use_storage::<S, _>("user-id", || 123)
70/// }
71/// ```
72pub fn use_storage<S, T>(key: S::Key, init: impl FnOnce() -> T) -> Signal<T>
73where
74    S: StorageBacking,
75    T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
76    S::Key: Clone,
77{
78    let mut init = Some(init);
79    let storage = use_hook(|| new_storage::<S, T>(key, || init.take().unwrap()()));
80    use_hydrate_storage::<S, T>(storage, init);
81    storage
82}
83
84#[allow(unused)]
85enum StorageMode {
86    Client,
87    HydrateClient,
88    Server,
89}
90
91impl StorageMode {
92    // Get the active mode
93    #[allow(unreachable_code)]
94    const fn current() -> Self {
95        server_only! {
96            return StorageMode::Server;
97        }
98
99        fullstack! {
100            return StorageMode::HydrateClient;
101        }
102
103        StorageMode::Client
104    }
105}
106
107/// Creates a Signal that can be used to store data that will persist across application reloads.
108///
109/// This hook returns a Signal that can be used to read and modify the state.
110///
111/// ## Usage
112///
113/// ```rust
114/// use dioxus_sdk_storage::{new_storage, StorageBacking};
115/// use dioxus::prelude::*;
116/// use dioxus_signals::Signal;
117///
118/// // This hook can be used with any storage backing without multiple versions of the hook
119/// fn user_id<S>() -> Signal<usize> where S: StorageBacking<Key=&'static str> {
120///     new_storage::<S, _>("user-id", || 123)
121/// }
122/// ```
123pub fn new_storage<S, T>(key: S::Key, init: impl FnOnce() -> T) -> Signal<T>
124where
125    S: StorageBacking,
126    T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
127    S::Key: Clone,
128{
129    let mode = StorageMode::current();
130
131    match mode {
132        // SSR does not support storage on the backend. We will just use a normal Signal to represent the initial state.
133        // The client will hydrate this with a correct StorageEntry and maintain state.
134        StorageMode::Server => Signal::new(init()),
135        _ => {
136            // Otherwise the client is rendered normally, so we can just use the storage entry.
137            let storage_entry = new_storage_entry::<S, T>(key, init);
138            storage_entry.save_to_storage_on_change();
139            storage_entry.data
140        }
141    }
142}
143
144/// 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.
145///
146/// This hook returns a Signal that can be used to read and modify the state.
147/// 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.
148pub fn use_synced_storage<S, T>(key: S::Key, init: impl FnOnce() -> T) -> Signal<T>
149where
150    S: StorageBacking + StorageSubscriber<S>,
151    T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
152    S::Key: Clone,
153{
154    let mut init = Some(init);
155    let storage = use_hook(|| new_synced_storage::<S, T>(key, || init.take().unwrap()()));
156    use_hydrate_storage::<S, T>(storage, init);
157    storage
158}
159
160/// 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.
161///
162/// This hook returns a Signal that can be used to read and modify the state.
163/// 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.
164pub fn new_synced_storage<S, T>(key: S::Key, init: impl FnOnce() -> T) -> Signal<T>
165where
166    S: StorageBacking + StorageSubscriber<S>,
167    T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
168    S::Key: Clone,
169{
170    {
171        let mode = StorageMode::current();
172
173        match mode {
174            // SSR does not support synced storage on the backend. We will just use a normal Signal to represent the initial state.
175            // The client will hydrate this with a correct SyncedStorageEntry and maintain state.
176            StorageMode::Server => Signal::new(init()),
177            _ => {
178                // The client is rendered normally, so we can just use the synced storage entry.
179                let storage_entry = new_synced_storage_entry::<S, T>(key, init);
180                storage_entry.save_to_storage_on_change();
181                storage_entry.subscribe_to_storage();
182                *storage_entry.data()
183            }
184        }
185    }
186}
187
188/// A hook that creates a StorageEntry with the latest value from storage or the init value if it doesn't exist.
189pub fn use_storage_entry<S, T>(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry<S, T>
190where
191    S: StorageBacking,
192    T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
193    S::Key: Clone,
194{
195    let mut init = Some(init);
196    let signal = use_hook(|| new_storage_entry::<S, T>(key, || init.take().unwrap()()));
197    use_hydrate_storage::<S, T>(*signal.data(), init);
198    signal
199}
200
201/// 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.
202pub fn use_synced_storage_entry<S, T>(
203    key: S::Key,
204    init: impl FnOnce() -> T,
205) -> SyncedStorageEntry<S, T>
206where
207    S: StorageBacking + StorageSubscriber<S>,
208    T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
209    S::Key: Clone,
210{
211    let mut init = Some(init);
212    let signal = use_hook(|| new_synced_storage_entry::<S, T>(key, || init.take().unwrap()()));
213    use_hydrate_storage::<S, T>(*signal.data(), init);
214    signal
215}
216
217/// Returns a StorageEntry with the latest value from storage or the init value if it doesn't exist.
218pub fn new_storage_entry<S, T>(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry<S, T>
219where
220    S: StorageBacking,
221    T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
222    S::Key: Clone,
223{
224    let data = get_from_storage::<S, T>(key.clone(), init);
225    StorageEntry::new(key, data)
226}
227
228/// Returns a synced StorageEntry with the latest value from storage or the init value if it doesn't exist.
229///
230/// This differs from `storage_entry` in that this one will return a channel to subscribe to updates to the underlying storage.
231pub fn new_synced_storage_entry<S, T>(
232    key: S::Key,
233    init: impl FnOnce() -> T,
234) -> SyncedStorageEntry<S, T>
235where
236    S: StorageBacking + StorageSubscriber<S>,
237    T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static,
238    S::Key: Clone,
239{
240    let data = get_from_storage::<S, T>(key.clone(), init);
241    SyncedStorageEntry::new(key, data)
242}
243
244/// Returns a value from storage or the init value if it doesn't exist.
245pub fn get_from_storage<
246    S: StorageBacking,
247    T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static,
248>(
249    key: S::Key,
250    init: impl FnOnce() -> T,
251) -> T {
252    S::get(&key).unwrap_or_else(|| {
253        let data = init();
254        S::set(key, &data);
255        data
256    })
257}
258
259/// A trait for common functionality between StorageEntry and SyncedStorageEntry
260pub trait StorageEntryTrait<S: StorageBacking, T: PartialEq + Clone + 'static>:
261    Clone + 'static
262{
263    /// Saves the current state to storage
264    fn save(&self);
265
266    /// Updates the state from storage
267    fn update(&mut self);
268
269    /// Gets the key used to store the data in storage
270    fn key(&self) -> &S::Key;
271
272    /// Gets the signal that can be used to read and modify the state
273    fn data(&self) -> &Signal<T>;
274
275    /// Creates a hook that will save the state to storage when the state changes
276    fn save_to_storage_on_change(&self)
277    where
278        S: StorageBacking,
279        T: Serialize + DeserializeOwned + Clone + PartialEq + 'static,
280    {
281        let entry_clone = self.clone();
282        let old = RefCell::new(None);
283        let data = *self.data();
284        spawn(async move {
285            loop {
286                let (rc, mut reactive_context) = ReactiveContext::new();
287                rc.run_in(|| {
288                    if old.borrow().as_ref() != Some(&*data.read()) {
289                        trace!("Saving to storage");
290                        entry_clone.save();
291                        old.replace(Some(data()));
292                    }
293                });
294                if reactive_context.next().await.is_none() {
295                    break;
296                }
297            }
298        });
299    }
300}
301
302/// A wrapper around StorageEntry that provides a channel to subscribe to updates to the underlying storage.
303#[derive(Clone)]
304pub struct SyncedStorageEntry<
305    S: StorageBacking + StorageSubscriber<S>,
306    T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
307> {
308    /// The underlying StorageEntry that is used to store the data and track the state
309    pub(crate) entry: StorageEntry<S, T>,
310    /// The channel to subscribe to updates to the underlying storage
311    pub(crate) channel: Receiver<StorageChannelPayload>,
312}
313
314impl<S, T> SyncedStorageEntry<S, T>
315where
316    S: StorageBacking + StorageSubscriber<S>,
317    T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
318{
319    pub fn new(key: S::Key, data: T) -> Self {
320        let channel = S::subscribe::<T>(&key);
321        Self {
322            entry: StorageEntry::new(key, data),
323            channel,
324        }
325    }
326
327    /// Gets the channel to subscribe to updates to the underlying storage
328    pub fn channel(&self) -> &Receiver<StorageChannelPayload> {
329        &self.channel
330    }
331
332    /// Creates a hook that will update the state when the underlying storage changes
333    pub fn subscribe_to_storage(&self) {
334        let storage_entry_signal = *self.data();
335        let channel = self.channel.clone();
336        spawn(async move {
337            to_owned![channel, storage_entry_signal];
338            loop {
339                // Wait for an update to the channel
340                if channel.changed().await.is_ok() {
341                    // Retrieve the latest value from the channel, mark it as read, and update the state
342                    let payload = channel.borrow_and_update();
343                    *storage_entry_signal.write() = payload
344                        .data
345                        .downcast_ref::<T>()
346                        .expect("Type mismatch with storage entry")
347                        .clone();
348                }
349            }
350        });
351    }
352}
353
354impl<S, T> StorageEntryTrait<S, T> for SyncedStorageEntry<S, T>
355where
356    S: StorageBacking + StorageSubscriber<S>,
357    T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
358{
359    #[allow(clippy::collapsible_if)]
360    fn save(&self) {
361        //  We want to save in the following conditions
362        //      - The value from the channel is different from the current value
363        //      - The value from the channel could not be determined, likely because it hasn't been set yet
364        if let Some(payload) = self.channel.borrow().data.downcast_ref::<T>() {
365            if *self.entry.data.read() == *payload {
366                return;
367            }
368        }
369        self.entry.save();
370    }
371
372    fn update(&mut self) {
373        self.entry.update();
374    }
375
376    fn key(&self) -> &S::Key {
377        self.entry.key()
378    }
379
380    fn data(&self) -> &Signal<T> {
381        &self.entry.data
382    }
383}
384
385/// 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.
386#[derive(Clone)]
387pub struct StorageEntry<
388    S: StorageBacking,
389    T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
390> {
391    /// The key used to store the data in storage
392    pub(crate) key: S::Key,
393    /// A signal that can be used to read and modify the state
394    pub(crate) data: Signal<T>,
395}
396
397impl<S, T> StorageEntry<S, T>
398where
399    S: StorageBacking,
400    T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
401    S::Key: Clone,
402{
403    /// Creates a new StorageEntry
404    pub fn new(key: S::Key, data: T) -> Self {
405        Self {
406            key,
407            data: Signal::new_in_scope(data, current_scope_id()),
408        }
409    }
410}
411
412impl<S, T> StorageEntryTrait<S, T> for StorageEntry<S, T>
413where
414    S: StorageBacking,
415    T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static,
416{
417    fn save(&self) {
418        S::set(self.key.clone(), &*self.data.read());
419    }
420
421    fn update(&mut self) {
422        self.data = S::get(&self.key).unwrap_or(self.data);
423    }
424
425    fn key(&self) -> &S::Key {
426        &self.key
427    }
428
429    fn data(&self) -> &Signal<T> {
430        &self.data
431    }
432}
433
434impl<S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync> Deref
435    for StorageEntry<S, T>
436{
437    type Target = Signal<T>;
438
439    fn deref(&self) -> &Signal<T> {
440        &self.data
441    }
442}
443
444impl<S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync> DerefMut
445    for StorageEntry<S, T>
446{
447    fn deref_mut(&mut self) -> &mut Signal<T> {
448        &mut self.data
449    }
450}
451
452impl<S: StorageBacking, T: Display + Serialize + DeserializeOwned + Clone + Send + Sync> Display
453    for StorageEntry<S, T>
454{
455    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456        self.data.fmt(f)
457    }
458}
459
460impl<S: StorageBacking, T: Debug + Serialize + DeserializeOwned + Clone + Send + Sync> Debug
461    for StorageEntry<S, T>
462{
463    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464        self.data.fmt(f)
465    }
466}
467
468/// A trait for a storage backing
469pub trait StorageBacking: Clone + 'static {
470    /// The key type used to store data in storage
471    type Key: PartialEq + Clone + Debug + Send + Sync + 'static;
472    /// Gets a value from storage for the given key
473    fn get<T: DeserializeOwned + Clone + 'static>(key: &Self::Key) -> Option<T>;
474    /// Sets a value in storage for the given key
475    fn set<T: Serialize + Send + Sync + Clone + 'static>(key: Self::Key, value: &T);
476}
477
478/// A trait for a subscriber to events from a storage backing
479pub trait StorageSubscriber<S: StorageBacking> {
480    /// Subscribes to events from a storage backing for the given key
481    fn subscribe<T: DeserializeOwned + Send + Sync + Clone + 'static>(
482        key: &S::Key,
483    ) -> Receiver<StorageChannelPayload>;
484    /// Unsubscribes from events from a storage backing for the given key
485    fn unsubscribe(key: &S::Key);
486}
487
488/// A struct to hold information about processing a storage event.
489pub struct StorageSubscription {
490    /// A getter function that will get the data from storage and return it as a StorageChannelPayload.
491    pub(crate) getter: Box<dyn Fn() -> StorageChannelPayload + 'static + Send + Sync>,
492
493    /// The channel to send the data to.
494    pub(crate) tx: Arc<Sender<StorageChannelPayload>>,
495}
496
497impl StorageSubscription {
498    pub fn new<
499        S: StorageBacking + StorageSubscriber<S>,
500        T: DeserializeOwned + Send + Sync + Clone + 'static,
501    >(
502        tx: Sender<StorageChannelPayload>,
503        key: S::Key,
504    ) -> Self {
505        let getter = move || {
506            let data = S::get::<T>(&key).unwrap();
507            StorageChannelPayload::new(data)
508        };
509        Self {
510            getter: Box::new(getter),
511            tx: Arc::new(tx),
512        }
513    }
514
515    /// Gets the latest data from storage and sends it to the channel.
516    pub fn get_and_send(&self) -> Result<(), SendError<StorageChannelPayload>> {
517        let payload = (self.getter)();
518        self.tx.send(payload)
519    }
520}
521
522/// A payload for a storage channel that contains the latest value from storage.
523#[derive(Clone, Debug)]
524pub struct StorageChannelPayload {
525    data: Arc<dyn Any + Send + Sync>,
526}
527
528impl StorageChannelPayload {
529    /// Creates a new StorageChannelPayload
530    pub fn new<T: Send + Sync + 'static>(data: T) -> Self {
531        Self {
532            data: Arc::new(data),
533        }
534    }
535
536    /// Gets the data from the payload
537    pub fn data<T: 'static>(&self) -> Option<&T> {
538        self.data.downcast_ref::<T>()
539    }
540}
541
542impl Default for StorageChannelPayload {
543    fn default() -> Self {
544        Self { data: Arc::new(()) }
545    }
546}
547
548// Helper functions
549
550/// Serializes a value to a string and compresses it.
551pub(crate) fn serde_to_string<T: Serialize>(value: &T) -> String {
552    let mut serialized = Vec::new();
553    ciborium::into_writer(value, &mut serialized).unwrap();
554    let compressed = yazi::compress(
555        &serialized,
556        yazi::Format::Zlib,
557        yazi::CompressionLevel::BestSize,
558    )
559    .unwrap();
560    let as_str: String = compressed
561        .iter()
562        .flat_map(|u| {
563            [
564                char::from_digit(((*u & 0xF0) >> 4).into(), 16).unwrap(),
565                char::from_digit((*u & 0x0F).into(), 16).unwrap(),
566            ]
567            .into_iter()
568        })
569        .collect();
570    as_str
571}
572
573#[allow(unused)]
574/// Deserializes a value from a string and unwraps errors.
575pub(crate) fn serde_from_string<T: DeserializeOwned>(value: &str) -> T {
576    try_serde_from_string(value).unwrap()
577}
578
579/// Deserializes and decompresses a value from a string and returns None if there is an error.
580pub(crate) fn try_serde_from_string<T: DeserializeOwned>(value: &str) -> Option<T> {
581    let mut bytes: Vec<u8> = Vec::new();
582    let mut chars = value.chars();
583    while let Some(c) = chars.next() {
584        let n1 = c.to_digit(16)?;
585        let c2 = chars.next()?;
586        let n2 = c2.to_digit(16)?;
587        bytes.push((n1 * 16 + n2) as u8);
588    }
589
590    match yazi::decompress(&bytes, yazi::Format::Zlib) {
591        Ok((decompressed, _)) => ciborium::from_reader(std::io::Cursor::new(decompressed)).ok(),
592        Err(_) => None,
593    }
594}
595
596// Take a signal and a storage key and hydrate the value if we are hydrating the client.
597pub(crate) fn use_hydrate_storage<S, T>(
598    mut signal: Signal<T>,
599    init: Option<impl FnOnce() -> T>,
600) -> Signal<T>
601where
602    S: StorageBacking,
603    T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
604    S::Key: Clone,
605{
606    let mode = StorageMode::current();
607    // We read the value from storage and store it here if we are hydrating the client.
608    let original_storage_value: Rc<RefCell<Option<T>>> = use_hook(|| Rc::new(RefCell::new(None)));
609
610    // If we are not hydrating the client
611    if let StorageMode::HydrateClient = mode {
612        if generation() == 0 {
613            // We always use the default value for the first render.
614            if let Some(default_value) = init {
615                // Read the value from storage before we reset it for hydration
616                original_storage_value
617                    .borrow_mut()
618                    .replace(signal.peek().clone());
619                signal.set(default_value());
620            }
621            // And we trigger a new render for after hydration
622            needs_update();
623        }
624        if generation() == 1 {
625            // After we hydrate, set the original value from storage
626            if let Some(original_storage_value) = original_storage_value.borrow_mut().take() {
627                signal.set(original_storage_value);
628            }
629        }
630    }
631    signal
632}