Skip to content

Commit 6af8191

Browse files
authored
refactor: use child scopes for pages (#230)
* refactor: made route setting imperative and set up child scopes So far, no scope disposer handling yet. * feat: created full page disposer system This isn't actually running yet though, since that leads to lifetime errors I haven't resolved yet. * feat: added `RouteManager` This enforces *some* lifetime constraints, but not all those we need just yet... * feat: added lifetime bounds for full functionality Sure, it's a fix, but it feels like a feat!
1 parent c232aed commit 6af8191

File tree

13 files changed

+270
-80
lines changed

13 files changed

+270
-80
lines changed

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ members = [
33
"packages/*",
44
"examples/core/*",
55
"examples/demos/*",
6-
"examples/comprehensive/*",
6+
# "examples/comprehensive/*",
77
"examples/website/*",
88
"website",
99
# We have the CLI subcrates as workspace members so we can actively develop on them

examples/core/plugins/src/plugin.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@ pub fn get_test_plugin<G: perseus::Html>() -> Plugin<G, TestPluginData> {
2424
if let Some(plugin_data) = plugin_data.downcast_ref::<TestPluginData>() {
2525
let about_page_greeting = plugin_data.about_page_greeting.to_string();
2626
vec![Template::new("about").template(
27-
move |cx, _| sycamore::view! { cx, p { (about_page_greeting) } },
27+
// This is the kind of weird thing we have to do if we don't use the
28+
// macros
29+
move |cx, route_manager, _| {
30+
route_manager.update_view(
31+
sycamore::view! { cx, p { (about_page_greeting) } },
32+
)
33+
},
2834
)]
2935
} else {
3036
unreachable!()

examples/core/router_state/src/templates/index.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ pub fn router_state_page<G: Html>(cx: Scope) -> View<G> {
1111
RouterLoadState::Loaded {
1212
template_name,
1313
path,
14-
} => format!("Loaded {} (template: {}).", path, template_name),
14+
} => {
15+
perseus::web_log!("Loaded.");
16+
format!("Loaded {} (template: {}).", path, template_name)
17+
}
1518
RouterLoadState::Loading {
1619
template_name,
1720
path,

packages/perseus-macro/src/template.rs

+32-7
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,11 @@ pub fn template_impl(input: TemplateFn, is_reactive: bool) -> TokenStream {
154154
};
155155
let name_string = name.to_string();
156156
quote! {
157-
#vis fn #name<G: ::sycamore::prelude::Html>(cx: ::sycamore::prelude::Scope, props: ::perseus::template::PageProps) -> ::sycamore::prelude::View<G> {
157+
#vis fn #name<'__perseus_cx, G: ::sycamore::prelude::Html>(
158+
cx: ::sycamore::prelude::Scope<'__perseus_cx>,
159+
mut route_manager: ::perseus::router::RouteManager<'__perseus_cx, G>,
160+
props: ::perseus::template::PageProps
161+
) {
158162
use ::perseus::state::{MakeRx, MakeRxRef, RxRef};
159163

160164
// The user's function, with Sycamore component annotations and the like preserved
@@ -185,10 +189,14 @@ pub fn template_impl(input: TemplateFn, is_reactive: bool) -> TokenStream {
185189
}
186190
};
187191

188-
#component_name(cx, props.to_ref_struct(cx))
192+
let disposer = ::sycamore::reactive::create_child_scope(cx, |child_cx| {
193+
let view = #component_name(child_cx, props.to_ref_struct(cx));
194+
route_manager.update_view(view);
195+
});
196+
route_manager.update_disposer(disposer);
189197
}
190198
}
191-
} else if fn_args.len() == 2 && is_reactive == false {
199+
} else if fn_args.len() == 2 && !is_reactive {
192200
// This template takes state that isn't reactive (but it must implement
193201
// `UnreactiveState`) Get the argument for the reactive scope
194202
let cx_arg = &fn_args[0];
@@ -205,7 +213,11 @@ pub fn template_impl(input: TemplateFn, is_reactive: bool) -> TokenStream {
205213
};
206214
let name_string = name.to_string();
207215
quote! {
208-
#vis fn #name<G: ::sycamore::prelude::Html>(cx: ::sycamore::prelude::Scope, props: ::perseus::template::PageProps) -> ::sycamore::prelude::View<G> {
216+
#vis fn #name<'__perseus_cx, G: ::sycamore::prelude::Html>(
217+
cx: ::sycamore::prelude::Scope<'__perseus_cx>,
218+
mut route_manager: ::perseus::router::RouteManager<'__perseus_cx, G>,
219+
props: ::perseus::template::PageProps
220+
) {
209221
use ::perseus::state::{MakeRx, MakeRxRef, RxRef, MakeUnrx};
210222

211223
// The user's function, with Sycamore component annotations and the like preserved
@@ -239,15 +251,24 @@ pub fn template_impl(input: TemplateFn, is_reactive: bool) -> TokenStream {
239251
};
240252

241253
// The `.make_unrx()` function will just convert back to the user's type
242-
#component_name(cx, props.make_unrx())
254+
let disposer = ::sycamore::reactive::create_child_scope(cx, |child_cx| {
255+
let view = #component_name(child_cx, props.make_unrx());
256+
route_manager.update_view(view);
257+
});
258+
route_manager.update_disposer(disposer);
243259
}
244260
}
245261
} else if fn_args.len() == 1 {
246262
// Get the argument for the reactive scope
247263
let cx_arg = &fn_args[0];
248264
// There are no arguments except for the scope
249265
quote! {
250-
#vis fn #name<G: ::sycamore::prelude::Html>(cx: ::sycamore::prelude::Scope, props: ::perseus::template::PageProps) -> ::sycamore::prelude::View<G> {
266+
// BUG Need to enforce that `cx` and `route_manager` have the same lifetime...
267+
#vis fn #name<'__perseus_cx, G: ::sycamore::prelude::Html>(
268+
cx: ::sycamore::prelude::Scope<'__perseus_cx>,
269+
mut route_manager: ::perseus::router::RouteManager<'__perseus_cx, G>,
270+
props: ::perseus::template::PageProps
271+
) {
251272
use ::perseus::state::{MakeRx, MakeRxRef};
252273

253274
// The user's function, with Sycamore component annotations and the like preserved
@@ -262,7 +283,11 @@ pub fn template_impl(input: TemplateFn, is_reactive: bool) -> TokenStream {
262283
let render_ctx = ::perseus::RenderCtx::from_ctx(cx);
263284
render_ctx.register_page_no_state(&props.path);
264285

265-
#component_name(cx)
286+
let disposer = ::sycamore::reactive::create_child_scope(cx, |child_cx| {
287+
let view = #component_name(child_cx);
288+
route_manager.update_view(view);
289+
});
290+
route_manager.update_disposer(disposer);
266291
}
267292
}
268293
} else {

packages/perseus/src/client.rs

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ pub fn run_client<M: MutableStore, T: TranslationsManager>(
7070

7171
// This top-level context is what we use for everything, allowing page state to
7272
// be registered and stored for the lifetime of the app
73+
// Note: root lifetime creation occurs here
7374
#[cfg(feature = "hydrate")]
7475
sycamore::hydrate_to(move |cx| perseus_router(cx, router_props), &root);
7576
#[cfg(not(feature = "hydrate"))]

packages/perseus/src/lib.rs

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ documentation, and this should mostly be used as a secondary reference source. Y
2020

2121
#![deny(missing_docs)]
2222
// #![deny(missing_debug_implementations)] // TODO Pending sycamore-rs/sycamore#412
23-
#![forbid(unsafe_code)]
2423
#![recursion_limit = "256"] // TODO Do we need this anymore?
2524

2625
/// Utilities for working with the engine-side, particularly with regards to

packages/perseus/src/router/get_initial_view.rs

+24-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::error_pages::ErrorPageData;
22
use crate::errors::*;
33
use crate::i18n::detect_locale;
4-
use crate::router::match_route;
4+
use crate::router::{match_route, RouteManager};
55
use crate::router::{RouteInfo, RouteVerdict, RouterLoadState};
66
use crate::template::{PageProps, RenderCtx, TemplateNodeType};
77
use crate::utils::checkpoint;
@@ -23,9 +23,10 @@ use web_sys::Element;
2323
/// Note that this will automatically update the router state just before it
2424
/// returns, meaning that any errors that may occur after this function has been
2525
/// called need to reset the router state to be an error.
26-
pub(crate) fn get_initial_view(
27-
cx: Scope,
26+
pub(crate) fn get_initial_view<'a>(
27+
cx: Scope<'a>,
2828
path: String, // The full path, not the template path (but parsed a little)
29+
route_manager: &'a RouteManager<'a, TemplateNodeType>,
2930
) -> InitialView {
3031
let render_ctx = RenderCtx::from_ctx(cx);
3132
let router_state = &render_ctx.router;
@@ -54,7 +55,7 @@ pub(crate) fn get_initial_view(
5455
// Since we're not requesting anything from the server, we don't need to worry about
5556
// whether it's an incremental match or not
5657
was_incremental_match: _,
57-
}) => InitialView::View({
58+
}) => {
5859
let path_with_locale = match locale.as_str() {
5960
"xx-XX" => path.clone(),
6061
locale => format!("{}/{}", locale, &path),
@@ -93,7 +94,7 @@ pub(crate) fn get_initial_view(
9394
router_state.set_load_state(RouterLoadState::ErrorLoaded {
9495
path: path_with_locale.clone(),
9596
});
96-
return InitialView::View(error_pages.get_view_and_render_head(
97+
return InitialView::Error(error_pages.get_view_and_render_head(
9798
cx,
9899
"*",
99100
500,
@@ -110,7 +111,7 @@ pub(crate) fn get_initial_view(
110111
router_state.set_load_state(RouterLoadState::ErrorLoaded {
111112
path: path_with_locale.clone(),
112113
});
113-
return InitialView::View(match &err {
114+
return InitialView::Error(match &err {
114115
// These errors happen because we couldn't get a translator, so they
115116
// certainly don't get one
116117
ClientError::FetchError(FetchError::NotOk {
@@ -171,8 +172,11 @@ pub(crate) fn get_initial_view(
171172
template_name: path,
172173
path: path_with_locale,
173174
});
174-
// Return the actual template, for rendering/hydration
175-
template.render_for_template_client(page_props, cx, translator)
175+
// Render the actual template to the root (done imperatively due to child
176+
// scopes)
177+
template.render_for_template_client(page_props, cx, route_manager, translator);
178+
179+
InitialView::Success
176180
}
177181
// We have an error that the server sent down, so we should just return that error
178182
// view
@@ -182,7 +186,7 @@ pub(crate) fn get_initial_view(
182186
path: path_with_locale.clone(),
183187
});
184188
// We don't need to replace the head, because the server's handled that for us
185-
error_pages.get_view(cx, &url, status, &err, None)
189+
InitialView::Error(error_pages.get_view(cx, &url, status, &err, None))
186190
}
187191
// The entire purpose of this function is to work with the initial state, so if this
188192
// is true, then we have a problem
@@ -194,17 +198,17 @@ pub(crate) fn get_initial_view(
194198
router_state.set_load_state(RouterLoadState::ErrorLoaded {
195199
path: path_with_locale.clone(),
196200
});
197-
error_pages.get_view_and_render_head(cx, "*", 400, "expected initial state render, found subsequent load (highly likely to be a core perseus bug)", None)
201+
InitialView::Error(error_pages.get_view_and_render_head(cx, "*", 400, "expected initial state render, found subsequent load (highly likely to be a core perseus bug)", None))
198202
}
199203
}
200-
}),
204+
}
201205
// If the user is using i18n, then they'll want to detect the locale on any paths
202206
// missing a locale Those all go to the same system that redirects to the
203207
// appropriate locale Note that `container` doesn't exist for this scenario
204208
RouteVerdict::LocaleDetection(path) => {
205209
InitialView::Redirect(detect_locale(path.clone(), &locales))
206210
}
207-
RouteVerdict::NotFound => InitialView::View({
211+
RouteVerdict::NotFound => InitialView::Error({
208212
checkpoint("not_found");
209213
if let InitialState::Error(ErrorPageData { url, status, err }) = get_initial_state() {
210214
router_state.set_load_state(RouterLoadState::ErrorLoaded { path: url.clone() });
@@ -232,8 +236,14 @@ pub(crate) fn get_initial_view(
232236
/// A representation of the possible outcomes of getting the view for the
233237
/// initial load.
234238
pub(crate) enum InitialView {
235-
/// A view is available to be rendered/hydrated.
236-
View(View<TemplateNodeType>),
239+
/// The page has been successfully rendered and sent through the given
240+
/// signal.
241+
///
242+
/// Due to the use of child scopes, we can't just return a actual view,
243+
/// this has to be done imperatively.
244+
Success,
245+
/// An error view has been produced, which should replace the current view.
246+
Error(View<TemplateNodeType>),
237247
/// We need to redirect somewhere else, and the path to redirect to is
238248
/// attached.
239249
///

packages/perseus/src/router/get_subsequent_view.rs

+28-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::errors::*;
22
use crate::page_data::PageDataPartial;
3-
use crate::router::{RouteVerdict, RouterLoadState};
3+
use crate::router::{RouteManager, RouteVerdict, RouterLoadState};
44
use crate::state::PssContains;
55
use crate::template::{PageProps, RenderCtx, Template, TemplateNodeType};
66
use crate::utils::checkpoint;
@@ -14,6 +14,8 @@ use sycamore::prelude::*;
1414
pub(crate) struct GetSubsequentViewProps<'a> {
1515
/// The app's reactive scope.
1616
pub cx: Scope<'a>,
17+
/// The app's route manager.
18+
pub route_manager: &'a RouteManager<'a, TemplateNodeType>,
1719
/// The path we're rendering for (not the template path, the full path,
1820
/// though parsed a little).
1921
pub path: String,
@@ -44,13 +46,14 @@ pub(crate) struct GetSubsequentViewProps<'a> {
4446
pub(crate) async fn get_subsequent_view(
4547
GetSubsequentViewProps {
4648
cx,
49+
route_manager,
4750
path,
4851
template,
4952
was_incremental_match,
5053
locale,
5154
route_verdict,
5255
}: GetSubsequentViewProps<'_>,
53-
) -> View<TemplateNodeType> {
56+
) -> SubsequentView {
5457
let render_ctx = RenderCtx::from_ctx(cx);
5558
let router_state = &render_ctx.router;
5659
let translations_manager = &render_ctx.translations_manager;
@@ -187,7 +190,7 @@ pub(crate) async fn get_subsequent_view(
187190
// Any errors will be prepared error pages ready for return
188191
let page_data = match page_data {
189192
Ok(page_data) => page_data,
190-
Err(view) => return view,
193+
Err(view) => return SubsequentView::Error(view),
191194
};
192195

193196
// Interpolate the metadata directly into the document's `<head>`
@@ -208,35 +211,41 @@ pub(crate) async fn get_subsequent_view(
208211
// These errors happen because we couldn't get a translator, so they certainly don't
209212
// get one
210213
ClientError::FetchError(FetchError::NotOk { url, status, .. }) => {
211-
return error_pages.get_view_and_render_head(
214+
return SubsequentView::Error(error_pages.get_view_and_render_head(
212215
cx,
213216
url,
214217
*status,
215218
&fmt_err(&err),
216219
None,
217-
)
220+
))
218221
}
219222
ClientError::FetchError(FetchError::SerFailed { url, .. }) => {
220-
return error_pages.get_view_and_render_head(cx, url, 500, &fmt_err(&err), None)
223+
return SubsequentView::Error(error_pages.get_view_and_render_head(
224+
cx,
225+
url,
226+
500,
227+
&fmt_err(&err),
228+
None,
229+
))
221230
}
222231
ClientError::LocaleNotSupported { locale } => {
223-
return error_pages.get_view_and_render_head(
232+
return SubsequentView::Error(error_pages.get_view_and_render_head(
224233
cx,
225234
&format!("/{}/...", locale),
226235
404,
227236
&fmt_err(&err),
228237
None,
229-
)
238+
))
230239
}
231240
// No other errors should be returned, but we'll give any a 400
232241
_ => {
233-
return error_pages.get_view_and_render_head(
242+
return SubsequentView::Error(error_pages.get_view_and_render_head(
234243
cx,
235244
&format!("/{}/...", locale),
236245
400,
237246
&fmt_err(&err),
238247
None,
239-
)
248+
))
240249
}
241250
}
242251
}
@@ -255,5 +264,13 @@ pub(crate) async fn get_subsequent_view(
255264
path: path_with_locale,
256265
});
257266
// Now return the view that should be rendered
258-
template.render_for_template_client(page_props, cx, translator)
267+
template.render_for_template_client(page_props, cx, route_manager, translator);
268+
SubsequentView::Success
269+
}
270+
271+
pub(crate) enum SubsequentView {
272+
/// The page view *has been* rendered *and* displayed.
273+
Success,
274+
/// An error view was rendered, and shoudl now be displayed.
275+
Error(View<TemplateNodeType>),
259276
}

packages/perseus/src/router/mod.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ mod get_initial_view;
55
#[cfg(target_arch = "wasm32")]
66
mod get_subsequent_view;
77
mod match_route;
8+
mod page_disposer;
9+
mod route_manager;
810
mod route_verdict;
911
#[cfg(target_arch = "wasm32")]
1012
mod router_component;
@@ -23,4 +25,7 @@ pub use router_state::{RouterLoadState, RouterState};
2325
#[cfg(target_arch = "wasm32")]
2426
pub(crate) use get_initial_view::{get_initial_view, InitialView};
2527
#[cfg(target_arch = "wasm32")]
26-
pub(crate) use get_subsequent_view::{get_subsequent_view, GetSubsequentViewProps};
28+
pub(crate) use get_subsequent_view::{get_subsequent_view, GetSubsequentViewProps, SubsequentView};
29+
30+
pub use page_disposer::PageDisposer;
31+
pub use route_manager::RouteManager;

0 commit comments

Comments
 (0)