Skip to content

Commit

Permalink
feat: added global state rehydration
Browse files Browse the repository at this point in the history
  • Loading branch information
arctic-hen7 committed Jan 22, 2022
1 parent 891f3bb commit 10634fb
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 28 deletions.
18 changes: 15 additions & 3 deletions examples/basic/.perseus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use perseus::{
shell::{app_shell, get_initial_state, get_render_cfg, InitialState, ShellProps},
},
plugins::PluginAction,
state::{AnyFreeze, PageStateStore},
state::{AnyFreeze, FrozenApp, PageStateStore},
templates::{RouterState, TemplateNodeType},
DomNode,
};
Expand Down Expand Up @@ -71,6 +71,17 @@ pub fn run() -> Result<(), JsValue> {
let global_state: Rc<RefCell<Box<dyn AnyFreeze>>> =
Rc::new(RefCell::new(Box::new(Option::<()>::None)));

// TODO Try to fetch a previous frozen app
let frozen_app: Option<Rc<FrozenApp>> = Some(Rc::new(FrozenApp {
global_state: r#"{"test":"Hello from the frozen app!"}"#.to_string(),
route: "".to_string(),
page_state_store: {
let mut map = std::collections::HashMap::new();
map.insert("".to_string(), r#"{"username":"Sam"}"#.to_string());
map
},
}));

// Create the router we'll use for this app, based on the user's app definition
create_app_route! {
name => AppRoute,
Expand All @@ -93,7 +104,7 @@ pub fn run() -> Result<(), JsValue> {
// Sycamore's reactivity is broken by a future, so we need to explicitly add the route to the reactive dependencies here
// We do need the future though (otherwise `container_rx` doesn't link to anything until it's too late)
let _ = route.get();
wasm_bindgen_futures::spawn_local(cloned!((locales, route, container_rx, router_state, pss, global_state, translations_manager, error_pages, initial_container) => async move {
wasm_bindgen_futures::spawn_local(cloned!((locales, route, container_rx, router_state, pss, global_state, frozen_app, translations_manager, error_pages, initial_container) => async move {
let container_rx_elem = container_rx.get::<DomNode>().unchecked_into::<web_sys::Element>();
checkpoint("router_entry");
match &route.get().as_ref().0 {
Expand All @@ -117,7 +128,8 @@ pub fn run() -> Result<(), JsValue> {
initial_container: initial_container.unwrap().clone(),
container_rx_elem: container_rx_elem.clone(),
page_state_store: pss.clone(),
global_state: global_state.clone()
global_state: global_state.clone(),
frozen_app
}
).await,
// If the user is using i18n, then they'll want to detect the locale on any paths missing a locale
Expand Down
34 changes: 29 additions & 5 deletions packages/perseus-macro/src/template2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,18 +167,42 @@ pub fn template_impl(input: TemplateFn, attr_args: AttributeArgs) -> TokenStream
// Deserialize the global state, make it reactive, and register it with the `RenderCtx`
// If it's already there, we'll leave it
// This means that we can pass an `Option<String>` around safely and then deal with it at the template site
let global_state_refcell = ::perseus::get_render_ctx!().global_state;
let render_ctx = ::perseus::get_render_ctx!();
let frozen_app = render_ctx.frozen_app;
let global_state_refcell = render_ctx.global_state;
let global_state = global_state_refcell.borrow();
// This will work if the global state hasn't been initialized yet, because it's the default value that Perseus sets
if global_state.as_any().downcast_ref::<::std::option::Option::<()>>().is_some() {
// We can downcast it as the type set by the core render system, so we're the first page to be loaded
// In that case, we'll set the global state properly
drop(global_state);
let mut global_state_mut = global_state_refcell.borrow_mut();
// This will be defined if we're the first page
let global_state_props = &props.global_state.unwrap();
let new_global_state = ::serde_json::from_str::<<#global_state_rx as ::perseus::state::MakeUnrx>::Unrx>(global_state_props).unwrap().make_rx();
*global_state_mut = ::std::boxed::Box::new(new_global_state);
// If there's a frozen app, we'll try to use that
let new_global_state = match frozen_app {
// If it hadn't been initialized yet when we froze, it would've been set to `None` here, and we'll use the one from the server
::std::option::Option::Some(frozen_app) if frozen_app.global_state != "None" => {
let global_state_str = frozen_app.global_state.clone();
let global_state = ::serde_json::from_str::<<#global_state_rx as ::perseus::state::MakeUnrx>::Unrx>(&global_state_str);
// We don't control the source of the frozen app, so we have to assume that it could well be invalid, in whcih case we'll turn to the server
match global_state {
::std::result::Result::Ok(global_state) => global_state,
::std::result::Result::Err(_) => {
// This will be defined if we're the first page
let global_state_str = props.global_state.unwrap();
// That's from the server, so it's unrecoverable if it doesn't deserialize
::serde_json::from_str::<<#global_state_rx as ::perseus::state::MakeUnrx>::Unrx>(&global_state_str).unwrap()
}
}
},
_ => {
// This will be defined if we're the first page
let global_state_str = props.global_state.unwrap();
// That's from the server, so it's unrecoverable if it doesn't deserialize
::serde_json::from_str::<<#global_state_rx as ::perseus::state::MakeUnrx>::Unrx>(&global_state_str).unwrap()
}
};
let new_global_state_rx = new_global_state.make_rx();
*global_state_mut = ::std::boxed::Box::new(new_global_state_rx);
// The component function can now access this in `RenderCtx`
}
// The user's function
Expand Down
1 change: 1 addition & 0 deletions packages/perseus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ pub mod state {
pub use crate::global_state::GlobalStateCreator;
pub use crate::page_state_store::PageStateStore;
pub use crate::rx_state::*;
pub use crate::template::FrozenApp;
}
/// A series of exports that should be unnecessary for nearly all uses of Perseus. These are used principally in developing alternative
/// engines.
Expand Down
12 changes: 6 additions & 6 deletions packages/perseus/src/page_state_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;

use crate::{rx_state::Freeze, state::AnyFreeze};
use crate::state::AnyFreeze;

/// A container for page state in Perseus. This is designed as a context store, in which one of each type can be stored. Therefore, it acts very similarly to Sycamore's context system,
/// though it's specifically designed for each page to store one reactive properties object. In theory, you could interact with this entirely independently of Perseus' state interface,
Expand Down Expand Up @@ -36,18 +36,18 @@ impl PageStateStore {
self.map.borrow().contains_key(url)
}
}
// Good for convenience, and there's no reason we can't do this
impl Freeze for PageStateStore {
impl PageStateStore {
/// Freezes the component entries into a new `HashMap` of `String`s to avoid extra layers of deserialization.
// TODO Avoid literally cloning all the page states here if possible
fn freeze(&self) -> String {
pub fn freeze_to_hash_map(&self) -> HashMap<String, String> {
let map = self.map.borrow();
let mut str_map = HashMap::new();
for (k, v) in map.iter() {
let v_str = v.freeze();
str_map.insert(k, v_str);
str_map.insert(k.to_string(), v_str);
}

serde_json::to_string(&str_map).unwrap()
str_map
}
}

Expand Down
16 changes: 8 additions & 8 deletions packages/perseus/src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::page_data::PageData;
use crate::path_prefix::get_path_prefix_client;
use crate::state::{AnyFreeze, PageStateStore};
use crate::template::Template;
use crate::templates::{PageProps, RouterLoadState, RouterState, TemplateNodeType};
use crate::templates::{FrozenApp, PageProps, RouterLoadState, RouterState, TemplateNodeType};
use crate::ErrorPages;
use fmterr::fmt_err;
use std::cell::RefCell;
Expand Down Expand Up @@ -255,6 +255,8 @@ pub struct ShellProps {
pub container_rx_elem: Element,
/// The global state store. Brekaing it out here prevents it being overriden every time a new template loads.
pub global_state: Rc<RefCell<Box<dyn AnyFreeze>>>,
/// A previous frozen state to be gradully rehydrated.
pub frozen_app: Option<Rc<FrozenApp>>,
}

/// Fetches the information for the given page and renders it. This should be provided the actual path of the page to render (not just the
Expand All @@ -273,6 +275,7 @@ pub async fn app_shell(
initial_container,
container_rx_elem,
global_state: curr_global_state,
frozen_app,
}: ShellProps,
) {
checkpoint("app_shell_entry");
Expand Down Expand Up @@ -304,13 +307,6 @@ pub async fn app_shell(
&JsValue::undefined(),
)
.unwrap();
// // Also do this for the global state
// Reflect::set(
// &JsValue::from(web_sys::window().unwrap()),
// &JsValue::from("__PERSEUS_GLOBAL_STATE"),
// &JsValue::undefined(),
// )
// .unwrap();
// We need to move the server-rendered content from its current container to the reactive container (otherwise Sycamore can't work with it properly)
let initial_html = initial_container.inner_html();
container_rx_elem.set_inner_html(&initial_html);
Expand Down Expand Up @@ -364,6 +360,7 @@ pub async fn app_shell(
router_state_2,
page_state_store,
curr_global_state,
frozen_app,
)
},
&container_rx_elem,
Expand All @@ -380,6 +377,7 @@ pub async fn app_shell(
router_state_2,
page_state_store,
curr_global_state,
frozen_app,
)
},
&container_rx_elem,
Expand Down Expand Up @@ -484,6 +482,7 @@ pub async fn app_shell(
router_state_2.clone(),
page_state_store,
curr_global_state,
frozen_app,
)
},
&container_rx_elem,
Expand All @@ -500,6 +499,7 @@ pub async fn app_shell(
router_state_2,
page_state_store,
curr_global_state,
frozen_app,
)
},
&container_rx_elem,
Expand Down
21 changes: 15 additions & 6 deletions packages/perseus/src/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ pub struct FrozenApp {
pub global_state: String,
/// The frozen route.
pub route: String,
/// The frozen page state store.
pub page_state_store: String,
/// The frozen page state store. We store this as a `HashMap` as this level so that we can avoid another deserialization.
pub page_state_store: HashMap<String, String>,
}

/// This encapsulates all elements of context currently provided to Perseus templates. While this can be used manually, there are macros
Expand All @@ -67,6 +67,8 @@ pub struct RenderCtx {
/// Because we store `dyn Any` in here, we initialize it as `Option::None`, and then the template macro (which does the heavy lifting for global state) will find that it can't downcast
/// to the user's global state type, which will prompt it to deserialize whatever global state it was given and then write that here.
pub global_state: Rc<RefCell<Box<dyn AnyFreeze>>>,
/// A previous state the app was once in, still serialized. This will be rehydrated graudally by the template macro.
pub frozen_app: Option<Rc<FrozenApp>>,
}
impl Freeze for RenderCtx {
/// 'Freezes' the relevant parts of the render configuration to a serialized `String` that can later be used to re-initialize the app to the same state at the time of freezing.
Expand All @@ -80,7 +82,7 @@ impl Freeze for RenderCtx {
RouterLoadState::Server => "SERVER",
}
.to_string(),
page_state_store: self.page_state_store.freeze(),
page_state_store: self.page_state_store.freeze_to_hash_map(),
};
serde_json::to_string(&frozen_app).unwrap()
}
Expand Down Expand Up @@ -288,6 +290,7 @@ impl<G: Html> Template<G> {

// Render executors
/// Executes the user-given function that renders the template on the client-side ONLY. This takes in an extsing global state.
#[allow(clippy::too_many_arguments)]
pub fn render_for_template_client(
&self,
props: PageProps,
Expand All @@ -296,6 +299,7 @@ impl<G: Html> Template<G> {
router_state: RouterState,
page_state_store: PageStateStore,
global_state: Rc<RefCell<Box<dyn AnyFreeze>>>,
frozen_app: Option<Rc<FrozenApp>>,
) -> View<G> {
view! {
// We provide the translator through context, which avoids having to define a separate variable for every translation due to Sycamore's `template!` macro taking ownership with `move` closures
Expand All @@ -305,7 +309,8 @@ impl<G: Html> Template<G> {
translator: translator.clone(),
router: router_state,
page_state_store,
global_state
global_state,
frozen_app
},
children: || (self.template)(props)
})
Expand All @@ -328,7 +333,9 @@ impl<G: Html> Template<G> {
translator: translator.clone(),
router: router_state,
page_state_store,
global_state: Rc::new(RefCell::new(Box::new(Option::<()>::None)))
global_state: Rc::new(RefCell::new(Box::new(Option::<()>::None))),
// Hydrating state on the server-side is pointless
frozen_app: None
},
children: || (self.template)(props)
})
Expand All @@ -349,7 +356,9 @@ impl<G: Html> Template<G> {
// The head string is rendered to a string, and so never has information about router or page state
router: RouterState::default(),
page_state_store: PageStateStore::default(),
global_state: Rc::new(RefCell::new(Box::new(Option::<()>::None)))
global_state: Rc::new(RefCell::new(Box::new(Option::<()>::None))),
// Hydrating state on the server-side is pointless
frozen_app: None,
},
children: || (self.head)(props)
})
Expand Down

0 comments on commit 10634fb

Please sign in to comment.