mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: local resources with .await
This commit is contained in:
parent
d5aecbe705
commit
dc21e4ff53
12 changed files with 723 additions and 130 deletions
|
@ -51,51 +51,63 @@ pub fn App() -> impl IntoView {
|
||||||
// out-of-order
|
// out-of-order
|
||||||
<ParentRoute
|
<ParentRoute
|
||||||
path=StaticSegment("out-of-order")
|
path=StaticSegment("out-of-order")
|
||||||
view=|| view! {
|
view=|| {
|
||||||
<SecondaryNav/>
|
view! {
|
||||||
<h1>"Out-of-Order"</h1>
|
<SecondaryNav/>
|
||||||
<Outlet/>
|
<h1>"Out-of-Order"</h1>
|
||||||
|
<Outlet/>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
<Route path=StaticSegment("") view=Nested/>
|
<Route path=StaticSegment("") view=Nested/>
|
||||||
<Route path=StaticSegment("inside") view=NestedResourceInside/>
|
<Route path=StaticSegment("inside") view=NestedResourceInside/>
|
||||||
<Route path=StaticSegment("single") view=Single/>
|
<Route path=StaticSegment("single") view=Single/>
|
||||||
<Route path=StaticSegment("parallel") view=Parallel/>
|
<Route path=StaticSegment("parallel") view=Parallel/>
|
||||||
<Route path=StaticSegment("inside-component") view=InsideComponent/>
|
<Route path=StaticSegment("inside-component") view=InsideComponent/>
|
||||||
|
<Route path=StaticSegment("local") view=LocalResource/>
|
||||||
<Route path=StaticSegment("none") view=None/>
|
<Route path=StaticSegment("none") view=None/>
|
||||||
</ParentRoute>
|
</ParentRoute>
|
||||||
// in-order
|
// in-order
|
||||||
<ParentRoute
|
<ParentRoute
|
||||||
path=StaticSegment("in-order")
|
path=StaticSegment("in-order")
|
||||||
ssr=SsrMode::InOrder
|
ssr=SsrMode::InOrder
|
||||||
view=|| view! {
|
view=|| {
|
||||||
<SecondaryNav/>
|
view! {
|
||||||
<h1>"In-Order"</h1>
|
<SecondaryNav/>
|
||||||
<Outlet/>
|
<h1>"In-Order"</h1>
|
||||||
|
<Outlet/>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
<Route path=StaticSegment("") view=Nested/>
|
<Route path=StaticSegment("") view=Nested/>
|
||||||
<Route path=StaticSegment("inside") view=NestedResourceInside/>
|
<Route path=StaticSegment("inside") view=NestedResourceInside/>
|
||||||
<Route path=StaticSegment("single") view=Single/>
|
<Route path=StaticSegment("single") view=Single/>
|
||||||
<Route path=StaticSegment("parallel") view=Parallel/>
|
<Route path=StaticSegment("parallel") view=Parallel/>
|
||||||
<Route path=StaticSegment("inside-component") view=InsideComponent/>
|
<Route path=StaticSegment("inside-component") view=InsideComponent/>
|
||||||
|
<Route path=StaticSegment("local") view=LocalResource/>
|
||||||
<Route path=StaticSegment("none") view=None/>
|
<Route path=StaticSegment("none") view=None/>
|
||||||
</ParentRoute>
|
</ParentRoute>
|
||||||
// async
|
// async
|
||||||
<ParentRoute
|
<ParentRoute
|
||||||
path=StaticSegment("async")
|
path=StaticSegment("async")
|
||||||
ssr=SsrMode::Async
|
ssr=SsrMode::Async
|
||||||
view=|| view! {
|
view=|| {
|
||||||
<SecondaryNav/>
|
view! {
|
||||||
<h1>"Async"</h1>
|
<SecondaryNav/>
|
||||||
<Outlet/>
|
<h1>"Async"</h1>
|
||||||
|
<Outlet/>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
<Route path=StaticSegment("") view=Nested/>
|
<Route path=StaticSegment("") view=Nested/>
|
||||||
<Route path=StaticSegment("inside") view=NestedResourceInside/>
|
<Route path=StaticSegment("inside") view=NestedResourceInside/>
|
||||||
<Route path=StaticSegment("single") view=Single/>
|
<Route path=StaticSegment("single") view=Single/>
|
||||||
<Route path=StaticSegment("parallel") view=Parallel/>
|
<Route path=StaticSegment("parallel") view=Parallel/>
|
||||||
<Route path=StaticSegment("inside-component") view=InsideComponent/>
|
<Route path=StaticSegment("inside-component") view=InsideComponent/>
|
||||||
|
<Route path=StaticSegment("local") view=LocalResource/>
|
||||||
<Route path=StaticSegment("none") view=None/>
|
<Route path=StaticSegment("none") view=None/>
|
||||||
</ParentRoute>
|
</ParentRoute>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
@ -108,11 +120,16 @@ pub fn App() -> impl IntoView {
|
||||||
fn SecondaryNav() -> impl IntoView {
|
fn SecondaryNav() -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<nav>
|
<nav>
|
||||||
<A href="" exact=true>"Nested"</A>
|
<A href="" exact=true>
|
||||||
<A href="inside" exact=true>"Nested (resource created inside)"</A>
|
"Nested"
|
||||||
|
</A>
|
||||||
|
<A href="inside" exact=true>
|
||||||
|
"Nested (resource created inside)"
|
||||||
|
</A>
|
||||||
<A href="single">"Single"</A>
|
<A href="single">"Single"</A>
|
||||||
<A href="parallel">"Parallel"</A>
|
<A href="parallel">"Parallel"</A>
|
||||||
<A href="inside-component">"Inside Component"</A>
|
<A href="inside-component">"Inside Component"</A>
|
||||||
|
<A href="local">"Local Resource"</A>
|
||||||
<A href="none">"No Resources"</A>
|
<A href="none">"No Resources"</A>
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
|
@ -126,21 +143,28 @@ fn Nested() -> impl IntoView {
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div>
|
<div>
|
||||||
<Suspense fallback=|| "Loading 1...">
|
<Suspense fallback=|| {
|
||||||
|
"Loading 1..."
|
||||||
|
}>
|
||||||
{move || {
|
{move || {
|
||||||
one_second.get().map(|_| view! {
|
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
|
||||||
<p id="loaded-1">"One Second: Loaded 1!"</p>
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
<Suspense fallback=|| "Loading 2...">
|
<Suspense fallback=|| {
|
||||||
|
"Loading 2..."
|
||||||
|
}>
|
||||||
{move || {
|
{move || {
|
||||||
two_second.get().map(|_| view! {
|
two_second
|
||||||
<p id="loaded-2">"Two Second: Loaded 2!"</p>
|
.get()
|
||||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
.map(|_| {
|
||||||
{count}
|
view! {
|
||||||
</button>
|
<p id="loaded-2">"Two Second: Loaded 2!"</p>
|
||||||
})
|
<button on:click=move |_| {
|
||||||
|
set_count.update(|n| *n += 1)
|
||||||
|
}>{count}</button>
|
||||||
|
}
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
@ -154,25 +178,27 @@ fn NestedResourceInside() -> impl IntoView {
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div>
|
<div>
|
||||||
<Suspense fallback=|| "Loading 1...">
|
<Suspense fallback=|| {
|
||||||
|
"Loading 1..."
|
||||||
|
}>
|
||||||
{Suspend(async move {
|
{Suspend(async move {
|
||||||
_ = one_second.await;
|
_ = one_second.await;
|
||||||
let two_second = Resource::new(|| (), move |_| async move {
|
let two_second = Resource::new(
|
||||||
second_wait_fn(WAIT_TWO_SECONDS).await
|
|| (),
|
||||||
});
|
move |_| async move { second_wait_fn(WAIT_TWO_SECONDS).await },
|
||||||
|
);
|
||||||
view! {
|
view! {
|
||||||
<p id="loaded-1">"One Second: Loaded 1!"</p>
|
<p id="loaded-1">"One Second: Loaded 1!"</p>
|
||||||
<Suspense fallback=|| "Loading 2...">
|
<Suspense fallback=|| "Loading 2...">
|
||||||
<span id="loaded-2">
|
<span id="loaded-2">
|
||||||
"Loaded 2 (created inside first suspense)!: "
|
"Loaded 2 (created inside first suspense)!: "
|
||||||
{Suspend(async move { format!("{:?}", two_second.await)})}
|
{Suspend(async move { format!("{:?}", two_second.await) })}
|
||||||
</span>
|
</span>
|
||||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
<button on:click=move |_| set_count.update(|n| *n += 1)>{count}</button>
|
||||||
{count}
|
|
||||||
</button>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -186,25 +212,42 @@ fn Parallel() -> impl IntoView {
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div>
|
<div>
|
||||||
<Suspense fallback=|| "Loading 1...">
|
<Suspense fallback=|| {
|
||||||
|
"Loading 1..."
|
||||||
|
}>
|
||||||
{move || {
|
{move || {
|
||||||
one_second.get().map(move |_| view! {
|
one_second
|
||||||
<p id="loaded-1">"One Second: Loaded 1!"</p>
|
.get()
|
||||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
.map(move |_| {
|
||||||
{count}
|
view! {
|
||||||
</button>
|
<p id="loaded-1">"One Second: Loaded 1!"</p>
|
||||||
})
|
<button on:click=move |_| {
|
||||||
|
set_count.update(|n| *n += 1)
|
||||||
|
}>{count}</button>
|
||||||
|
}
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Suspense fallback=|| "Loading 2...">
|
<Suspense fallback=|| {
|
||||||
|
"Loading 2..."
|
||||||
|
}>
|
||||||
{move || {
|
{move || {
|
||||||
two_second.get().map(move |_| view! {
|
two_second
|
||||||
<p id="loaded-2">"Two Second: Loaded 2!"</p>
|
.get()
|
||||||
<button id="second-count" on:click=move |_| set_count.update(|n| *n += 1)>
|
.map(move |_| {
|
||||||
{count}
|
view! {
|
||||||
</button>
|
<p id="loaded-2">"Two Second: Loaded 2!"</p>
|
||||||
})
|
<button
|
||||||
|
id="second-count"
|
||||||
|
on:click=move |_| set_count.update(|n| *n += 1)
|
||||||
|
>
|
||||||
|
{count}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -217,18 +260,17 @@ fn Single() -> impl IntoView {
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div>
|
<div>
|
||||||
<Suspense fallback=|| "Loading 1...">
|
<Suspense fallback=|| {
|
||||||
{move || {
|
"Loading 1..."
|
||||||
one_second.get().map(|_| view! {
|
}>
|
||||||
<p id="loaded-1">"One Second: Loaded 1!"</p>
|
{move || {
|
||||||
})
|
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
|
||||||
}}
|
}}
|
||||||
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<p id="following-message">"Children following Suspense should hydrate properly."</p>
|
<p id="following-message">"Children following Suspense should hydrate properly."</p>
|
||||||
<div>
|
<div>
|
||||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
<button on:click=move |_| set_count.update(|n| *n += 1)>{count}</button>
|
||||||
{count}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -244,9 +286,7 @@ fn InsideComponent() -> impl IntoView {
|
||||||
<InsideComponentChild/>
|
<InsideComponentChild/>
|
||||||
<p id="following-message">"Children following Suspense should hydrate properly."</p>
|
<p id="following-message">"Children following Suspense should hydrate properly."</p>
|
||||||
<div>
|
<div>
|
||||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
<button on:click=move |_| set_count.update(|n| *n += 1)>{count}</button>
|
||||||
{count}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -256,16 +296,47 @@ fn InsideComponent() -> impl IntoView {
|
||||||
fn InsideComponentChild() -> impl IntoView {
|
fn InsideComponentChild() -> impl IntoView {
|
||||||
let one_second = Resource::new(|| WAIT_ONE_SECOND, first_wait_fn);
|
let one_second = Resource::new(|| WAIT_ONE_SECOND, first_wait_fn);
|
||||||
view! {
|
view! {
|
||||||
<Suspense fallback=|| "Loading 1...">
|
<Suspense fallback=|| {
|
||||||
{move || {
|
"Loading 1..."
|
||||||
one_second.get().map(|_| view! {
|
}>
|
||||||
<p id="loaded-1">"One Second: Loaded 1!"</p>
|
{move || {
|
||||||
})
|
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
|
||||||
}}
|
}}
|
||||||
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn LocalResource() -> impl IntoView {
|
||||||
|
let one_second = Resource::new(|| WAIT_ONE_SECOND, first_wait_fn);
|
||||||
|
let local = LocalResource::new(|| first_wait_fn(WAIT_ONE_SECOND));
|
||||||
|
let (count, set_count) = signal(0);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<Suspense fallback=|| {
|
||||||
|
"Loading 1..."
|
||||||
|
}>
|
||||||
|
{move || {
|
||||||
|
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
|
||||||
|
}}
|
||||||
|
{move || {
|
||||||
|
Suspend(async move {
|
||||||
|
let value = local.await;
|
||||||
|
view! { <p id="loaded-2">"One Second: Local Loaded " {value} "!"</p> }
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
|
||||||
|
</Suspense>
|
||||||
|
<p id="following-message">"Children following Suspense should hydrate properly."</p>
|
||||||
|
<div>
|
||||||
|
<button on:click=move |_| set_count.update(|n| *n += 1)>{count}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn None() -> impl IntoView {
|
fn None() -> impl IntoView {
|
||||||
let (count, set_count) = signal(0);
|
let (count, set_count) = signal(0);
|
||||||
|
@ -274,9 +345,7 @@ fn None() -> impl IntoView {
|
||||||
<div>
|
<div>
|
||||||
<Suspense fallback=|| "Loading 1...">
|
<Suspense fallback=|| "Loading 1...">
|
||||||
<p id="inside-message">"Children inside Suspense should hydrate properly."</p>
|
<p id="inside-message">"Children inside Suspense should hydrate properly."</p>
|
||||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
<button on:click=move |_| set_count.update(|n| *n += 1)>{count}</button>
|
||||||
{count}
|
|
||||||
</button>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<p id="following-message">"Children following Suspense should hydrate properly."</p>
|
<p id="following-message">"Children following Suspense should hydrate properly."</p>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -84,4 +84,12 @@ impl SharedContext for CsrSharedContext {
|
||||||
fn await_deferred(&self) -> Option<PinnedFuture<()>> {
|
fn await_deferred(&self) -> Option<PinnedFuture<()>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn set_incomplete_chunk(&self, _id: SerializedDataId) {}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn get_incomplete_chunk(&self, _id: &SerializedDataId) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@ extern "C" {
|
||||||
static __RESOLVED_RESOURCES: Array;
|
static __RESOLVED_RESOURCES: Array;
|
||||||
|
|
||||||
static __SERIALIZED_ERRORS: Array;
|
static __SERIALIZED_ERRORS: Array;
|
||||||
|
|
||||||
|
static __INCOMPLETE_CHUNKS: Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialized_errors() -> Vec<(SerializedDataId, ErrorId, Error)> {
|
fn serialized_errors() -> Vec<(SerializedDataId, ErrorId, Error)> {
|
||||||
|
@ -38,6 +40,16 @@ fn serialized_errors() -> Vec<(SerializedDataId, ErrorId, Error)> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn incomplete_chunks() -> Vec<SerializedDataId> {
|
||||||
|
__INCOMPLETE_CHUNKS
|
||||||
|
.iter()
|
||||||
|
.map(|value| {
|
||||||
|
let id = value.as_f64().unwrap() as usize;
|
||||||
|
SerializedDataId(id)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// An error that has been serialized across the network boundary.
|
/// An error that has been serialized across the network boundary.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct SerializedError(String);
|
struct SerializedError(String);
|
||||||
|
@ -57,6 +69,7 @@ pub struct HydrateSharedContext {
|
||||||
is_hydrating: AtomicBool,
|
is_hydrating: AtomicBool,
|
||||||
during_hydration: AtomicBool,
|
during_hydration: AtomicBool,
|
||||||
errors: Lazy<Vec<(SerializedDataId, ErrorId, Error)>>,
|
errors: Lazy<Vec<(SerializedDataId, ErrorId, Error)>>,
|
||||||
|
incomplete: Lazy<Vec<SerializedDataId>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HydrateSharedContext {
|
impl HydrateSharedContext {
|
||||||
|
@ -67,6 +80,7 @@ impl HydrateSharedContext {
|
||||||
is_hydrating: AtomicBool::new(true),
|
is_hydrating: AtomicBool::new(true),
|
||||||
during_hydration: AtomicBool::new(true),
|
during_hydration: AtomicBool::new(true),
|
||||||
errors: Lazy::new(serialized_errors),
|
errors: Lazy::new(serialized_errors),
|
||||||
|
incomplete: Lazy::new(incomplete_chunks),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +94,7 @@ impl HydrateSharedContext {
|
||||||
is_hydrating: AtomicBool::new(false),
|
is_hydrating: AtomicBool::new(false),
|
||||||
during_hydration: AtomicBool::new(true),
|
during_hydration: AtomicBool::new(true),
|
||||||
errors: Lazy::new(serialized_errors),
|
errors: Lazy::new(serialized_errors),
|
||||||
|
incomplete: Lazy::new(incomplete_chunks),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,4 +181,11 @@ impl SharedContext for HydrateSharedContext {
|
||||||
fn await_deferred(&self) -> Option<PinnedFuture<()>> {
|
fn await_deferred(&self) -> Option<PinnedFuture<()>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn set_incomplete_chunk(&self, _id: SerializedDataId) {}
|
||||||
|
|
||||||
|
fn get_incomplete_chunk(&self, id: &SerializedDataId) -> bool {
|
||||||
|
self.incomplete.iter().any(|entry| entry == id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,4 +140,11 @@ pub trait SharedContext: Debug {
|
||||||
///
|
///
|
||||||
/// In browser implementations, this should be a no-op.
|
/// In browser implementations, this should be a no-op.
|
||||||
fn await_deferred(&self) -> Option<PinnedFuture<()>>;
|
fn await_deferred(&self) -> Option<PinnedFuture<()>>;
|
||||||
|
|
||||||
|
/// Tells the client that this chunk is being sent from the server before all its data have
|
||||||
|
/// loaded, and it may be in a fallback state.
|
||||||
|
fn set_incomplete_chunk(&self, id: SerializedDataId);
|
||||||
|
|
||||||
|
/// Checks whether this chunk is being sent from the server before all its data have loaded.
|
||||||
|
fn get_incomplete_chunk(&self, id: &SerializedDataId) -> bool;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use super::{SerializedDataId, SharedContext};
|
||||||
use crate::{PinnedFuture, PinnedStream};
|
use crate::{PinnedFuture, PinnedStream};
|
||||||
use futures::{
|
use futures::{
|
||||||
future::join_all,
|
future::join_all,
|
||||||
stream::{self},
|
stream::{self, once},
|
||||||
Stream, StreamExt,
|
Stream, StreamExt,
|
||||||
};
|
};
|
||||||
use or_poisoned::OrPoisoned;
|
use or_poisoned::OrPoisoned;
|
||||||
|
@ -33,6 +33,7 @@ pub struct SsrSharedContext {
|
||||||
errors: ErrorBuf,
|
errors: ErrorBuf,
|
||||||
sealed_error_boundaries: SealedErrors,
|
sealed_error_boundaries: SealedErrors,
|
||||||
deferred: Mutex<Vec<PinnedFuture<()>>>,
|
deferred: Mutex<Vec<PinnedFuture<()>>>,
|
||||||
|
incomplete: Arc<Mutex<Vec<SerializedDataId>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SsrSharedContext {
|
impl SsrSharedContext {
|
||||||
|
@ -178,8 +179,19 @@ impl SharedContext for SsrSharedContext {
|
||||||
sealed_error_boundaries: Arc::clone(&self.sealed_error_boundaries),
|
sealed_error_boundaries: Arc::clone(&self.sealed_error_boundaries),
|
||||||
};
|
};
|
||||||
|
|
||||||
let stream =
|
let incomplete = Arc::clone(&self.incomplete);
|
||||||
stream::once(async move { initial_chunk }).chain(async_data);
|
|
||||||
|
let stream = stream::once(async move { initial_chunk })
|
||||||
|
.chain(async_data)
|
||||||
|
.chain(once(async move {
|
||||||
|
let mut script = String::new();
|
||||||
|
script.push_str("__INCOMPLETE_CHUNKS=[");
|
||||||
|
for chunk in mem::take(&mut *incomplete.lock().or_poisoned()) {
|
||||||
|
_ = write!(script, "{},", chunk.0);
|
||||||
|
}
|
||||||
|
script.push_str("];");
|
||||||
|
script
|
||||||
|
}));
|
||||||
Some(Box::pin(stream))
|
Some(Box::pin(stream))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,6 +215,18 @@ impl SharedContext for SsrSharedContext {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_incomplete_chunk(&self, id: SerializedDataId) {
|
||||||
|
self.incomplete.lock().or_poisoned().push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_incomplete_chunk(&self, id: &SerializedDataId) -> bool {
|
||||||
|
self.incomplete
|
||||||
|
.lock()
|
||||||
|
.or_poisoned()
|
||||||
|
.iter()
|
||||||
|
.any(|entry| entry == id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AsyncDataStream {
|
struct AsyncDataStream {
|
||||||
|
|
|
@ -2,14 +2,18 @@ use crate::{
|
||||||
children::{TypedChildren, ViewFnOnce},
|
children::{TypedChildren, ViewFnOnce},
|
||||||
IntoView,
|
IntoView,
|
||||||
};
|
};
|
||||||
use futures::FutureExt;
|
use futures::{select, FutureExt, StreamExt};
|
||||||
|
use hydration_context::SerializedDataId;
|
||||||
use leptos_macro::component;
|
use leptos_macro::component;
|
||||||
use reactive_graph::{
|
use reactive_graph::{
|
||||||
computed::{suspense::SuspenseContext, ArcMemo, ScopedFuture},
|
computed::{
|
||||||
|
suspense::{LocalResourceNotifier, SuspenseContext},
|
||||||
|
ArcMemo, ScopedFuture,
|
||||||
|
},
|
||||||
effect::RenderEffect,
|
effect::RenderEffect,
|
||||||
owner::{provide_context, use_context, Owner},
|
owner::{provide_context, use_context, Owner},
|
||||||
signal::ArcRwSignal,
|
signal::ArcRwSignal,
|
||||||
traits::{Get, Read, Track, With},
|
traits::{Get, GetUntracked, Read, Track, With},
|
||||||
};
|
};
|
||||||
use slotmap::{DefaultKey, SlotMap};
|
use slotmap::{DefaultKey, SlotMap};
|
||||||
use tachys::{
|
use tachys::{
|
||||||
|
@ -36,15 +40,31 @@ pub fn Suspense<Chil>(
|
||||||
where
|
where
|
||||||
Chil: IntoView + Send + 'static,
|
Chil: IntoView + Send + 'static,
|
||||||
{
|
{
|
||||||
|
let (starts_local, id) = {
|
||||||
|
Owner::current_shared_context()
|
||||||
|
.map(|sc| {
|
||||||
|
let id = sc.next_id();
|
||||||
|
(sc.get_incomplete_chunk(&id), id)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| (false, Default::default()))
|
||||||
|
};
|
||||||
let fallback = fallback.run();
|
let fallback = fallback.run();
|
||||||
let children = children.into_inner()();
|
let children = children.into_inner()();
|
||||||
let tasks = ArcRwSignal::new(SlotMap::<DefaultKey, ()>::new());
|
let tasks = ArcRwSignal::new(SlotMap::<DefaultKey, ()>::new());
|
||||||
provide_context(SuspenseContext {
|
provide_context(SuspenseContext {
|
||||||
tasks: tasks.clone(),
|
tasks: tasks.clone(),
|
||||||
});
|
});
|
||||||
let none_pending = ArcMemo::new(move |_| tasks.with(SlotMap::is_empty));
|
let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
|
||||||
|
tasks.track();
|
||||||
|
if prev.is_none() && starts_local {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
tasks.with(SlotMap::is_empty)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
OwnedView::new(SuspenseBoundary::<false, _, _> {
|
OwnedView::new(SuspenseBoundary::<false, _, _> {
|
||||||
|
id,
|
||||||
none_pending,
|
none_pending,
|
||||||
fallback,
|
fallback,
|
||||||
children,
|
children,
|
||||||
|
@ -52,6 +72,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct SuspenseBoundary<const TRANSITION: bool, Fal, Chil> {
|
pub(crate) struct SuspenseBoundary<const TRANSITION: bool, Fal, Chil> {
|
||||||
|
pub id: SerializedDataId,
|
||||||
pub none_pending: ArcMemo<bool>,
|
pub none_pending: ArcMemo<bool>,
|
||||||
pub fallback: Fal,
|
pub fallback: Fal,
|
||||||
pub children: Chil,
|
pub children: Chil,
|
||||||
|
@ -130,11 +151,13 @@ where
|
||||||
{
|
{
|
||||||
let attr = attr.into_cloneable_owned();
|
let attr = attr.into_cloneable_owned();
|
||||||
let SuspenseBoundary {
|
let SuspenseBoundary {
|
||||||
|
id,
|
||||||
none_pending,
|
none_pending,
|
||||||
fallback,
|
fallback,
|
||||||
children,
|
children,
|
||||||
} = self;
|
} = self;
|
||||||
SuspenseBoundary {
|
SuspenseBoundary {
|
||||||
|
id,
|
||||||
none_pending,
|
none_pending,
|
||||||
fallback,
|
fallback,
|
||||||
children: children.add_any_attr(attr),
|
children: children.add_any_attr(attr),
|
||||||
|
@ -183,15 +206,21 @@ where
|
||||||
|
|
||||||
let owner = Owner::current().unwrap();
|
let owner = Owner::current().unwrap();
|
||||||
|
|
||||||
let tasks = suspense_context.tasks.clone();
|
// we need to wait for one of two things: either
|
||||||
let (tx, rx) = futures::channel::oneshot::channel::<()>();
|
// 1. all tasks are finished loading, or
|
||||||
|
// 2. we read from a local resource, meaning this Suspense can never resolve on the server
|
||||||
|
|
||||||
let mut tx = Some(tx);
|
// first, create listener for tasks
|
||||||
|
let tasks = suspense_context.tasks.clone();
|
||||||
|
let (tasks_tx, mut tasks_rx) =
|
||||||
|
futures::channel::oneshot::channel::<()>();
|
||||||
|
|
||||||
|
let mut tasks_tx = Some(tasks_tx);
|
||||||
let eff = reactive_graph::effect::RenderEffect::new_isomorphic({
|
let eff = reactive_graph::effect::RenderEffect::new_isomorphic({
|
||||||
move |_| {
|
move |_| {
|
||||||
tasks.track();
|
tasks.track();
|
||||||
if tasks.read().is_empty() {
|
if tasks.read().is_empty() {
|
||||||
if let Some(tx) = tx.take() {
|
if let Some(tx) = tasks_tx.take() {
|
||||||
// If the receiver has dropped, it means the ScopedFuture has already
|
// If the receiver has dropped, it means the ScopedFuture has already
|
||||||
// dropped, so it doesn't matter if we manage to send this.
|
// dropped, so it doesn't matter if we manage to send this.
|
||||||
_ = tx.send(());
|
_ = tx.send(());
|
||||||
|
@ -200,37 +229,75 @@ where
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// now, create listener for local resources
|
||||||
|
let (local_tx, mut local_rx) =
|
||||||
|
futures::channel::oneshot::channel::<()>();
|
||||||
|
provide_context(LocalResourceNotifier::from(local_tx));
|
||||||
|
|
||||||
// walk over the tree of children once to make sure that all resource loads are registered
|
// walk over the tree of children once to make sure that all resource loads are registered
|
||||||
self.children.dry_resolve();
|
self.children.dry_resolve();
|
||||||
|
|
||||||
let mut fut =
|
let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new(
|
||||||
Box::pin(ScopedFuture::new(ErrorHookFuture::new(async move {
|
async move {
|
||||||
// wait for all the resources to have loaded before trying to resolve the body
|
// race the local resource notifier against the set of tasks
|
||||||
|
//
|
||||||
|
// if there are local resources, we just return the fallback immediately
|
||||||
|
//
|
||||||
|
// otherwise, we want to wait for resources to load before trying to resolve the body
|
||||||
|
//
|
||||||
// this is *less efficient* than just resolving the body
|
// this is *less efficient* than just resolving the body
|
||||||
// however, it means that you can use reactive accesses to resources/async derived
|
// however, it means that you can use reactive accesses to resources/async derived
|
||||||
// inside component props, at any level, and have those picked up by Suspense, and
|
// inside component props, at any level, and have those picked up by Suspense, and
|
||||||
// that it will wait for those to resolve
|
// that it will wait for those to resolve
|
||||||
_ = rx.await;
|
select! {
|
||||||
|
// if there are local resources, bail
|
||||||
|
// this will only have fired by this point for local resources accessed
|
||||||
|
// *synchronously*
|
||||||
|
_ = local_rx => {
|
||||||
|
let sc = Owner::current_shared_context().expect("no shared context");
|
||||||
|
sc.set_incomplete_chunk(self.id);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ = tasks_rx => {
|
||||||
|
// if we ran this earlier, reactive reads would always be registered as None
|
||||||
|
// this is fine in the case where we want to use Suspend and .await on some future
|
||||||
|
// but in situations like a <For each=|| some_resource.snapshot()/> we actually
|
||||||
|
// want to be able to 1) synchronously read a resource's value, but still 2) wait
|
||||||
|
// for it to load before we render everything
|
||||||
|
let mut children = Box::pin(self.children.resolve().fuse());
|
||||||
|
|
||||||
// if we ran this earlier, reactive reads would always be registered as None
|
// we continue racing the children against the "do we have any local
|
||||||
// this is fine in the case where we want to use Suspend and .await on some future
|
// resources?" Future
|
||||||
// but in situations like a <For each=|| some_resource.snapshot()/> we actually
|
select! {
|
||||||
// want to be able to 1) synchronously read a resource's value, but still 2) wait
|
_ = local_rx => {
|
||||||
// for it to load before we render everything
|
let sc = Owner::current_shared_context().expect("no shared context");
|
||||||
let children = self.children.resolve().await;
|
sc.set_incomplete_chunk(self.id);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
children = children => {
|
||||||
|
// clean up the (now useless) effect
|
||||||
|
drop(eff);
|
||||||
|
|
||||||
// clean up the (now useless) effect
|
Some(OwnedView::new_with_owner(children, owner))
|
||||||
drop(eff);
|
}
|
||||||
|
}
|
||||||
OwnedView::new_with_owner(children, owner)
|
}
|
||||||
})));
|
}
|
||||||
|
},
|
||||||
|
)));
|
||||||
match fut.as_mut().now_or_never() {
|
match fut.as_mut().now_or_never() {
|
||||||
Some(resolved) => {
|
Some(Some(resolved)) => {
|
||||||
Either::<Fal, _>::Right(resolved)
|
Either::<Fal, _>::Right(resolved)
|
||||||
.to_html_async_with_buf::<OUT_OF_ORDER>(
|
.to_html_async_with_buf::<OUT_OF_ORDER>(
|
||||||
buf, position, escape,
|
buf, position, escape,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Some(None) => {
|
||||||
|
Either::<_, Chil>::Left(self.fallback)
|
||||||
|
.to_html_async_with_buf::<OUT_OF_ORDER>(
|
||||||
|
buf, position, escape,
|
||||||
|
);
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
let id = buf.clone_id();
|
let id = buf.clone_id();
|
||||||
|
|
||||||
|
@ -244,14 +311,16 @@ where
|
||||||
buf.push_async({
|
buf.push_async({
|
||||||
let mut position = *position;
|
let mut position = *position;
|
||||||
async move {
|
async move {
|
||||||
let value = fut.await;
|
let value = match fut.await {
|
||||||
|
None => Either::Left(self.fallback),
|
||||||
|
Some(value) => Either::Right(value),
|
||||||
|
};
|
||||||
let mut builder = StreamBuilder::new(id);
|
let mut builder = StreamBuilder::new(id);
|
||||||
Either::<Fal, _>::Right(value)
|
value.to_html_async_with_buf::<OUT_OF_ORDER>(
|
||||||
.to_html_async_with_buf::<OUT_OF_ORDER>(
|
&mut builder,
|
||||||
&mut builder,
|
&mut position,
|
||||||
&mut position,
|
escape,
|
||||||
escape,
|
);
|
||||||
);
|
|
||||||
builder.finish().take_chunks()
|
builder.finish().take_chunks()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -268,6 +337,7 @@ where
|
||||||
) -> Self::State {
|
) -> Self::State {
|
||||||
let cursor = cursor.to_owned();
|
let cursor = cursor.to_owned();
|
||||||
let position = position.to_owned();
|
let position = position.to_owned();
|
||||||
|
|
||||||
let mut children = Some(self.children);
|
let mut children = Some(self.children);
|
||||||
let mut fallback = Some(self.fallback);
|
let mut fallback = Some(self.fallback);
|
||||||
let none_pending = self.none_pending;
|
let none_pending = self.none_pending;
|
||||||
|
|
|
@ -7,9 +7,9 @@ use leptos_macro::component;
|
||||||
use reactive_graph::{
|
use reactive_graph::{
|
||||||
computed::{suspense::SuspenseContext, ArcMemo},
|
computed::{suspense::SuspenseContext, ArcMemo},
|
||||||
effect::Effect,
|
effect::Effect,
|
||||||
owner::provide_context,
|
owner::{provide_context, Owner},
|
||||||
signal::ArcRwSignal,
|
signal::ArcRwSignal,
|
||||||
traits::{Get, With},
|
traits::{Get, Track, With},
|
||||||
wrappers::write::SignalSetter,
|
wrappers::write::SignalSetter,
|
||||||
};
|
};
|
||||||
use slotmap::{DefaultKey, SlotMap};
|
use slotmap::{DefaultKey, SlotMap};
|
||||||
|
@ -31,13 +31,28 @@ pub fn Transition<Chil>(
|
||||||
where
|
where
|
||||||
Chil: IntoView + Send + 'static,
|
Chil: IntoView + Send + 'static,
|
||||||
{
|
{
|
||||||
|
let (starts_local, id) = {
|
||||||
|
Owner::current_shared_context()
|
||||||
|
.map(|sc| {
|
||||||
|
let id = sc.next_id();
|
||||||
|
(sc.get_incomplete_chunk(&id), id)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| (false, Default::default()))
|
||||||
|
};
|
||||||
let fallback = fallback.run();
|
let fallback = fallback.run();
|
||||||
let children = children.into_inner()();
|
let children = children.into_inner()();
|
||||||
let tasks = ArcRwSignal::new(SlotMap::<DefaultKey, ()>::new());
|
let tasks = ArcRwSignal::new(SlotMap::<DefaultKey, ()>::new());
|
||||||
provide_context(SuspenseContext {
|
provide_context(SuspenseContext {
|
||||||
tasks: tasks.clone(),
|
tasks: tasks.clone(),
|
||||||
});
|
});
|
||||||
let none_pending = ArcMemo::new(move |_| tasks.with(SlotMap::is_empty));
|
let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
|
||||||
|
tasks.track();
|
||||||
|
if prev.is_none() && starts_local {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
tasks.with(SlotMap::is_empty)
|
||||||
|
}
|
||||||
|
});
|
||||||
if let Some(set_pending) = set_pending {
|
if let Some(set_pending) = set_pending {
|
||||||
Effect::new_isomorphic({
|
Effect::new_isomorphic({
|
||||||
let none_pending = none_pending.clone();
|
let none_pending = none_pending.clone();
|
||||||
|
@ -48,6 +63,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
OwnedView::new(SuspenseBoundary::<true, _, _> {
|
OwnedView::new(SuspenseBoundary::<true, _, _> {
|
||||||
|
id,
|
||||||
none_pending,
|
none_pending,
|
||||||
fallback,
|
fallback,
|
||||||
children,
|
children,
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
mod action;
|
mod action;
|
||||||
pub use action::*;
|
pub use action::*;
|
||||||
|
mod local_resource;
|
||||||
|
pub use local_resource::*;
|
||||||
mod multi_action;
|
mod multi_action;
|
||||||
pub use multi_action::*;
|
pub use multi_action::*;
|
||||||
mod resource;
|
mod resource;
|
||||||
|
|
334
leptos_server/src/local_resource.rs
Normal file
334
leptos_server/src/local_resource.rs
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
use reactive_graph::{
|
||||||
|
computed::{
|
||||||
|
suspense::{LocalResourceNotifier, SuspenseContext},
|
||||||
|
ArcAsyncDerived, AsyncDerived, AsyncDerivedFuture,
|
||||||
|
},
|
||||||
|
graph::{
|
||||||
|
AnySource, AnySubscriber, ReactiveNode, Source, Subscriber,
|
||||||
|
ToAnySource, ToAnySubscriber,
|
||||||
|
},
|
||||||
|
owner::use_context,
|
||||||
|
signal::guards::{AsyncPlain, ReadGuard},
|
||||||
|
traits::{DefinedAt, ReadUntracked},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
future::{pending, Future, IntoFuture},
|
||||||
|
panic::Location,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct ArcLocalResource<T> {
|
||||||
|
data: ArcAsyncDerived<T>,
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: &'static Location<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for ArcLocalResource<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
data: self.data.clone(),
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: self.defined_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ArcLocalResource<T> {
|
||||||
|
#[track_caller]
|
||||||
|
pub fn new<Fut>(fetcher: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
let fetcher = move || {
|
||||||
|
let fut = fetcher();
|
||||||
|
async move {
|
||||||
|
// in SSR mode, this will simply always be pending
|
||||||
|
// if we try to read from it, we will trigger Suspense automatically to fall back
|
||||||
|
// so this will never need to return anything
|
||||||
|
if cfg!(feature = "ssr") {
|
||||||
|
pending().await
|
||||||
|
} else {
|
||||||
|
fut.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
data: ArcAsyncDerived::new(fetcher),
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: Location::caller(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> IntoFuture for ArcLocalResource<T>
|
||||||
|
where
|
||||||
|
T: Clone + 'static,
|
||||||
|
{
|
||||||
|
type Output = T;
|
||||||
|
type IntoFuture = AsyncDerivedFuture<T>;
|
||||||
|
|
||||||
|
fn into_future(self) -> Self::IntoFuture {
|
||||||
|
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||||
|
notifier.notify();
|
||||||
|
} else if cfg!(feature = "ssr") {
|
||||||
|
panic!(
|
||||||
|
"Reading from a LocalResource outside Suspense in `ssr` mode \
|
||||||
|
will cause the response to hang, because LocalResources are \
|
||||||
|
always pending on the server."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.data.into_future()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DefinedAt for ArcLocalResource<T> {
|
||||||
|
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
Some(self.defined_at)
|
||||||
|
}
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ReadUntracked for ArcLocalResource<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
type Value = ReadGuard<Option<T>, AsyncPlain<Option<T>>>;
|
||||||
|
|
||||||
|
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||||
|
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||||
|
notifier.notify();
|
||||||
|
} else if cfg!(feature = "ssr") {
|
||||||
|
panic!(
|
||||||
|
"Reading from a LocalResource outside Suspense in `ssr` mode \
|
||||||
|
will cause the response to hang, because LocalResources are \
|
||||||
|
always pending on the server."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.data.try_read_untracked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> ToAnySource for ArcLocalResource<T> {
|
||||||
|
fn to_any_source(&self) -> AnySource {
|
||||||
|
self.data.to_any_source()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> ToAnySubscriber for ArcLocalResource<T> {
|
||||||
|
fn to_any_subscriber(&self) -> AnySubscriber {
|
||||||
|
self.data.to_any_subscriber()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Source for ArcLocalResource<T> {
|
||||||
|
fn add_subscriber(&self, subscriber: AnySubscriber) {
|
||||||
|
self.data.add_subscriber(subscriber)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_subscriber(&self, subscriber: &AnySubscriber) {
|
||||||
|
self.data.remove_subscriber(subscriber);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_subscribers(&self) {
|
||||||
|
self.data.clear_subscribers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ReactiveNode for ArcLocalResource<T> {
|
||||||
|
fn mark_dirty(&self) {
|
||||||
|
self.data.mark_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_check(&self) {
|
||||||
|
self.data.mark_check();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_subscribers_check(&self) {
|
||||||
|
self.data.mark_subscribers_check();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_if_necessary(&self) -> bool {
|
||||||
|
self.data.update_if_necessary()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Subscriber for ArcLocalResource<T> {
|
||||||
|
fn add_source(&self, source: AnySource) {
|
||||||
|
self.data.add_source(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_sources(&self, subscriber: &AnySubscriber) {
|
||||||
|
self.data.clear_sources(subscriber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LocalResource<T> {
|
||||||
|
data: AsyncDerived<T>,
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: &'static Location<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for LocalResource<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Copy for LocalResource<T> {}
|
||||||
|
|
||||||
|
impl<T> LocalResource<T> {
|
||||||
|
#[track_caller]
|
||||||
|
pub fn new<Fut>(fetcher: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
let fetcher = move || {
|
||||||
|
let fut = fetcher();
|
||||||
|
async move {
|
||||||
|
// in SSR mode, this will simply always be pending
|
||||||
|
// if we try to read from it, we will trigger Suspense automatically to fall back
|
||||||
|
// so this will never need to return anything
|
||||||
|
if cfg!(feature = "ssr") {
|
||||||
|
pending().await
|
||||||
|
} else {
|
||||||
|
fut.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
data: AsyncDerived::new(fetcher),
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: Location::caller(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> IntoFuture for LocalResource<T>
|
||||||
|
where
|
||||||
|
T: Clone + 'static,
|
||||||
|
{
|
||||||
|
type Output = T;
|
||||||
|
type IntoFuture = AsyncDerivedFuture<T>;
|
||||||
|
|
||||||
|
fn into_future(self) -> Self::IntoFuture {
|
||||||
|
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||||
|
notifier.notify();
|
||||||
|
} else if cfg!(feature = "ssr") {
|
||||||
|
panic!(
|
||||||
|
"Reading from a LocalResource outside Suspense in `ssr` mode \
|
||||||
|
will cause the response to hang, because LocalResources are \
|
||||||
|
always pending on the server."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.data.into_future()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DefinedAt for LocalResource<T> {
|
||||||
|
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
Some(self.defined_at)
|
||||||
|
}
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ReadUntracked for LocalResource<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
type Value = ReadGuard<Option<T>, AsyncPlain<Option<T>>>;
|
||||||
|
|
||||||
|
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||||
|
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||||
|
notifier.notify();
|
||||||
|
} else if cfg!(feature = "ssr") {
|
||||||
|
panic!(
|
||||||
|
"Reading from a LocalResource outside Suspense in `ssr` mode \
|
||||||
|
will cause the response to hang, because LocalResources are \
|
||||||
|
always pending on the server."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.data.try_read_untracked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> ToAnySource for LocalResource<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
fn to_any_source(&self) -> AnySource {
|
||||||
|
self.data.to_any_source()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> ToAnySubscriber for LocalResource<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
fn to_any_subscriber(&self) -> AnySubscriber {
|
||||||
|
self.data.to_any_subscriber()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Source for LocalResource<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
fn add_subscriber(&self, subscriber: AnySubscriber) {
|
||||||
|
self.data.add_subscriber(subscriber)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_subscriber(&self, subscriber: &AnySubscriber) {
|
||||||
|
self.data.remove_subscriber(subscriber);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_subscribers(&self) {
|
||||||
|
self.data.clear_subscribers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ReactiveNode for LocalResource<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
fn mark_dirty(&self) {
|
||||||
|
self.data.mark_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_check(&self) {
|
||||||
|
self.data.mark_check();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_subscribers_check(&self) {
|
||||||
|
self.data.mark_subscribers_check();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_if_necessary(&self) -> bool {
|
||||||
|
self.data.update_if_necessary()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Subscriber for LocalResource<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
fn add_source(&self, source: AnySource) {
|
||||||
|
self.data.add_source(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_sources(&self, subscriber: &AnySubscriber) {
|
||||||
|
self.data.clear_sources(subscriber);
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,7 +41,7 @@ impl<T: Send + Sync + 'static> AsyncDerived<T> {
|
||||||
pub fn new<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
pub fn new<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
where
|
where
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + Sync + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
|
|
@ -81,7 +81,27 @@ pub mod suspense {
|
||||||
signal::ArcRwSignal,
|
signal::ArcRwSignal,
|
||||||
traits::{Update, Writeable},
|
traits::{Update, Writeable},
|
||||||
};
|
};
|
||||||
|
use futures::channel::oneshot::Sender;
|
||||||
|
use or_poisoned::OrPoisoned;
|
||||||
use slotmap::{DefaultKey, SlotMap};
|
use slotmap::{DefaultKey, SlotMap};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LocalResourceNotifier(Arc<Mutex<Option<Sender<()>>>>);
|
||||||
|
|
||||||
|
impl LocalResourceNotifier {
|
||||||
|
pub fn notify(&mut self) {
|
||||||
|
if let Some(tx) = self.0.lock().or_poisoned().take() {
|
||||||
|
tx.send(()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Sender<()>> for LocalResourceNotifier {
|
||||||
|
fn from(value: Sender<()>) -> Self {
|
||||||
|
Self(Arc::new(Mutex::new(Some(value))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct SuspenseContext {
|
pub struct SuspenseContext {
|
||||||
|
|
|
@ -138,7 +138,7 @@ impl StreamBuilder {
|
||||||
|
|
||||||
pub fn push_async_out_of_order<View, Rndr>(
|
pub fn push_async_out_of_order<View, Rndr>(
|
||||||
&mut self,
|
&mut self,
|
||||||
view: impl Future<Output = View> + Send + 'static,
|
view: impl Future<Output = Option<View>> + Send + 'static,
|
||||||
position: &mut Position,
|
position: &mut Position,
|
||||||
) where
|
) where
|
||||||
View: RenderHtml<Rndr>,
|
View: RenderHtml<Rndr>,
|
||||||
|
@ -160,18 +160,24 @@ impl StreamBuilder {
|
||||||
write!(&mut id, "{}-", piece).unwrap();
|
write!(&mut id, "{}-", piece).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(id) = subbuilder.id.as_mut() {
|
if let Some(id) = subbuilder.id.as_mut() {
|
||||||
id.push(0);
|
id.push(0);
|
||||||
}
|
}
|
||||||
view.to_html_async_with_buf::<true>(
|
let replace = view.is_some();
|
||||||
&mut subbuilder,
|
if let Some(view) = view {
|
||||||
&mut position,
|
view.to_html_async_with_buf::<true>(
|
||||||
true,
|
&mut subbuilder,
|
||||||
);
|
&mut position,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
let chunks = subbuilder.finish().take_chunks();
|
let chunks = subbuilder.finish().take_chunks();
|
||||||
|
|
||||||
OooChunk { id, chunks }
|
OooChunk {
|
||||||
|
id,
|
||||||
|
chunks,
|
||||||
|
replace,
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -201,6 +207,7 @@ pub enum StreamChunk {
|
||||||
struct OooChunk {
|
struct OooChunk {
|
||||||
id: String,
|
id: String,
|
||||||
chunks: VecDeque<StreamChunk>,
|
chunks: VecDeque<StreamChunk>,
|
||||||
|
replace: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OooChunk {
|
impl OooChunk {
|
||||||
|
@ -211,7 +218,7 @@ impl OooChunk {
|
||||||
buf.push_str("\">");
|
buf.push_str("\">");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_end(id: &str, buf: &mut String) {
|
pub fn push_end(replace: bool, id: &str, buf: &mut String) {
|
||||||
buf.push_str("</template>");
|
buf.push_str("</template>");
|
||||||
|
|
||||||
// TODO nonce
|
// TODO nonce
|
||||||
|
@ -219,20 +226,26 @@ impl OooChunk {
|
||||||
buf.push_str(r#">(function() { let id = ""#);
|
buf.push_str(r#">(function() { let id = ""#);
|
||||||
buf.push_str(id);
|
buf.push_str(id);
|
||||||
buf.push_str(
|
buf.push_str(
|
||||||
"\";let open = undefined;let close = undefined;let walker \
|
"\";let open = undefined;let close = undefined;let walker = \
|
||||||
= document.createTreeWalker(document.body, \
|
document.createTreeWalker(document.body, \
|
||||||
NodeFilter.SHOW_COMMENT);while(walker.nextNode()) \
|
NodeFilter.SHOW_COMMENT);while(walker.nextNode()) \
|
||||||
{if(walker.currentNode.textContent == `s-${id}o`){ \
|
{if(walker.currentNode.textContent == `s-${id}o`){ \
|
||||||
open=walker.currentNode; } else \
|
open=walker.currentNode; } else \
|
||||||
if(walker.currentNode.textContent == `s-${id}c`) { close \
|
if(walker.currentNode.textContent == `s-${id}c`) { close = \
|
||||||
= walker.currentNode;}}let range = new Range(); \
|
walker.currentNode;}}let range = new Range(); \
|
||||||
range.setStartBefore(open); range.setEndBefore(close); \
|
range.setStartBefore(open); range.setEndBefore(close);",
|
||||||
range.deleteContents(); let tpl = \
|
|
||||||
document.getElementById(`${id}f`); \
|
|
||||||
close.parentNode.insertBefore(tpl.content.\
|
|
||||||
cloneNode(true), close);close.remove();})()",
|
|
||||||
);
|
);
|
||||||
buf.push_str("</script>");
|
if replace {
|
||||||
|
buf.push_str(
|
||||||
|
"range.deleteContents(); let tpl = \
|
||||||
|
document.getElementById(`${id}f`); \
|
||||||
|
close.parentNode.insertBefore(tpl.content.cloneNode(true), \
|
||||||
|
close);close.remove();",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
buf.push_str("close.remove();open.remove();");
|
||||||
|
}
|
||||||
|
buf.push_str("})()</script>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,7 +292,11 @@ impl Stream for StreamBuilder {
|
||||||
// now, handle out-of-order chunks
|
// now, handle out-of-order chunks
|
||||||
if let Some(mut pending) = this.pending_ooo.pop_front() {
|
if let Some(mut pending) = this.pending_ooo.pop_front() {
|
||||||
match pending.as_mut().poll(cx) {
|
match pending.as_mut().poll(cx) {
|
||||||
Poll::Ready(OooChunk { id, chunks }) => {
|
Poll::Ready(OooChunk {
|
||||||
|
id,
|
||||||
|
chunks,
|
||||||
|
replace,
|
||||||
|
}) => {
|
||||||
let opening = format!("<!--s-{id}o-->");
|
let opening = format!("<!--s-{id}o-->");
|
||||||
let placeholder_at =
|
let placeholder_at =
|
||||||
this.sync_buf.find(&opening);
|
this.sync_buf.find(&opening);
|
||||||
|
@ -324,7 +341,11 @@ impl Stream for StreamBuilder {
|
||||||
this.chunks.push_front(chunk);
|
this.chunks.push_front(chunk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OooChunk::push_end(&id, &mut this.sync_buf);
|
OooChunk::push_end(
|
||||||
|
replace,
|
||||||
|
&id,
|
||||||
|
&mut this.sync_buf,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
self.poll_next(cx)
|
self.poll_next(cx)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue