2023-10-27 12:37:41 +01:00
|
|
|
use crate::{
|
2023-10-27 13:57:36 +01:00
|
|
|
core::{MaybeRwSignal, StorageType},
|
|
|
|
use_event_listener_with_options, use_window, UseEventListenerOptions,
|
2023-10-27 12:37:41 +01:00
|
|
|
};
|
2023-10-27 14:50:29 +01:00
|
|
|
use cfg_if::cfg_if;
|
2023-10-26 11:46:47 +01:00
|
|
|
use leptos::*;
|
2023-10-27 10:05:02 +01:00
|
|
|
use std::{rc::Rc, str::FromStr};
|
2023-10-26 11:46:47 +01:00
|
|
|
use thiserror::Error;
|
|
|
|
use wasm_bindgen::JsValue;
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
2023-10-27 13:57:36 +01:00
|
|
|
pub struct UseStorageOptions<T: 'static, C: Codec<T>> {
|
2023-10-27 10:05:02 +01:00
|
|
|
codec: C,
|
|
|
|
on_error: Rc<dyn Fn(UseStorageError<C::Error>)>,
|
2023-10-26 12:01:04 +01:00
|
|
|
listen_to_storage_changes: bool,
|
2023-10-27 13:57:36 +01:00
|
|
|
default_value: MaybeSignal<T>,
|
2023-10-26 11:46:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Session handling errors returned by [`use_storage`].
|
|
|
|
#[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),
|
2023-10-27 10:05:02 +01:00
|
|
|
#[error("failed to encode / decode item value")]
|
|
|
|
ItemCodecError(Err),
|
2023-10-26 11:46:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Hook for using local storage. Returns a result of a signal and a setter / deleter.
|
|
|
|
pub fn use_local_storage<T>(key: impl AsRef<str>) -> (Memo<T>, impl Fn(Option<T>) -> ())
|
|
|
|
where
|
2023-10-27 10:05:02 +01:00
|
|
|
T: Clone + Default + FromStr + PartialEq + ToString,
|
2023-10-26 11:46:47 +01:00
|
|
|
{
|
2023-10-27 12:37:41 +01:00
|
|
|
use_storage_with_options(StorageType::Local, key, UseStorageOptions::string_codec())
|
2023-10-26 11:46:47 +01:00
|
|
|
}
|
|
|
|
|
2023-10-27 10:05:02 +01:00
|
|
|
pub fn use_local_storage_with_options<T, C>(
|
2023-10-26 11:46:47 +01:00
|
|
|
key: impl AsRef<str>,
|
2023-10-27 10:05:02 +01:00
|
|
|
options: UseStorageOptions<T, C>,
|
2023-10-26 11:46:47 +01:00
|
|
|
) -> (Memo<T>, impl Fn(Option<T>) -> ())
|
2023-10-27 11:56:47 +01:00
|
|
|
where
|
2023-10-27 13:57:36 +01:00
|
|
|
T: Clone + PartialEq,
|
2023-10-27 11:56:47 +01:00
|
|
|
C: Codec<T>,
|
|
|
|
{
|
2023-10-27 12:37:41 +01:00
|
|
|
use_storage_with_options(StorageType::Local, key, options)
|
2023-10-27 11:56:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Hook for using session storage. Returns a result of a signal and a setter / deleter.
|
|
|
|
pub fn use_session_storage<T>(key: impl AsRef<str>) -> (Memo<T>, impl Fn(Option<T>) -> ())
|
|
|
|
where
|
|
|
|
T: Clone + Default + FromStr + PartialEq + ToString,
|
|
|
|
{
|
2023-10-27 12:37:41 +01:00
|
|
|
use_storage_with_options(StorageType::Session, key, UseStorageOptions::string_codec())
|
2023-10-27 11:56:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn use_session_storage_with_options<T, C>(
|
|
|
|
key: impl AsRef<str>,
|
|
|
|
options: UseStorageOptions<T, C>,
|
|
|
|
) -> (Memo<T>, impl Fn(Option<T>) -> ())
|
|
|
|
where
|
2023-10-27 13:57:36 +01:00
|
|
|
T: Clone + PartialEq,
|
2023-10-27 11:56:47 +01:00
|
|
|
C: Codec<T>,
|
|
|
|
{
|
2023-10-27 12:37:41 +01:00
|
|
|
use_storage_with_options(StorageType::Session, key, options)
|
2023-10-27 11:56:47 +01:00
|
|
|
}
|
|
|
|
|
2023-10-27 12:37:41 +01:00
|
|
|
/// Hook for using any kind of storage. Returns a result of a signal and a setter / deleter.
|
2023-10-27 14:15:40 +01:00
|
|
|
pub fn use_storage_with_options<T, C>(
|
2023-10-27 12:37:41 +01:00
|
|
|
storage_type: StorageType,
|
2023-10-27 11:56:47 +01:00
|
|
|
key: impl AsRef<str>,
|
|
|
|
options: UseStorageOptions<T, C>,
|
|
|
|
) -> (Memo<T>, impl Fn(Option<T>) -> ())
|
2023-10-26 11:46:47 +01:00
|
|
|
where
|
2023-10-27 13:57:36 +01:00
|
|
|
T: Clone + PartialEq,
|
2023-10-27 10:05:02 +01:00
|
|
|
C: Codec<T>,
|
2023-10-26 11:46:47 +01:00
|
|
|
{
|
2023-10-27 14:50:29 +01:00
|
|
|
cfg_if! { if #[cfg(feature = "ssr")] {
|
|
|
|
let (data, set_data) = create_signal(None);
|
|
|
|
let set_value = move |value: Option<T>| {
|
|
|
|
set_data.set(value);
|
|
|
|
};
|
|
|
|
let value = create_memo(move |_| data.get().unwrap_or_default());
|
|
|
|
return (value, set_value);
|
|
|
|
} else {
|
|
|
|
// Continue
|
|
|
|
}}
|
|
|
|
|
2023-10-26 12:01:04 +01:00
|
|
|
let UseStorageOptions {
|
2023-10-27 10:05:02 +01:00
|
|
|
codec,
|
2023-10-26 12:01:04 +01:00
|
|
|
on_error,
|
|
|
|
listen_to_storage_changes,
|
2023-10-27 13:57:36 +01:00
|
|
|
default_value,
|
2023-10-26 12:01:04 +01:00
|
|
|
} = options;
|
2023-10-26 11:46:47 +01:00
|
|
|
|
2023-10-27 11:56:47 +01:00
|
|
|
// Get storage API
|
2023-10-27 12:37:41 +01:00
|
|
|
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-27 11:56:47 +01:00
|
|
|
|
|
|
|
// Fetch initial value (undecoded)
|
2023-10-26 11:46:47 +01:00
|
|
|
let initial_value = storage
|
|
|
|
.to_owned()
|
2023-10-27 11:56:47 +01:00
|
|
|
// Pull from storage
|
2023-10-26 11:46:47 +01:00
|
|
|
.and_then(|s| {
|
|
|
|
let result = s
|
|
|
|
.get_item(key.as_ref())
|
|
|
|
.map_err(UseStorageError::GetItemFailed);
|
|
|
|
handle_error(&on_error, result)
|
|
|
|
})
|
|
|
|
.unwrap_or_default();
|
2023-10-27 11:56:47 +01:00
|
|
|
// Decode initial value
|
2023-10-27 10:05:02 +01:00
|
|
|
let initial_value = decode_item(&codec, initial_value, &on_error);
|
2023-10-27 11:56:47 +01:00
|
|
|
|
2023-10-26 11:46:47 +01:00
|
|
|
let (data, set_data) = create_signal(initial_value);
|
|
|
|
|
|
|
|
// Update storage value
|
|
|
|
let set_value = {
|
|
|
|
let storage = storage.to_owned();
|
|
|
|
let key = key.as_ref().to_owned();
|
2023-10-27 10:05:02 +01:00
|
|
|
let codec = codec.to_owned();
|
2023-10-26 11:46:47 +01:00
|
|
|
let on_error = on_error.to_owned();
|
|
|
|
move |value: Option<T>| {
|
|
|
|
let key = key.as_str();
|
|
|
|
// Attempt to update storage
|
|
|
|
let _ = storage.as_ref().map(|storage| {
|
|
|
|
let result = match value {
|
|
|
|
// Update
|
2023-10-27 10:05:02 +01:00
|
|
|
Some(ref value) => codec
|
|
|
|
.encode(&value)
|
|
|
|
.map_err(UseStorageError::ItemCodecError)
|
|
|
|
.and_then(|enc_value| {
|
|
|
|
storage
|
|
|
|
.set_item(key, &enc_value)
|
|
|
|
.map_err(UseStorageError::SetItemFailed)
|
|
|
|
}),
|
2023-10-26 11:46:47 +01:00
|
|
|
// Remove
|
|
|
|
None => storage
|
|
|
|
.remove_item(key)
|
|
|
|
.map_err(UseStorageError::RemoveItemFailed),
|
|
|
|
};
|
|
|
|
handle_error(&on_error, result)
|
|
|
|
});
|
|
|
|
|
|
|
|
// Notify signal of change
|
|
|
|
set_data.set(value);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Listen for storage events
|
|
|
|
// Note: we only receive events from other tabs / windows, not from internal updates.
|
2023-10-26 12:01:04 +01:00
|
|
|
if listen_to_storage_changes {
|
2023-10-26 11:46:47 +01:00
|
|
|
let key = key.as_ref().to_owned();
|
2023-10-26 12:01:04 +01:00
|
|
|
let _ = use_event_listener_with_options(
|
2023-10-26 11:46:47 +01:00
|
|
|
use_window(),
|
|
|
|
leptos::ev::storage,
|
|
|
|
move |ev| {
|
|
|
|
// Update storage value if our key matches
|
|
|
|
if let Some(k) = ev.key() {
|
|
|
|
if k == key {
|
2023-10-27 10:05:02 +01:00
|
|
|
let value = decode_item(&codec, ev.new_value(), &on_error);
|
2023-10-26 11:46:47 +01:00
|
|
|
set_data.set(value)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// All keys deleted
|
|
|
|
set_data.set(None)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
UseEventListenerOptions::default().passive(true),
|
2023-10-26 12:01:04 +01:00
|
|
|
);
|
2023-10-26 11:46:47 +01:00
|
|
|
};
|
|
|
|
|
2023-10-27 13:57:36 +01:00
|
|
|
// Apply default value
|
|
|
|
let value = create_memo(move |_| data.get().unwrap_or_else(|| default_value.get()));
|
|
|
|
|
2023-10-26 11:46:47 +01:00
|
|
|
(value, set_value)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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-27 10:05:02 +01:00
|
|
|
fn decode_item<T, C: Codec<T>>(
|
|
|
|
codec: &C,
|
2023-10-26 11:46:47 +01:00
|
|
|
str: Option<String>,
|
2023-10-27 10:05:02 +01:00
|
|
|
on_error: &Rc<dyn Fn(UseStorageError<C::Error>)>,
|
2023-10-26 11:46:47 +01:00
|
|
|
) -> Option<T> {
|
|
|
|
str.map(|str| {
|
2023-10-27 10:05:02 +01:00
|
|
|
let result = codec.decode(str).map_err(UseStorageError::ItemCodecError);
|
2023-10-26 11:46:47 +01:00
|
|
|
handle_error(&on_error, result)
|
|
|
|
})
|
|
|
|
.transpose()
|
|
|
|
// We've sent our error so unwrap to drop () error
|
|
|
|
.unwrap_or_default()
|
|
|
|
}
|
|
|
|
|
2023-10-27 13:57:36 +01:00
|
|
|
impl<T: Clone + Default, C: Codec<T>> UseStorageOptions<T, C> {
|
2023-10-27 14:32:03 +01:00
|
|
|
pub(super) fn new(codec: C) -> Self {
|
2023-10-26 11:46:47 +01:00
|
|
|
Self {
|
2023-10-27 10:05:02 +01:00
|
|
|
codec,
|
2023-10-26 11:46:47 +01:00
|
|
|
on_error: Rc::new(|_err| ()),
|
2023-10-26 12:01:04 +01:00
|
|
|
listen_to_storage_changes: true,
|
2023-10-27 13:57:36 +01:00
|
|
|
default_value: MaybeSignal::default(),
|
2023-10-26 11:46:47 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-27 10:05:02 +01:00
|
|
|
pub fn on_error(self, on_error: impl Fn(UseStorageError<C::Error>) + 'static) -> Self {
|
2023-10-26 11:46:47 +01:00
|
|
|
Self {
|
|
|
|
on_error: Rc::new(on_error),
|
2023-10-26 12:01:04 +01:00
|
|
|
..self
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn listen_to_storage_changes(self, listen_to_storage_changes: bool) -> Self {
|
|
|
|
Self {
|
|
|
|
listen_to_storage_changes,
|
|
|
|
..self
|
2023-10-26 11:46:47 +01:00
|
|
|
}
|
|
|
|
}
|
2023-10-27 13:57:36 +01:00
|
|
|
|
|
|
|
pub fn default_value(self, values: impl Into<MaybeRwSignal<T>>) -> Self {
|
|
|
|
Self {
|
|
|
|
default_value: values.into().into_signal().0.into(),
|
|
|
|
..self
|
|
|
|
}
|
|
|
|
}
|
2023-10-26 11:46:47 +01:00
|
|
|
}
|
2023-10-27 10:05:02 +01:00
|
|
|
|
|
|
|
pub trait Codec<T>: Clone + 'static {
|
|
|
|
type Error;
|
|
|
|
fn encode(&self, val: &T) -> Result<String, Self::Error>;
|
|
|
|
fn decode(&self, str: String) -> Result<T, Self::Error>;
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, PartialEq)]
|
|
|
|
pub struct StringCodec();
|
|
|
|
|
|
|
|
impl<T: FromStr + ToString> Codec<T> for StringCodec {
|
|
|
|
type Error = T::Err;
|
|
|
|
|
|
|
|
fn encode(&self, val: &T) -> Result<String, Self::Error> {
|
|
|
|
Ok(val.to_string())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn decode(&self, str: String) -> Result<T, Self::Error> {
|
|
|
|
T::from_str(&str)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-27 13:57:36 +01:00
|
|
|
impl<T: Clone + Default + FromStr + ToString> UseStorageOptions<T, StringCodec> {
|
2023-10-27 10:05:02 +01:00
|
|
|
pub fn string_codec() -> Self {
|
|
|
|
Self::new(StringCodec())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_string_codec() {
|
|
|
|
let s = String::from("party time 🎉");
|
|
|
|
let codec = StringCodec();
|
|
|
|
assert_eq!(codec.encode(&s), Ok(s.clone()));
|
|
|
|
assert_eq!(codec.decode(s.clone()), Ok(s));
|
|
|
|
}
|
|
|
|
}
|