#![allow(clippy::collapsible_if)]
mod client_storage;
mod persistence;
pub use client_storage::{LocalStorage, SessionStorage};
use dioxus::core::{ReactiveContext, current_scope_id, generation, needs_update};
use dioxus::logger::tracing::trace;
use futures_util::stream::StreamExt;
pub use persistence::{
new_persistent, new_singleton_persistent, use_persistent, use_singleton_persistent,
};
use std::cell::RefCell;
use std::rc::Rc;
use dioxus::prelude::*;
use serde::{Serialize, de::DeserializeOwned};
use std::any::Any;
use std::fmt::{Debug, Display};
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use tokio::sync::watch::error::SendError;
use tokio::sync::watch::{Receiver, Sender};
#[cfg(not(target_family = "wasm"))]
pub use client_storage::{set_dir_name, set_directory};
pub fn use_storage<S, T>(key: S::Key, init: impl FnOnce() -> T) -> Signal<T>
where
S: StorageBacking,
T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
S::Key: Clone,
{
let mut init = Some(init);
let storage = use_hook(|| new_storage::<S, T>(key, || init.take().unwrap()()));
use_hydrate_storage::<S, T>(storage, init);
storage
}
#[allow(unused)]
enum StorageMode {
Client,
HydrateClient,
Server,
}
impl StorageMode {
#[allow(unreachable_code)]
const fn current() -> Self {
server_only! {
return StorageMode::Server;
}
fullstack! {
return StorageMode::HydrateClient;
}
StorageMode::Client
}
}
pub fn new_storage<S, T>(key: S::Key, init: impl FnOnce() -> T) -> Signal<T>
where
S: StorageBacking,
T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
S::Key: Clone,
{
let mode = StorageMode::current();
match mode {
StorageMode::Server => Signal::new(init()),
_ => {
let storage_entry = new_storage_entry::<S, T>(key, init);
storage_entry.save_to_storage_on_change();
storage_entry.data
}
}
}
pub fn use_synced_storage<S, T>(key: S::Key, init: impl FnOnce() -> T) -> Signal<T>
where
S: StorageBacking + StorageSubscriber<S>,
T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
S::Key: Clone,
{
let mut init = Some(init);
let storage = use_hook(|| new_synced_storage::<S, T>(key, || init.take().unwrap()()));
use_hydrate_storage::<S, T>(storage, init);
storage
}
pub fn new_synced_storage<S, T>(key: S::Key, init: impl FnOnce() -> T) -> Signal<T>
where
S: StorageBacking + StorageSubscriber<S>,
T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
S::Key: Clone,
{
{
let mode = StorageMode::current();
match mode {
StorageMode::Server => Signal::new(init()),
_ => {
let storage_entry = new_synced_storage_entry::<S, T>(key, init);
storage_entry.save_to_storage_on_change();
storage_entry.subscribe_to_storage();
*storage_entry.data()
}
}
}
}
pub fn use_storage_entry<S, T>(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry<S, T>
where
S: StorageBacking,
T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
S::Key: Clone,
{
let mut init = Some(init);
let signal = use_hook(|| new_storage_entry::<S, T>(key, || init.take().unwrap()()));
use_hydrate_storage::<S, T>(*signal.data(), init);
signal
}
pub fn use_synced_storage_entry<S, T>(
key: S::Key,
init: impl FnOnce() -> T,
) -> SyncedStorageEntry<S, T>
where
S: StorageBacking + StorageSubscriber<S>,
T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
S::Key: Clone,
{
let mut init = Some(init);
let signal = use_hook(|| new_synced_storage_entry::<S, T>(key, || init.take().unwrap()()));
use_hydrate_storage::<S, T>(*signal.data(), init);
signal
}
pub fn new_storage_entry<S, T>(key: S::Key, init: impl FnOnce() -> T) -> StorageEntry<S, T>
where
S: StorageBacking,
T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
S::Key: Clone,
{
let data = get_from_storage::<S, T>(key.clone(), init);
StorageEntry::new(key, data)
}
pub fn new_synced_storage_entry<S, T>(
key: S::Key,
init: impl FnOnce() -> T,
) -> SyncedStorageEntry<S, T>
where
S: StorageBacking + StorageSubscriber<S>,
T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static,
S::Key: Clone,
{
let data = get_from_storage::<S, T>(key.clone(), init);
SyncedStorageEntry::new(key, data)
}
pub fn get_from_storage<
S: StorageBacking,
T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static,
>(
key: S::Key,
init: impl FnOnce() -> T,
) -> T {
S::get(&key).unwrap_or_else(|| {
let data = init();
S::set(key, &data);
data
})
}
pub trait StorageEntryTrait<S: StorageBacking, T: PartialEq + Clone + 'static>:
Clone + 'static
{
fn save(&self);
fn update(&mut self);
fn key(&self) -> &S::Key;
fn data(&self) -> &Signal<T>;
fn save_to_storage_on_change(&self)
where
S: StorageBacking,
T: Serialize + DeserializeOwned + Clone + PartialEq + 'static,
{
let entry_clone = self.clone();
let old = RefCell::new(None);
let data = *self.data();
spawn(async move {
loop {
let (rc, mut reactive_context) = ReactiveContext::new();
rc.run_in(|| {
if old.borrow().as_ref() != Some(&*data.read()) {
trace!("Saving to storage");
entry_clone.save();
old.replace(Some(data()));
}
});
if reactive_context.next().await.is_none() {
break;
}
}
});
}
}
#[derive(Clone)]
pub struct SyncedStorageEntry<
S: StorageBacking + StorageSubscriber<S>,
T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
> {
pub(crate) entry: StorageEntry<S, T>,
pub(crate) channel: Receiver<StorageChannelPayload>,
}
impl<S, T> SyncedStorageEntry<S, T>
where
S: StorageBacking + StorageSubscriber<S>,
T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
{
pub fn new(key: S::Key, data: T) -> Self {
let channel = S::subscribe::<T>(&key);
Self {
entry: StorageEntry::new(key, data),
channel,
}
}
pub fn channel(&self) -> &Receiver<StorageChannelPayload> {
&self.channel
}
pub fn subscribe_to_storage(&self) {
let storage_entry_signal = *self.data();
let channel = self.channel.clone();
spawn(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::<T>()
.expect("Type mismatch with storage entry")
.clone();
}
}
});
}
}
impl<S, T> StorageEntryTrait<S, T> for SyncedStorageEntry<S, T>
where
S: StorageBacking + StorageSubscriber<S>,
T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
{
#[allow(clippy::collapsible_if)]
fn save(&self) {
if let Some(payload) = self.channel.borrow().data.downcast_ref::<T>() {
if *self.entry.data.read() == *payload {
return;
}
}
self.entry.save();
}
fn update(&mut self) {
self.entry.update();
}
fn key(&self) -> &S::Key {
self.entry.key()
}
fn data(&self) -> &Signal<T> {
&self.entry.data
}
}
#[derive(Clone)]
pub struct StorageEntry<
S: StorageBacking,
T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
> {
pub(crate) key: S::Key,
pub(crate) data: Signal<T>,
}
impl<S, T> StorageEntry<S, T>
where
S: StorageBacking,
T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
S::Key: Clone,
{
pub fn new(key: S::Key, data: T) -> Self {
Self {
key,
data: Signal::new_in_scope(data, current_scope_id()),
}
}
}
impl<S, T> StorageEntryTrait<S, T> for StorageEntry<S, T>
where
S: StorageBacking,
T: Serialize + DeserializeOwned + Clone + PartialEq + Send + Sync + 'static,
{
fn save(&self) {
S::set(self.key.clone(), &*self.data.read());
}
fn update(&mut self) {
self.data = S::get(&self.key).unwrap_or(self.data);
}
fn key(&self) -> &S::Key {
&self.key
}
fn data(&self) -> &Signal<T> {
&self.data
}
}
impl<S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync> Deref
for StorageEntry<S, T>
{
type Target = Signal<T>;
fn deref(&self) -> &Signal<T> {
&self.data
}
}
impl<S: StorageBacking, T: Serialize + DeserializeOwned + Clone + Send + Sync> DerefMut
for StorageEntry<S, T>
{
fn deref_mut(&mut self) -> &mut Signal<T> {
&mut self.data
}
}
impl<S: StorageBacking, T: Display + Serialize + DeserializeOwned + Clone + Send + Sync> Display
for StorageEntry<S, T>
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.data.fmt(f)
}
}
impl<S: StorageBacking, T: Debug + Serialize + DeserializeOwned + Clone + Send + Sync> Debug
for StorageEntry<S, T>
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.data.fmt(f)
}
}
pub trait StorageBacking: Clone + 'static {
type Key: PartialEq + Clone + Debug + Send + Sync + 'static;
fn get<T: DeserializeOwned + Clone + 'static>(key: &Self::Key) -> Option<T>;
fn set<T: Serialize + Send + Sync + Clone + 'static>(key: Self::Key, value: &T);
}
pub trait StorageSubscriber<S: StorageBacking> {
fn subscribe<T: DeserializeOwned + Send + Sync + Clone + 'static>(
key: &S::Key,
) -> Receiver<StorageChannelPayload>;
fn unsubscribe(key: &S::Key);
}
pub struct StorageSubscription {
pub(crate) getter: Box<dyn Fn() -> StorageChannelPayload + 'static + Send + Sync>,
pub(crate) tx: Arc<Sender<StorageChannelPayload>>,
}
impl StorageSubscription {
pub fn new<
S: StorageBacking + StorageSubscriber<S>,
T: DeserializeOwned + Send + Sync + Clone + 'static,
>(
tx: Sender<StorageChannelPayload>,
key: S::Key,
) -> Self {
let getter = move || {
let data = S::get::<T>(&key).unwrap();
StorageChannelPayload::new(data)
};
Self {
getter: Box::new(getter),
tx: Arc::new(tx),
}
}
pub fn get_and_send(&self) -> Result<(), SendError<StorageChannelPayload>> {
let payload = (self.getter)();
self.tx.send(payload)
}
}
#[derive(Clone, Debug)]
pub struct StorageChannelPayload {
data: Arc<dyn Any + Send + Sync>,
}
impl StorageChannelPayload {
pub fn new<T: Send + Sync + 'static>(data: T) -> Self {
Self {
data: Arc::new(data),
}
}
pub fn data<T: 'static>(&self) -> Option<&T> {
self.data.downcast_ref::<T>()
}
}
impl Default for StorageChannelPayload {
fn default() -> Self {
Self { data: Arc::new(()) }
}
}
pub(crate) fn serde_to_string<T: Serialize>(value: &T) -> String {
let mut serialized = Vec::new();
ciborium::into_writer(value, &mut serialized).unwrap();
let compressed = yazi::compress(
&serialized,
yazi::Format::Zlib,
yazi::CompressionLevel::BestSize,
)
.unwrap();
let as_str: String = compressed
.iter()
.flat_map(|u| {
[
char::from_digit(((*u & 0xF0) >> 4).into(), 16).unwrap(),
char::from_digit((*u & 0x0F).into(), 16).unwrap(),
]
.into_iter()
})
.collect();
as_str
}
#[allow(unused)]
pub(crate) fn serde_from_string<T: DeserializeOwned>(value: &str) -> T {
try_serde_from_string(value).unwrap()
}
pub(crate) fn try_serde_from_string<T: DeserializeOwned>(value: &str) -> Option<T> {
let mut bytes: Vec<u8> = Vec::new();
let mut chars = value.chars();
while let Some(c) = chars.next() {
let n1 = c.to_digit(16)?;
let c2 = chars.next()?;
let n2 = c2.to_digit(16)?;
bytes.push((n1 * 16 + n2) as u8);
}
match yazi::decompress(&bytes, yazi::Format::Zlib) {
Ok((decompressed, _)) => ciborium::from_reader(std::io::Cursor::new(decompressed)).ok(),
Err(_) => None,
}
}
pub(crate) fn use_hydrate_storage<S, T>(
mut signal: Signal<T>,
init: Option<impl FnOnce() -> T>,
) -> Signal<T>
where
S: StorageBacking,
T: Serialize + DeserializeOwned + Clone + Send + Sync + PartialEq + 'static,
S::Key: Clone,
{
let mode = StorageMode::current();
let original_storage_value: Rc<RefCell<Option<T>>> = use_hook(|| Rc::new(RefCell::new(None)));
if let StorageMode::HydrateClient = mode {
if generation() == 0 {
if let Some(default_value) = init {
original_storage_value
.borrow_mut()
.replace(signal.peek().clone());
signal.set(default_value());
}
needs_update();
}
if generation() == 1 {
if let Some(original_storage_value) = original_storage_value.borrow_mut().take() {
signal.set(original_storage_value);
}
}
}
signal
}