pub(crate) mod diff;
pub(crate) mod patch;
pub(crate) mod renderer;
use std::{
cell::{Ref, RefCell},
fmt::Debug,
rc::Rc,
};
pub mod util;
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{BeforeUnloadEvent, Node};
use renderer::render_node;
use crate::{
node::{DispatchFn, RespoAction, RespoNode},
states_tree::{RespoStatesTree, RespoUpdateState},
};
const RESPO_APP_STORE_KEY: &str = "respo_app_respo_store_default";
pub trait RespoApp {
type Model: RespoStore + Debug + Clone + 'static;
fn dispatch(store: Rc<RefCell<Self::Model>>, action: <Self::Model as RespoStore>::Action) -> Result<(), String>;
fn pick_storage_key() -> &'static str {
RESPO_APP_STORE_KEY
}
fn get_mount_target(&self) -> &Node;
fn get_store(&self) -> &Rc<RefCell<Self::Model>>;
fn get_loop_delay() -> Option<i32> {
Some(100)
}
fn view(store: Ref<Self::Model>) -> Result<RespoNode<<Self::Model as RespoStore>::Action>, String>;
fn render_loop(&self) -> Result<(), String> {
let mount_target = self.get_mount_target();
let global_store = self.get_store();
let dispatch_action = {
let store_to_action = global_store.to_owned();
move |op: <Self::Model as RespoStore>::Action| -> Result<(), String> {
Self::dispatch(store_to_action.to_owned(), op)?;
Ok(())
}
};
render_node(
mount_target.to_owned(),
Box::new({
let store_to_action = global_store.to_owned();
move || store_to_action.borrow().to_owned()
}),
Box::new({
let store = global_store.to_owned();
move || -> Result<RespoNode<<Self::Model as RespoStore>::Action>, String> {
Self::view(store.borrow())
}
}),
DispatchFn::new(dispatch_action),
Self::get_loop_delay(),
)
.unwrap_or_else(|e| {
util::error_log!("render loop error: {:?}", e);
});
Ok(())
}
fn backup_model_beforeunload(&self) -> Result<(), String> {
let window = web_sys::window().expect("window");
let storage = match window.local_storage() {
Ok(Some(storage)) => storage,
_ => return Err("Failed to access local storage".to_owned()),
};
let beforeunload = Closure::wrap(Box::new({
let p = Self::pick_storage_key();
let store = self.get_store().to_owned();
move |_e: BeforeUnloadEvent| {
let content = store.as_ref().borrow().to_string();
storage.set_item(p, &content).expect("save storage");
}
}) as Box<dyn FnMut(BeforeUnloadEvent)>);
window.set_onbeforeunload(Some(beforeunload.as_ref().unchecked_ref()));
beforeunload.forget();
Ok(())
}
fn try_load_storage(&self) -> Result<(), String> {
let window = web_sys::window().expect("window");
let storage = match window.local_storage() {
Ok(Some(storage)) => storage,
_ => return Err("Failed to access local storage".to_owned()),
};
let key = Self::pick_storage_key();
match storage.get_item(key) {
Ok(Some(s)) => match Self::Model::try_from_string(&s) {
Ok(s) => {
let store = self.get_store();
*store.borrow_mut() = s;
}
Err(e) => {
util::error_log!("error: {:?}", e);
}
},
_ => {
util::log!("no storage");
}
}
Ok(())
}
}
pub trait RespoStore {
type Action: Debug + Clone + RespoAction;
fn update(&mut self, op: Self::Action) -> Result<(), String>;
fn get_states(&mut self) -> &mut RespoStatesTree;
fn update_states(&mut self, op: RespoUpdateState) {
self.get_states().set_in_mut(op);
}
fn to_string(&self) -> String;
fn try_from_string(s: &str) -> Result<Self, String>
where
Self: Sized;
}