1#![allow(clippy::collapsible_if)]
2
3mod 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
56pub 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 #[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
107pub 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 StorageMode::Server => Signal::new(init()),
135 _ => {
136 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
144pub 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
160pub 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 StorageMode::Server => Signal::new(init()),
177 _ => {
178 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
188pub 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
201pub 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
217pub 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
228pub 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
244pub 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
259pub trait StorageEntryTrait<S: StorageBacking, T: PartialEq + Clone + 'static>:
261 Clone + 'static
262{
263 fn save(&self);
265
266 fn update(&mut self);
268
269 fn key(&self) -> &S::Key;
271
272 fn data(&self) -> &Signal<T>;
274
275 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#[derive(Clone)]
304pub struct SyncedStorageEntry<
305 S: StorageBacking + StorageSubscriber<S>,
306 T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
307> {
308 pub(crate) entry: StorageEntry<S, T>,
310 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 pub fn channel(&self) -> &Receiver<StorageChannelPayload> {
329 &self.channel
330 }
331
332 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 if channel.changed().await.is_ok() {
341 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 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#[derive(Clone)]
387pub struct StorageEntry<
388 S: StorageBacking,
389 T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
390> {
391 pub(crate) key: S::Key,
393 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 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
468pub trait StorageBacking: Clone + 'static {
470 type Key: PartialEq + Clone + Debug + Send + Sync + 'static;
472 fn get<T: DeserializeOwned + Clone + 'static>(key: &Self::Key) -> Option<T>;
474 fn set<T: Serialize + Send + Sync + Clone + 'static>(key: Self::Key, value: &T);
476}
477
478pub trait StorageSubscriber<S: StorageBacking> {
480 fn subscribe<T: DeserializeOwned + Send + Sync + Clone + 'static>(
482 key: &S::Key,
483 ) -> Receiver<StorageChannelPayload>;
484 fn unsubscribe(key: &S::Key);
486}
487
488pub struct StorageSubscription {
490 pub(crate) getter: Box<dyn Fn() -> StorageChannelPayload + 'static + Send + Sync>,
492
493 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 pub fn get_and_send(&self) -> Result<(), SendError<StorageChannelPayload>> {
517 let payload = (self.getter)();
518 self.tx.send(payload)
519 }
520}
521
522#[derive(Clone, Debug)]
524pub struct StorageChannelPayload {
525 data: Arc<dyn Any + Send + Sync>,
526}
527
528impl StorageChannelPayload {
529 pub fn new<T: Send + Sync + 'static>(data: T) -> Self {
531 Self {
532 data: Arc::new(data),
533 }
534 }
535
536 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
548pub(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)]
574pub(crate) fn serde_from_string<T: DeserializeOwned>(value: &str) -> T {
576 try_serde_from_string(value).unwrap()
577}
578
579pub(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
596pub(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 let original_storage_value: Rc<RefCell<Option<T>>> = use_hook(|| Rc::new(RefCell::new(None)));
609
610 if let StorageMode::HydrateClient = mode {
612 if generation() == 0 {
613 if let Some(default_value) = init {
615 original_storage_value
617 .borrow_mut()
618 .replace(signal.peek().clone());
619 signal.set(default_value());
620 }
621 needs_update();
623 }
624 if generation() == 1 {
625 if let Some(original_storage_value) = original_storage_value.borrow_mut().take() {
627 signal.set(original_storage_value);
628 }
629 }
630 }
631 signal
632}