use alloc::boxed::Box;
use alloc::rc::{Rc, Weak};
use alloc::vec::Vec;
use core::any::Any;
use core::cell::{Cell, RefCell};
use core::panic::Location;
use std::panic::{AssertUnwindSafe, catch_unwind};
use crate::ownership::{self, OwnerTally};
use crate::{NodeId, trace_targets};
thread_local! {
static OWNER_STACK: RefCell<Vec<Option<Rc<OwnerFrame>>>> = const { RefCell::new(Vec::new()) };
static TEARDOWN_DEPTH: Cell<u32> = const { Cell::new(0) };
}
struct TeardownMark;
impl TeardownMark {
fn enter() -> Self {
let _ = TEARDOWN_DEPTH.try_with(|depth| depth.set(depth.get() + 1));
Self
}
}
impl Drop for TeardownMark {
fn drop(&mut self) {
let _ = TEARDOWN_DEPTH.try_with(|depth| depth.set(depth.get().saturating_sub(1)));
}
}
fn in_teardown() -> bool {
TEARDOWN_DEPTH.try_with(Cell::get).unwrap_or(0) > 0
}
type ErrorHandler = Rc<dyn Fn(ErrorInfo)>;
pub(crate) trait OwnedDisposable {
fn dispose_owned(&self);
fn is_disposed_owned(&self) -> bool {
false
}
}
pub(crate) struct OwnerFrame {
children: RefCell<Vec<Rc<dyn OwnedDisposable>>>,
swept_at: Cell<usize>,
cleanups: RefCell<Vec<Box<dyn FnOnce()>>>,
disposed: Cell<bool>,
_tally: OwnerTally,
parent: RefCell<Option<Weak<OwnerFrame>>>,
error_handler: RefCell<Option<ErrorHandler>>,
}
impl OwnerFrame {
pub(crate) fn new() -> Rc<Self> {
let frame = Rc::new(Self {
children: RefCell::new(Vec::new()),
swept_at: Cell::new(0),
cleanups: RefCell::new(Vec::new()),
disposed: Cell::new(false),
_tally: OwnerTally::new(),
parent: RefCell::new(current_owner().as_ref().map(Rc::downgrade)),
error_handler: RefCell::new(None),
});
ownership::register(&frame);
frame
}
#[cfg(debug_assertions)]
pub(crate) fn pending_cleanups(&self) -> usize {
self.cleanups.borrow().len()
}
#[cfg(debug_assertions)]
pub(crate) fn owned_children(&self) -> usize {
self.children.borrow().len()
}
fn error_handler(self: &Rc<Self>) -> Option<ErrorHandler> {
let mut frame = Rc::clone(self);
loop {
if let Some(handler) = frame.error_handler.borrow().as_ref() {
return Some(Rc::clone(handler));
}
let parent = frame.parent.borrow().as_ref().and_then(Weak::upgrade);
frame = parent?;
}
}
pub(crate) fn adopt(&self, child: Rc<dyn OwnedDisposable>) {
if self.disposed.get() {
child.dispose_owned();
return;
}
let _released = self.release_disposed_children();
self.children.borrow_mut().push(child);
ownership::child_adopted();
}
fn release_disposed_children(&self) -> Vec<Rc<dyn OwnedDisposable>> {
let mut children = self.children.borrow_mut();
if children.len() < 8 || children.len() < self.swept_at.get().saturating_mul(2) {
return Vec::new();
}
let released = children
.extract_if(.., |child| child.is_disposed_owned())
.collect::<Vec<_>>();
self.swept_at.set(children.len());
drop(children);
ownership::children_taken(released.len());
released
}
pub(crate) fn add_cleanup(&self, cleanup: Box<dyn FnOnce()>) {
if self.disposed.get() {
ownership::cleanup_run_immediately();
cleanup();
return;
}
self.cleanups.borrow_mut().push(cleanup);
ownership::cleanup_registered();
}
pub(crate) fn reset(&self) {
let mut cleanups = core::mem::take(&mut *self.cleanups.borrow_mut());
let mut children = core::mem::take(&mut *self.children.borrow_mut());
ownership::cleanups_taken(cleanups.len());
ownership::children_taken(children.len());
self.swept_at.set(0);
let mut first_panic: Option<Box<dyn Any + Send>> = None;
crate::untrack(|| {
with_owner_entry(None, || {
let _teardown = TeardownMark::enter();
for cleanup in cleanups.drain(..).rev() {
capture_panic(&mut first_panic, cleanup);
}
for child in children.drain(..).rev() {
capture_panic(&mut first_panic, || child.dispose_owned());
}
});
});
{
let mut slot = self.cleanups.borrow_mut();
if slot.is_empty() && slot.capacity() < cleanups.capacity() {
*slot = cleanups;
}
}
{
let mut slot = self.children.borrow_mut();
if slot.is_empty() && slot.capacity() < children.capacity() {
*slot = children;
}
}
let Some(payload) = first_panic else {
return;
};
if std::thread::panicking() {
tracing::error!(
target: trace_targets::SCOPE,
event = "cleanup_panic_during_unwind",
"a reactive cleanup panicked while the thread was already unwinding; teardown \
completed and the panic was discarded to avoid aborting the process"
);
return;
}
std::panic::resume_unwind(payload);
}
pub(crate) fn dispose(&self) {
if self.disposed.replace(true) {
return;
}
ownership::owner_disposed();
tracing::debug!(
target: trace_targets::SCOPE,
event = "dispose_owner",
"disposing reactive owner"
);
self.reset();
}
pub(crate) fn is_disposed(&self) -> bool {
self.disposed.get()
}
}
impl Drop for OwnerFrame {
fn drop(&mut self) {
self.dispose();
}
}
fn capture_panic(slot: &mut Option<Box<dyn Any + Send>>, f: impl FnOnce()) {
let Err(payload) = catch_unwind(AssertUnwindSafe(f)) else {
return;
};
if slot.is_none() {
*slot = Some(payload);
} else {
tracing::error!(
target: trace_targets::SCOPE,
event = "cleanup_panic_discarded",
"a reactive cleanup panicked during teardown that was already failing; the first \
panic is the one that propagates"
);
}
}
pub(crate) fn with_owner<T>(frame: &Rc<OwnerFrame>, f: impl FnOnce() -> T) -> T {
with_owner_entry(Some(Rc::clone(frame)), f)
}
fn with_owner_entry<T>(entry: Option<Rc<OwnerFrame>>, f: impl FnOnce() -> T) -> T {
let pushed = OWNER_STACK
.try_with(|stack| stack.borrow_mut().push(entry))
.is_ok();
struct Guard {
pushed: bool,
}
impl Drop for Guard {
fn drop(&mut self) {
if !self.pushed {
return;
}
let _ = OWNER_STACK.try_with(|stack| {
stack.borrow_mut().pop();
});
}
}
let _guard = Guard { pushed };
f()
}
pub(crate) fn current_owner() -> Option<Rc<OwnerFrame>> {
OWNER_STACK
.try_with(|stack| stack.borrow().last().cloned().flatten())
.unwrap_or(None)
}
pub(crate) fn adopt_into_current(child: Rc<dyn OwnedDisposable>) -> bool {
match current_owner() {
Some(owner) => {
owner.adopt(child);
true
}
None => false,
}
}
#[derive(Clone)]
pub struct Owner {
frame: Rc<OwnerFrame>,
}
impl Owner {
pub fn run_in<T>(&self, f: impl FnOnce() -> T) -> T {
with_owner(&self.frame, f)
}
pub fn is_disposed(&self) -> bool {
self.frame.is_disposed()
}
}
impl core::fmt::Debug for Owner {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Owner")
.field("disposed", &self.frame.is_disposed())
.finish()
}
}
pub fn owner() -> Option<Owner> {
current_owner().map(|frame| Owner { frame })
}
pub fn unowned<T>(f: impl FnOnce() -> T) -> T {
with_owner_entry(None, f)
}
pub fn on_cleanup(cleanup: impl FnOnce() + 'static) {
let Some(owner) = current_owner() else {
if in_teardown() {
panic!(
"adaptite: on_cleanup called from inside a cleanup, while its owner is being \
torn down; the owner's cleanup list has already been taken, so there is no \
point at which this cleanup could run. Register it from the effect body or \
scope that is being cleaned up, or create a `scope` inside the cleanup if you \
need resources released after it"
);
}
panic!(
"adaptite: on_cleanup called outside a reactive owner (an effect run or a scope); \
the cleanup would never execute"
);
};
owner.add_cleanup(Box::new(cleanup));
}
pub struct ErrorInfo {
payload: Box<dyn Any + Send>,
node: NodeId,
origin: Option<&'static Location<'static>>,
}
impl ErrorInfo {
pub fn payload(&self) -> &(dyn Any + Send) {
&*self.payload
}
pub fn into_payload(self) -> Box<dyn Any + Send> {
self.payload
}
pub fn message(&self) -> Option<&str> {
if let Some(message) = self.payload.downcast_ref::<&'static str>() {
return Some(message);
}
self.payload
.downcast_ref::<alloc::string::String>()
.map(alloc::string::String::as_str)
}
pub fn node(&self) -> NodeId {
self.node
}
pub fn origin(&self) -> Option<&'static Location<'static>> {
self.origin
}
}
impl core::fmt::Debug for ErrorInfo {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ErrorInfo")
.field("node", &self.node)
.field("message", &self.message())
.field("origin", &self.origin)
.finish_non_exhaustive()
}
}
impl core::fmt::Display for ErrorInfo {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "effect {} panicked", self.node.get())?;
if let Some(origin) = self.origin {
write!(f, " (created at {origin})")?;
}
if let Some(message) = self.message() {
write!(f, ": {message}")?;
}
Ok(())
}
}
pub(crate) fn build_error_info(
payload: Box<dyn Any + Send>,
node: NodeId,
origin: Option<&'static Location<'static>>,
) -> ErrorInfo {
ErrorInfo {
payload,
node,
origin,
}
}
pub(crate) fn error_handler_for(frame: &Rc<OwnerFrame>) -> Option<ErrorHandler> {
frame.error_handler()
}
pub fn scope_catch<T>(
f: impl FnOnce() -> T,
on_error: impl Fn(ErrorInfo) + 'static,
) -> (ScopeHandle, T) {
let frame = OwnerFrame::new();
*frame.error_handler.borrow_mut() = Some(Rc::new(on_error));
tracing::debug!(
target: trace_targets::SCOPE,
event = "create_scope",
catching = true,
"created reactive scope with an error boundary"
);
let result = with_owner(&frame, f);
let handle = ScopeHandle {
inner: Rc::new(ScopeInner { frame }),
};
let inner: Rc<dyn OwnedDisposable> = handle.inner.clone();
let _ = adopt_into_current(inner);
(handle, result)
}
pub fn scope<T>(f: impl FnOnce() -> T) -> (ScopeHandle, T) {
let frame = OwnerFrame::new();
tracing::debug!(
target: trace_targets::SCOPE,
event = "create_scope",
"created reactive scope"
);
let result = with_owner(&frame, f);
let handle = ScopeHandle {
inner: Rc::new(ScopeInner { frame }),
};
let inner: Rc<dyn OwnedDisposable> = handle.inner.clone();
let _ = adopt_into_current(inner);
(handle, result)
}
#[derive(Clone)]
#[must_use = "scopes are disposed when dropped, so you must keep the handle or explicitly leak it"]
pub struct ScopeHandle {
inner: Rc<ScopeInner>,
}
impl ScopeHandle {
pub fn dispose(&self) {
self.inner.frame.dispose();
}
pub fn is_disposed(&self) -> bool {
self.inner.frame.is_disposed()
}
pub fn leak(self) {
core::mem::forget(self);
}
pub fn owner(&self) -> Owner {
Owner {
frame: Rc::clone(&self.inner.frame),
}
}
}
impl core::fmt::Debug for ScopeHandle {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ScopeHandle")
.field("disposed", &self.inner.frame.is_disposed())
.finish()
}
}
struct ScopeInner {
frame: Rc<OwnerFrame>,
}
impl OwnedDisposable for ScopeInner {
fn dispose_owned(&self) {
self.frame.dispose();
}
fn is_disposed_owned(&self) -> bool {
self.frame.is_disposed()
}
}
impl Drop for ScopeInner {
fn drop(&mut self) {
self.frame.dispose();
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::rc::Rc;
use runite::{queue_macrotask, run};
use super::{ErrorInfo, ScopeHandle, on_cleanup, scope, scope_catch, unowned};
use crate::{Reactor, signal_in};
fn error_log() -> (Rc<RefCell<Vec<String>>>, impl Fn(ErrorInfo) + 'static) {
let log = Rc::new(RefCell::new(Vec::new()));
let handler = {
let log = Rc::clone(&log);
move |error: ErrorInfo| {
log.borrow_mut()
.push(error.message().unwrap_or("<opaque>").to_string());
}
};
(log, handler)
}
#[test]
fn a_boundary_contains_a_panicking_effect_and_spares_its_siblings() {
let reactor = Reactor::new();
let (errors, handler) = error_log();
let value = signal_in(&reactor, 1);
let sibling_runs = Rc::new(RefCell::new(0));
let (boundary, ()) = scope_catch(
{
let reactor = reactor.clone();
let value = value.clone();
let sibling_runs = Rc::clone(&sibling_runs);
move || {
reactor
.effect({
let value = value.clone();
move || {
if value.get() == 2 {
panic!("boom");
}
}
})
.leak();
reactor
.effect({
let value = value.clone();
let sibling_runs = Rc::clone(&sibling_runs);
move || {
value.get();
*sibling_runs.borrow_mut() += 1;
}
})
.leak();
}
},
handler,
);
reactor.flush_now();
assert!(errors.borrow().is_empty());
assert_eq!(*sibling_runs.borrow(), 1);
value.set(2);
reactor.flush_now();
assert_eq!(*errors.borrow(), ["boom"]);
assert_eq!(
*sibling_runs.borrow(),
2,
"the sibling ran despite the panic"
);
value.set(3);
reactor.flush_now();
assert_eq!(*sibling_runs.borrow(), 3);
assert_eq!(
*errors.borrow(),
["boom"],
"reported once, not on every run"
);
boundary.dispose();
}
#[test]
fn the_panicking_effect_is_disposed_and_its_cleanups_run() {
let reactor = Reactor::new();
let (errors, handler) = error_log();
let value = signal_in(&reactor, 1);
let torn_down = Rc::new(RefCell::new(false));
let (boundary, ()) = scope_catch(
{
let reactor = reactor.clone();
let value = value.clone();
let torn_down = Rc::clone(&torn_down);
move || {
reactor
.effect({
let value = value.clone();
let torn_down = Rc::clone(&torn_down);
move || {
on_cleanup({
let torn_down = Rc::clone(&torn_down);
move || *torn_down.borrow_mut() = true
});
if value.get() == 2 {
panic!("boom");
}
}
})
.leak();
}
},
handler,
);
reactor.flush_now();
assert!(!*torn_down.borrow());
value.set(2);
reactor.flush_now();
assert_eq!(*errors.borrow(), ["boom"]);
assert!(
*torn_down.borrow(),
"disposal must run the failed effect's cleanups"
);
boundary.dispose();
}
#[test]
fn without_a_boundary_a_panic_still_propagates() {
let reactor = Reactor::new();
let value = signal_in(&reactor, 1);
let effect = reactor.effect({
let value = value.clone();
move || {
if value.get() == 2 {
panic!("boom");
}
}
});
reactor.flush_now();
value.set(2);
let result = catch_unwind(AssertUnwindSafe(|| reactor.flush_now()));
assert!(result.is_err(), "an uncaught panic must not be swallowed");
effect.dispose();
}
#[test]
fn the_nearest_boundary_wins_and_boundaries_nest() {
let reactor = Reactor::new();
let (outer_errors, outer_handler) = error_log();
let (inner_errors, inner_handler) = error_log();
let value = signal_in(&reactor, 1);
let (boundary, ()) = scope_catch(
{
let reactor = reactor.clone();
let value = value.clone();
move || {
reactor
.effect({
let value = value.clone();
move || {
if value.get() == 3 {
panic!("outer child");
}
}
})
.leak();
let (inner, ()) = scope_catch(
{
let reactor = reactor.clone();
let value = value.clone();
move || {
reactor
.effect({
let value = value.clone();
move || {
if value.get() == 2 {
panic!("inner child");
}
}
})
.leak();
}
},
inner_handler,
);
inner.leak();
}
},
outer_handler,
);
reactor.flush_now();
value.set(2);
reactor.flush_now();
assert_eq!(*inner_errors.borrow(), ["inner child"]);
assert!(
outer_errors.borrow().is_empty(),
"the inner boundary caught it"
);
value.set(3);
reactor.flush_now();
assert_eq!(*outer_errors.borrow(), ["outer child"]);
assert_eq!(*inner_errors.borrow(), ["inner child"], "unchanged");
boundary.dispose();
}
#[test]
fn a_boundary_catches_effects_nested_below_it() {
let reactor = Reactor::new();
let (errors, handler) = error_log();
let value = signal_in(&reactor, 1);
let (boundary, ()) = scope_catch(
{
let reactor = reactor.clone();
let value = value.clone();
move || {
let (nested, ()) = scope({
let reactor = reactor.clone();
let value = value.clone();
move || {
reactor
.effect({
let value = value.clone();
move || {
if value.get() == 2 {
panic!("deep");
}
}
})
.leak();
}
});
nested.leak();
}
},
handler,
);
reactor.flush_now();
value.set(2);
reactor.flush_now();
assert_eq!(
*errors.borrow(),
["deep"],
"a plain scope between the effect and the boundary is transparent"
);
boundary.dispose();
}
#[test]
fn error_info_reports_the_effects_creation_site() {
let reactor = Reactor::new();
type Captured = Rc<RefCell<Option<(u64, Option<String>, Option<u32>)>>>;
let captured: Captured = Rc::new(RefCell::new(None));
let value = signal_in(&reactor, 1);
let (boundary, creation_line) = scope_catch(
{
let reactor = reactor.clone();
let value = value.clone();
move || {
let body = move || {
if value.get() == 2 {
panic!("located");
}
};
let creation_line = line!() + 1;
reactor.effect(body).leak();
creation_line
}
},
{
let captured = Rc::clone(&captured);
move |error: ErrorInfo| {
*captured.borrow_mut() = Some((
error.node().get(),
error.origin().and_then(|origin| {
origin
.file()
.rsplit(['/', '\\'])
.next()
.map(alloc::string::ToString::to_string)
}),
error.origin().map(core::panic::Location::line),
));
}
},
);
reactor.flush_now();
value.set(2);
reactor.flush_now();
let captured = captured.borrow();
let (node, file, line) = captured.as_ref().expect("the handler ran");
assert!(*node > 0);
assert_eq!(file.as_deref(), Some("scope.rs"));
assert_eq!(
*line,
Some(creation_line),
"the origin points at the effect's creation site, not where it panicked"
);
boundary.dispose();
}
#[test]
fn a_panic_during_dependency_verification_reaches_the_boundary() {
let reactor = Reactor::new();
let (errors, handler) = error_log();
let value = signal_in(&reactor, 1);
let upstream = reactor.memo({
let value = value.clone();
move || {
if value.get() == 2 {
panic!("upstream exploded");
}
value.get()
}
});
let (boundary, ()) = scope_catch(
{
let reactor = reactor.clone();
let upstream = upstream.clone();
move || {
reactor
.effect(move || {
upstream.get();
})
.leak();
}
},
handler,
);
reactor.flush_now();
assert!(errors.borrow().is_empty());
value.set(2);
reactor.flush_now();
assert_eq!(*errors.borrow(), ["upstream exploded"]);
reactor.flush_now();
assert_eq!(*errors.borrow(), ["upstream exploded"]);
boundary.dispose();
}
#[test]
fn an_unowned_effect_is_outside_every_boundary() {
let reactor = Reactor::new();
let (errors, handler) = error_log();
let value = signal_in(&reactor, 1);
let (boundary, effect) = scope_catch(
{
let reactor = reactor.clone();
let value = value.clone();
move || {
unowned(|| {
reactor.effect(move || {
if value.get() == 2 {
panic!("escaped");
}
})
})
}
},
handler,
);
reactor.flush_now();
value.set(2);
let result = catch_unwind(AssertUnwindSafe(|| reactor.flush_now()));
assert!(result.is_err(), "the panic propagates past the boundary");
assert!(errors.borrow().is_empty(), "the handler was not consulted");
effect.dispose();
boundary.dispose();
}
#[test]
fn on_cleanup_outside_an_owner_panics() {
let result = catch_unwind(AssertUnwindSafe(|| on_cleanup(|| {})));
let panic = result.expect_err("on_cleanup should panic outside an owner");
let message = panic
.downcast_ref::<&str>()
.expect("panic payload should be a string");
assert!(message.contains("outside a reactive owner"));
}
#[test]
fn disposing_a_scope_disposes_owned_effects_and_runs_cleanups() {
let seen = Rc::new(RefCell::new(Vec::new()));
let events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
queue_macrotask({
let seen = Rc::clone(&seen);
let events = Rc::clone(&events);
move || {
let reactor = Reactor::new();
let source = signal_in(&reactor, 1usize);
let (handle, source) = scope({
let seen = Rc::clone(&seen);
let events = Rc::clone(&events);
move || {
let _ = reactor.effect({
let seen = Rc::clone(&seen);
let source = source.clone();
move || seen.borrow_mut().push(source.get())
});
on_cleanup({
let events = Rc::clone(&events);
move || events.borrow_mut().push("scope cleanup")
});
source
}
});
runite::queue_macrotask({
let events = Rc::clone(&events);
move || {
source.set(2);
runite::queue_macrotask(move || {
assert!(!handle.is_disposed());
handle.dispose();
assert!(handle.is_disposed());
events.borrow_mut().push("disposed");
source.set(3);
});
}
});
}
});
run();
assert_eq!(&*seen.borrow(), &[1, 2]);
assert_eq!(&*events.borrow(), &["scope cleanup", "disposed"]);
}
#[test]
fn effect_reruns_dispose_nested_effects_and_run_cleanups() {
let events = Rc::new(RefCell::new(Vec::<String>::new()));
let scope_slot = Rc::new(RefCell::new(None::<ScopeHandle>));
queue_macrotask({
let events = Rc::clone(&events);
let scope_slot = Rc::clone(&scope_slot);
move || {
let reactor = Reactor::new();
let outer_dep = signal_in(&reactor, 0usize);
let inner_dep = signal_in(&reactor, 0usize);
let (handle, ()) = scope({
let events = Rc::clone(&events);
let outer_dep = outer_dep.clone();
let inner_dep = inner_dep.clone();
let inner_reactor = reactor.clone();
move || {
let _ = reactor.effect({
let events = Rc::clone(&events);
let outer_dep = outer_dep.clone();
let inner_dep = inner_dep.clone();
move || {
let generation = outer_dep.get();
events.borrow_mut().push(format!("outer {generation}"));
on_cleanup({
let events = Rc::clone(&events);
move || {
events.borrow_mut().push(format!("cleanup {generation}"))
}
});
let _ = crate::effect_in(&inner_reactor, {
let events = Rc::clone(&events);
let inner_dep = inner_dep.clone();
move || {
events.borrow_mut().push(format!(
"inner {generation}: {}",
inner_dep.get()
));
}
});
}
});
}
});
*scope_slot.borrow_mut() = Some(handle);
runite::queue_macrotask({
let inner_dep = inner_dep.clone();
move || {
inner_dep.set(1);
runite::queue_macrotask({
let outer_dep = outer_dep.clone();
move || {
outer_dep.set(1);
runite::queue_macrotask(move || {
inner_dep.set(2);
});
}
});
}
});
}
});
run();
assert_eq!(
&*events.borrow(),
&[
"outer 0",
"inner 0: 0",
"inner 0: 1",
"cleanup 0",
"outer 1",
"inner 1: 1",
"inner 1: 2",
]
);
}
#[test]
fn a_panicking_scope_closure_still_runs_registered_cleanups() {
use std::cell::Cell;
let ran = Rc::new(Cell::new(false));
let result = catch_unwind(AssertUnwindSafe({
let ran = Rc::clone(&ran);
move || {
let _ = scope(move || {
on_cleanup(move || ran.set(true));
panic!("scope body panics");
});
}
}));
assert!(result.is_err());
assert!(
ran.get(),
"cleanups registered before the panic must still run"
);
}
#[test]
fn problem_effects_created_after_an_await_escape_their_scope() {
let seen = Rc::new(RefCell::new(Vec::new()));
queue_macrotask({
let seen = Rc::clone(&seen);
move || {
let reactor = Reactor::new();
let data = signal_in(&reactor, 0usize);
let (handle, ()) = scope({
let reactor = reactor.clone();
let data = data.clone();
let seen = Rc::clone(&seen);
move || {
std::mem::drop(runite::spawn(async move {
runite::yield_now().await;
crate::effect_in(&reactor, {
let data = data.clone();
let seen = Rc::clone(&seen);
move || seen.borrow_mut().push(data.get())
})
.leak();
}));
}
});
runite::queue_macrotask({
let data = data.clone();
move || {
handle.dispose();
data.set(1);
}
});
}
});
run();
assert_eq!(
&*seen.borrow(),
&[0, 1],
"without owner capture, the effect escapes its scope (this documents the problem)"
);
}
#[test]
fn owner_run_in_reattaches_async_work_to_its_scope() {
let seen = Rc::new(RefCell::new(Vec::new()));
queue_macrotask({
let seen = Rc::clone(&seen);
move || {
let reactor = Reactor::new();
let data = signal_in(&reactor, 0usize);
let (handle, ()) = scope({
let reactor = reactor.clone();
let data = data.clone();
let seen = Rc::clone(&seen);
move || {
let scope_owner = super::owner().expect("scope should be the owner");
std::mem::drop(runite::spawn(async move {
runite::yield_now().await;
scope_owner.run_in(|| {
let _ = crate::effect_in(&reactor, {
let data = data.clone();
let seen = Rc::clone(&seen);
move || seen.borrow_mut().push(data.get())
});
});
}));
}
});
runite::queue_macrotask({
let data = data.clone();
move || {
handle.dispose();
data.set(1);
}
});
}
});
run();
assert_eq!(
&*seen.borrow(),
&[0],
"the re-attached effect must be disposed with its scope"
);
}
#[test]
fn run_in_on_a_disposed_owner_disposes_children_immediately() {
let seen = Rc::new(RefCell::new(Vec::new()));
queue_macrotask({
let seen = Rc::clone(&seen);
move || {
let reactor = Reactor::new();
let data = signal_in(&reactor, 0usize);
let (handle, ()) = scope(|| {});
let scope_owner = handle.owner();
handle.dispose();
assert!(scope_owner.is_disposed());
scope_owner.run_in(|| {
let _ = crate::effect_in(&reactor, {
let data = data.clone();
let seen = Rc::clone(&seen);
move || seen.borrow_mut().push(data.get())
});
});
reactor.flush_now();
}
});
run();
assert!(
seen.borrow().is_empty(),
"children created under a disposed owner must never run"
);
}
#[test]
fn unowned_suppresses_the_enclosing_owner_at_the_boundary_only() {
let (_handle, ()) = scope(|| {
assert!(super::owner().is_some(), "the scope is the current owner");
super::unowned(|| {
assert!(
super::owner().is_none(),
"no owner is current inside unowned"
);
let (_inner, ()) = scope(|| {
assert!(
super::owner().is_some(),
"a scope created inside unowned is the current owner"
);
});
});
assert!(
super::owner().is_some(),
"the enclosing owner is restored after unowned returns"
);
});
}
#[test]
fn unowned_effects_escape_their_scope_and_die_with_their_handle() {
let seen = Rc::new(RefCell::new(Vec::new()));
queue_macrotask({
let seen = Rc::clone(&seen);
move || {
let reactor = Reactor::new();
let data = signal_in(&reactor, 0usize);
let (handle, background) = scope({
let reactor = reactor.clone();
let data = data.clone();
let seen = Rc::clone(&seen);
move || {
super::unowned(|| {
crate::effect_in(&reactor, {
let data = data.clone();
let seen = Rc::clone(&seen);
move || seen.borrow_mut().push(data.get())
})
})
}
});
runite::queue_macrotask({
let data = data.clone();
move || {
handle.dispose();
data.set(1);
runite::queue_macrotask(move || {
drop(background);
data.set(2);
});
}
});
}
});
run();
assert_eq!(
&*seen.borrow(),
&[0, 1],
"the effect must survive scope disposal and die when its handle drops"
);
}
#[test]
fn nested_scopes_are_disposed_with_their_parent() {
let (parent, child) = scope(|| {
let (child, ()) = scope(|| {});
child
});
assert!(!child.is_disposed());
parent.dispose();
assert!(
child.is_disposed(),
"disposing the parent scope must dispose nested scopes"
);
}
}