Skip to content

Commit fa2be59

Browse files
authored
feat: better error handling for ScopedFuture (#1810)
1 parent 321c522 commit fa2be59

File tree

1 file changed

+79
-57
lines changed

1 file changed

+79
-57
lines changed

leptos_reactive/src/runtime.rs

Lines changed: 79 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use std::{
2626
rc::Rc,
2727
task::Poll,
2828
};
29+
use thiserror::Error;
2930

3031
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
3132

@@ -97,9 +98,6 @@ pub struct Owner(pub(crate) NodeId);
9798

9899
impl Owner {
99100
/// Returns the current reactive owner.
100-
///
101-
/// ## Panics
102-
/// Panics if there is no current reactive runtime.
103101
pub fn current() -> Option<Owner> {
104102
with_runtime(|runtime| runtime.owner.get())
105103
.ok()
@@ -686,16 +684,19 @@ impl Debug for Runtime {
686684
instrument(level = "trace", skip_all,)
687685
)]
688686
#[inline(always)] // it monomorphizes anyway
689-
pub(crate) fn with_runtime<T>(f: impl FnOnce(&Runtime) -> T) -> Result<T, ()> {
687+
pub(crate) fn with_runtime<T>(
688+
f: impl FnOnce(&Runtime) -> T,
689+
) -> Result<T, ReactiveSystemError> {
690690
// in the browser, everything should exist under one runtime
691691
cfg_if! {
692692
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
693693
Ok(RUNTIME.with(|runtime| f(runtime)))
694694
} else {
695695
RUNTIMES.with(|runtimes| {
696696
let runtimes = runtimes.borrow();
697-
match runtimes.get(Runtime::current()) {
698-
None => Err(()),
697+
let rt = Runtime::current();
698+
match runtimes.get(rt) {
699+
None => Err(ReactiveSystemError::RuntimeDisposed(rt)),
699700
Some(runtime) => Ok(f(runtime))
700701
}
701702
})
@@ -820,38 +821,49 @@ where
820821
/// ## Panics
821822
/// Panics if there is no current reactive runtime.
822823
pub fn with_owner<T>(owner: Owner, f: impl FnOnce() -> T) -> T {
823-
try_with_owner(owner, f)
824-
.expect("runtime/scope should be alive when with_owner runs")
824+
try_with_owner(owner, f).unwrap()
825+
}
826+
827+
#[derive(Error, Debug)]
828+
pub enum ReactiveSystemError {
829+
#[error("Runtime {0:?} has been disposed.")]
830+
RuntimeDisposed(RuntimeId),
831+
#[error("Owner {0:?} has been disposed.")]
832+
OwnerDisposed(Owner),
833+
#[error("Error borrowing runtime.nodes {0:?}")]
834+
Borrow(std::cell::BorrowError),
825835
}
826836

827837
/// Runs the given code with the given reactive owner.
828-
pub fn try_with_owner<T>(owner: Owner, f: impl FnOnce() -> T) -> Option<T> {
838+
pub fn try_with_owner<T>(
839+
owner: Owner,
840+
f: impl FnOnce() -> T,
841+
) -> Result<T, ReactiveSystemError> {
829842
with_runtime(|runtime| {
830-
runtime
831-
.nodes
832-
.try_borrow()
833-
.map(|nodes| nodes.contains_key(owner.0))
834-
.map(|scope_exists| {
835-
scope_exists.then(|| {
836-
let prev_observer = runtime.observer.take();
837-
let prev_owner = runtime.owner.take();
843+
let scope_exists = {
844+
let nodes = runtime
845+
.nodes
846+
.try_borrow()
847+
.map_err(ReactiveSystemError::Borrow)?;
848+
nodes.contains_key(owner.0)
849+
};
850+
if scope_exists {
851+
let prev_observer = runtime.observer.take();
852+
let prev_owner = runtime.owner.take();
838853

839-
runtime.owner.set(Some(owner.0));
840-
runtime.observer.set(Some(owner.0));
854+
runtime.owner.set(Some(owner.0));
855+
runtime.observer.set(Some(owner.0));
841856

842-
let v = f();
857+
let v = f();
843858

844-
runtime.observer.set(prev_observer);
845-
runtime.owner.set(prev_owner);
859+
runtime.observer.set(prev_observer);
860+
runtime.owner.set(prev_owner);
846861

847-
v
848-
})
849-
})
850-
.ok()
851-
.flatten()
852-
})
853-
.ok()
854-
.flatten()
862+
Ok(v)
863+
} else {
864+
Err(ReactiveSystemError::OwnerDisposed(owner))
865+
}
866+
})?
855867
}
856868

857869
/// Runs the given function as a child of the current Owner, once.
@@ -1494,23 +1506,25 @@ pub struct ScopedFuture<Fut: Future> {
14941506
future: Fut,
14951507
}
14961508

1509+
/// Errors that can occur when trying to spawn a [`ScopedFuture`].
1510+
#[derive(Error, Debug, Clone)]
1511+
pub enum ScopedFutureError {
1512+
#[error(
1513+
"Tried to spawn a scoped Future without a current reactive Owner."
1514+
)]
1515+
NoCurrentOwner,
1516+
}
1517+
14971518
impl<Fut: Future + 'static> Future for ScopedFuture<Fut> {
14981519
type Output = Option<Fut::Output>;
14991520

15001521
fn poll(
15011522
self: Pin<&mut Self>,
15021523
cx: &mut std::task::Context<'_>,
15031524
) -> Poll<Self::Output> {
1504-
// TODO: we need to think about how to make this
1505-
// not panic for scopes that have been cleaned up...
1506-
// or perhaps we can force the scope to not be cleaned
1507-
// up until all futures that have a handle to them are
1508-
// dropped...
1509-
15101525
let this = self.project();
15111526

1512-
if let Some(poll) = try_with_owner(*this.owner, || this.future.poll(cx))
1513-
{
1527+
if let Ok(poll) = try_with_owner(*this.owner, || this.future.poll(cx)) {
15141528
match poll {
15151529
Poll::Ready(res) => Poll::Ready(Some(res)),
15161530
Poll::Pending => Poll::Pending,
@@ -1529,33 +1543,31 @@ impl<Fut: Future> ScopedFuture<Fut> {
15291543
}
15301544

15311545
/// Runs the future in the current [`Owner`]'s scope context.
1532-
///
1533-
/// # Panics
1534-
/// Panics if there is no current [`Owner`] context available.
15351546
#[track_caller]
1536-
pub fn new_current(fut: Fut) -> Self {
1537-
Self {
1538-
owner: Owner::current().expect(
1539-
"`ScopedFuture::new_current()` to be called within an `Owner` \
1540-
context",
1541-
),
1542-
future: fut,
1543-
}
1547+
pub fn new_current(fut: Fut) -> Result<Self, ScopedFutureError> {
1548+
Owner::current()
1549+
.map(|owner| Self { owner, future: fut })
1550+
.ok_or(ScopedFutureError::NoCurrentOwner)
15441551
}
15451552
}
15461553

15471554
/// Runs a future that has access to the provided [`Owner`]'s
15481555
/// scope context.
1556+
#[track_caller]
15491557
pub fn spawn_local_with_owner(
15501558
owner: Owner,
15511559
fut: impl Future<Output = ()> + 'static,
15521560
) {
15531561
let scoped_future = ScopedFuture::new(owner, fut);
1562+
#[cfg(debug_assertions)]
1563+
let loc = std::panic::Location::caller();
15541564

15551565
crate::spawn_local(async move {
15561566
if scoped_future.await.is_none() {
1557-
// TODO: should we warn here?
1558-
// /* warning message */
1567+
crate::macros::debug_warn!(
1568+
"`spawn_local_with_owner` called at {loc} returned `None`, \
1569+
i.e., its Owner was disposed before the `Future` resolved."
1570+
);
15591571
}
15601572
});
15611573
}
@@ -1566,15 +1578,23 @@ pub fn spawn_local_with_owner(
15661578
/// # Panics
15671579
/// Panics if there is no [`Owner`] context available.
15681580
#[track_caller]
1569-
pub fn spawn_local_with_current_owner(fut: impl Future<Output = ()> + 'static) {
1570-
let scoped_future = ScopedFuture::new_current(fut);
1581+
pub fn spawn_local_with_current_owner(
1582+
fut: impl Future<Output = ()> + 'static,
1583+
) -> Result<(), ScopedFutureError> {
1584+
let scoped_future = ScopedFuture::new_current(fut)?;
1585+
#[cfg(debug_assertions)]
1586+
let loc = std::panic::Location::caller();
15711587

15721588
crate::spawn_local(async move {
15731589
if scoped_future.await.is_none() {
1574-
// TODO: should we warn here?
1575-
// /* warning message */
1590+
crate::macros::debug_warn!(
1591+
"`spawn_local_with_owner` called at {loc} returned `None`, \
1592+
i.e., its Owner was disposed before the `Future` resolved."
1593+
);
15761594
}
15771595
});
1596+
1597+
Ok(())
15781598
}
15791599

15801600
/// Runs a future that has access to the provided [`Owner`]'s
@@ -1616,12 +1636,14 @@ pub fn try_spawn_local_with_owner(
16161636
pub fn try_spawn_local_with_current_owner(
16171637
fut: impl Future<Output = ()> + 'static,
16181638
on_cancelled: impl FnOnce() + 'static,
1619-
) {
1620-
let scoped_future = ScopedFuture::new_current(fut);
1639+
) -> Result<(), ScopedFutureError> {
1640+
let scoped_future = ScopedFuture::new_current(fut)?;
16211641

16221642
crate::spawn_local(async move {
16231643
if scoped_future.await.is_none() {
16241644
on_cancelled();
16251645
}
16261646
});
1647+
1648+
Ok(())
16271649
}

0 commit comments

Comments
 (0)