fix: nested Suspense/Transition with cascading resources (#1214)

This commit is contained in:
Greg Johnston 2023-06-21 16:39:58 -04:00 committed by GitHub
parent 1d7235d4ca
commit 9da4084561
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 145 additions and 64 deletions

View file

@ -17,7 +17,7 @@ leptos_actix = { path = "../../../../integrations/actix", optional = true }
leptos_router = { path = "../../../../router", default-features = false }
log = "0.4"
simple_logger = "4"
wasm-bindgen = "0.2.85"
wasm-bindgen = "0.2.87"
serde = "1.0.159"
tokio = { version = "1.27.0", features = ["time"], optional = true }

View file

@ -53,6 +53,7 @@ pub fn App(cx: Scope) -> impl IntoView {
}
>
<Route path="" view=|cx| view! { cx, <Nested/> }/>
<Route path="inside" view=|cx| view! { cx, <NestedResourceInside/> }/>
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
@ -69,6 +70,7 @@ pub fn App(cx: Scope) -> impl IntoView {
}
>
<Route path="" view=|cx| view! { cx, <Nested/> }/>
<Route path="inside" view=|cx| view! { cx, <NestedResourceInside/> }/>
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
@ -85,6 +87,7 @@ pub fn App(cx: Scope) -> impl IntoView {
}
>
<Route path="" view=|cx| view! { cx, <Nested/> }/>
<Route path="inside" view=|cx| view! { cx, <NestedResourceInside/> }/>
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
@ -101,6 +104,7 @@ fn SecondaryNav(cx: Scope) -> impl IntoView {
view! { cx,
<nav>
<A href="" exact=true>"Nested"</A>
<A href="inside" exact=true>"Nested (resource created inside)"</A>
<A href="single">"Single"</A>
<A href="parallel">"Parallel"</A>
<A href="inside-component">"Inside Component"</A>
@ -139,6 +143,43 @@ fn Nested(cx: Scope) -> impl IntoView {
}
}
#[component]
fn NestedResourceInside(cx: Scope) -> impl IntoView {
let one_second = create_resource(cx, || (), one_second_fn);
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<div>
<Suspense fallback=|| "Loading 1...">
"One Second: "
{move || {
one_second.read(cx).map(|_| {
let two_second = create_resource(cx, || (), move |_| async move {
leptos::log!("creating two_second resource");
two_second_fn(()).await
});
view! { cx,
<p>{move || one_second.read(cx).map(|_| "Loaded 1!")}</p>
<Suspense fallback=|| "Loading 2...">
"Two Second: "
{move || {
two_second.read(cx).map(|x| view! { cx,
"Loaded 2 (created inside first suspense)!: "
{format!("{x:?}")}
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
})
}}
</Suspense>
}
})
}}
</Suspense>
</div>
}
}
#[component]
fn Parallel(cx: Scope) -> impl IntoView {
let one_second = create_resource(cx, || (), one_second_fn);

View file

@ -8,7 +8,7 @@ cfg_if! {
use wasm_bindgen::JsCast;
// We can tell if we start in hydration mode by checking to see if the
// id "_0-0-0" is present in the DOM. If it is, we know we are hydrating from
// id "_0-1" is present in the DOM. If it is, we know we are hydrating from
// the server, if not, we are starting off in CSR
thread_local! {
static HYDRATION_COMMENTS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {

View file

@ -221,66 +221,105 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
blocking_fragments
.push(async move { (fragment_id, data.out_of_order.await) });
} else {
fragments
.push(async move { (fragment_id, data.out_of_order.await) });
fragments.push(Box::pin(async move {
(fragment_id.clone(), data.out_of_order.await)
})
as Pin<Box<dyn Future<Output = (String, String)>>>);
}
}
let stream = futures::stream::once(
// HTML for the view function and script to store resources
async move {
let resolvers = format!(
"<script>__LEPTOS_PENDING_RESOURCES = \
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
);
if replace_blocks {
let mut blocks = Vec::with_capacity(blocking_fragments.len());
while let Some((blocked_id, blocked_fragment)) =
blocking_fragments.next().await
{
blocks.push((blocked_id, blocked_fragment));
}
let prefix = prefix(cx);
let mut shell = shell;
for (blocked_id, blocked_fragment) in blocks {
let open = format!("<!--suspense-open-{blocked_id}-->");
let close = format!("<!--suspense-close-{blocked_id}-->");
let (first, rest) =
shell.split_once(&open).unwrap_or_default();
let (_fallback, rest) =
rest.split_once(&close).unwrap_or_default();
shell = format!("{first}{blocked_fragment}{rest}").into();
}
format!("{prefix}{shell}{resolvers}")
} else {
let mut blocking = String::new();
let mut blocking_fragments =
fragments_to_chunks(blocking_fragments);
while let Some(fragment) = blocking_fragments.next().await {
blocking.push_str(&fragment);
}
let prefix = prefix(cx);
format!("{prefix}{shell}{resolvers}{blocking}")
}
},
)
.chain(ooo_body_stream_recurse(cx, fragments, serializers));
(stream, runtime, scope)
}
fn ooo_body_stream_recurse(
cx: Scope,
fragments: FuturesUnordered<PinnedFuture<(String, String)>>,
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
) -> Pin<Box<dyn Stream<Item = String>>> {
// resources and fragments
// stream HTML for each <Suspense/> as it resolves
let fragments = fragments_to_chunks(fragments);
// stream data for each Resource as it resolves
let resources = render_serializers(serializers);
// HTML for the view function and script to store resources
let stream = futures::stream::once(async move {
let resolvers = format!(
"<script>__LEPTOS_PENDING_RESOURCES = \
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
);
if replace_blocks {
let mut blocks = Vec::with_capacity(blocking_fragments.len());
while let Some((blocked_id, blocked_fragment)) =
blocking_fragments.next().await
{
blocks.push((blocked_id, blocked_fragment));
}
let prefix = prefix(cx);
let mut shell = shell;
for (blocked_id, blocked_fragment) in blocks {
let open = format!("<!--suspense-open-{blocked_id}-->");
let close = format!("<!--suspense-close-{blocked_id}-->");
let (first, rest) = shell.split_once(&open).unwrap_or_default();
let (_fallback, rest) =
rest.split_once(&close).unwrap_or_default();
shell = format!("{first}{blocked_fragment}{rest}").into();
}
format!("{prefix}{shell}{resolvers}")
} else {
let mut blocking = String::new();
let mut blocking_fragments =
fragments_to_chunks(blocking_fragments);
while let Some(fragment) = blocking_fragments.next().await {
blocking.push_str(&fragment);
}
let prefix = prefix(cx);
format!("{prefix}{shell}{resolvers}{blocking}")
}
})
// TODO these should be combined again in a way that chains them appropriately
// such that individual resources can resolve before all fragments are done
.chain(fragments)
.chain(resources);
(stream, runtime, scope)
Box::pin(
// TODO these should be combined again in a way that chains them appropriately
// such that individual resources can resolve before all fragments are done
fragments.chain(resources).chain(
futures::stream::once(async move {
let pending = cx.pending_fragments();
if pending.len() > 0 {
let fragments = FuturesUnordered::new();
let serializers = cx.serialization_resolvers();
for (fragment_id, data) in pending {
fragments.push(Box::pin(async move {
(fragment_id.clone(), data.out_of_order.await)
})
as Pin<Box<dyn Future<Output = (String, String)>>>);
}
Box::pin(ooo_body_stream_recurse(
cx,
fragments,
serializers,
))
as Pin<Box<dyn Stream<Item = String>>>
} else {
Box::pin(futures::stream::once(async move {
Default::default()
}))
}
})
.flatten(),
),
)
}
#[cfg_attr(

View file

@ -94,13 +94,7 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
let runtime = create_runtime();
let (
(
blocking_fragments_ready,
chunks,
prefix,
pending_resources,
serializers,
),
(blocking_fragments_ready, chunks, prefix, pending_resources),
scope_id,
_,
) = run_scope_undisposed(runtime, |cx| {
@ -115,7 +109,6 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
view.into_stream_chunks(cx),
prefix,
serde_json::to_string(&cx.pending_resources()).unwrap(),
cx.serialization_resolvers(),
)
});
let cx = Scope {
@ -130,7 +123,7 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
let remaining_chunks = handle_blocking_chunks(tx.clone(), chunks).await;
let prefix = prefix(cx);
prefix_tx.send(prefix).expect("to send prefix");
handle_chunks(tx, remaining_chunks).await;
handle_chunks(cx, tx, remaining_chunks).await;
});
let stream = futures::stream::once(async move {
@ -147,7 +140,13 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
)
})
.chain(rx)
.chain(render_serializers(serializers));
.chain(
futures::stream::once(async move {
let serializers = cx.serialization_resolvers();
render_serializers(serializers)
})
.flatten(),
);
(stream, runtime, scope_id)
}
@ -196,6 +195,7 @@ async fn handle_blocking_chunks(
#[tracing::instrument(level = "trace", skip_all)]
#[async_recursion(?Send)]
async fn handle_chunks(
cx: Scope,
tx: UnboundedSender<String>,
chunks: VecDeque<StreamChunk>,
) {
@ -210,7 +210,7 @@ async fn handle_chunks(
// send the inner stream
let suspended = chunks.await;
handle_chunks(tx.clone(), suspended).await;
handle_chunks(cx, tx.clone(), suspended).await;
}
}
}

View file

@ -96,6 +96,7 @@ mod trigger;
pub use context::*;
pub use diagnostics::SpecialNonReactiveZone;
pub use effect::*;
pub use hydration::FragmentData;
pub use memo::*;
pub use resource::*;
use runtime::*;