From 85d7f4a0b57ed5d956ab2571a9d386e9d2c109cd Mon Sep 17 00:00:00 2001 From: arctic_hen7 Date: Tue, 8 Nov 2022 22:17:46 +0000 Subject: [PATCH] feat: improved global state system (#223) * feat: made global state accessible through function This removes the function parameter method of accessing it. * refactor: made global state functions private on `RenderCtx` The macros don't handle global state now, and the user should just need `.get_global_state()`/`.try_get_global_state()`. * refactor: removed global state glob imports These used to be necessary to ensure that the macros could access the intermediate types, but the new functional access system prevents this! * chore: removed old commented code from tempalte macro * feat: removed need for importing intermediate reactive types These are now accessed purely through trait linkage, meaning no more weird glob imports for apps that stored their reactive state in different modules (or apps that were accessing state from another page outside that page). * fix: fixed `BorrowMut` errors on global state thawing * refactor: privatized global state on `RenderCtx` This prevents `BorrowMut` panics that are fairly likely if users access the global state manually. If this affects anyone's apps, please let me know! * chore: removed `get_render_ctx!()` macro This is pointless with `RenderCtx::from_ctx`. Also added `RenderCtx` to `perseus::prelude` for convenience. * chore: cleaned up a few things BREAKING CHANGES: removed `get_render_ctx!(cx)` in favor of `RenderCtx::from_ctx(cx)`; privatized global state on `RenderCtx`; made global state accessible through `render_ctx.get_global_state::(cx)`, rather than through template function parameter --- .../src/templates/about.rs | 10 +- .../src/templates/index.rs | 14 +- .../core/global_state/src/templates/about.rs | 12 +- .../core/global_state/src/templates/index.rs | 17 +- .../core/idb_freezing/src/templates/about.rs | 10 +- .../core/idb_freezing/src/templates/index.rs | 14 +- .../core/router_state/src/templates/index.rs | 7 +- examples/demos/auth/src/templates/index.rs | 6 +- packages/perseus-macro/src/rx_state.rs | 16 +- packages/perseus-macro/src/template_rx.rs | 151 ++------------ packages/perseus/src/build.rs | 30 ++- packages/perseus/src/lib.rs | 6 +- packages/perseus/src/router/app_route.rs | 7 +- .../perseus/src/router/get_initial_view.rs | 26 +-- .../perseus/src/router/get_subsequent_view.rs | 5 +- packages/perseus/src/router/mod.rs | 2 +- packages/perseus/src/server/render.rs | 53 +++-- packages/perseus/src/state/global_state.rs | 73 ++++++- packages/perseus/src/state/mod.rs | 4 +- packages/perseus/src/state/rx_state.rs | 33 ++-- packages/perseus/src/template/core.rs | 12 +- packages/perseus/src/template/page_props.rs | 5 +- packages/perseus/src/template/render_ctx.rs | 184 +++++++++++++++--- 23 files changed, 402 insertions(+), 295 deletions(-) diff --git a/examples/core/freezing_and_thawing/src/templates/about.rs b/examples/core/freezing_and_thawing/src/templates/about.rs index 9d95c8200a..02b0eb8a2e 100644 --- a/examples/core/freezing_and_thawing/src/templates/about.rs +++ b/examples/core/freezing_and_thawing/src/templates/about.rs @@ -1,15 +1,17 @@ +use perseus::prelude::*; use perseus::state::Freeze; -use perseus::{Html, Template}; use sycamore::prelude::*; -use crate::global_state::*; +use crate::global_state::AppStateRx; #[perseus::template_rx] -pub fn about_page<'a, G: Html>(cx: Scope<'a>, _: (), global_state: AppStateRx<'a>) -> View { +pub fn about_page<'a, G: Html>(cx: Scope<'a>) -> View { // This is not part of our data model, we do NOT want the frozen app // synchronized as part of our page's state, it should be separate let frozen_app = create_signal(cx, String::new()); - let render_ctx = perseus::get_render_ctx!(cx); + let render_ctx = RenderCtx::from_ctx(cx); + + let global_state = render_ctx.get_global_state::(cx); view! { cx, p(id = "global_state") { (global_state.test.get()) } diff --git a/examples/core/freezing_and_thawing/src/templates/index.rs b/examples/core/freezing_and_thawing/src/templates/index.rs index 75cbaf5060..f9e1fa37ac 100644 --- a/examples/core/freezing_and_thawing/src/templates/index.rs +++ b/examples/core/freezing_and_thawing/src/templates/index.rs @@ -1,8 +1,8 @@ +use perseus::prelude::*; use perseus::state::Freeze; -use perseus::{Html, RenderFnResultWithCause, Template}; use sycamore::prelude::*; -use crate::global_state::*; +use crate::global_state::AppStateRx; #[perseus::make_rx(IndexPropsRx)] pub struct IndexProps { @@ -10,15 +10,13 @@ pub struct IndexProps { } #[perseus::template_rx] -pub fn index_page<'a, G: Html>( - cx: Scope<'a>, - state: IndexPropsRx<'a>, - global_state: AppStateRx<'a>, -) -> View { +pub fn index_page<'a, G: Html>(cx: Scope<'a>, state: IndexPropsRx<'a>) -> View { // This is not part of our data model, we do NOT want the frozen app // synchronized as part of our page's state, it should be separate let frozen_app = create_signal(cx, String::new()); - let render_ctx = perseus::get_render_ctx!(cx); + let render_ctx = RenderCtx::from_ctx(cx); + + let global_state = render_ctx.get_global_state::(cx); view! { cx, // For demonstration, we'll let the user modify the page's state and the global state arbitrarily diff --git a/examples/core/global_state/src/templates/about.rs b/examples/core/global_state/src/templates/about.rs index fd51db2f95..cf4ba1515a 100644 --- a/examples/core/global_state/src/templates/about.rs +++ b/examples/core/global_state/src/templates/about.rs @@ -1,12 +1,12 @@ -use perseus::{Html, Template}; -use sycamore::prelude::{view, Scope, SsrNode, View}; +use perseus::prelude::*; +use sycamore::prelude::*; -use crate::global_state::*; // See the index page for why we need this +use crate::global_state::AppStateRx; -// This template needs global state, but doesn't have any state of its own, so -// the first argument is the unit type `()` (which the macro will detect) #[perseus::template_rx] -pub fn about_page<'a, G: Html>(cx: Scope<'a>, _: (), global_state: AppStateRx<'a>) -> View { +pub fn about_page<'a, G: Html>(cx: Scope<'a>) -> View { + let global_state = RenderCtx::from_ctx(cx).get_global_state::(cx); + view! { cx, // The user can change the global state through an input, and the changes they make will be reflected throughout the app p { (global_state.test.get()) } diff --git a/examples/core/global_state/src/templates/index.rs b/examples/core/global_state/src/templates/index.rs index b1eb5b588f..3e52e7fe58 100644 --- a/examples/core/global_state/src/templates/index.rs +++ b/examples/core/global_state/src/templates/index.rs @@ -1,13 +1,16 @@ -use perseus::{Html, Template}; -use sycamore::prelude::{view, Scope, SsrNode, View}; +use perseus::prelude::*; +use sycamore::prelude::*; -use crate::global_state::*; // This is necessary because Perseus generates an invisible intermediary - // `struct` that the template needs access to +use crate::global_state::AppStateRx; -// This template needs global state, but doesn't have any state of its own, so -// the first argument is the unit type `()` (which the macro will detect) +// Note that this template takes no state of its own in this example, but it +// certainly could #[perseus::template_rx] -pub fn index_page<'a, G: Html>(cx: Scope<'a>, _: (), global_state: AppStateRx<'a>) -> View { +pub fn index_page<'a, G: Html>(cx: Scope<'a>) -> View { + // We access the global state through the render context, extracted from + // Sycamore's context system + let global_state = RenderCtx::from_ctx(cx).get_global_state::(cx); + view! { cx, // The user can change the global state through an input, and the changes they make will be reflected throughout the app p { (global_state.test.get()) } diff --git a/examples/core/idb_freezing/src/templates/about.rs b/examples/core/idb_freezing/src/templates/about.rs index d517dad732..d0950a6fbd 100644 --- a/examples/core/idb_freezing/src/templates/about.rs +++ b/examples/core/idb_freezing/src/templates/about.rs @@ -1,17 +1,17 @@ -use perseus::{Html, Template}; +use perseus::prelude::*; use sycamore::prelude::*; -use crate::global_state::*; +use crate::global_state::AppStateRx; #[perseus::template_rx] -pub fn about_page<'a, G: Html>(cx: Scope<'a>, _: (), global_state: AppStateRx<'a>) -> View { +pub fn about_page<'a, G: Html>(cx: Scope<'a>) -> View { // This is not part of our data model let freeze_status = create_signal(cx, String::new()); // It's faster to get this only once and rely on reactivity // But it's unused when this runs on the server-side because of the target-gate // below - #[allow(unused_variables)] - let render_ctx = perseus::get_render_ctx!(cx); + let render_ctx = RenderCtx::from_ctx(cx); + let global_state = render_ctx.get_global_state::(cx); view! { cx, p(id = "global_state") { (global_state.test.get()) } diff --git a/examples/core/idb_freezing/src/templates/index.rs b/examples/core/idb_freezing/src/templates/index.rs index bd1c696af2..ee522befd2 100644 --- a/examples/core/idb_freezing/src/templates/index.rs +++ b/examples/core/idb_freezing/src/templates/index.rs @@ -1,7 +1,7 @@ -use perseus::{Html, RenderFnResultWithCause, Template}; +use perseus::prelude::*; use sycamore::prelude::*; -use crate::global_state::*; +use crate::global_state::AppStateRx; #[perseus::make_rx(IndexPropsRx)] pub struct IndexProps { @@ -9,19 +9,15 @@ pub struct IndexProps { } #[perseus::template_rx] -pub fn index_page<'a, G: Html>( - cx: Scope<'a>, - state: IndexPropsRx<'a>, - global_state: AppStateRx<'a>, -) -> View { +pub fn index_page<'a, G: Html>(cx: Scope<'a>, state: IndexPropsRx<'a>) -> View { // This is not part of our data model let freeze_status = create_signal(cx, String::new()); let thaw_status = create_signal(cx, String::new()); // It's faster to get this only once and rely on reactivity // But it's unused when this runs on the server-side because of the target-gate // below - #[allow(unused_variables)] - let render_ctx = perseus::get_render_ctx!(cx); + let render_ctx = RenderCtx::from_ctx(cx); + let global_state = render_ctx.get_global_state::(cx); view! { cx, // For demonstration, we'll let the user modify the page's state and the global state arbitrarily diff --git a/examples/core/router_state/src/templates/index.rs b/examples/core/router_state/src/templates/index.rs index 932e1c1200..68977a2d7a 100644 --- a/examples/core/router_state/src/templates/index.rs +++ b/examples/core/router_state/src/templates/index.rs @@ -1,9 +1,10 @@ -use perseus::{router::RouterLoadState, Html, Template}; -use sycamore::prelude::{create_memo, view, Scope, View}; +use perseus::prelude::*; +use perseus::router::RouterLoadState; +use sycamore::prelude::*; #[perseus::template_rx] pub fn router_state_page(cx: Scope) -> View { - let load_state = perseus::get_render_ctx!(cx).router.get_load_state(cx); + let load_state = RenderCtx::from_ctx(cx).router.get_load_state(cx); // This uses Sycamore's `create_memo` to create a state that will update // whenever the router state changes let load_state_str = create_memo(cx, || match (*load_state.get()).clone() { diff --git a/examples/demos/auth/src/templates/index.rs b/examples/demos/auth/src/templates/index.rs index 85b06fb54c..75585f7c45 100644 --- a/examples/demos/auth/src/templates/index.rs +++ b/examples/demos/auth/src/templates/index.rs @@ -1,9 +1,11 @@ use crate::global_state::*; -use perseus::{Html, Template}; +use perseus::prelude::*; use sycamore::prelude::*; #[perseus::template_rx] -fn index_view<'a, G: Html>(cx: Scope<'a>, _: (), AppStateRx { auth }: AppStateRx<'a>) -> View { +fn index_view<'a, G: Html>(cx: Scope<'a>) -> View { + let AppStateRx { auth } = RenderCtx::from_ctx(cx).get_global_state::(cx); + let AuthDataRx { state, username } = auth; // This isn't part of our data model because it's only used here to pass to the // login function diff --git a/packages/perseus-macro/src/rx_state.rs b/packages/perseus-macro/src/rx_state.rs index 7813b9df49..a1e5239ea8 100644 --- a/packages/perseus-macro/src/rx_state.rs +++ b/packages/perseus-macro/src/rx_state.rs @@ -11,6 +11,9 @@ pub fn make_rx_impl(mut orig_struct: ItemStruct, name_raw: Ident) -> TokenStream // fields, we'll just copy the struct and change the parts we want to // We won't create the final `struct` yet to avoid more operations than // necessary + // Note that we leave this as whatever visibility the original state was to + // avoid compiler errors (since it will be exposed as a trait-linked type + // through the ref struct) let mut mid_struct = orig_struct.clone(); // This will use `RcSignal`s, and will be stored in context let ItemStruct { ident: orig_name, @@ -301,12 +304,17 @@ pub fn make_rx_impl(mut orig_struct: ItemStruct, name_raw: Ident) -> TokenStream ::serde_json::to_string(&unrx).unwrap() } } - #[derive(::std::clone::Clone)] - #ref_struct - impl #generics #mid_name #generics { - pub fn to_ref_struct(self, cx: ::sycamore::prelude::Scope) -> #ref_name #generics { + // TODO Generics + impl ::perseus::state::MakeRxRef for #mid_name { + type RxRef<'a> = #ref_name<'a>; + fn to_ref_struct<'a>(self, cx: ::sycamore::prelude::Scope<'a>) -> #ref_name<'a> { #make_ref_fields } } + #[derive(::std::clone::Clone)] + #ref_struct + impl<'a> ::perseus::state::RxRef for #ref_name<'a> { + type RxNonRef = #mid_name; + } } } diff --git a/packages/perseus-macro/src/template_rx.rs b/packages/perseus-macro/src/template_rx.rs index 6633230803..04a9677ce4 100644 --- a/packages/perseus-macro/src/template_rx.rs +++ b/packages/perseus-macro/src/template_rx.rs @@ -84,7 +84,7 @@ impl Parse for TemplateFn { } // We can have anywhere between 1 and 3 arguments (scope, ?state, ?global state) if args.len() > 3 || args.is_empty() { - return Err(syn::Error::new_spanned(&sig.inputs, "template functions accept between one and three arguments (reactive scope; then one for custom properties and another for global state, both optional)")); + return Err(syn::Error::new_spanned(&sig.inputs, "template functions accept either one or two arguments (reactive scope; then one optional for custom properties)")); } Ok(Self { @@ -105,11 +105,12 @@ impl Parse for TemplateFn { } } -/// Converts the user-given name of a final reactive `struct` into the -/// intermediary name used for the one we'll interface with. This will remove -/// any associated lifetimes because we want just the type name. This will leave -/// generics intact though. -fn make_mid(ty: &Type) -> Type { +/// Converts the user-given name of a final reactive `struct` with lifetimes +/// into the same type, just without those lifetimes, so we can use it outside +/// the scope in which those lifetimes have been defined. +/// +/// See the callers of this function to see exactly why it's necessary. +fn remove_lifetimes(ty: &Type) -> Type { // Don't run any transformation if this is the unit type match ty { Type::Tuple(TypeTuple { elems, .. }) if elems.is_empty() => ty.clone(), @@ -121,8 +122,6 @@ fn make_mid(ty: &Type) -> Type { let ty_str = Regex::new(r#"(('.*?) |<\s*('[^, ]*?)\s*>)"#) .unwrap() .replace_all(&ty_str, ""); - // And now actually make the replacement we need (ref to intermediate) - let ty_str = ty_str.trim().to_string() + "PerseusRxIntermediary"; Type::Verbatim(TokenStream::from_str(&ty_str).unwrap()) } } @@ -142,125 +141,7 @@ pub fn template_impl(input: TemplateFn) -> TokenStream { let component_name = Ident::new(&(name.to_string() + "_component"), Span::call_site()); - // We create a wrapper function that can be easily provided to `.template()` - // that does deserialization automatically if needed This is dependent on - // what arguments the template takes - if fn_args.len() == 3 { - // Get the argument for the reactive scope - let cx_arg = &fn_args[0]; - // There's an argument for page properties that needs to have state extracted, - // so the wrapper will deserialize it We'll also make it reactive and - // add it to the page state store - let state_arg = &fn_args[1]; - let rx_props_ty = match state_arg { - FnArg::Typed(PatType { ty, .. }) => make_mid(ty), - FnArg::Receiver(_) => unreachable!(), - }; - // There's also a second argument for the global state, which we'll deserialize - // and make global if it's not already (aka. if any other pages have loaded - // before this one) Sycamore won't let us have more than one argument to - // a component though, so we sneakily extract it and literally construct it as a - // variable (this should be fine?) - let global_state_arg = &fn_args[2]; - let (global_state_arg_pat, global_state_rx) = match global_state_arg { - FnArg::Typed(PatType { pat, ty, .. }) => (pat, make_mid(ty)), - FnArg::Receiver(_) => unreachable!(), - }; - let name_string = name.to_string(); - // Handle the case in which the template is just using global state and the - // first argument is the unit type That's represented for Syn as a tuple - // with no elements - match rx_props_ty { - // This template takes dummy state and global state - Type::Tuple(TypeTuple { elems, .. }) if elems.is_empty() => quote! { - #vis fn #name(cx: ::sycamore::prelude::Scope, props: ::perseus::template::PageProps) -> ::sycamore::prelude::View { - use ::perseus::state::MakeRx; - - let render_ctx = ::perseus::get_render_ctx!(cx); - // Get the frozen or active global state (the render context manages thawing preferences) - // This isn't completely pointless, this method mutates as well to set up the global state as appropriate - // If there's no active or frozen global state, then we'll fall back to the generated one from the server (which we know will be there, since if this is `None` we must be - // the first page to access the global state). - if render_ctx.get_active_or_frozen_global_state::<#global_state_rx>().is_none() { - // Because this came from the server, we assume it's valid - render_ctx.register_global_state_str::<#global_state_rx>(&props.global_state.unwrap()).unwrap(); - } - - // The user's function - // We know this won't be async because Sycamore doesn't allow that - #(#attrs)* - #[::sycamore::component] - // WARNING: I removed the `#state_arg` here because the new Sycamore throws errors for unit type props (possible consequences?) - fn #component_name #generics(#cx_arg) -> #return_type { - let __perseus_global_state_intermediate: #global_state_rx = { - let global_state = ::perseus::get_render_ctx!(cx).global_state.0.borrow(); - // We can guarantee that it will downcast correctly now, because we'll only invoke the component from this function, which sets up the global state correctly - let global_state_ref = global_state.as_any().downcast_ref::<#global_state_rx>().unwrap(); - (*global_state_ref).clone() - }; - let #global_state_arg_pat = __perseus_global_state_intermediate.to_ref_struct(cx); - #block - } - - // Declare that this page will never take any state to enable full caching - render_ctx.register_page_no_state(&props.path); - - #component_name(cx) - } - }, - // This template takes its own state and global state - _ => quote! { - #vis fn #name(cx: ::sycamore::prelude::Scope, props: ::perseus::template::PageProps) -> ::sycamore::prelude::View { - use ::perseus::state::MakeRx; - - let render_ctx = ::perseus::get_render_ctx!(cx).clone(); - // Get the frozen or active global state (the render context manages thawing preferences) - // This isn't completely pointless, this method mutates as well to set up the global state as appropriate - // If there's no active or frozen global state, then we'll fall back to the generated one from the server (which we know will be there, since if this is `None` we must be - // the first page to access the global state). - if render_ctx.get_active_or_frozen_global_state::<#global_state_rx>().is_none() { - // Because this came from the server, we assume it's valid - render_ctx.register_global_state_str::<#global_state_rx>(&props.global_state.unwrap()).unwrap(); - } - - // The user's function - // We know this won't be async because Sycamore doesn't allow that - #(#attrs)* - #[::sycamore::component] - fn #component_name #generics(#cx_arg, #state_arg) -> #return_type { - let #global_state_arg_pat: #global_state_rx = { - let global_state = ::perseus::get_render_ctx!(cx).global_state.0.borrow(); - // We can guarantee that it will downcast correctly now, because we'll only invoke the component from this function, which sets up the global state correctly - let global_state_ref = global_state.as_any().downcast_ref::<#global_state_rx>().unwrap(); - (*global_state_ref).clone() - }; - let #global_state_arg_pat = #global_state_arg_pat.to_ref_struct(cx); - #block - } - - let props = { - // Check if properties of the reactive type are already in the page state store - // If they are, we'll use them (so state persists for templates across the whole app) - let render_ctx = ::perseus::get_render_ctx!(cx); - // The render context will automatically handle prioritizing frozen or active state for us for this page as long as we have a reactive state type, which we do! - match render_ctx.get_active_or_frozen_page_state::<#rx_props_ty>(&props.path) { - // If we navigated back to this page, and it's still in the PSS, the given state will be a dummy, but we don't need to worry because it's never checked if this evaluates - ::std::option::Option::Some(existing_state) => existing_state, - // Again, frozen state has been dealt with already, so we'll fall back to generated state - ::std::option::Option::None => { - // Again, the render context can do the heavy lifting for us (this returns what we need, and can do type checking) - // We also assume that any state we have is valid because it comes from the server - // The user really should have a generation function, but if they don't then they'd get a panic, so give them a nice error message - render_ctx.register_page_state_str::<#rx_props_ty>(&props.path, &props.state.unwrap_or_else(|| panic!("template `{}` takes a state, but no state generation functions were provided (please add at least one to use state)", #name_string))).unwrap() - } - } - }; - - #component_name(cx, props.to_ref_struct(cx)) - } - }, - } - } else if fn_args.len() == 2 { + if fn_args.len() == 2 { // Get the argument for the reactive scope let cx_arg = &fn_args[0]; // There's an argument for page properties that needs to have state extracted, @@ -268,13 +149,13 @@ pub fn template_impl(input: TemplateFn) -> TokenStream { // add it to the page state store let arg = &fn_args[1]; let rx_props_ty = match arg { - FnArg::Typed(PatType { ty, .. }) => make_mid(ty), + FnArg::Typed(PatType { ty, .. }) => remove_lifetimes(ty), FnArg::Receiver(_) => unreachable!(), }; let name_string = name.to_string(); quote! { #vis fn #name(cx: ::sycamore::prelude::Scope, props: ::perseus::template::PageProps) -> ::sycamore::prelude::View { - use ::perseus::state::MakeRx; + use ::perseus::state::{MakeRx, MakeRxRef, RxRef}; // The user's function, with Sycamore component annotations and the like preserved // We know this won't be async because Sycamore doesn't allow that @@ -287,9 +168,11 @@ pub fn template_impl(input: TemplateFn) -> TokenStream { let props = { // Check if properties of the reactive type are already in the page state store // If they are, we'll use them (so state persists for templates across the whole app) - let render_ctx = ::perseus::get_render_ctx!(cx); + let render_ctx = ::perseus::RenderCtx::from_ctx(cx); // The render context will automatically handle prioritizing frozen or active state for us for this page as long as we have a reactive state type, which we do! - match render_ctx.get_active_or_frozen_page_state::<#rx_props_ty>(&props.path) { + // We need there to be no lifetimes in `rx_props_ty` here, since the lifetimes the user decalred are defined inside the above function, which we + // aren't inside! + match render_ctx.get_active_or_frozen_page_state::<<#rx_props_ty as RxRef>::RxNonRef>(&props.path) { // If we navigated back to this page, and it's still in the PSS, the given state will be a dummy, but we don't need to worry because it's never checked if this evaluates ::std::option::Option::Some(existing_state) => existing_state, // Again, frozen state has been dealt with already, so we'll fall back to generated state @@ -297,7 +180,7 @@ pub fn template_impl(input: TemplateFn) -> TokenStream { // Again, the render context can do the heavy lifting for us (this returns what we need, and can do type checking) // We also assume that any state we have is valid because it comes from the server // The user really should have a generation function, but if they don't then they'd get a panic, so give them a nice error message - render_ctx.register_page_state_str::<#rx_props_ty>(&props.path, &props.state.unwrap_or_else(|| panic!("template `{}` takes a state, but no state generation functions were provided (please add at least one to use state)", #name_string))).unwrap() + render_ctx.register_page_state_str::<<#rx_props_ty as RxRef>::RxNonRef>(&props.path, &props.state.unwrap_or_else(|| panic!("template `{}` takes a state, but no state generation functions were provided (please add at least one to use state)", #name_string))).unwrap() } } }; @@ -311,7 +194,7 @@ pub fn template_impl(input: TemplateFn) -> TokenStream { // There are no arguments except for the scope quote! { #vis fn #name(cx: ::sycamore::prelude::Scope, props: ::perseus::template::PageProps) -> ::sycamore::prelude::View { - use ::perseus::state::MakeRx; + use ::perseus::state::{MakeRx, MakeRxRef}; // The user's function, with Sycamore component annotations and the like preserved // We know this won't be async because Sycamore doesn't allow that @@ -322,7 +205,7 @@ pub fn template_impl(input: TemplateFn) -> TokenStream { } // Declare that this page will never take any state to enable full caching - let render_ctx = ::perseus::get_render_ctx!(cx); + let render_ctx = ::perseus::RenderCtx::from_ctx(cx); render_ctx.register_page_no_state(&props.path); #component_name(cx) diff --git a/packages/perseus/src/build.rs b/packages/perseus/src/build.rs index 555ce613bf..aa5d9dc062 100644 --- a/packages/perseus/src/build.rs +++ b/packages/perseus/src/build.rs @@ -147,11 +147,15 @@ async fn gen_state_for_path( let page_props = PageProps { path: full_path_with_locale.clone(), state: Some(initial_state), - global_state: global_state.clone(), }; // Prerender the template using that state let prerendered = sycamore::render_to_string(|cx| { - template.render_for_template_server(page_props.clone(), cx, translator) + template.render_for_template_server( + page_props.clone(), + global_state.clone(), + cx, + translator, + ) }); minify(&prerendered, true)?; // Write that prerendered HTML to a static file @@ -161,7 +165,7 @@ async fn gen_state_for_path( // Prerender the document `` with that state // If the page also uses request state, amalgamation will be applied as for the // normal content - let head_str = template.render_head_str(page_props, translator); + let head_str = template.render_head_str(page_props, global_state.clone(), translator); minify(&head_str, true)?; mutable_store .write( @@ -186,11 +190,15 @@ async fn gen_state_for_path( let page_props = PageProps { path: full_path_with_locale.clone(), state: Some(initial_state), - global_state: global_state.clone(), }; // Prerender the template using that state let prerendered = sycamore::render_to_string(|cx| { - template.render_for_template_server(page_props.clone(), cx, translator) + template.render_for_template_server( + page_props.clone(), + global_state.clone(), + cx, + translator, + ) }); minify(&prerendered, true)?; // Write that prerendered HTML to a static file @@ -200,7 +208,7 @@ async fn gen_state_for_path( // Prerender the document `` with that state // If the page also uses request state, amalgamation will be applied as for the // normal content - let head_str = template.render_head_str(page_props, translator); + let head_str = template.render_head_str(page_props, global_state.clone(), translator); immutable_store .write( &format!("static/{}.head.html", full_path_encoded), @@ -242,12 +250,16 @@ async fn gen_state_for_path( let page_props = PageProps { path: full_path_with_locale, state: None, - global_state: global_state.clone(), }; let prerendered = sycamore::render_to_string(|cx| { - template.render_for_template_server(page_props.clone(), cx, translator) + template.render_for_template_server( + page_props.clone(), + global_state.clone(), + cx, + translator, + ) }); - let head_str = template.render_head_str(page_props, translator); + let head_str = template.render_head_str(page_props, global_state.clone(), translator); // Write that prerendered HTML to a static file immutable_store .write(&format!("static/{}.html", full_path_encoded), &prerendered) diff --git a/packages/perseus/src/lib.rs b/packages/perseus/src/lib.rs index a82ec0c030..bb7a426fc4 100644 --- a/packages/perseus/src/lib.rs +++ b/packages/perseus/src/lib.rs @@ -91,7 +91,7 @@ pub use crate::{ error_pages::ErrorPages, errors::{ErrorCause, GenericErrorWithCause}, init::*, - template::{RenderFnResult, RenderFnResultWithCause, Template}, + template::{RenderCtx, RenderFnResult, RenderFnResultWithCause, Template}, }; // Browser-side only #[cfg(target_arch = "wasm32")] @@ -129,7 +129,7 @@ pub mod prelude { #[cfg(feature = "i18n")] pub use crate::{link, t}; pub use crate::{ - ErrorCause, ErrorPages, GenericErrorWithCause, PerseusApp, PerseusRoot, RenderFnResult, - RenderFnResultWithCause, Template, + ErrorCause, ErrorPages, GenericErrorWithCause, PerseusApp, PerseusRoot, RenderCtx, + RenderFnResult, RenderFnResultWithCause, Template, }; } diff --git a/packages/perseus/src/router/app_route.rs b/packages/perseus/src/router/app_route.rs index cae2e2d33c..905414600b 100644 --- a/packages/perseus/src/router/app_route.rs +++ b/packages/perseus/src/router/app_route.rs @@ -1,10 +1,5 @@ use super::{match_route, RouteVerdict}; -use crate::{ - i18n::Locales, - template::{RenderCtx, TemplateMap, TemplateNodeType}, -}; -use std::collections::HashMap; -use std::rc::Rc; +use crate::template::{RenderCtx, TemplateNodeType}; use sycamore::prelude::Scope; use sycamore_router::Route; diff --git a/packages/perseus/src/router/get_initial_view.rs b/packages/perseus/src/router/get_initial_view.rs index 39a464ec9f..9f9b1f7ade 100644 --- a/packages/perseus/src/router/get_initial_view.rs +++ b/packages/perseus/src/router/get_initial_view.rs @@ -71,7 +71,6 @@ pub(crate) fn get_initial_view( match initial_state { InitialState::Present(state) => { checkpoint("initial_state_present"); - let global_state = get_global_state(); // Unset the initial state variable so we perform subsequent renders correctly // This monstrosity is needed until `web-sys` adds a `.set()` method on `Window` // We don't do this for the global state because it should hang around @@ -133,7 +132,6 @@ pub(crate) fn get_initial_view( let page_props = PageProps { path: path_with_locale.clone(), state, - global_state, }; // Pre-emptively declare the page interactive since all we do from this point // is hydrate @@ -273,28 +271,6 @@ fn get_initial_state() -> InitialState { } } -/// Gets the global state injected by the server, if there was any. If there are -/// errors in this, we can return `None` and not worry about it, they'll be -/// handled by the initial state. -pub(crate) fn get_global_state() -> Option { - let val_opt = web_sys::window().unwrap().get("__PERSEUS_GLOBAL_STATE"); - let js_obj = match val_opt { - Some(js_obj) => js_obj, - None => return None, - }; - // The object should only actually contain the string value that was injected - let state_str = match js_obj.as_string() { - Some(state_str) => state_str, - None => return None, - }; - // On the server-side, we encode a `None` value directly (otherwise it will be - // some convoluted stringified JSON) - match state_str.as_str() { - "None" => None, - state_str => Some(state_str.to_string()), - } -} - /// Gets the translations injected by the server, if there was any. If there are /// errors in this, we can return `None` and not worry about it, they'll be /// handled by the initial state. @@ -327,7 +303,7 @@ fn get_head() -> String { let head_node = document.query_selector("head").unwrap().unwrap(); // Get all the elements after the head boundary (otherwise we'd be duplicating // the initial stuff) - let els = document + let els = head_node .query_selector_all(r#"meta[itemprop='__perseus_head_boundary'] ~ *"#) .unwrap(); // No, `NodeList` does not have an iterator implementation... diff --git a/packages/perseus/src/router/get_subsequent_view.rs b/packages/perseus/src/router/get_subsequent_view.rs index c596c11e53..934c4b7a9a 100644 --- a/packages/perseus/src/router/get_subsequent_view.rs +++ b/packages/perseus/src/router/get_subsequent_view.rs @@ -1,6 +1,6 @@ use crate::errors::*; use crate::page_data::PageDataPartial; -use crate::router::{get_global_state, RouteVerdict, RouterLoadState}; +use crate::router::{RouteVerdict, RouterLoadState}; use crate::state::PssContains; use crate::template::{PageProps, RenderCtx, Template, TemplateNodeType}; use crate::utils::checkpoint; @@ -207,9 +207,6 @@ pub(crate) async fn get_subsequent_view( let page_props = PageProps { path: path_with_locale.clone(), state: page_data.state, - // This will probably be overridden by the already-set version (unless no - // page has used global state yet) - global_state: get_global_state(), }; let template_name = template.get_path(); // Pre-emptively update the router state diff --git a/packages/perseus/src/router/mod.rs b/packages/perseus/src/router/mod.rs index e1dc3c99ad..52bde8a38a 100644 --- a/packages/perseus/src/router/mod.rs +++ b/packages/perseus/src/router/mod.rs @@ -21,6 +21,6 @@ pub(crate) use router_component::{perseus_router, PerseusRouterProps}; pub use router_state::{RouterLoadState, RouterState}; #[cfg(target_arch = "wasm32")] -pub(crate) use get_initial_view::{get_global_state, get_initial_view, InitialView}; +pub(crate) use get_initial_view::{get_initial_view, InitialView}; #[cfg(target_arch = "wasm32")] pub(crate) use get_subsequent_view::{get_subsequent_view, GetSubsequentViewProps}; diff --git a/packages/perseus/src/server/render.rs b/packages/perseus/src/server/render.rs index d6cc3e9eda..0065179dce 100644 --- a/packages/perseus/src/server/render.rs +++ b/packages/perseus/src/server/render.rs @@ -156,16 +156,20 @@ async fn render_amalgamated_state( let page_props = PageProps { path: path_with_locale, state: state.clone(), - global_state: global_state.clone(), }; let html = if render_html { sycamore::render_to_string(|cx| { - template.render_for_template_server(page_props.clone(), cx, translator) + template.render_for_template_server( + page_props.clone(), + global_state.clone(), + cx, + translator, + ) }) } else { String::new() }; - let head = template.render_head_str(page_props, translator); + let head = template.render_head_str(page_props, global_state.clone(), translator); Ok((html, head, state)) } @@ -281,12 +285,16 @@ async fn revalidate( let page_props = PageProps { path: path_with_locale, state: state.clone(), - global_state: global_state.clone(), }; let html = sycamore::render_to_string(|cx| { - template.render_for_template_server(page_props.clone(), cx, translator) + template.render_for_template_server( + page_props.clone(), + global_state.clone(), + cx, + translator, + ) }); - let head = template.render_head_str(page_props, translator); + let head = template.render_head_str(page_props, global_state.clone(), translator); // Handle revalidation, we need to parse any given time strings into datetimes // We don't need to worry about revalidation that operates by logic, that's // request-time only @@ -463,12 +471,17 @@ pub async fn get_page_for_template( let page_props = PageProps { path: path_with_locale.clone(), state: state.clone(), - global_state: global_state.clone(), }; let html_val = sycamore::render_to_string(|cx| { - template.render_for_template_server(page_props.clone(), cx, &translator) + template.render_for_template_server( + page_props.clone(), + global_state.clone(), + cx, + &translator, + ) }); - let head_val = template.render_head_str(page_props, &translator); + let head_val = + template.render_head_str(page_props, global_state.clone(), &translator); // Handle revalidation, we need to parse any given time strings into datetimes // We don't need to worry about revalidation that operates by logic, that's // request-time only Obviously we don't need to revalidate @@ -589,14 +602,19 @@ pub async fn get_page_for_template( let page_props = PageProps { path: path_with_locale, state: state.clone(), - global_state: global_state.clone(), }; - let head_val = template.render_head_str(page_props.clone(), &translator); + let head_val = + template.render_head_str(page_props.clone(), global_state.clone(), &translator); head = head_val; // We should only render the HTML if necessary, since we're not caching if render_html { let html_val = sycamore::render_to_string(|cx| { - template.render_for_template_server(page_props, cx, &translator) + template.render_for_template_server( + page_props, + global_state.clone(), + cx, + &translator, + ) }); html = html_val; } @@ -631,13 +649,18 @@ pub async fn get_page_for_template( let page_props = PageProps { path: path_with_locale, state: state.clone(), - global_state: global_state.clone(), }; - let head_val = template.render_head_str(page_props.clone(), &translator); + let head_val = + template.render_head_str(page_props.clone(), global_state.clone(), &translator); // We should only render the HTML if necessary, since we're not caching if render_html { let html_val = sycamore::render_to_string(|cx| { - template.render_for_template_server(page_props, cx, &translator) + template.render_for_template_server( + page_props, + global_state.clone(), + cx, + &translator, + ) }); html = html_val; } diff --git a/packages/perseus/src/state/global_state.rs b/packages/perseus/src/state/global_state.rs index 2bfee21f76..15ae314941 100644 --- a/packages/perseus/src/state/global_state.rs +++ b/packages/perseus/src/state/global_state.rs @@ -1,4 +1,5 @@ use super::rx_state::AnyFreeze; +use super::{Freeze, MakeRx, MakeUnrx}; #[cfg(not(target_arch = "wasm32"))] // To suppress warnings use crate::errors::*; use crate::make_async_trait; @@ -68,12 +69,62 @@ impl GlobalStateCreator { } } -/// A representation of the global state in an app. +/// A representation of the global state in an app. This will be initialized as +/// a string of whatever was given by the server, until a page requests it and +/// deserializes it properly (since we can't know the type until that happens). #[derive(Clone)] -pub struct GlobalState(pub Rc>>); -impl Default for GlobalState { - fn default() -> Self { - Self(Rc::new(RefCell::new(Box::new(Option::<()>::None)))) +pub struct GlobalState(pub Rc>); +impl GlobalState { + /// A convenience function for creating a new global state from an + /// underlying type of global state. + pub(crate) fn new(ty: GlobalStateType) -> Self { + Self(Rc::new(RefCell::new(ty))) + } +} + +/// The backend for the different types of global state. +pub enum GlobalStateType { + /// The global state has been deserialized and loaded, and is ready for use. + Loaded(Box), + /// The global state is in string form from the server. + Server(String), + /// There was no global state provided by the server. + None, +} +impl GlobalStateType { + /// Parses the global state into the given reactive type if possible. If the + /// state from the server hasn't been parsed yet, this will return + /// `None`. + /// + /// In other words, this will only return something if the global state has + /// already been requested and loaded. + pub fn parse_active(&self) -> Option<::Rx> + where + R: Clone + AnyFreeze + MakeUnrx, + // We need this so that the compiler understands that the reactive version of the + // unreactive version of `R` has the same properties as `R` itself + <::Unrx as MakeRx>::Rx: Clone + AnyFreeze + MakeUnrx, + { + match &self { + // If there's an issue deserializing to this type, we'll fall back to the server + Self::Loaded(any) => any + .as_any() + .downcast_ref::<::Rx>() + .cloned(), + Self::Server(_) => None, + Self::None => None, + } + } +} +impl Freeze for GlobalStateType { + fn freeze(&self) -> String { + match &self { + Self::Loaded(state) => state.freeze(), + // There's no point in serializing state that was sent from the server, since we can + // easily get it again later (it definitionally hasn't changed) + Self::Server(_) => "Server".to_string(), + Self::None => "None".to_string(), + } } } impl std::fmt::Debug for GlobalState { @@ -81,3 +132,15 @@ impl std::fmt::Debug for GlobalState { f.debug_struct("GlobalState").finish() } } + +// /// A representation of global state parsed into a specific type. +// pub enum ParsedGlobalState { +// /// The global state has been deserialized and loaded, and is ready for +// use. Loaded(R), +// /// We couldn't parse to the desired reactive type. +// ParseError, +// /// The global state is in string form from the server. +// Server(String), +// /// There was no global state provided by the server. +// None, +// } diff --git a/packages/perseus/src/state/mod.rs b/packages/perseus/src/state/mod.rs index 32bb60bd2e..b15ffcc458 100644 --- a/packages/perseus/src/state/mod.rs +++ b/packages/perseus/src/state/mod.rs @@ -4,9 +4,9 @@ mod page_state_store; mod rx_state; pub use freeze::{FrozenApp, PageThawPrefs, ThawPrefs}; -pub use global_state::{GlobalState, GlobalStateCreator}; +pub use global_state::{GlobalState, GlobalStateCreator, GlobalStateType}; pub use page_state_store::{PageStateStore, PssContains, PssEntry, PssState}; -pub use rx_state::{AnyFreeze, Freeze, MakeRx, MakeUnrx}; +pub use rx_state::{AnyFreeze, Freeze, MakeRx, MakeRxRef, MakeUnrx, RxRef}; #[cfg(all(feature = "idb-freezing", target_arch = "wasm32"))] mod freeze_idb; diff --git a/packages/perseus/src/state/rx_state.rs b/packages/perseus/src/state/rx_state.rs index f2539cd74f..7c0c8ba950 100644 --- a/packages/perseus/src/state/rx_state.rs +++ b/packages/perseus/src/state/rx_state.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use std::any::Any; +use sycamore::prelude::Scope; /// A trait for `struct`s that can be made reactive. Typically, this will be /// derived with the `#[make_rx]` macro, though it can be implemented manually @@ -28,6 +29,27 @@ pub trait MakeUnrx { fn make_unrx(self) -> Self::Unrx; } +/// A trait for reactive `struct`s that can be made to use `&'a Signal`s +/// rather than `RcSignal`s, when provided with a Sycamore reactive scope. +/// This is necessary for reaping the benefits of the ergonomics of Sycamore's +/// v2 reactive primitives. +pub trait MakeRxRef { + /// The type of the reactive `struct` using `&'a Signal`s (into which + /// the type implementing this trait can be converted). + type RxRef<'a>; + /// Convert this into a version using `&'a Signal`s using `create_ref()`. + fn to_ref_struct<'a>(self, cx: Scope<'a>) -> Self::RxRef<'a>; +} + +/// A trait for `struct`s that are both reactive *and* using `&'a Signal`s +/// to store their underlying data. This exists solely to link such types to +/// their intermediate, `RcSignal`, equivalents. +pub trait RxRef { + /// The linked intermediate type using `RcSignal`s. Note that this is + /// itself reactive, just not very ergonomic. + type RxNonRef: MakeUnrx; +} + /// A trait for reactive `struct`s that can be made unreactive and serialized to /// a `String`. `struct`s that implement this should implement `MakeUnrx` for /// simplicity, but they technically don't have to (they always do in Perseus @@ -38,17 +60,6 @@ pub trait Freeze { fn freeze(&self) -> String; } -// Perseus initializes the global state as an `Option::<()>::None`, so it has to -// implement `Freeze`. It may seem silly, because we wouldn't want to freeze the -// global state if it hadn't been initialized, but that means it's unmodified -// from the server, so there would be no point in freezing it (just as there'd -// be no point in freezing the router state). -impl Freeze for Option<()> { - fn freeze(&self) -> String { - "None".to_string() - } -} - /// A convenience super-trait for `Freeze`able things that can be downcast to /// concrete types. pub trait AnyFreeze: Freeze + Any { diff --git a/packages/perseus/src/template/core.rs b/packages/perseus/src/template/core.rs index 5d8ca61f56..399aa51dbb 100644 --- a/packages/perseus/src/template/core.rs +++ b/packages/perseus/src/template/core.rs @@ -277,13 +277,14 @@ impl Template { pub fn render_for_template_server<'a>( &self, props: PageProps, + global_state: Option, cx: Scope<'a>, translator: &Translator, ) -> View { // The context we have here has no context elements set on it, so we set all the // defaults (job of the router component on the client-side) // We don't need the value, we just want the context instantiations - let _ = RenderCtx::default().set_ctx(cx); + let _ = RenderCtx::server(global_state).set_ctx(cx); // And now provide a translator separately provide_context_signal_replace(cx, translator.clone()); @@ -294,13 +295,18 @@ impl Template { /// function will not take effect due to this string rendering. Note that /// this function will provide a translator context. #[cfg(not(target_arch = "wasm32"))] - pub fn render_head_str(&self, props: PageProps, translator: &Translator) -> String { + pub fn render_head_str( + &self, + props: PageProps, + global_state: Option, + translator: &Translator, + ) -> String { sycamore::render_to_string(|cx| { // The context we have here has no context elements set on it, so we set all the // defaults (job of the router component on the client-side) // We don't need the value, we just want the context instantiations // We don't need any page state store here - let _ = RenderCtx::default().set_ctx(cx); + let _ = RenderCtx::server(global_state).set_ctx(cx); // And now provide a translator separately provide_context_signal_replace(cx, translator.clone()); // We don't want to generate hydration keys for the head because it is static. diff --git a/packages/perseus/src/template/page_props.rs b/packages/perseus/src/template/page_props.rs index ef8db73b07..7a0724158f 100644 --- a/packages/perseus/src/template/page_props.rs +++ b/packages/perseus/src/template/page_props.rs @@ -1,5 +1,7 @@ /// The properties that every page will be initialized with. You shouldn't ever /// need to interact with this unless you decide not to use the template macros. +/// +/// *Note: this used to include global state, which is now handled separately.* #[derive(Clone, Debug)] pub struct PageProps { /// The path it's rendering at. @@ -7,7 +9,4 @@ pub struct PageProps { /// The state provided to the page. This will be `Some(_)` if state was /// generated, we just can't prove that to the compiler. pub state: Option, - /// The global state, stringified. This will be `Some(_)` if state was - /// generated, we just can't prove that to the compiler. - pub global_state: Option, } diff --git a/packages/perseus/src/template/render_ctx.rs b/packages/perseus/src/template/render_ctx.rs index 9726acddc8..ae1fb7f337 100644 --- a/packages/perseus/src/template/render_ctx.rs +++ b/packages/perseus/src/template/render_ctx.rs @@ -3,7 +3,8 @@ use super::TemplateNodeType; use crate::errors::*; use crate::router::{RouterLoadState, RouterState}; use crate::state::{ - AnyFreeze, Freeze, FrozenApp, GlobalState, MakeRx, MakeUnrx, PageStateStore, ThawPrefs, + AnyFreeze, Freeze, FrozenApp, GlobalState, GlobalStateType, MakeRx, MakeRxRef, MakeUnrx, + PageStateStore, RxRef, ThawPrefs, }; use std::cell::RefCell; use std::rc::Rc; @@ -37,7 +38,18 @@ pub struct RenderCtx { /// 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: GlobalState, + /// + /// This is `pub(crate)` to prevent users accessing this without using the + /// wrappers in `RenderCtx` that handle thawing. If this direct access were + /// to occur, the likelihood of a `BorrowMut` panic would be *substantial*! + /// Essentially, you don't know when you get the global state how it's going + /// to be initialized (e.g. it might come from the server, or maybe from a + /// frozen state), so the seemingly read-only method + /// `.get_global_state()` might actually need to mutate the underlying + /// state multiple times, which would fail with a panic if there were + /// any existing borrows of the global state. By making this private, we + /// prevent this. + pub(crate) global_state: GlobalState, /// A previous state the app was once in, still serialized. This will be /// rehydrated gradually by the template macro. pub frozen_app: Rc>>, @@ -92,13 +104,18 @@ impl Freeze for RenderCtx { } } #[cfg(not(target_arch = "wasm32"))] // To prevent foot-shooting -impl Default for RenderCtx { - fn default() -> Self { +impl RenderCtx { + /// Initializes a new `RenderCtx` on the server-side with the given global + /// state. + pub(crate) fn server(global_state: Option) -> Self { Self { router: RouterState::default(), page_state_store: PageStateStore::new(0), /* There will be no need for the PSS on the * server-side */ - global_state: GlobalState::default(), + global_state: match global_state { + Some(global_state) => GlobalState::new(GlobalStateType::Server(global_state)), + None => GlobalState::new(GlobalStateType::None), + }, frozen_app: Rc::new(RefCell::new(None)), } } @@ -106,9 +123,13 @@ impl Default for RenderCtx { impl RenderCtx { /// Creates a new instance of the render context, with the given maximum /// size for the page state store, and other properties. - #[cfg(target_arch = "wasm32")] // To prevent foot-shooting - /// Note: this is designed for client-side usage, use `::default()` on the + /// + /// Note: this is designed for client-side usage, use `::server()` on the /// engine-side. + /// + /// Note also that this will automatically extract global state from page + /// variables. + #[cfg(target_arch = "wasm32")] // To prevent foot-shooting pub(crate) fn new( pss_max_size: usize, locales: crate::i18n::Locales, @@ -120,7 +141,10 @@ impl RenderCtx { Self { router: RouterState::default(), page_state_store: PageStateStore::new(pss_max_size), - global_state: GlobalState::default(), + global_state: match get_global_state() { + Some(global_state) => GlobalState::new(GlobalStateType::Server(global_state)), + None => GlobalState::new(GlobalStateType::None), + }, frozen_app: Rc::new(RefCell::new(None)), is_first: Rc::new(std::cell::Cell::new(true)), error_pages, @@ -213,7 +237,7 @@ impl RenderCtx { /// Preloads the given URL from the server and caches it, preventing /// future network requests to fetch that page. #[cfg(target_arch = "wasm32")] - pub async fn _preload(&self, path: &str, is_route_preload: bool) -> Result<(), ClientError> { + async fn _preload(&self, path: &str, is_route_preload: bool) -> Result<(), ClientError> { use crate::router::{match_route, RouteVerdict}; let path_segments = path @@ -233,7 +257,7 @@ impl RenderCtx { let route_info = match verdict { RouteVerdict::Found(info) => info, RouteVerdict::NotFound => return Err(ClientError::PreloadNotFound), - RouteVerdict::LocaleDetection(dest) => return Err(ClientError::PreloadLocaleDetection), + RouteVerdict::LocaleDetection(_) => return Err(ClientError::PreloadLocaleDetection), }; // We just needed to acquire the arguments to this function @@ -413,6 +437,10 @@ impl RenderCtx { /// An internal getter for the frozen global state. When this is called, it /// will also add any frozen state to the registered global state, /// removing whatever was there before. + /// + /// If the app was frozen before the global state from the server had been + /// parsed, this will return `None` (i.e. there will only be frozen + /// global state if the app had handled it before it froze). fn get_frozen_global_state_and_register(&self) -> Option<::Rx> where R: Clone + AnyFreeze + MakeUnrx, @@ -427,8 +455,12 @@ impl RenderCtx { if thaw_prefs.global_prefer_frozen { // Get the serialized and unreactive frozen state from the store match frozen_app.global_state.as_str() { - // See `rx_state.rs` for why this would be the default value + // There was no global state (hypothetically, a bundle change could mean there's + // some now...) "None" => None, + // There was some global state from the server that hadn't been dealt with yet + // (it will be extracted) + "Server" => None, state_str => { // Deserialize into the unreactive version let unrx = match serde_json::from_str::(state_str) { @@ -443,7 +475,7 @@ impl RenderCtx { let rx = unrx.make_rx(); // And we'll register this as the new active global state let mut active_global_state = self.global_state.0.borrow_mut(); - *active_global_state = Box::new(rx.clone()); + *active_global_state = GlobalStateType::Loaded(Box::new(rx.clone())); // Now we should remove this from the frozen state so we don't fall back to // it again drop(frozen_app_full); @@ -463,6 +495,9 @@ impl RenderCtx { } } /// An internal getter for the active (already registered) global state. + /// + /// If the global state from the server hasn't been parsed yet, this will + /// return `None`. fn get_active_global_state(&self) -> Option<::Rx> where R: Clone + AnyFreeze + MakeUnrx, @@ -470,17 +505,15 @@ impl RenderCtx { // unreactive version of `R` has the same properties as `R` itself <::Unrx as MakeRx>::Rx: Clone + AnyFreeze + MakeUnrx, { - self.global_state - .0 - .borrow() - .as_any() - .downcast_ref::<::Rx>() - .cloned() + self.global_state.0.borrow().parse_active::() } /// Gets either the active or the frozen global state, depending on thaw /// preferences. Otherwise, this is exactly the same as /// `.get_active_or_frozen_state()`. - pub fn get_active_or_frozen_global_state(&self) -> Option<::Rx> + /// + /// If the state from the server hadn't been handled by the previous freeze, + /// and it hasn't been handled yet, this will return `None`. + fn get_active_or_frozen_global_state(&self) -> Option<::Rx> where R: Clone + AnyFreeze + MakeUnrx, // We need this so that the compiler understands that the reactive version of the @@ -543,7 +576,7 @@ impl RenderCtx { } /// Registers a serialized and unreactive state string as the new active /// global state, returning a fully reactive version. - pub fn register_global_state_str( + fn register_global_state_str( &self, state_str: &str, ) -> Result<::Rx, ClientError> @@ -559,7 +592,7 @@ impl RenderCtx { .map_err(|err| ClientError::StateInvalid { source: err })?; let rx = unrx.make_rx(); let mut active_global_state = self.global_state.0.borrow_mut(); - *active_global_state = Box::new(rx.clone()); + *active_global_state = GlobalStateType::Loaded(Box::new(rx.clone())); Ok(rx) } @@ -569,12 +602,111 @@ impl RenderCtx { pub fn register_page_no_state(&self, url: &str) { self.page_state_store.set_state_never(url); } + + /// Gets the global state. + /// + /// This is operating on a render context that has already defined its + /// thawing preferences, so we can intelligently choose whether or not + /// to use any frozen global state here if nothing has been initialized. + /// + /// # Panics + /// + /// This will panic if the app has no global state. If you don't know + /// whether or not there is global state, use `.try_global_state()` + /// instead. + // This function takes the final ref struct as a type parameter! That + // complicates everything substantially. + pub fn get_global_state<'a, R>( + &self, + cx: Scope<'a>, + ) -> <<<::RxNonRef as MakeUnrx>::Unrx as MakeRx>::Rx as MakeRxRef>::RxRef<'a> + where + R: RxRef, + // We need this so that the compiler understands that the reactive version of the + // unreactive version of `R` has the same properties as `R` itself + <<::RxNonRef as MakeUnrx>::Unrx as MakeRx>::Rx: + Clone + AnyFreeze + MakeUnrx + MakeRxRef, + ::RxNonRef: MakeUnrx + AnyFreeze + Clone, + { + self.try_get_global_state::(cx).unwrap().unwrap() + } + /// The underlying logic for `.get_global_state()`, except this will return + /// `None` if the app does not have global state. + /// + /// This will return an error if the state from the server was found to be + /// invalid. + pub fn try_get_global_state<'a, R>( + &self, + cx: Scope<'a>, + ) -> Result< + Option< + // Note: I am sorry. + <<<::RxNonRef as MakeUnrx>::Unrx as MakeRx>::Rx as MakeRxRef>::RxRef<'a>, + >, + ClientError, + > + where + R: RxRef, + // We need this so that the compiler understands that the reactive version of the + // unreactive version of `R` has the same properties as `R` itself + <<::RxNonRef as MakeUnrx>::Unrx as MakeRx>::Rx: + Clone + AnyFreeze + MakeUnrx + MakeRxRef, + ::RxNonRef: MakeUnrx + AnyFreeze + Clone, + { + let global_state_ty = self.global_state.0.borrow(); + // Bail early if the app doesn't support global state + if let GlobalStateType::None = *global_state_ty { + return Ok(None); + } + // Check if there's an actively loaded state or a frozen state (note + // that this will set up global state if there was something in the frozen data, + // hence it needs any other borrows to be dismissed) + drop(global_state_ty); + let rx_state = if let Some(rx_state) = + self.get_active_or_frozen_global_state::<::RxNonRef>() + { + rx_state + } else { + // There was nothing, so we'll load from the server + let global_state_ty = self.global_state.0.borrow(); + if let GlobalStateType::Server(server_str) = &*global_state_ty { + let server_str = server_str.to_string(); + // The registration borrows mutably, so we have to drop here + drop(global_state_ty); + self.register_global_state_str::<::RxNonRef>(&server_str)? + } else { + // We bailed on `None` earlier, and `.get_active_or_frozen_global_state()` + // would've caught `Loaded` + unreachable!() + } + }; + + // Now use the context we have to convert that to a reference struct + let ref_rx_state = rx_state.to_ref_struct(cx); + + Ok(Some(ref_rx_state)) + } } -/// Gets the `RenderCtx` efficiently. -#[macro_export] -macro_rules! get_render_ctx { - ($cx:expr) => { - ::perseus::template::RenderCtx::from_ctx($cx) +/// Gets the global state injected by the server, if there was any. If there are +/// errors in this, we can return `None` and not worry about it, they'll be +/// handled by the initial state. +#[cfg(target_arch = "wasm32")] +fn get_global_state() -> Option { + let val_opt = web_sys::window().unwrap().get("__PERSEUS_GLOBAL_STATE"); + let js_obj = match val_opt { + Some(js_obj) => js_obj, + None => return None, }; + // The object should only actually contain the string value that was injected + let state_str = match js_obj.as_string() { + Some(state_str) => state_str, + None => return None, + }; + // On the server-side, we encode a `None` value directly (otherwise it will be + // some convoluted stringified JSON) + match state_str.as_str() { + "None" => None, + state_str => Some(state_str.to_string()), + } }