use crate::{
children::{TypedChildren, ViewFnOnce},
error::ErrorBoundarySuspendedChildren,
IntoView,
};
use futures::{channel::oneshot, select, FutureExt};
use hydration_context::SerializedDataId;
use leptos_macro::component;
use or_poisoned::OrPoisoned;
use reactive_graph::{
computed::{
suspense::{LocalResourceNotifier, SuspenseContext},
ArcMemo, ScopedFuture,
},
effect::RenderEffect,
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
traits::{
Dispose, Get, Read, ReadUntracked, Track, With, WithUntracked,
WriteValue,
},
};
use slotmap::{DefaultKey, SlotMap};
use std::sync::{Arc, Mutex};
use tachys::{
either::Either,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
reactive_graph::{OwnedView, OwnedViewState},
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr,
either::{EitherKeepAlive, EitherKeepAliveState},
Mountable, Position, PositionState, Render, RenderHtml,
},
};
use throw_error::ErrorHookFuture;
#[component]
pub fn Suspense<Chil>(
#[prop(optional, into)]
fallback: ViewFnOnce,
children: TypedChildren<Chil>,
) -> impl IntoView
where
Chil: IntoView + Send + 'static,
{
let error_boundary_parent = use_context::<ErrorBoundarySuspendedChildren>();
let owner = Owner::new();
owner.with(|| {
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 children = children.into_inner()();
let tasks = ArcRwSignal::new(SlotMap::<DefaultKey, ()>::new());
provide_context(SuspenseContext {
tasks: tasks.clone(),
});
let none_pending = ArcMemo::new({
let tasks = tasks.clone();
move |prev: Option<&bool>| {
tasks.track();
if prev.is_none() && starts_local {
false
} else {
tasks.with(SlotMap::is_empty)
}
}
});
let has_tasks =
Arc::new(move || !tasks.with_untracked(SlotMap::is_empty));
OwnedView::new(SuspenseBoundary::<false, _, _> {
id,
none_pending,
fallback,
children,
error_boundary_parent,
has_tasks,
})
})
}
fn nonce_or_not() -> Option<Arc<str>> {
#[cfg(feature = "nonce")]
{
use crate::nonce::Nonce;
use_context::<Nonce>().map(|n| n.0)
}
#[cfg(not(feature = "nonce"))]
{
None
}
}
pub(crate) struct SuspenseBoundary<const TRANSITION: bool, Fal, Chil> {
pub id: SerializedDataId,
pub none_pending: ArcMemo<bool>,
pub fallback: Fal,
pub children: Chil,
pub error_boundary_parent: Option<ErrorBoundarySuspendedChildren>,
pub has_tasks: Arc<dyn Fn() -> bool + Send + Sync>,
}
impl<const TRANSITION: bool, Fal, Chil> Render
for SuspenseBoundary<TRANSITION, Fal, Chil>
where
Fal: Render + Send + 'static,
Chil: Render + Send + 'static,
{
type State = RenderEffect<
OwnedViewState<EitherKeepAliveState<Chil::State, Fal::State>>,
>;
fn build(self) -> Self::State {
let mut children = Some(self.children);
let mut fallback = Some(self.fallback);
let none_pending = self.none_pending;
let mut nth_run = 0;
let outer_owner = Owner::new();
RenderEffect::new(move |prev| {
let show_b = !none_pending.get() && (!TRANSITION || nth_run < 2);
nth_run += 1;
let this = OwnedView::new_with_owner(
EitherKeepAlive {
a: children.take(),
b: fallback.take(),
show_b,
},
outer_owner.clone(),
);
let state = if let Some(mut state) = prev {
this.rebuild(&mut state);
state
} else {
this.build()
};
if nth_run == 1 && !(self.has_tasks)() {
nth_run += 1;
}
state
})
}
fn rebuild(self, state: &mut Self::State) {
let new = self.build();
let mut old = std::mem::replace(state, new);
old.insert_before_this(state);
old.unmount();
}
}
impl<const TRANSITION: bool, Fal, Chil> AddAnyAttr
for SuspenseBoundary<TRANSITION, Fal, Chil>
where
Fal: RenderHtml + Send + 'static,
Chil: RenderHtml + Send + 'static,
{
type Output<SomeNewAttr: Attribute> = SuspenseBoundary<
TRANSITION,
Fal,
Chil::Output<SomeNewAttr::CloneableOwned>,
>;
fn add_any_attr<NewAttr: Attribute>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
{
let attr = attr.into_cloneable_owned();
let SuspenseBoundary {
id,
none_pending,
fallback,
children,
error_boundary_parent,
has_tasks,
} = self;
SuspenseBoundary {
id,
none_pending,
fallback,
children: children.add_any_attr(attr),
error_boundary_parent,
has_tasks,
}
}
}
impl<const TRANSITION: bool, Fal, Chil> RenderHtml
for SuspenseBoundary<TRANSITION, Fal, Chil>
where
Fal: RenderHtml + Send + 'static,
Chil: RenderHtml + Send + 'static,
{
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {
self
}
fn to_html_with_buf(
self,
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
self.fallback.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
mut self,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
buf.next_id();
let suspense_context = use_context::<SuspenseContext>().unwrap();
let owner = Owner::current().unwrap();
let mut notify_error_boundary =
self.error_boundary_parent.map(|children| {
let (tx, rx) = oneshot::channel();
children.write_value().push(rx);
tx
});
let tasks = suspense_context.tasks.clone();
let (tasks_tx, mut tasks_rx) =
futures::channel::oneshot::channel::<()>();
let mut tasks_tx = Some(tasks_tx);
let (local_tx, mut local_rx) =
futures::channel::oneshot::channel::<()>();
provide_context(LocalResourceNotifier::from(local_tx));
self.children.dry_resolve();
let children = Arc::new(Mutex::new(Some(self.children)));
let eff = reactive_graph::effect::Effect::new_isomorphic({
let children = Arc::clone(&children);
move |double_checking: Option<bool>| {
if double_checking.is_none() {
tasks.track();
}
if let Some(curr_tasks) = tasks.try_read_untracked() {
if curr_tasks.is_empty() {
if double_checking == Some(true) {
if let Some(tx) = tasks_tx.take() {
_ = tx.send(());
}
if let Some(tx) = notify_error_boundary.take() {
_ = tx.send(());
}
} else {
drop(curr_tasks);
if let Some(children) =
children.lock().or_poisoned().as_mut()
{
children.dry_resolve();
}
if tasks
.try_read()
.map(|n| n.is_empty())
.unwrap_or(false)
{
if let Some(tx) = tasks_tx.take() {
_ = tx.send(());
}
if let Some(tx) = notify_error_boundary.take() {
_ = tx.send(());
}
}
return true;
}
} else {
tasks.track();
}
}
false
}
});
let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new(
async move {
select! {
_ = local_rx => {
let sc = Owner::current_shared_context().expect("no shared context");
sc.set_incomplete_chunk(self.id);
None
}
_ = tasks_rx => {
let children = {
let mut children_lock = children.lock().or_poisoned();
children_lock.take().expect("children should not be removed until we render here")
};
let mut children = Box::pin(children.resolve().fuse());
select! {
_ = local_rx => {
let sc = Owner::current_shared_context().expect("no shared context");
sc.set_incomplete_chunk(self.id);
None
}
children = children => {
eff.dispose();
Some(OwnedView::new_with_owner(children, owner))
}
}
}
}
},
)));
match fut.as_mut().now_or_never() {
Some(Some(resolved)) => {
Either::<Fal, _>::Right(resolved)
.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
Some(None) => {
Either::<_, Chil>::Left(self.fallback)
.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
None => {
let id = buf.clone_id();
if OUT_OF_ORDER {
let mut fallback_position = *position;
buf.push_fallback(
self.fallback,
&mut fallback_position,
mark_branches,
extra_attrs.clone(),
);
buf.push_async_out_of_order_with_nonce(
fut,
position,
mark_branches,
nonce_or_not(),
extra_attrs,
);
} else {
self.fallback.dry_resolve();
buf.push_async({
let mut position = *position;
async move {
let value = match fut.await {
None => Either::Left(self.fallback),
Some(value) => Either::Right(value),
};
let mut builder = StreamBuilder::new(id);
value.to_html_async_with_buf::<OUT_OF_ORDER>(
&mut builder,
&mut position,
escape,
mark_branches,
extra_attrs,
);
builder.finish().take_chunks()
}
});
*position = Position::NextChild;
}
}
};
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
let cursor = cursor.to_owned();
let position = position.to_owned();
let mut children = Some(self.children);
let mut fallback = Some(self.fallback);
let none_pending = self.none_pending;
let mut nth_run = 0;
let outer_owner = Owner::new();
RenderEffect::new(move |prev| {
let show_b = !none_pending.get() && (!TRANSITION || nth_run < 1);
nth_run += 1;
let this = OwnedView::new_with_owner(
EitherKeepAlive {
a: children.take(),
b: fallback.take(),
show_b,
},
outer_owner.clone(),
);
if let Some(mut state) = prev {
this.rebuild(&mut state);
state
} else {
this.hydrate::<FROM_SERVER>(&cursor, &position)
}
})
}
fn into_owned(self) -> Self::Owned {
self
}
}
pub struct Unsuspend<T>(Box<dyn FnOnce() -> T + Send>);
impl<T> Unsuspend<T> {
pub fn new(fun: impl FnOnce() -> T + Send + 'static) -> Self {
Self(Box::new(fun))
}
}
impl<T> Render for Unsuspend<T>
where
T: Render,
{
type State = T::State;
fn build(self) -> Self::State {
(self.0)().build()
}
fn rebuild(self, state: &mut Self::State) {
(self.0)().rebuild(state);
}
}
impl<T> AddAnyAttr for Unsuspend<T>
where
T: AddAnyAttr + 'static,
{
type Output<SomeNewAttr: Attribute> =
Unsuspend<T::Output<SomeNewAttr::CloneableOwned>>;
fn add_any_attr<NewAttr: Attribute>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
{
let attr = attr.into_cloneable_owned();
Unsuspend::new(move || (self.0)().add_any_attr(attr))
}
}
impl<T> RenderHtml for Unsuspend<T>
where
T: RenderHtml + 'static,
{
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = T::MIN_LENGTH;
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {
self
}
fn to_html_with_buf(
self,
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
(self.0)().to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
(self.0)().to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
(self.0)().hydrate::<FROM_SERVER>(cursor, position)
}
fn into_owned(self) -> Self::Owned {
self
}
}