fix/change: proper concurrent data loading for routes

This commit is contained in:
Greg Johnston
2025-07-17 09:27:18 -04:00
parent 0fa8155adc
commit 12f5676bd1
5 changed files with 158 additions and 69 deletions

View File

@@ -141,15 +141,14 @@ where
}
let mut view = Box::pin(owner.with(|| {
ScopedFuture::new({
let url = url.clone();
let matched = matched.clone();
async move {
provide_context(params_memo);
provide_context(url);
provide_context(Matched(ArcMemo::from(matched)));
OwnedView::new(view.choose().await)
}
provide_context(params_memo);
provide_context(url.clone());
provide_context(Matched(ArcMemo::from(matched.clone())));
let data = view.data();
ScopedFuture::new(async move {
OwnedView::new(view.choose(data).await)
})
}));
@@ -292,24 +291,26 @@ where
.map(|nav| nav.is_back().get_untracked())
.unwrap_or(false);
Executor::spawn_local(owner.with(|| {
provide_context(url);
provide_context(params_memo);
provide_context(Matched(ArcMemo::from(new_matched)));
ScopedFuture::new({
let state = Rc::clone(state);
async move {
provide_context(url);
provide_context(params_memo);
provide_context(Matched(ArcMemo::from(
new_matched,
)));
let view = OwnedView::new(
if let Some(set_is_routing) = set_is_routing {
set_is_routing.set(true);
let value =
AsyncTransition::run(|| view.choose())
.await;
let value = AsyncTransition::run(|| {
let data = view.data();
view.choose(data)
})
.await;
set_is_routing.set(false);
value
} else {
view.choose().await
let data = view.data();
view.choose(data).await
},
);
@@ -472,6 +473,14 @@ impl RenderHtml for MatchedRoute {
self.1.hydrate::<FROM_SERVER>(cursor, position)
}
async fn hydrate_async(
self,
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
self.1.hydrate_async(cursor, position).await
}
fn into_owned(self) -> Self::Owned {
self
}
@@ -513,12 +522,15 @@ where
let (view, _) = new_match.into_view_and_child();
let view = owner
.with(|| {
ScopedFuture::new(async move {
provide_context(url);
provide_context(params_memo);
provide_context(Matched(ArcMemo::from(matched)));
view.choose().await
})
provide_context(url);
provide_context(params_memo);
provide_context(Matched(ArcMemo::from(matched)));
let data = view.data();
ScopedFuture::new(
async move { view.choose(data).await },
)
})
.now_or_never()
.expect("async route used in SSR");
@@ -696,15 +708,14 @@ where
}
let mut view = Box::pin(owner.with(|| {
ScopedFuture::new({
let url = url.clone();
let matched = matched.clone();
async move {
provide_context(params_memo);
provide_context(url);
provide_context(Matched(ArcMemo::from(matched)));
OwnedView::new(view.choose().await)
}
provide_context(params_memo);
provide_context(url.clone());
provide_context(Matched(ArcMemo::from(matched.clone())));
let data = view.data();
ScopedFuture::new(async move {
OwnedView::new(view.choose(data).await)
})
}));
@@ -795,15 +806,14 @@ where
}
let view = Box::pin(owner.with(|| {
ScopedFuture::new({
let url = url.clone();
let matched = matched.clone();
async move {
provide_context(params_memo);
provide_context(url);
provide_context(Matched(ArcMemo::from(matched)));
OwnedView::new(view.choose().await)
}
provide_context(params_memo);
provide_context(url.clone());
provide_context(Matched(ArcMemo::from(matched.clone())));
let data = view.data();
ScopedFuture::new(async move {
OwnedView::new(view.choose(data).await)
})
}));

View File

@@ -7,7 +7,8 @@ use tachys::{erased::Erased, view::any_view::AnyView};
pub struct AnyChooseView {
value: Erased,
clone: fn(&Erased) -> AnyChooseView,
choose: fn(Erased) -> Pin<Box<dyn Future<Output = AnyView>>>,
choose: fn(Erased, Erased) -> Pin<Box<dyn Future<Output = AnyView>>>,
data: fn(&Erased) -> Erased,
preload: for<'a> fn(&'a Erased) -> Pin<Box<dyn Future<Output = ()> + 'a>>,
}
@@ -25,8 +26,12 @@ impl AnyChooseView {
fn choose<T: ChooseView>(
value: Erased,
data: Erased,
) -> Pin<Box<dyn Future<Output = AnyView>>> {
value.into_inner::<T>().choose().boxed_local()
value
.into_inner::<T>()
.choose(data.into_inner::<T::Data>())
.boxed_local()
}
fn preload<'a, T: ChooseView>(
@@ -35,21 +40,32 @@ impl AnyChooseView {
value.get_ref::<T>().preload().boxed_local()
}
fn data<'a, T: ChooseView>(value: &'a Erased) -> Erased {
Erased::new(value.get_ref::<T>().data())
}
Self {
value: Erased::new(value),
clone: clone::<T>,
choose: choose::<T>,
data: data::<T>,
preload: preload::<T>,
}
}
}
impl ChooseView for AnyChooseView {
async fn choose(self) -> AnyView {
(self.choose)(self.value).await
type Data = Erased;
async fn choose(self, data: Self::Data) -> AnyView {
(self.choose)(self.value, data).await
}
async fn preload(&self) {
(self.preload)(&self.value).await;
}
fn data(&self) -> Self::Data {
(self.data)(&self.value)
}
}

View File

@@ -6,9 +6,13 @@ pub trait ChooseView
where
Self: Send + Clone + 'static,
{
fn choose(self) -> impl Future<Output = AnyView>;
type Data: Send + 'static;
fn choose(self, data: Self::Data) -> impl Future<Output = AnyView>;
fn preload(&self) -> impl Future<Output = ()>;
fn data(&self) -> Self::Data;
}
impl<F, View> ChooseView for F
@@ -16,30 +20,40 @@ where
F: Fn() -> View + Send + Clone + 'static,
View: IntoAny,
{
async fn choose(self) -> AnyView {
type Data = ();
async fn choose(self, _data: ()) -> AnyView {
self().into_any()
}
async fn preload(&self) {}
fn data(&self) -> Self::Data {}
}
impl<T> ChooseView for Lazy<T>
where
T: LazyRoute,
{
async fn choose(self) -> AnyView {
T::data().view().await.into_any()
type Data = T;
async fn choose(self, data: T) -> AnyView {
T::view(data).await
}
async fn preload(&self) {
T::preload().await;
}
fn data(&self) -> Self::Data {
T::data()
}
}
pub trait LazyRoute: Send + 'static {
fn data() -> Self;
fn view(self) -> impl Future<Output = AnyView>;
fn view(this: Self) -> impl Future<Output = AnyView>;
fn preload() -> impl Future<Output = ()> {
async {}
@@ -72,11 +86,15 @@ impl<T> Default for Lazy<T> {
}
impl ChooseView for () {
async fn choose(self) -> AnyView {
type Data = ();
async fn choose(self, _data: ()) -> AnyView {
().into_any()
}
async fn preload(&self) {}
fn data(&self) -> Self::Data {}
}
impl<A, B> ChooseView for Either<A, B>
@@ -84,10 +102,15 @@ where
A: ChooseView,
B: ChooseView,
{
async fn choose(self) -> AnyView {
match self {
Either::Left(f) => f.choose().await.into_any(),
Either::Right(f) => f.choose().await.into_any(),
type Data = Either<A::Data, B::Data>;
async fn choose(self, data: Self::Data) -> AnyView {
match (self, data) {
(Either::Left(f), Either::Left(d)) => f.choose(d).await.into_any(),
(Either::Right(f), Either::Right(d)) => {
f.choose(d).await.into_any()
}
_ => unreachable!(),
}
}
@@ -97,6 +120,13 @@ where
Either::Right(f) => f.preload().await,
}
}
fn data(&self) -> Self::Data {
match self {
Either::Left(f) => Either::Left(f.data()),
Either::Right(f) => Either::Right(f.data()),
}
}
}
macro_rules! tuples {
@@ -105,9 +135,14 @@ macro_rules! tuples {
where
$($ty: ChooseView,)*
{
async fn choose(self ) -> AnyView {
match self {
$($either::$ty(f) => f.choose().await.into_any(),)*
type Data = $either<$($ty::Data),*>;
async fn choose(self, data: Self::Data) -> AnyView {
match (self, data) {
$(
($either::$ty(f), $either::$ty(d)) => f.choose(d).await.into_any(),
)*
_ => unreachable!()
}
}
@@ -116,6 +151,12 @@ macro_rules! tuples {
$($either::$ty(f) => f.preload().await,)*
}
}
fn data(&self) -> Self::Data {
match self {
$($either::$ty(f) => $either::$ty(f.data()),)*
}
}
}
};
}

View File

@@ -689,6 +689,10 @@ where
let url = url.clone();
let matched = matched.clone();
async move {
provide_context(params.clone());
provide_context(url.clone());
provide_context(matched.clone());
let mut data = Some(view.data());
view.preload().await;
let child = outlet.child.clone();
*view_fn.lock().or_poisoned() =
@@ -702,7 +706,9 @@ where
let matched = matched.clone();
owner_where_used.with({
let matched = matched.clone();
move || {
|| {
let data =
data.take().unwrap_or_else(|| view.data());
let child = child.clone();
Suspend::new(Box::pin(async move {
provide_context(child.clone());
@@ -710,7 +716,7 @@ where
provide_context(url.clone());
provide_context(matched.clone());
let view = SendWrapper::new(
ScopedFuture::new(view.choose()),
ScopedFuture::new(view.choose(data)),
);
let view = view.await;
let view = MatchedRoute(
@@ -832,8 +838,6 @@ where
})
};
// assign a new owner, so that contexts and signals owned by the previous route
// in this outlet can be dropped
let (full_tx, full_rx) = oneshot::channel();
let full_tx = Mutex::new(Some(full_tx));
full_loaders.push(full_rx);
@@ -850,7 +854,6 @@ where
let route_owner = Arc::clone(&current.owner);
let child = outlet.child.clone();
async move {
view.preload().await;
let child = child.clone();
*view_fn.lock().or_poisoned() =
Box::new(move |owner_where_used| {
@@ -876,11 +879,18 @@ where
ScopedFuture::new(async move {
if set_is_routing {
AsyncTransition::run(
|| view.choose(),
|| {
let data =
view.data();
view.choose(
data,
)
},
)
.await
} else {
view.choose().await
let data = view.data();
view.choose(data).await
}
})
}),

View File

@@ -8,7 +8,9 @@ use proc_macro::{TokenStream, TokenTree};
use proc_macro2::Span;
use proc_macro_error2::{abort, proc_macro_error};
use quote::{quote, ToTokens};
use syn::{spanned::Spanned, Ident, ImplItem, ItemImpl, Path, Type, TypePath};
use syn::{
spanned::Spanned, FnArg, Ident, ImplItem, ItemImpl, Path, Type, TypePath,
};
const RFC3986_UNRESERVED: [char; 4] = ['-', '.', '_', '~'];
const RFC3986_PCHAR_OTHER: [char; 1] = ['@'];
@@ -268,12 +270,22 @@ fn lazy_route_impl(
match item {
None => abort!(im.span(), "must contain a fn called `view`"),
Some(fun) => {
let first_arg = fun.sig.inputs.first().unwrap_or_else(|| {
abort!(fun.sig.span(), "must have an argument")
});
let FnArg::Typed(first_arg) = first_arg else {
abort!(
first_arg.span(),
"this must be a typed argument like `this: Self`"
)
};
let first_arg_pat = &*first_arg.pat;
let body = std::mem::replace(
&mut fun.block,
syn::parse(
quote! {
{
#lazy_view_ident(self).await
#lazy_view_ident(#first_arg_pat).await
}
}
.into(),
@@ -283,7 +295,7 @@ fn lazy_route_impl(
return quote! {
#[::leptos::lazy]
async fn #lazy_view_ident(this: #self_ty) -> ::leptos::prelude::AnyView {
async fn #lazy_view_ident(#first_arg_pat: #self_ty) -> ::leptos::prelude::AnyView {
#body
}