use std::{any::type_name, rc::Rc};
use serde::{de::DeserializeOwned, Serialize};
use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
use web_sys::{Event, Storage};
use crate::{dispatch::Dispatch, listener::Listener, store::Store};
#[derive(Debug, thiserror::Error)]
pub enum StorageError {
#[error("Window not found")]
WindowNotFound,
#[error("Could not access {0:?} storage")]
StorageAccess(Area),
#[error("A web-sys error occurred")]
WebSys(JsValue),
#[error("A serde error occurred")]
Serde(#[from] serde_json::Error),
}
#[derive(Debug, Clone, Copy)]
pub enum Area {
Local,
Session,
}
pub struct StorageListener<T> {
area: Area,
_marker: std::marker::PhantomData<T>,
}
impl<T> StorageListener<T> {
pub fn new(area: Area) -> Self {
Self {
area,
_marker: Default::default(),
}
}
}
impl<T> Listener for StorageListener<T>
where
T: Store + Serialize,
{
type Store = T;
fn on_change(&mut self, state: Rc<Self::Store>) {
if let Err(err) = save(state.as_ref(), self.area) {
crate::log::error!("Error saving state to storage: {:?}", err);
}
}
}
fn get_storage(area: Area) -> Result<Storage, StorageError> {
let window = web_sys::window().ok_or(StorageError::WindowNotFound)?;
let storage = match area {
Area::Local => window.local_storage(),
Area::Session => window.session_storage(),
};
storage
.map_err(StorageError::WebSys)?
.ok_or(StorageError::StorageAccess(area))
}
pub fn save<T: Serialize>(state: &T, area: Area) -> Result<(), StorageError> {
let storage = get_storage(area)?;
let value = &serde_json::to_string(state).map_err(StorageError::Serde)?;
storage
.set(type_name::<T>(), value)
.map_err(StorageError::WebSys)?;
Ok(())
}
pub fn load<T: DeserializeOwned>(area: Area) -> Result<Option<T>, StorageError> {
let storage = get_storage(area)?;
let value = storage
.get(type_name::<T>())
.map_err(StorageError::WebSys)?;
match value {
Some(value) => {
let state = serde_json::from_str(&value).map_err(StorageError::Serde)?;
Ok(Some(state))
}
None => Ok(None),
}
}
pub fn init_tab_sync<S: Store + DeserializeOwned>(area: Area) -> Result<(), StorageError> {
let closure = Closure::wrap(Box::new(move |_: &Event| match load(area) {
Ok(Some(state)) => {
Dispatch::<S>::new().set(state);
}
Err(e) => {
crate::log::error!("Unable to load state: {:?}", e);
}
_ => {}
}) as Box<dyn FnMut(&Event)>);
web_sys::window()
.ok_or(StorageError::WindowNotFound)?
.add_event_listener_with_callback("storage", closure.as_ref().unchecked_ref())
.map_err(StorageError::WebSys)?;
closure.forget();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
#[derive(Deserialize)]
struct TestStore;
impl Store for TestStore {
fn new() -> Self {
Self
}
fn should_notify(&self, _old: &Self) -> bool {
true
}
}
#[test]
fn tab_sync() {
init_tab_sync::<TestStore>(Area::Local).unwrap();
}
}