use std::future::Future;
use std::num::NonZeroU32;
use sycamore_futures::{
create_detached_suspense_scope, create_suspense_scope, create_suspense_task,
};
use sycamore_macro::{component, Props};
use crate::*;
#[derive(Props)]
pub struct SuspenseProps {
#[prop(default = Box::new(|| view! {}), setter(transform = |f: impl Fn() -> View + 'static| Box::new(f) as Box<dyn Fn() -> View>))]
fallback: Box<dyn Fn() -> View>,
children: Children,
#[prop(default = Box::new(|_| {}), setter(transform = |f: impl FnMut(bool) + 'static| Box::new(f) as Box<dyn FnMut(bool)>))]
set_is_loading: Box<dyn FnMut(bool) + 'static>,
}
#[component]
pub fn Suspense(props: SuspenseProps) -> View {
let SuspenseProps {
fallback,
children,
mut set_is_loading,
} = props;
is_ssr! {
use futures::FutureExt;
let _ = &mut set_is_loading;
let mode = use_context::<SsrMode>();
match mode {
SsrMode::Sync => view! {
Show(when=true) {
(fallback())
}
Show(when=false) {}
},
SsrMode::Blocking => {
let key = use_suspense_key();
let start = view! { suspense-start(data-key=key.to_string()) };
let (view, _suspense_scope) = create_suspense_scope(move || HydrationRegistry::in_suspense_scope(key, move || children.call()));
view! {
(start)
NoSsr {}
Show(when=move || true) {
(view)
}
}
},
SsrMode::Streaming => {
let key = use_suspense_key();
let start = view! { suspense-start(data-key=key.to_string()) };
let (view, suspense_scope) = create_suspense_scope(move || HydrationRegistry::in_suspense_scope(key, move || children.call()));
let suspense_stream = use_context::<SuspenseStream>();
suspense_stream.futures.borrow_mut().push(async move {
suspense_scope.until_finished().await;
debug_assert!(!suspense_scope.sent.get(), "suspense scope should not yet be sent");
let (tx, rx) = futures::channel::oneshot::channel();
let mut tx = Some(tx);
create_effect(move || {
#[expect(clippy::unnecessary_map_or, reason = "MSRV is at 1.81")]
if !suspense_scope.sent.get() && suspense_scope.parent.as_ref().map_or(true, |parent| parent.get().sent.get()) {
suspense_scope.sent.set(true);
tx.take().unwrap().send(()).unwrap();
}
});
rx.await.unwrap();
SuspenseFragment::new(key, view! { Show(when=true) { (view) } })
}.boxed_local());
let end = view! { NoHydrate { suspense-end(data-key=key.to_string()) } };
view! {
NoSsr {}
(start)
NoHydrate(children=Children::new(fallback))
(end)
}
}
}
}
is_not_ssr! {
let mode = if IS_HYDRATING.get() {
use_context::<SsrMode>()
} else {
SsrMode::Sync
};
match mode {
SsrMode::Sync => {
let (view, suspense_scope) = create_suspense_scope(move || children.call());
let is_loading = suspense_scope.is_loading();
create_effect(move || {
set_is_loading(is_loading.get());
});
view! {
Show(when=is_loading) {
(fallback())
}
Show(when=move || !is_loading.get()) {
(view)
}
}
}
SsrMode::Blocking | SsrMode::Streaming => {
let start = view! { suspense-start() };
let node = start.nodes[0].as_web_sys().unchecked_ref::<web_sys::Element>();
let key: NonZeroU32 = node.get_attribute("data-key").unwrap().parse().unwrap();
let (view, suspense_scope) = HydrationRegistry::in_suspense_scope(key, move || create_suspense_scope(move || children.call()));
let is_loading = suspense_scope.is_loading();
create_effect(move || set_is_loading(is_loading.get()));
view! {
NoSsr {
Show(when=move || is_loading.get()) {
(fallback())
}
}
Show(when=move || !is_loading.get()) {
(view)
}
}
}
}
}
}
#[component]
pub fn Transition(props: SuspenseProps) -> View {
#[component(inline_props)]
fn TransitionInner(children: Children, set_is_loading: Box<dyn FnMut(bool)>) -> View {
let mut set_is_loading = set_is_loading;
let (children, scope) = create_detached_suspense_scope(move || children.call());
create_suspense_task(scope.until_finished());
let is_loading = scope.is_loading();
create_effect(move || {
set_is_loading(is_loading.get());
});
view! {
(children)
}
}
view! {
Suspense(fallback=props.fallback, children=Children::new(move || {
view! { TransitionInner(children=props.children, set_is_loading=props.set_is_loading) }
}))
}
}
#[component]
pub fn WrapAsync<F: Future<Output = View>>(f: impl FnOnce() -> F + 'static) -> View {
is_not_ssr! {
let mode = if IS_HYDRATING.get() {
use_context::<SsrMode>()
} else {
SsrMode::Sync
};
match mode {
SsrMode::Sync => {
let view = create_signal(View::default());
let ret = view! { ({
view.track();
view.update_silent(std::mem::take)
}) };
create_suspense_task(async move {
view.set(f().await);
});
ret
}
SsrMode::Blocking | SsrMode::Streaming => {
create_suspense_task(async move { f().await; });
view! {}
}
}
}
is_ssr! {
let node = create_signal(View::default());
create_suspense_task(async move {
node.set(f().await);
});
View::from_dynamic(move || {
node.track();
node.update_silent(std::mem::take)
})
}
}
#[cfg_ssr]
pub(crate) struct SuspenseFragment {
pub key: NonZeroU32,
pub view: View,
}
#[cfg_ssr]
impl SuspenseFragment {
pub fn new(key: NonZeroU32, view: View) -> Self {
Self { key, view }
}
}
#[cfg_ssr]
#[derive(Clone)]
pub(crate) struct SuspenseStream {
pub futures: std::rc::Rc<
std::cell::RefCell<
futures::stream::FuturesUnordered<
futures::future::LocalBoxFuture<'static, SuspenseFragment>,
>,
>,
>,
}
#[derive(Debug, Clone, Copy)]
struct SuspenseCounter {
next: Signal<NonZeroU32>,
}
impl SuspenseCounter {
fn new() -> Self {
Self {
next: create_signal(NonZeroU32::new(1).unwrap()),
}
}
}
pub fn use_suspense_key() -> NonZeroU32 {
let global_scope = use_global_scope();
let counter = global_scope.run_in(|| use_context_or_else(SuspenseCounter::new));
let next = counter.next.get();
counter.next.set(next.checked_add(1).unwrap());
next
}