use crate::{core::MaybeRwSignal, storage::StorageType, utils::FilterOptions};
use codee::{CodecError, Decoder, Encoder};
use default_struct_builder::DefaultBuilder;
use leptos::prelude::*;
use leptos::reactive::wrappers::read::Signal;
use std::sync::Arc;
use thiserror::Error;
use wasm_bindgen::JsValue;
const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
#[inline(always)]
pub fn use_storage<T, C>(
storage_type: StorageType,
key: impl Into<Signal<String>>,
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone + Send + Sync)
where
T: Default + Clone + PartialEq + Send + Sync + 'static,
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
{
use_storage_with_options::<T, C>(storage_type, key, UseStorageOptions::default())
}
pub fn use_storage_with_options<T, C>(
storage_type: StorageType,
key: impl Into<Signal<String>>,
options: UseStorageOptions<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone + Send + Sync)
where
T: Clone + PartialEq + Send + Sync,
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
{
let UseStorageOptions {
on_error,
listen_to_storage_changes,
initial_value,
filter,
delay_during_hydration,
} = options;
let (data, set_data) = initial_value.into_signal();
let default = data.get_untracked();
#[cfg(feature = "ssr")]
{
let _ = on_error;
let _ = listen_to_storage_changes;
let _ = filter;
let _ = delay_during_hydration;
let _ = storage_type;
let _ = key;
let _ = INTERNAL_STORAGE_EVENT;
let remove = move || {
set_data.set(default.clone());
};
(data, set_data, remove)
}
#[cfg(not(feature = "ssr"))]
{
use crate::{
WatchOptions, sendwrap_fn, use_event_listener, use_window, watch_with_options,
};
use send_wrapper::SendWrapper;
let delaying = StoredValue::new(
delay_during_hydration
&& Owner::current_shared_context()
.map(|sc| sc.during_hydration())
.unwrap_or_default(),
);
let key = key.into();
let storage = storage_type
.into_storage()
.map_err(UseStorageError::StorageNotAvailable)
.and_then(|s| s.ok_or(UseStorageError::StorageReturnedNone));
let storage = handle_error(&on_error, storage);
let dispatch_storage_event = {
let on_error = on_error.to_owned();
move || {
let on_error = on_error.to_owned();
queue_microtask(move || {
let custom = web_sys::CustomEventInit::new();
custom.set_detail(&JsValue::from_str(&key.get_untracked()));
let result = window()
.dispatch_event(
&web_sys::CustomEvent::new_with_event_init_dict(
INTERNAL_STORAGE_EVENT,
&custom,
)
.expect("failed to create custom storage event"),
)
.map_err(UseStorageError::NotifyItemChangedFailed);
let _ = handle_error(&on_error, result);
})
}
};
let read_from_storage = {
let storage = storage.to_owned();
let on_error = on_error.to_owned();
move || {
storage
.to_owned()
.and_then(|storage| {
let result = storage
.get_item(&key.get_untracked())
.map_err(UseStorageError::GetItemFailed);
handle_error(&on_error, result)
})
.unwrap_or_default() .as_ref()
.map(|encoded| {
let result = C::decode(encoded)
.map_err(|e| UseStorageError::ItemCodecError(CodecError::Decode(e)));
handle_error(&on_error, result)
})
.transpose()
.unwrap_or_default() }
};
let fetch_from_storage = {
let default = default.clone();
let read_from_storage = read_from_storage.clone();
SendWrapper::new(move || {
let fetched = read_from_storage();
match fetched {
Some(value) => {
if value != data.get_untracked() {
set_data.set(value)
}
}
None => set_data.set(default.clone()),
};
})
};
let notify = ArcTrigger::new();
let notify_id = Memo::<usize>::new({
let notify = notify.clone();
let fetch_from_storage = fetch_from_storage.clone();
move |prev| {
notify.track();
match prev {
None => 1, Some(prev) => {
fetch_from_storage();
prev + 1
}
}
}
});
{
let storage = storage.to_owned();
let on_error = on_error.to_owned();
let dispatch_storage_event = dispatch_storage_event.to_owned();
let _ = watch_with_options(
move || (notify_id.get(), data.get()),
move |(id, value), prev, _| {
let change_from_external_event =
prev.map(|(prev_id, _)| *prev_id != *id).unwrap_or_default();
if change_from_external_event {
return;
}
if value == &default && (read_from_storage().is_none() || delaying.get_value())
{
return;
}
if let Ok(storage) = &storage {
let result = C::encode(value)
.map_err(|e| UseStorageError::ItemCodecError(CodecError::Encode(e)))
.and_then(|enc_value| {
storage
.set_item(&key.get_untracked(), &enc_value)
.map_err(UseStorageError::SetItemFailed)
});
let result = handle_error(&on_error, result);
if result.is_ok() {
dispatch_storage_event();
}
}
},
WatchOptions::default().filter(filter).immediate(true),
);
}
if delaying.get_value() {
request_animation_frame({
let fetch_from_storage = fetch_from_storage.clone();
move || {
delaying.set_value(false);
fetch_from_storage()
}
});
} else {
fetch_from_storage();
}
if listen_to_storage_changes {
let _ = use_event_listener(use_window(), leptos::ev::storage, {
let notify = notify.clone();
move |ev| {
let ev_key = ev.key();
if ev_key == Some(key.get_untracked()) || ev_key.is_none() {
notify.notify()
}
}
});
let _ = use_event_listener(
use_window(),
leptos::ev::Custom::new(INTERNAL_STORAGE_EVENT),
{
let notify = notify.clone();
move |ev: web_sys::CustomEvent| {
if Some(key.get_untracked()) == ev.detail().as_string() {
notify.notify()
}
}
},
);
};
Effect::watch(
move || key.get(),
{
let notify = notify.clone();
move |_, _, _| notify.notify()
},
false,
);
let remove = {
sendwrap_fn!(move || {
let _ = storage.as_ref().map(|storage| {
let result = storage
.remove_item(&key.get_untracked())
.map_err(UseStorageError::RemoveItemFailed);
let _ = handle_error(&on_error, result);
notify.notify();
dispatch_storage_event();
});
})
};
(data, set_data, remove)
}
}
#[derive(Error, Debug)]
pub enum UseStorageError<E, D> {
#[error("storage not available")]
StorageNotAvailable(JsValue),
#[error("storage not returned from window")]
StorageReturnedNone,
#[error("failed to get item")]
GetItemFailed(JsValue),
#[error("failed to set item")]
SetItemFailed(JsValue),
#[error("failed to delete item")]
RemoveItemFailed(JsValue),
#[error("failed to notify item changed")]
NotifyItemChangedFailed(JsValue),
#[error("failed to encode / decode item value")]
ItemCodecError(CodecError<E, D>),
}
#[derive(DefaultBuilder)]
pub struct UseStorageOptions<T, E, D>
where
T: Send + Sync + 'static,
{
#[builder(skip)]
on_error: Arc<dyn Fn(UseStorageError<E, D>) + Send + Sync>,
listen_to_storage_changes: bool,
#[builder(skip)]
initial_value: MaybeRwSignal<T>,
#[builder(into)]
filter: FilterOptions,
delay_during_hydration: bool,
}
#[cfg(not(feature = "ssr"))]
fn handle_error<T, E, D>(
on_error: &Arc<dyn Fn(UseStorageError<E, D>) + Send + Sync>,
result: Result<T, UseStorageError<E, D>>,
) -> Result<T, ()> {
result.map_err(|err| (on_error)(err))
}
impl<T: Default, E, D> Default for UseStorageOptions<T, E, D>
where
T: Send + Sync + 'static,
{
fn default() -> Self {
Self {
on_error: Arc::new(|_err| ()),
listen_to_storage_changes: true,
initial_value: MaybeRwSignal::default(),
filter: FilterOptions::default(),
delay_during_hydration: false,
}
}
}
impl<T: Default, E, D> UseStorageOptions<T, E, D>
where
T: Send + Sync + 'static,
{
pub fn on_error(
self,
on_error: impl Fn(UseStorageError<E, D>) + Send + Sync + 'static,
) -> Self {
Self {
on_error: Arc::new(on_error),
..self
}
}
pub fn initial_value(self, initial: impl Into<MaybeRwSignal<T>>) -> Self {
Self {
initial_value: initial.into(),
..self
}
}
}