leptos-use/src/storage/use_storage.rs

434 lines
16 KiB
Rust
Raw Normal View History

use crate::{
core::{MaybeRwSignal, StorageType},
use_event_listener, use_window,
utils::FilterOptions,
watch_with_options, WatchOptions,
};
use cfg_if::cfg_if;
use leptos::*;
2023-10-28 12:19:33 +01:00
use std::rc::Rc;
use thiserror::Error;
use wasm_bindgen::JsValue;
const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
2023-10-29 11:41:59 +00:00
/// A codec for encoding and decoding values to and from UTF-16 strings. These strings are then stored in browser storage.
2023-10-28 12:19:33 +01:00
pub trait Codec<T>: Clone + 'static {
2023-10-29 11:41:59 +00:00
/// The error type returned when encoding or decoding fails.
2023-10-28 12:19:33 +01:00
type Error;
2023-10-29 11:41:59 +00:00
/// Encodes a value to a UTF-16 string.
2023-10-28 12:19:33 +01:00
fn encode(&self, val: &T) -> Result<String, Self::Error>;
2023-10-29 11:41:59 +00:00
/// Decodes a UTF-16 string to a value. Should be able to decode any string encoded by [`encode`].
2023-10-28 12:19:33 +01:00
fn decode(&self, str: String) -> Result<T, Self::Error>;
}
2023-10-29 11:41:59 +00:00
/// Options for use with [`use_local_storage_with_options`], [`use_session_storage_with_options`] and [`use_storage_with_options`].
pub struct UseStorageOptions<T: 'static, C: Codec<T>> {
codec: C,
on_error: Rc<dyn Fn(UseStorageError<C::Error>)>,
listen_to_storage_changes: bool,
default_value: MaybeRwSignal<T>,
filter: FilterOptions,
}
2023-10-29 11:57:02 +00:00
/// Session handling errors returned by [`use_storage_with_options`].
#[derive(Error, Debug)]
pub enum UseStorageError<Err> {
#[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(Err),
}
2023-10-29 11:41:59 +00:00
/// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
///
/// LocalStorage stores data in the browser with no expiration time. Access is given to all pages from the same origin (e.g., all pages from "https://example.com" share the same origin). While data doesn't expire the user can view, modify and delete all data stored. Browsers allow 5MB of data to be stored.
///
/// This is contrast to [`use_session_storage`] which clears data when the page session ends and is not shared.
///
/// See [`use_storage_with_options`] for more details on how to use.
pub fn use_local_storage<T, C>(
key: impl AsRef<str>,
) -> (Signal<T>, WriteSignal<T>, impl Fn() -> () + Clone)
where
T: Clone + Default + PartialEq,
C: Codec<T> + Default,
{
use_storage_with_options(
StorageType::Local,
key,
UseStorageOptions::<T, C>::default(),
)
}
2023-10-29 11:41:59 +00:00
/// Accepts [`UseStorageOptions`]. See [`use_local_storage`] for details.
pub fn use_local_storage_with_options<T, C>(
key: impl AsRef<str>,
options: UseStorageOptions<T, C>,
) -> (Signal<T>, WriteSignal<T>, impl Fn() -> () + Clone)
where
T: Clone + PartialEq,
C: Codec<T>,
{
use_storage_with_options(StorageType::Local, key, options)
}
2023-10-29 11:41:59 +00:00
/// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage).
///
/// SessionStorages stores data in the browser that is deleted when the page session ends. A page session ends when the browser closes the tab. Data is not shared between pages. While data doesn't expire the user can view, modify and delete all data stored. Browsers allow 5MB of data to be stored.
///
/// Use [`use_local_storage`] to store data that is shared amongst all pages with the same origin and persists between page sessions.
///
/// See [`use_storage_with_options`] for more details on how to use.
pub fn use_session_storage<T, C>(
key: impl AsRef<str>,
) -> (Signal<T>, WriteSignal<T>, impl Fn() -> () + Clone)
where
T: Clone + Default + PartialEq,
C: Codec<T> + Default,
{
use_storage_with_options(
StorageType::Session,
key,
UseStorageOptions::<T, C>::default(),
)
}
2023-10-29 11:41:59 +00:00
/// Accepts [`UseStorageOptions`]. See [`use_session_storage`] for details.
pub fn use_session_storage_with_options<T, C>(
key: impl AsRef<str>,
options: UseStorageOptions<T, C>,
) -> (Signal<T>, WriteSignal<T>, impl Fn() -> () + Clone)
where
T: Clone + PartialEq,
C: Codec<T>,
{
use_storage_with_options(StorageType::Session, key, options)
}
2023-10-29 11:41:59 +00:00
/// Reactive [Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage).
///
/// * [See a demo](https://leptos-use.rs/storage/use_storage.html)
/// * [See a full example](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_storage)
///
/// ## Usage
///
/// Pass a [`StorageType`] to determine the kind of key-value browser storage to use. The specified key is where data is stored. All values are stored as UTF-16 strings which is then encoded and decoded via the given [`Codec`]. This value is synced with other calls using the same key on the smae page and across tabs for local storage. See [`UseStorageOptions`] to see how behaviour can be further customised.
2023-10-29 11:41:59 +00:00
///
/// Returns a triplet `(read_signal, write_signal, delete_from_storage_fn)`.
///
/// ## Example
///
/// ```
/// # use leptos::*;
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec};
/// # use serde::{Deserialize, Serialize};
/// #
/// # pub fn Demo() -> impl IntoView {
/// // Binds a struct:
/// let (state, set_state, _) = use_local_storage::<MyState, JsonCodec>("my-state");
///
/// // Binds a bool, stored as a string:
/// let (flag, set_flag, remove_flag) = use_session_storage::<bool, StringCodec>("my-flag");
///
/// // Binds a number, stored as a string:
/// let (count, set_count, _) = use_session_storage::<i32, StringCodec>("my-count");
/// // Binds a number, stored in JSON:
/// let (count, set_count, _) = use_session_storage::<i32, JsonCodec>("my-count-kept-in-js");
///
/// // Bind string with SessionStorage stored in ProtoBuf format:
/// let (id, set_id, _) = use_storage_with_options::<String, ProstCodec>(
/// StorageType::Session,
/// "my-id",
/// UseStorageOptions::default(),
2023-10-29 11:41:59 +00:00
/// );
/// # view! { }
/// # }
///
/// // Data stored in JSON must implement Serialize, Deserialize:
/// #[derive(Serialize, Deserialize, Clone, PartialEq)]
/// pub struct MyState {
/// pub hello: String,
/// pub greeting: String,
/// }
///
/// // Default can be used to implement intial or deleted values.
/// // You can also use a signal via UseStorageOptions::default_value`
/// impl Default for MyState {
/// fn default() -> Self {
/// Self {
/// hello: "hi".to_string(),
/// greeting: "Hello".to_string()
/// }
/// }
/// }
/// ```
pub fn use_storage_with_options<T, C>(
storage_type: StorageType,
key: impl AsRef<str>,
options: UseStorageOptions<T, C>,
) -> (Signal<T>, WriteSignal<T>, impl Fn() -> () + Clone)
where
T: Clone + PartialEq,
C: Codec<T>,
{
let UseStorageOptions {
codec,
on_error,
listen_to_storage_changes,
default_value,
filter,
} = options;
let (data, set_data) = default_value.into_signal();
let default = data.get_untracked();
2023-10-29 12:18:45 +00:00
cfg_if! { if #[cfg(feature = "ssr")] {
let remove = move || {
set_data.set(default.clone());
2023-10-29 12:18:45 +00:00
};
(data.into(), set_data, remove)
} else {
// Get storage API
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);
2023-10-29 12:18:45 +00:00
// Schedules a storage event microtask. Uses a queue to avoid re-entering the runtime
let dispatch_storage_event = {
let key = key.as_ref().to_owned();
let on_error = on_error.to_owned();
2023-10-29 12:18:45 +00:00
move || {
let key = key.to_owned();
let on_error = on_error.to_owned();
queue_microtask(move || {
// Note: we cannot construct a full StorageEvent so we _must_ rely on a custom event
let mut custom = web_sys::CustomEventInit::new();
custom.detail(&JsValue::from_str(&key));
let result = window()
.dispatch_event(
&web_sys::CustomEvent::new_with_event_init_dict(
INTERNAL_STORAGE_EVENT,
&custom,
)
.expect("failed to create custom storage event"),
)
2023-10-29 12:18:45 +00:00
.map_err(UseStorageError::NotifyItemChangedFailed);
let _ = handle_error(&on_error, result);
})
}
};
// Fetches direct from browser storage and fills set_data if changed (memo)
let fetch_from_storage = {
2023-10-29 12:18:45 +00:00
let storage = storage.to_owned();
let codec = codec.to_owned();
let key = key.as_ref().to_owned();
let on_error = on_error.to_owned();
move || {
let fetched = storage
2023-10-29 12:18:45 +00:00
.to_owned()
.and_then(|storage| {
// Get directly from storage
let result = storage
.get_item(&key)
.map_err(UseStorageError::GetItemFailed);
handle_error(&on_error, result)
})
.unwrap_or_default() // Drop handled Err(())
.map(|encoded| {
// Decode item
let result = codec
.decode(encoded)
.map_err(UseStorageError::ItemCodecError);
handle_error(&on_error, result)
})
.transpose()
.unwrap_or_default(); // Drop handled Err(())
match fetched {
Some(value) => {
// Replace data if changed
if value != data.get_untracked() {
set_data.set(value)
}
}
// Revert to default
None => set_data.set(default.clone()),
};
}
2023-10-29 12:18:45 +00:00
};
// Fetch initial value
fetch_from_storage();
// Fires when storage needs to be fetched
let notify = create_trigger();
// Refetch from storage. Keeps track of how many times we've been notified. Does not increment for calls to set_data
let notify_id = create_memo::<usize>(move |prev| {
notify.track();
match prev {
None => 1, // Avoid async fetch of initial value
Some(prev) => {
fetch_from_storage();
prev + 1
}
}
});
// Set item on internal (non-event) page changes to the data signal
2023-10-29 12:18:45 +00:00
{
let storage = storage.to_owned();
let codec = codec.to_owned();
let key = key.as_ref().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, _| {
// Skip setting storage on changes from external events. The ID will change on external events.
if prev.map(|(prev_id, _)| *prev_id != *id).unwrap_or_default() {
return;
}
2023-10-29 12:18:45 +00:00
if let Ok(storage) = &storage {
// Encode value
let result = codec
.encode(value)
.map_err(UseStorageError::ItemCodecError)
.and_then(|enc_value| {
// Set storage -- sends a global event
storage
.set_item(&key, &enc_value)
.map_err(UseStorageError::SetItemFailed)
});
let result = handle_error(&on_error, result);
// Send internal storage event
if result.is_ok() {
dispatch_storage_event();
}
}
2023-10-29 12:18:45 +00:00
},
WatchOptions::default().filter(filter),
);
}
2023-10-29 12:18:45 +00:00
if listen_to_storage_changes {
let check_key = key.as_ref().to_owned();
// Listen to global storage events
let _ = use_event_listener(use_window(), leptos::ev::storage, move |ev| {
let ev_key = ev.key();
// Key matches or all keys deleted (None)
if ev_key == Some(check_key.clone()) || ev_key.is_none() {
notify.notify()
}
});
2023-10-29 12:18:45 +00:00
// Listen to internal storage events
let check_key = key.as_ref().to_owned();
let _ = use_event_listener(
use_window(),
ev::Custom::new(INTERNAL_STORAGE_EVENT),
move |ev: web_sys::CustomEvent| {
if Some(check_key.clone()) == ev.detail().as_string() {
notify.notify()
}
},
);
};
// Remove from storage fn
let remove = {
let key = key.as_ref().to_owned();
move || {
let _ = storage.as_ref().map(|storage| {
// Delete directly from storage
let result = storage
.remove_item(&key)
.map_err(UseStorageError::RemoveItemFailed);
let _ = handle_error(&on_error, result);
notify.notify();
dispatch_storage_event();
});
}
};
2023-10-29 12:18:45 +00:00
(data.into(), set_data, remove)
}}
}
/// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling.
fn handle_error<T, Err>(
on_error: &Rc<dyn Fn(UseStorageError<Err>)>,
result: Result<T, UseStorageError<Err>>,
) -> Result<T, ()> {
result.or_else(|err| Err((on_error)(err)))
}
2023-10-28 12:28:26 +01:00
impl<T: Default, C: Codec<T> + Default> Default for UseStorageOptions<T, C> {
fn default() -> Self {
Self {
codec: C::default(),
on_error: Rc::new(|_err| ()),
listen_to_storage_changes: true,
default_value: MaybeRwSignal::default(),
filter: FilterOptions::default(),
}
}
}
impl<T: Default, C: Codec<T>> UseStorageOptions<T, C> {
2023-10-29 11:58:14 +00:00
/// Sets the codec to use for encoding and decoding values to and from UTF-16 strings.
pub fn codec(self, codec: impl Into<C>) -> Self {
Self {
codec: codec.into(),
..self
}
}
2023-10-29 11:41:59 +00:00
/// Optional callback whenever an error occurs.
pub fn on_error(self, on_error: impl Fn(UseStorageError<C::Error>) + 'static) -> Self {
Self {
on_error: Rc::new(on_error),
..self
}
}
2023-10-29 11:41:59 +00:00
/// Listen to changes to this storage key from browser and page events. Defaults to true.
pub fn listen_to_storage_changes(self, listen_to_storage_changes: bool) -> Self {
Self {
listen_to_storage_changes,
..self
}
}
2023-10-29 11:41:59 +00:00
/// Default value to use when the storage key is not set. Accepts a signal.
pub fn default_value(self, values: impl Into<MaybeRwSignal<T>>) -> Self {
Self {
default_value: values.into(),
..self
}
}
2023-10-29 11:41:59 +00:00
/// Debounce or throttle the writing to storage whenever the value changes.
pub fn filter(self, filter: impl Into<FilterOptions>) -> Self {
Self {
filter: filter.into(),
..self
}
}
}