Skip to content

Commit 4f3a26c

Browse files
authored
fix: track resources in Suspense that are read conditionally behind other resource reads (see #4430) (#4444)
1 parent 83a848b commit 4f3a26c

File tree

1 file changed

+63
-14
lines changed

1 file changed

+63
-14
lines changed

leptos/src/suspense_component.rs

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::{
66
use futures::{channel::oneshot, select, FutureExt};
77
use hydration_context::SerializedDataId;
88
use leptos_macro::component;
9+
use or_poisoned::OrPoisoned;
910
use reactive_graph::{
1011
computed::{
1112
suspense::{LocalResourceNotifier, SuspenseContext},
@@ -14,10 +15,10 @@ use reactive_graph::{
1415
effect::RenderEffect,
1516
owner::{provide_context, use_context, Owner},
1617
signal::ArcRwSignal,
17-
traits::{Dispose, Get, Read, Track, With, WriteValue},
18+
traits::{Dispose, Get, Read, ReadUntracked, Track, With, WriteValue},
1819
};
1920
use slotmap::{DefaultKey, SlotMap};
20-
use std::sync::Arc;
21+
use std::sync::{Arc, Mutex};
2122
use tachys::{
2223
either::Either,
2324
html::attribute::{any_attribute::AnyAttribute, Attribute},
@@ -320,23 +321,66 @@ where
320321

321322
// walk over the tree of children once to make sure that all resource loads are registered
322323
self.children.dry_resolve();
324+
let children = Arc::new(Mutex::new(Some(self.children)));
323325

324326
// check the set of tasks to see if it is empty, now or later
325327
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-
// If the receiver has dropped, it means the ScopedFuture has already
332-
// dropped, so it doesn't matter if we manage to send this.
333-
_ = tx.send(());
334-
}
335-
if let Some(tx) = notify_error_boundary.take() {
336-
_ = tx.send(());
328+
let children = Arc::clone(&children);
329+
move |double_checking: Option<bool>| {
330+
// on the first run, always track the tasks
331+
if double_checking.is_none() {
332+
tasks.track();
333+
}
334+
335+
if let Some(curr_tasks) = tasks.try_read_untracked() {
336+
if curr_tasks.is_empty() {
337+
if double_checking == Some(true) {
338+
// we have finished loading, and checking the children again told us there are
339+
// no more pending tasks. so we can render both the children and the error boundary
340+
341+
if let Some(tx) = tasks_tx.take() {
342+
// If the receiver has dropped, it means the ScopedFuture has already
343+
// dropped, so it doesn't matter if we manage to send this.
344+
_ = tx.send(());
345+
}
346+
if let Some(tx) = notify_error_boundary.take() {
347+
_ = tx.send(());
348+
}
349+
} else {
350+
// release the read guard on tasks, as we'll be updating it again
351+
drop(curr_tasks);
352+
// check the children for additional pending tasks
353+
// the will catch additional resource reads nested inside a conditional depending on initial resource reads
354+
if let Some(children) =
355+
children.lock().or_poisoned().as_mut()
356+
{
357+
children.dry_resolve();
358+
}
359+
360+
if tasks
361+
.try_read()
362+
.map(|n| n.is_empty())
363+
.unwrap_or(false)
364+
{
365+
// there are no additional pending tasks, and we can simply return
366+
if let Some(tx) = tasks_tx.take() {
367+
// If the receiver has dropped, it means the ScopedFuture has already
368+
// dropped, so it doesn't matter if we manage to send this.
369+
_ = tx.send(());
370+
}
371+
if let Some(tx) = notify_error_boundary.take() {
372+
_ = tx.send(());
373+
}
374+
}
375+
376+
// tell ourselves that we're just double-checking
377+
return true;
337378
}
379+
} else {
380+
tasks.track();
338381
}
339382
}
383+
false
340384
}
341385
});
342386

@@ -362,12 +406,17 @@ where
362406
None
363407
}
364408
_ = tasks_rx => {
409+
let children = {
410+
let mut children_lock = children.lock().or_poisoned();
411+
children_lock.take().expect("children should not be removed until we render here")
412+
};
413+
365414
// if we ran this earlier, reactive reads would always be registered as None
366415
// this is fine in the case where we want to use Suspend and .await on some future
367416
// but in situations like a <For each=|| some_resource.snapshot()/> we actually
368417
// want to be able to 1) synchronously read a resource's value, but still 2) wait
369418
// for it to load before we render everything
370-
let mut children = Box::pin(self.children.resolve().fuse());
419+
let mut children = Box::pin(children.resolve().fuse());
371420

372421
// we continue racing the children against the "do we have any local
373422
// resources?" Future

0 commit comments

Comments
 (0)