1use crate::{
2 children::{TypedChildren, ViewFnOnce},
3 error::ErrorBoundarySuspendedChildren,
4 IntoView,
5};
6use futures::{channel::oneshot, select, FutureExt};
7use hydration_context::SerializedDataId;
8use leptos_macro::component;
9use reactive_graph::{
10 computed::{
11 suspense::{LocalResourceNotifier, SuspenseContext},
12 ArcMemo, ScopedFuture,
13 },
14 effect::RenderEffect,
15 owner::{provide_context, use_context, Owner},
16 signal::ArcRwSignal,
17 traits::{Dispose, Get, Read, Track, With, WriteValue},
18};
19use slotmap::{DefaultKey, SlotMap};
20use std::sync::Arc;
21use tachys::{
22 either::Either,
23 html::attribute::{any_attribute::AnyAttribute, Attribute},
24 hydration::Cursor,
25 reactive_graph::{OwnedView, OwnedViewState},
26 ssr::StreamBuilder,
27 view::{
28 add_attr::AddAnyAttr,
29 either::{EitherKeepAlive, EitherKeepAliveState},
30 Mountable, Position, PositionState, Render, RenderHtml,
31 },
32};
33use throw_error::ErrorHookFuture;
34
35#[component]
91pub fn Suspense<Chil>(
92 #[prop(optional, into)]
95 fallback: ViewFnOnce,
96 children: TypedChildren<Chil>,
99) -> impl IntoView
100where
101 Chil: IntoView + Send + 'static,
102{
103 let error_boundary_parent = use_context::<ErrorBoundarySuspendedChildren>();
104
105 let owner = Owner::new();
106 owner.with(|| {
107 let (starts_local, id) = {
108 Owner::current_shared_context()
109 .map(|sc| {
110 let id = sc.next_id();
111 (sc.get_incomplete_chunk(&id), id)
112 })
113 .unwrap_or_else(|| (false, Default::default()))
114 };
115 let fallback = fallback.run();
116 let children = children.into_inner()();
117 let tasks = ArcRwSignal::new(SlotMap::<DefaultKey, ()>::new());
118 provide_context(SuspenseContext {
119 tasks: tasks.clone(),
120 });
121 let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
122 tasks.track();
123 if prev.is_none() && starts_local {
124 false
125 } else {
126 tasks.with(SlotMap::is_empty)
127 }
128 });
129
130 OwnedView::new(SuspenseBoundary::<false, _, _> {
131 id,
132 none_pending,
133 fallback,
134 children,
135 error_boundary_parent,
136 })
137 })
138}
139
140fn nonce_or_not() -> Option<Arc<str>> {
141 #[cfg(feature = "nonce")]
142 {
143 use crate::nonce::Nonce;
144 use_context::<Nonce>().map(|n| n.0)
145 }
146 #[cfg(not(feature = "nonce"))]
147 {
148 None
149 }
150}
151
152pub(crate) struct SuspenseBoundary<const TRANSITION: bool, Fal, Chil> {
153 pub id: SerializedDataId,
154 pub none_pending: ArcMemo<bool>,
155 pub fallback: Fal,
156 pub children: Chil,
157 pub error_boundary_parent: Option<ErrorBoundarySuspendedChildren>,
158}
159
160impl<const TRANSITION: bool, Fal, Chil> Render
161 for SuspenseBoundary<TRANSITION, Fal, Chil>
162where
163 Fal: Render + Send + 'static,
164 Chil: Render + Send + 'static,
165{
166 type State = RenderEffect<
167 OwnedViewState<EitherKeepAliveState<Chil::State, Fal::State>>,
168 >;
169
170 fn build(self) -> Self::State {
171 let mut children = Some(self.children);
172 let mut fallback = Some(self.fallback);
173 let none_pending = self.none_pending;
174 let mut nth_run = 0;
175 let outer_owner = Owner::new();
176
177 RenderEffect::new(move |prev| {
178 let show_b = !none_pending.get() && (!TRANSITION || nth_run < 2);
184 nth_run += 1;
185 let this = OwnedView::new_with_owner(
186 EitherKeepAlive {
187 a: children.take(),
188 b: fallback.take(),
189 show_b,
190 },
191 outer_owner.clone(),
192 );
193
194 if let Some(mut state) = prev {
195 this.rebuild(&mut state);
196 state
197 } else {
198 this.build()
199 }
200 })
201 }
202
203 fn rebuild(self, state: &mut Self::State) {
204 let new = self.build();
205 let mut old = std::mem::replace(state, new);
206 old.insert_before_this(state);
207 old.unmount();
208 }
209}
210
211impl<const TRANSITION: bool, Fal, Chil> AddAnyAttr
212 for SuspenseBoundary<TRANSITION, Fal, Chil>
213where
214 Fal: RenderHtml + Send + 'static,
215 Chil: RenderHtml + Send + 'static,
216{
217 type Output<SomeNewAttr: Attribute> = SuspenseBoundary<
218 TRANSITION,
219 Fal,
220 Chil::Output<SomeNewAttr::CloneableOwned>,
221 >;
222
223 fn add_any_attr<NewAttr: Attribute>(
224 self,
225 attr: NewAttr,
226 ) -> Self::Output<NewAttr>
227 where
228 Self::Output<NewAttr>: RenderHtml,
229 {
230 let attr = attr.into_cloneable_owned();
231 let SuspenseBoundary {
232 id,
233 none_pending,
234 fallback,
235 children,
236 error_boundary_parent,
237 } = self;
238 SuspenseBoundary {
239 id,
240 none_pending,
241 fallback,
242 children: children.add_any_attr(attr),
243 error_boundary_parent,
244 }
245 }
246}
247
248impl<const TRANSITION: bool, Fal, Chil> RenderHtml
249 for SuspenseBoundary<TRANSITION, Fal, Chil>
250where
251 Fal: RenderHtml + Send + 'static,
252 Chil: RenderHtml + Send + 'static,
253{
254 type AsyncOutput = Self;
257 type Owned = Self;
258
259 const MIN_LENGTH: usize = Chil::MIN_LENGTH;
260
261 fn dry_resolve(&mut self) {}
262
263 async fn resolve(self) -> Self::AsyncOutput {
264 self
265 }
266
267 fn to_html_with_buf(
268 self,
269 buf: &mut String,
270 position: &mut Position,
271 escape: bool,
272 mark_branches: bool,
273 extra_attrs: Vec<AnyAttribute>,
274 ) {
275 self.fallback.to_html_with_buf(
276 buf,
277 position,
278 escape,
279 mark_branches,
280 extra_attrs,
281 );
282 }
283
284 fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
285 mut self,
286 buf: &mut StreamBuilder,
287 position: &mut Position,
288 escape: bool,
289 mark_branches: bool,
290 extra_attrs: Vec<AnyAttribute>,
291 ) where
292 Self: Sized,
293 {
294 buf.next_id();
295 let suspense_context = use_context::<SuspenseContext>().unwrap();
296 let owner = Owner::current().unwrap();
297
298 let mut notify_error_boundary =
299 self.error_boundary_parent.map(|children| {
300 let (tx, rx) = oneshot::channel();
301 children.write_value().push(rx);
302 tx
303 });
304
305 let tasks = suspense_context.tasks.clone();
311 let (tasks_tx, mut tasks_rx) =
312 futures::channel::oneshot::channel::<()>();
313
314 let mut tasks_tx = Some(tasks_tx);
315
316 let (local_tx, mut local_rx) =
318 futures::channel::oneshot::channel::<()>();
319 provide_context(LocalResourceNotifier::from(local_tx));
320
321 self.children.dry_resolve();
323
324 let eff = reactive_graph::effect::Effect::new_isomorphic({
326 move |_| {
327 tasks.track();
328 if let Some(tasks) = tasks.try_read() {
329 if tasks.is_empty() {
330 if let Some(tx) = tasks_tx.take() {
331 _ = tx.send(());
334 }
335 if let Some(tx) = notify_error_boundary.take() {
336 _ = tx.send(());
337 }
338 }
339 }
340 }
341 });
342
343 let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new(
344 async move {
345 select! {
356 _ = local_rx => {
360 let sc = Owner::current_shared_context().expect("no shared context");
361 sc.set_incomplete_chunk(self.id);
362 None
363 }
364 _ = tasks_rx => {
365 let mut children = Box::pin(self.children.resolve().fuse());
371
372 select! {
375 _ = local_rx => {
376 let sc = Owner::current_shared_context().expect("no shared context");
377 sc.set_incomplete_chunk(self.id);
378 None
379 }
380 children = children => {
381 eff.dispose();
383
384 Some(OwnedView::new_with_owner(children, owner))
385 }
386 }
387 }
388 }
389 },
390 )));
391 match fut.as_mut().now_or_never() {
392 Some(Some(resolved)) => {
393 Either::<Fal, _>::Right(resolved)
394 .to_html_async_with_buf::<OUT_OF_ORDER>(
395 buf,
396 position,
397 escape,
398 mark_branches,
399 extra_attrs,
400 );
401 }
402 Some(None) => {
403 Either::<_, Chil>::Left(self.fallback)
404 .to_html_async_with_buf::<OUT_OF_ORDER>(
405 buf,
406 position,
407 escape,
408 mark_branches,
409 extra_attrs,
410 );
411 }
412 None => {
413 let id = buf.clone_id();
414
415 if OUT_OF_ORDER {
418 let mut fallback_position = *position;
419 buf.push_fallback(
420 self.fallback,
421 &mut fallback_position,
422 mark_branches,
423 extra_attrs.clone(),
424 );
425 buf.push_async_out_of_order_with_nonce(
426 fut,
427 position,
428 mark_branches,
429 nonce_or_not(),
430 extra_attrs,
431 );
432 } else {
433 self.fallback.dry_resolve();
437
438 buf.push_async({
439 let mut position = *position;
440 async move {
441 let value = match fut.await {
442 None => Either::Left(self.fallback),
443 Some(value) => Either::Right(value),
444 };
445 let mut builder = StreamBuilder::new(id);
446 value.to_html_async_with_buf::<OUT_OF_ORDER>(
447 &mut builder,
448 &mut position,
449 escape,
450 mark_branches,
451 extra_attrs,
452 );
453 builder.finish().take_chunks()
454 }
455 });
456 *position = Position::NextChild;
457 }
458 }
459 };
460 }
461
462 fn hydrate<const FROM_SERVER: bool>(
463 self,
464 cursor: &Cursor,
465 position: &PositionState,
466 ) -> Self::State {
467 let cursor = cursor.to_owned();
468 let position = position.to_owned();
469
470 let mut children = Some(self.children);
471 let mut fallback = Some(self.fallback);
472 let none_pending = self.none_pending;
473 let mut nth_run = 0;
474 let outer_owner = Owner::new();
475
476 RenderEffect::new(move |prev| {
477 let show_b = !none_pending.get() && (!TRANSITION || nth_run < 1);
483 nth_run += 1;
484 let this = OwnedView::new_with_owner(
485 EitherKeepAlive {
486 a: children.take(),
487 b: fallback.take(),
488 show_b,
489 },
490 outer_owner.clone(),
491 );
492
493 if let Some(mut state) = prev {
494 this.rebuild(&mut state);
495 state
496 } else {
497 this.hydrate::<FROM_SERVER>(&cursor, &position)
498 }
499 })
500 }
501
502 fn into_owned(self) -> Self::Owned {
503 self
504 }
505}
506
507pub struct Unsuspend<T>(Box<dyn FnOnce() -> T + Send>);
510
511impl<T> Unsuspend<T> {
512 pub fn new(fun: impl FnOnce() -> T + Send + 'static) -> Self {
514 Self(Box::new(fun))
515 }
516}
517
518impl<T> Render for Unsuspend<T>
519where
520 T: Render,
521{
522 type State = T::State;
523
524 fn build(self) -> Self::State {
525 (self.0)().build()
526 }
527
528 fn rebuild(self, state: &mut Self::State) {
529 (self.0)().rebuild(state);
530 }
531}
532
533impl<T> AddAnyAttr for Unsuspend<T>
534where
535 T: AddAnyAttr + 'static,
536{
537 type Output<SomeNewAttr: Attribute> =
538 Unsuspend<T::Output<SomeNewAttr::CloneableOwned>>;
539
540 fn add_any_attr<NewAttr: Attribute>(
541 self,
542 attr: NewAttr,
543 ) -> Self::Output<NewAttr>
544 where
545 Self::Output<NewAttr>: RenderHtml,
546 {
547 let attr = attr.into_cloneable_owned();
548 Unsuspend::new(move || (self.0)().add_any_attr(attr))
549 }
550}
551
552impl<T> RenderHtml for Unsuspend<T>
553where
554 T: RenderHtml + 'static,
555{
556 type AsyncOutput = Self;
557 type Owned = Self;
558
559 const MIN_LENGTH: usize = T::MIN_LENGTH;
560
561 fn dry_resolve(&mut self) {}
562
563 async fn resolve(self) -> Self::AsyncOutput {
564 self
565 }
566
567 fn to_html_with_buf(
568 self,
569 buf: &mut String,
570 position: &mut Position,
571 escape: bool,
572 mark_branches: bool,
573 extra_attrs: Vec<AnyAttribute>,
574 ) {
575 (self.0)().to_html_with_buf(
576 buf,
577 position,
578 escape,
579 mark_branches,
580 extra_attrs,
581 );
582 }
583
584 fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
585 self,
586 buf: &mut StreamBuilder,
587 position: &mut Position,
588 escape: bool,
589 mark_branches: bool,
590 extra_attrs: Vec<AnyAttribute>,
591 ) where
592 Self: Sized,
593 {
594 (self.0)().to_html_async_with_buf::<OUT_OF_ORDER>(
595 buf,
596 position,
597 escape,
598 mark_branches,
599 extra_attrs,
600 );
601 }
602
603 fn hydrate<const FROM_SERVER: bool>(
604 self,
605 cursor: &Cursor,
606 position: &PositionState,
607 ) -> Self::State {
608 (self.0)().hydrate::<FROM_SERVER>(cursor, position)
609 }
610
611 fn into_owned(self) -> Self::Owned {
612 self
613 }
614}