#![cfg_attr(not(feature = "router"), allow(rustdoc::broken_intra_doc_links))]
use crate::{
Result,
context::{Cx, identity, try_app_context, try_request_context},
core::identity::SiteKey,
view::{Child, RegionId, View, component, internal::SuspenseView},
};
#[component]
pub async fn suspense(
cx: &Cx,
#[into]
fallback: Child<'_>,
#[default]
child: Child<'_>,
#[into]
#[default]
mode: Option<SuspenseMode>,
) -> Result<impl View> {
const SITE: SiteKey = SiteKey::new(file!(), line!(), column!(), 0);
let region = RegionId::new(identity(cx), SITE);
let mode = mode.unwrap_or_else(|| suspense_mode(cx));
let wait = mode == SuspenseMode::Wait;
Ok(SuspenseView::new(region, fallback, child, wait))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SuspenseMode {
#[default]
Stream,
Wait,
}
#[must_use]
pub fn suspense_mode(cx: &Cx) -> SuspenseMode {
try_request_context::<SuspenseMode>(cx)
.or_else(|| try_app_context::<SuspenseMode>(cx))
.copied()
.unwrap_or_default()
}
#[cfg(feature = "router")]
pub trait RouterSuspenseExt {
#[must_use]
#[track_caller]
fn suspense(self, mode: SuspenseMode) -> Self;
}
#[cfg(feature = "router")]
impl RouterSuspenseExt for crate::router::RouterBuilder {
#[track_caller]
fn suspense(self, mode: SuspenseMode) -> Self {
self.app_context(mode)
}
}
#[cfg(all(test, feature = "router"))]
mod tests {
use super::*;
use crate::{
router::{Body, Router, RouterBuilder, page, request::Request, to_bytes},
view::view,
};
#[component]
async fn deferred() -> Result<impl View> {
tokio::task::yield_now().await;
Ok(view! { <p>"content"</p> })
}
#[page("/")]
async fn home() -> Result<impl View> {
Ok(view! { suspense(fallback: view! { <p>"loading"</p> }, deferred()) })
}
async fn fetch(router: Router) -> String {
let request = Request::builder().uri("/").body(Body::empty()).unwrap();
let response = router.handle(request).await;
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
String::from_utf8(bytes.to_vec()).unwrap()
}
#[tokio::test]
async fn a_router_streams_suspense_by_default() {
let html = fetch(RouterBuilder::new().page(home).build()).await;
assert!(html.contains("<p>loading</p>"), "{html}");
assert!(html.contains("<p>content</p>"), "{html}");
}
#[tokio::test]
async fn a_waiting_router_sends_suspense_content_settled() {
let router = RouterBuilder::new()
.suspense(SuspenseMode::Wait)
.page(home)
.build();
let html = fetch(router).await;
assert!(!html.contains("loading"), "{html}");
assert!(!html.contains("topcoat::region"), "{html}");
assert!(html.contains("<p>content</p>"), "{html}");
}
}