Skip to content

Commit 10634fb

Browse files
committed
feat: added global state rehydration
1 parent 891f3bb commit 10634fb

File tree

6 files changed

+74
-28
lines changed

6 files changed

+74
-28
lines changed

examples/basic/.perseus/src/lib.rs

+15-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use perseus::{
1010
shell::{app_shell, get_initial_state, get_render_cfg, InitialState, ShellProps},
1111
},
1212
plugins::PluginAction,
13-
state::{AnyFreeze, PageStateStore},
13+
state::{AnyFreeze, FrozenApp, PageStateStore},
1414
templates::{RouterState, TemplateNodeType},
1515
DomNode,
1616
};
@@ -71,6 +71,17 @@ pub fn run() -> Result<(), JsValue> {
7171
let global_state: Rc<RefCell<Box<dyn AnyFreeze>>> =
7272
Rc::new(RefCell::new(Box::new(Option::<()>::None)));
7373

74+
// TODO Try to fetch a previous frozen app
75+
let frozen_app: Option<Rc<FrozenApp>> = Some(Rc::new(FrozenApp {
76+
global_state: r#"{"test":"Hello from the frozen app!"}"#.to_string(),
77+
route: "".to_string(),
78+
page_state_store: {
79+
let mut map = std::collections::HashMap::new();
80+
map.insert("".to_string(), r#"{"username":"Sam"}"#.to_string());
81+
map
82+
},
83+
}));
84+
7485
// Create the router we'll use for this app, based on the user's app definition
7586
create_app_route! {
7687
name => AppRoute,
@@ -93,7 +104,7 @@ pub fn run() -> Result<(), JsValue> {
93104
// Sycamore's reactivity is broken by a future, so we need to explicitly add the route to the reactive dependencies here
94105
// We do need the future though (otherwise `container_rx` doesn't link to anything until it's too late)
95106
let _ = route.get();
96-
wasm_bindgen_futures::spawn_local(cloned!((locales, route, container_rx, router_state, pss, global_state, translations_manager, error_pages, initial_container) => async move {
107+
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 {
97108
let container_rx_elem = container_rx.get::<DomNode>().unchecked_into::<web_sys::Element>();
98109
checkpoint("router_entry");
99110
match &route.get().as_ref().0 {
@@ -117,7 +128,8 @@ pub fn run() -> Result<(), JsValue> {
117128
initial_container: initial_container.unwrap().clone(),
118129
container_rx_elem: container_rx_elem.clone(),
119130
page_state_store: pss.clone(),
120-
global_state: global_state.clone()
131+
global_state: global_state.clone(),
132+
frozen_app
121133
}
122134
).await,
123135
// If the user is using i18n, then they'll want to detect the locale on any paths missing a locale

packages/perseus-macro/src/template2.rs

+29-5
Original file line numberDiff line numberDiff line change
@@ -167,18 +167,42 @@ pub fn template_impl(input: TemplateFn, attr_args: AttributeArgs) -> TokenStream
167167
// Deserialize the global state, make it reactive, and register it with the `RenderCtx`
168168
// If it's already there, we'll leave it
169169
// This means that we can pass an `Option<String>` around safely and then deal with it at the template site
170-
let global_state_refcell = ::perseus::get_render_ctx!().global_state;
170+
let render_ctx = ::perseus::get_render_ctx!();
171+
let frozen_app = render_ctx.frozen_app;
172+
let global_state_refcell = render_ctx.global_state;
171173
let global_state = global_state_refcell.borrow();
172174
// This will work if the global state hasn't been initialized yet, because it's the default value that Perseus sets
173175
if global_state.as_any().downcast_ref::<::std::option::Option::<()>>().is_some() {
174176
// We can downcast it as the type set by the core render system, so we're the first page to be loaded
175177
// In that case, we'll set the global state properly
176178
drop(global_state);
177179
let mut global_state_mut = global_state_refcell.borrow_mut();
178-
// This will be defined if we're the first page
179-
let global_state_props = &props.global_state.unwrap();
180-
let new_global_state = ::serde_json::from_str::<<#global_state_rx as ::perseus::state::MakeUnrx>::Unrx>(global_state_props).unwrap().make_rx();
181-
*global_state_mut = ::std::boxed::Box::new(new_global_state);
180+
// If there's a frozen app, we'll try to use that
181+
let new_global_state = match frozen_app {
182+
// 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
183+
::std::option::Option::Some(frozen_app) if frozen_app.global_state != "None" => {
184+
let global_state_str = frozen_app.global_state.clone();
185+
let global_state = ::serde_json::from_str::<<#global_state_rx as ::perseus::state::MakeUnrx>::Unrx>(&global_state_str);
186+
// 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
187+
match global_state {
188+
::std::result::Result::Ok(global_state) => global_state,
189+
::std::result::Result::Err(_) => {
190+
// This will be defined if we're the first page
191+
let global_state_str = props.global_state.unwrap();
192+
// That's from the server, so it's unrecoverable if it doesn't deserialize
193+
::serde_json::from_str::<<#global_state_rx as ::perseus::state::MakeUnrx>::Unrx>(&global_state_str).unwrap()
194+
}
195+
}
196+
},
197+
_ => {
198+
// This will be defined if we're the first page
199+
let global_state_str = props.global_state.unwrap();
200+
// That's from the server, so it's unrecoverable if it doesn't deserialize
201+
::serde_json::from_str::<<#global_state_rx as ::perseus::state::MakeUnrx>::Unrx>(&global_state_str).unwrap()
202+
}
203+
};
204+
let new_global_state_rx = new_global_state.make_rx();
205+
*global_state_mut = ::std::boxed::Box::new(new_global_state_rx);
182206
// The component function can now access this in `RenderCtx`
183207
}
184208
// The user's function

packages/perseus/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ pub mod state {
103103
pub use crate::global_state::GlobalStateCreator;
104104
pub use crate::page_state_store::PageStateStore;
105105
pub use crate::rx_state::*;
106+
pub use crate::template::FrozenApp;
106107
}
107108
/// A series of exports that should be unnecessary for nearly all uses of Perseus. These are used principally in developing alternative
108109
/// engines.

packages/perseus/src/page_state_store.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::cell::RefCell;
22
use std::collections::HashMap;
33
use std::rc::Rc;
44

5-
use crate::{rx_state::Freeze, state::AnyFreeze};
5+
use crate::state::AnyFreeze;
66

77
/// 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,
88
/// 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,
@@ -36,18 +36,18 @@ impl PageStateStore {
3636
self.map.borrow().contains_key(url)
3737
}
3838
}
39-
// Good for convenience, and there's no reason we can't do this
40-
impl Freeze for PageStateStore {
39+
impl PageStateStore {
40+
/// Freezes the component entries into a new `HashMap` of `String`s to avoid extra layers of deserialization.
4141
// TODO Avoid literally cloning all the page states here if possible
42-
fn freeze(&self) -> String {
42+
pub fn freeze_to_hash_map(&self) -> HashMap<String, String> {
4343
let map = self.map.borrow();
4444
let mut str_map = HashMap::new();
4545
for (k, v) in map.iter() {
4646
let v_str = v.freeze();
47-
str_map.insert(k, v_str);
47+
str_map.insert(k.to_string(), v_str);
4848
}
4949

50-
serde_json::to_string(&str_map).unwrap()
50+
str_map
5151
}
5252
}
5353

packages/perseus/src/shell.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::page_data::PageData;
55
use crate::path_prefix::get_path_prefix_client;
66
use crate::state::{AnyFreeze, PageStateStore};
77
use crate::template::Template;
8-
use crate::templates::{PageProps, RouterLoadState, RouterState, TemplateNodeType};
8+
use crate::templates::{FrozenApp, PageProps, RouterLoadState, RouterState, TemplateNodeType};
99
use crate::ErrorPages;
1010
use fmterr::fmt_err;
1111
use std::cell::RefCell;
@@ -255,6 +255,8 @@ pub struct ShellProps {
255255
pub container_rx_elem: Element,
256256
/// The global state store. Brekaing it out here prevents it being overriden every time a new template loads.
257257
pub global_state: Rc<RefCell<Box<dyn AnyFreeze>>>,
258+
/// A previous frozen state to be gradully rehydrated.
259+
pub frozen_app: Option<Rc<FrozenApp>>,
258260
}
259261

260262
/// 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
@@ -273,6 +275,7 @@ pub async fn app_shell(
273275
initial_container,
274276
container_rx_elem,
275277
global_state: curr_global_state,
278+
frozen_app,
276279
}: ShellProps,
277280
) {
278281
checkpoint("app_shell_entry");
@@ -304,13 +307,6 @@ pub async fn app_shell(
304307
&JsValue::undefined(),
305308
)
306309
.unwrap();
307-
// // Also do this for the global state
308-
// Reflect::set(
309-
// &JsValue::from(web_sys::window().unwrap()),
310-
// &JsValue::from("__PERSEUS_GLOBAL_STATE"),
311-
// &JsValue::undefined(),
312-
// )
313-
// .unwrap();
314310
// We need to move the server-rendered content from its current container to the reactive container (otherwise Sycamore can't work with it properly)
315311
let initial_html = initial_container.inner_html();
316312
container_rx_elem.set_inner_html(&initial_html);
@@ -364,6 +360,7 @@ pub async fn app_shell(
364360
router_state_2,
365361
page_state_store,
366362
curr_global_state,
363+
frozen_app,
367364
)
368365
},
369366
&container_rx_elem,
@@ -380,6 +377,7 @@ pub async fn app_shell(
380377
router_state_2,
381378
page_state_store,
382379
curr_global_state,
380+
frozen_app,
383381
)
384382
},
385383
&container_rx_elem,
@@ -484,6 +482,7 @@ pub async fn app_shell(
484482
router_state_2.clone(),
485483
page_state_store,
486484
curr_global_state,
485+
frozen_app,
487486
)
488487
},
489488
&container_rx_elem,
@@ -500,6 +499,7 @@ pub async fn app_shell(
500499
router_state_2,
501500
page_state_store,
502501
curr_global_state,
502+
frozen_app,
503503
)
504504
},
505505
&container_rx_elem,

packages/perseus/src/template.rs

+15-6
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ pub struct FrozenApp {
4141
pub global_state: String,
4242
/// The frozen route.
4343
pub route: String,
44-
/// The frozen page state store.
45-
pub page_state_store: String,
44+
/// The frozen page state store. We store this as a `HashMap` as this level so that we can avoid another deserialization.
45+
pub page_state_store: HashMap<String, String>,
4646
}
4747

4848
/// This encapsulates all elements of context currently provided to Perseus templates. While this can be used manually, there are macros
@@ -67,6 +67,8 @@ pub struct RenderCtx {
6767
/// 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
6868
/// to the user's global state type, which will prompt it to deserialize whatever global state it was given and then write that here.
6969
pub global_state: Rc<RefCell<Box<dyn AnyFreeze>>>,
70+
/// A previous state the app was once in, still serialized. This will be rehydrated graudally by the template macro.
71+
pub frozen_app: Option<Rc<FrozenApp>>,
7072
}
7173
impl Freeze for RenderCtx {
7274
/// '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.
@@ -80,7 +82,7 @@ impl Freeze for RenderCtx {
8082
RouterLoadState::Server => "SERVER",
8183
}
8284
.to_string(),
83-
page_state_store: self.page_state_store.freeze(),
85+
page_state_store: self.page_state_store.freeze_to_hash_map(),
8486
};
8587
serde_json::to_string(&frozen_app).unwrap()
8688
}
@@ -288,6 +290,7 @@ impl<G: Html> Template<G> {
288290

289291
// Render executors
290292
/// Executes the user-given function that renders the template on the client-side ONLY. This takes in an extsing global state.
293+
#[allow(clippy::too_many_arguments)]
291294
pub fn render_for_template_client(
292295
&self,
293296
props: PageProps,
@@ -296,6 +299,7 @@ impl<G: Html> Template<G> {
296299
router_state: RouterState,
297300
page_state_store: PageStateStore,
298301
global_state: Rc<RefCell<Box<dyn AnyFreeze>>>,
302+
frozen_app: Option<Rc<FrozenApp>>,
299303
) -> View<G> {
300304
view! {
301305
// 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
@@ -305,7 +309,8 @@ impl<G: Html> Template<G> {
305309
translator: translator.clone(),
306310
router: router_state,
307311
page_state_store,
308-
global_state
312+
global_state,
313+
frozen_app
309314
},
310315
children: || (self.template)(props)
311316
})
@@ -328,7 +333,9 @@ impl<G: Html> Template<G> {
328333
translator: translator.clone(),
329334
router: router_state,
330335
page_state_store,
331-
global_state: Rc::new(RefCell::new(Box::new(Option::<()>::None)))
336+
global_state: Rc::new(RefCell::new(Box::new(Option::<()>::None))),
337+
// Hydrating state on the server-side is pointless
338+
frozen_app: None
332339
},
333340
children: || (self.template)(props)
334341
})
@@ -349,7 +356,9 @@ impl<G: Html> Template<G> {
349356
// The head string is rendered to a string, and so never has information about router or page state
350357
router: RouterState::default(),
351358
page_state_store: PageStateStore::default(),
352-
global_state: Rc::new(RefCell::new(Box::new(Option::<()>::None)))
359+
global_state: Rc::new(RefCell::new(Box::new(Option::<()>::None))),
360+
// Hydrating state on the server-side is pointless
361+
frozen_app: None,
353362
},
354363
children: || (self.head)(props)
355364
})

0 commit comments

Comments
 (0)