From 2f3c72f2bb17d419e4368fc8a7509cb4bf810f01 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Thu, 26 Oct 2023 11:46:47 +0100 Subject: [PATCH] Prototype use_storage replacement that uses TryFrom --- src/lib.rs | 2 + src/use_storage.rs | 165 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 src/use_storage.rs diff --git a/src/lib.rs b/src/lib.rs index a9700c1..c16506d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,7 @@ mod use_raf_fn; mod use_scroll; mod use_service_worker; mod use_sorted; +mod use_storage; mod use_supported; mod use_throttle_fn; mod use_timestamp; @@ -109,6 +110,7 @@ pub use use_raf_fn::*; pub use use_scroll::*; pub use use_service_worker::*; pub use use_sorted::*; +pub use use_storage::*; pub use use_supported::*; pub use use_throttle_fn::*; pub use use_timestamp::*; diff --git a/src/use_storage.rs b/src/use_storage.rs new file mode 100644 index 0000000..b5b8edb --- /dev/null +++ b/src/use_storage.rs @@ -0,0 +1,165 @@ +use crate::{use_event_listener_with_options, use_window, UseEventListenerOptions}; +use leptos::*; +use std::rc::Rc; +use thiserror::Error; +use wasm_bindgen::JsValue; +use web_sys::Storage; + +#[derive(Clone)] +pub struct UseStorageOptions { + on_error: Rc)>, +} + +/// Session handling errors returned by [`use_storage`]. +#[derive(Error, Debug)] +pub enum UseStorageError { + #[error("window not available")] + WindowReturnedNone, + #[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 parse item value")] + ParseItemError(Err), +} + +/// Hook for using local storage. Returns a result of a signal and a setter / deleter. +pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + PartialEq + TryFrom + ToString, + T::Error: std::fmt::Debug, +{ + use_local_storage_with_options(key, UseStorageOptions::default()) +} + +/// Hook for using local storage. Returns a result of a signal and a setter / deleter. +pub fn use_local_storage_with_options( + key: impl AsRef, + options: UseStorageOptions, +) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + PartialEq + TryFrom + ToString, +{ + // TODO ssr + let UseStorageOptions { on_error } = options; + let storage: Result = handle_error(&on_error, try_storage()); + + let initial_value = storage + .to_owned() + // Get initial item from storage + .and_then(|s| { + let result = s + .get_item(key.as_ref()) + .map_err(UseStorageError::GetItemFailed); + handle_error(&on_error, result) + }) + .unwrap_or_default(); + // Attempt to parse the item string + let initial_value = parse_item(initial_value, &on_error); + 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(); + let on_error = on_error.to_owned(); + move |value: Option| { + let key = key.as_str(); + // Attempt to update storage + let _ = storage.as_ref().map(|storage| { + let result = match value { + // Update + Some(ref value) => storage + .set_item(key, &value.to_string()) + .map_err(UseStorageError::SetItemFailed), + // 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. + let _ = { + let key = key.as_ref().to_owned(); + use_event_listener_with_options( + use_window(), + leptos::ev::storage, + move |ev| { + // Update storage value if our key matches + if let Some(k) = ev.key() { + if k == key { + let value = parse_item(ev.new_value(), &on_error); + set_data.set(value) + } + } else { + // All keys deleted + set_data.set(None) + } + }, + UseEventListenerOptions::default().passive(true), + ) + }; + + let value = create_memo(move |_| data.get().unwrap_or_default()); + (value, set_value) +} + +fn try_storage() -> Result> { + use_window() + .as_ref() + .ok_or_else(|| UseStorageError::WindowReturnedNone)? + .local_storage() + .map_err(|err| UseStorageError::StorageNotAvailable(err))? + .ok_or_else(|| UseStorageError::StorageReturnedNone) +} + +/// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling. +fn handle_error( + on_error: &Rc)>, + result: Result>, +) -> Result { + result.or_else(|err| Err((on_error)(err))) +} + +fn parse_item>( + str: Option, + on_error: &Rc)>, +) -> Option { + str.map(|str| { + let result = T::try_from(str).map_err(UseStorageError::ParseItemError); + handle_error(&on_error, result) + }) + .transpose() + // We've sent our error so unwrap to drop () error + .unwrap_or_default() +} + +impl Default for UseStorageOptions { + fn default() -> Self { + Self { + on_error: Rc::new(|_err| ()), + } + } +} + +impl UseStorageOptions { + pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { + Self { + on_error: Rc::new(on_error), + } + } +}