use alloc::boxed::Box;
use alloc::rc::Rc;
use alloc::vec::Vec;
use core::cell::{Cell, RefCell};
use crate::trace_targets;
thread_local! {
static OWNER_STACK: RefCell<Vec<Option<Rc<OwnerFrame>>>> = const { RefCell::new(Vec::new()) };
}
pub(crate) trait OwnedDisposable {
fn dispose_owned(&self);
}
#[derive(Default)]
pub(crate) struct OwnerFrame {
children: RefCell<Vec<Rc<dyn OwnedDisposable>>>,
cleanups: RefCell<Vec<Box<dyn FnOnce()>>>,
disposed: Cell<bool>,
}
impl OwnerFrame {
pub(crate) fn new() -> Rc<Self> {
Rc::new(Self::default())
}
pub(crate) fn adopt(&self, child: Rc<dyn OwnedDisposable>) {
if self.disposed.get() {
child.dispose_owned();
return;
}
self.children.borrow_mut().push(child);
}
pub(crate) fn add_cleanup(&self, cleanup: Box<dyn FnOnce()>) {
if self.disposed.get() {
cleanup();
return;
}
self.cleanups.borrow_mut().push(cleanup);
}
pub(crate) fn reset(&self) {
struct RunRemaining(Vec<Box<dyn FnOnce()>>);
impl Drop for RunRemaining {
fn drop(&mut self) {
while let Some(cleanup) = self.0.pop() {
cleanup();
}
}
}
struct DisposeRemaining(Vec<Rc<dyn OwnedDisposable>>);
impl Drop for DisposeRemaining {
fn drop(&mut self) {
while let Some(child) = self.0.pop() {
child.dispose_owned();
}
}
}
crate::untrack(|| {
drop(RunRemaining(core::mem::take(
&mut *self.cleanups.borrow_mut(),
)));
drop(DisposeRemaining(core::mem::take(
&mut *self.children.borrow_mut(),
)));
});
}
pub(crate) fn dispose(&self) {
if self.disposed.replace(true) {
return;
}
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();
}
}
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 {
OWNER_STACK.with(|stack| stack.borrow_mut().push(entry));
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
OWNER_STACK.with(|stack| {
stack.borrow_mut().pop();
});
}
}
let _guard = Guard;
f()
}
pub(crate) fn current_owner() -> Option<Rc<OwnerFrame>> {
OWNER_STACK.with(|stack| stack.borrow().last().cloned().flatten())
}
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 {
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 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();
}
}
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::{ScopeHandle, on_cleanup, scope};
use crate::{Reactor, signal_in};
#[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"
);
}
}