mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
fix: nested Suspense/Transition with cascading resources (#1214)
This commit is contained in:
parent
1d7235d4ca
commit
9da4084561
6 changed files with 145 additions and 64 deletions
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(|| {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
|
|
Loading…
Reference in a new issue