use core::cell::RefCell;
extern crate alloc;
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::rc::Rc;
use super::runtime::{EffectTiming, NodeId, NodeType, Observer, try_with_runtime, with_runtime};
type EffectFn = Box<dyn FnMut() + 'static>;
type EffectSlot = Option<EffectFn>;
type CleanupSlot = Rc<RefCell<Option<Box<dyn FnOnce()>>>>;
thread_local! {
static EFFECT_FUNCTIONS: RefCell<BTreeMap<NodeId, EffectSlot>> = RefCell::new(BTreeMap::new());
}
thread_local! {
static EFFECT_TIMING: RefCell<BTreeMap<NodeId, EffectTiming>> = const { RefCell::new(BTreeMap::new()) };
}
pub(crate) fn get_effect_timing(effect_id: NodeId) -> Option<EffectTiming> {
EFFECT_TIMING.with(|storage| storage.borrow().get(&effect_id).copied())
}
pub struct Effect {
id: NodeId,
disposed: Rc<RefCell<bool>>,
cleanup_slot: CleanupSlot,
}
impl Effect {
pub fn new<F>(mut f: F) -> Self
where
F: FnMut() + 'static,
{
let id = NodeId::new();
let disposed = Rc::new(RefCell::new(false));
let disposed_clone = disposed.clone();
EFFECT_FUNCTIONS.with(|storage| {
storage.borrow_mut().insert(
id,
Some(Box::new(move || {
if !*disposed_clone.borrow() {
f();
}
})),
);
});
EFFECT_TIMING.with(|storage| {
storage.borrow_mut().insert(id, EffectTiming::Passive);
});
Self::execute_effect(id);
Self {
id,
disposed,
cleanup_slot: Rc::new(RefCell::new(None)),
}
}
pub fn new_with_timing<F>(mut f: F, timing: EffectTiming) -> Self
where
F: FnMut() + 'static,
{
let id = NodeId::new();
let disposed = Rc::new(RefCell::new(false));
let disposed_clone = disposed.clone();
EFFECT_FUNCTIONS.with(|storage| {
storage.borrow_mut().insert(
id,
Some(Box::new(move || {
if !*disposed_clone.borrow() {
f();
}
})),
);
});
EFFECT_TIMING.with(|storage| {
storage.borrow_mut().insert(id, timing);
});
Self::execute_effect(id);
Self {
id,
disposed,
cleanup_slot: Rc::new(RefCell::new(None)),
}
}
#[allow(dead_code)]
pub fn new_with_deps<F, C>(f: F, deps: super::deps::Deps) -> Self
where
F: FnMut() -> Option<C> + 'static,
C: FnOnce() + 'static,
{
Self::new_with_deps_internal(f, deps, EffectTiming::Passive)
}
fn new_with_deps_internal<F, C>(mut f: F, deps: super::deps::Deps, timing: EffectTiming) -> Self
where
F: FnMut() -> Option<C> + 'static,
C: FnOnce() + 'static,
{
let id = NodeId::new();
let disposed = Rc::new(RefCell::new(false));
let cleanup_slot: CleanupSlot = Rc::new(RefCell::new(None));
let deps_for_closure = deps.into_inner();
let cleanup_for_closure = cleanup_slot.clone();
let disposed_for_closure = disposed.clone();
let wrapped = move || {
if *disposed_for_closure.borrow() {
return;
}
let previous_cleanup = { cleanup_for_closure.borrow_mut().take() };
if let Some(prev) = previous_cleanup {
prev();
}
let next = super::runtime::run_without_observer(&mut f);
if let Some(c) = next {
*cleanup_for_closure.borrow_mut() = Some(Box::new(c));
}
for &dep in &deps_for_closure {
super::runtime::subscribe_node_to_observer(dep, id);
}
};
EFFECT_FUNCTIONS.with(|storage| {
storage.borrow_mut().insert(id, Some(Box::new(wrapped)));
});
EFFECT_TIMING.with(|storage| {
storage.borrow_mut().insert(id, timing);
});
Self::execute_effect(id);
Self {
id,
disposed,
cleanup_slot,
}
}
#[allow(dead_code)]
pub fn new_with_deps_and_timing<F, C>(
f: F,
deps: super::deps::Deps,
timing: EffectTiming,
) -> Self
where
F: FnMut() -> Option<C> + 'static,
C: FnOnce() + 'static,
{
Self::new_with_deps_internal(f, deps, timing)
}
pub(crate) fn execute_effect(effect_id: NodeId) {
let timing = EFFECT_TIMING.with(|storage| {
storage
.borrow()
.get(&effect_id)
.copied()
.unwrap_or(EffectTiming::Passive)
});
with_runtime(|rt| {
rt.clear_dependencies(effect_id);
rt.push_observer(Observer {
id: effect_id,
node_type: NodeType::Effect,
timing,
cleanup: None,
});
});
struct EffectFnGuard {
effect_id: NodeId,
effect_fn: Option<EffectFn>,
}
impl Drop for EffectFnGuard {
fn drop(&mut self) {
let still_alive =
EFFECT_TIMING.with(|storage| storage.borrow().contains_key(&self.effect_id));
if still_alive && let Some(f) = self.effect_fn.take() {
EFFECT_FUNCTIONS.with(|storage| {
storage.borrow_mut().insert(self.effect_id, Some(f));
});
}
}
}
let mut guard = EffectFnGuard {
effect_id,
effect_fn: EFFECT_FUNCTIONS.with(|storage| {
storage
.borrow_mut()
.get_mut(&effect_id)
.and_then(Option::take)
}),
};
if let Some(ref mut f) = guard.effect_fn {
f();
}
with_runtime(|rt| {
rt.pop_observer();
});
}
pub fn id(&self) -> NodeId {
self.id
}
pub fn dispose(&self) {
*self.disposed.borrow_mut() = true;
let cleanup = { self.cleanup_slot.borrow_mut().take() };
if let Some(c) = cleanup {
c();
}
let _ = try_with_runtime(|rt| rt.remove_node(self.id));
let mut effect_fn = None;
let _ = EFFECT_FUNCTIONS.try_with(|storage| {
let mut functions = storage.borrow_mut();
effect_fn = functions.get_mut(&self.id).and_then(Option::take);
functions.remove(&self.id);
});
drop(effect_fn);
let _ = EFFECT_TIMING.try_with(|storage| {
storage.borrow_mut().remove(&self.id);
});
}
}
impl Drop for Effect {
fn drop(&mut self) {
self.dispose();
}
}
impl super::runtime::Runtime {
fn execute_scheduled_effect(&self, effect_id: NodeId) {
Effect::execute_effect(effect_id);
}
pub fn flush_updates(&self) {
*self.update_scheduled.borrow_mut() = false;
let pending = core::mem::take(&mut *self.pending_updates.borrow_mut());
for node_id in pending {
let still_registered =
EFFECT_TIMING.with(|storage| storage.borrow().contains_key(&node_id));
if still_registered {
self.execute_scheduled_effect(node_id);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reactive::Signal;
use serial_test::serial;
#[test]
#[serial]
fn test_effect_runs_immediately() {
let run_count = Rc::new(RefCell::new(0));
let run_count_clone = run_count.clone();
let _effect = Effect::new(move || {
*run_count_clone.borrow_mut() += 1;
});
assert_eq!(*run_count.borrow(), 1);
}
#[test]
#[serial]
fn test_effect_tracks_dependency() {
let signal = Signal::new(0);
let run_count = Rc::new(RefCell::new(0));
let run_count_clone = run_count.clone();
let signal_clone = signal.clone();
let _effect = Effect::new(move || {
let _ = signal_clone.get(); *run_count_clone.borrow_mut() += 1;
});
assert_eq!(*run_count.borrow(), 1);
with_runtime(|rt| {
let graph = rt.dependency_graph.borrow();
let signal_node = graph.get(&signal.id()).unwrap();
assert_eq!(signal_node.subscribers.len(), 1);
});
}
#[test]
#[serial]
fn test_effect_reruns_on_signal_change() {
let signal = Signal::new(0);
let values = Rc::new(RefCell::new(alloc::vec::Vec::new()));
let values_clone = values.clone();
let signal_clone = signal.clone();
let _effect = Effect::new(move || {
values_clone.borrow_mut().push(signal_clone.get());
});
assert_eq!(*values.borrow(), alloc::vec![0]);
signal.set(10);
with_runtime(|rt| rt.flush_updates());
assert_eq!(*values.borrow(), alloc::vec![0, 10]);
signal.set(20);
with_runtime(|rt| rt.flush_updates());
assert_eq!(*values.borrow(), alloc::vec![0, 10, 20]);
}
#[test]
#[serial]
fn test_effect_with_multiple_signals() {
let signal1 = Signal::new(1);
let signal2 = Signal::new(2);
let sum = Rc::new(RefCell::new(0));
let sum_clone = sum.clone();
let s1 = signal1.clone();
let s2 = signal2.clone();
let _effect = Effect::new(move || {
*sum_clone.borrow_mut() = s1.get() + s2.get();
});
assert_eq!(*sum.borrow(), 3);
signal1.set(10);
with_runtime(|rt| rt.flush_updates());
assert_eq!(*sum.borrow(), 12);
signal2.set(20);
with_runtime(|rt| rt.flush_updates());
assert_eq!(*sum.borrow(), 30);
}
#[test]
#[serial]
fn test_effect_dispose() {
let signal = Signal::new(0);
let run_count = Rc::new(RefCell::new(0));
let run_count_clone = run_count.clone();
let signal_clone = signal.clone();
let effect = Effect::new(move || {
let _ = signal_clone.get();
*run_count_clone.borrow_mut() += 1;
});
assert_eq!(*run_count.borrow(), 1);
effect.dispose();
signal.set(10);
with_runtime(|rt| rt.flush_updates());
assert_eq!(*run_count.borrow(), 1); }
#[test]
#[serial]
fn test_effect_drop_cleans_up() {
let signal = Signal::new(0);
let run_count = Rc::new(RefCell::new(0));
let run_count_clone = run_count.clone();
{
let signal_clone = signal.clone();
let _effect = Effect::new(move || {
let _ = signal_clone.get();
*run_count_clone.borrow_mut() += 1;
});
assert_eq!(*run_count.borrow(), 1);
}
signal.set(10);
with_runtime(|rt| rt.flush_updates());
assert_eq!(*run_count.borrow(), 1); }
#[rstest::rstest]
#[serial]
fn test_nested_effect_creation() {
let outer_ran = Rc::new(RefCell::new(false));
let inner_ran = Rc::new(RefCell::new(false));
let outer_ran_clone = outer_ran.clone();
let inner_ran_clone = inner_ran.clone();
let _outer = Effect::new(move || {
*outer_ran_clone.borrow_mut() = true;
let inner_ran_inner = inner_ran_clone.clone();
let _inner = Effect::new(move || {
*inner_ran_inner.borrow_mut() = true;
});
});
assert!(*outer_ran.borrow());
assert!(*inner_ran.borrow());
}
#[rstest::rstest]
#[serial]
fn test_effect_creates_signal_and_effect() {
let outer_ran = Rc::new(RefCell::new(false));
let inner_value = Rc::new(RefCell::new(0));
let outer_ran_clone = outer_ran.clone();
let inner_value_clone = inner_value.clone();
let _outer = Effect::new(move || {
*outer_ran_clone.borrow_mut() = true;
let new_signal = Signal::new(42);
let signal_for_inner = new_signal.clone();
let value_capture = inner_value_clone.clone();
let _inner = Effect::new(move || {
*value_capture.borrow_mut() = signal_for_inner.get();
});
});
assert!(*outer_ran.borrow());
assert_eq!(*inner_value.borrow(), 42);
}
#[rstest::rstest]
#[serial]
fn test_effect_dispose_during_execution() {
let signal = Signal::new(0);
let run_count = Rc::new(RefCell::new(0));
let run_count_clone = run_count.clone();
let effect_holder: Rc<RefCell<Option<Effect>>> = Rc::new(RefCell::new(None));
let holder_clone = effect_holder.clone();
let signal_clone = signal.clone();
let effect = Effect::new(move || {
let _val = signal_clone.get(); *run_count_clone.borrow_mut() += 1;
if let Some(e) = holder_clone.borrow().as_ref() {
e.dispose();
}
});
*effect_holder.borrow_mut() = Some(effect);
assert_eq!(*run_count.borrow(), 1);
signal.set(1);
with_runtime(|rt| rt.flush_updates());
assert_eq!(*run_count.borrow(), 2);
signal.set(2);
with_runtime(|rt| rt.flush_updates());
assert_eq!(*run_count.borrow(), 2);
}
#[test]
#[serial]
fn test_flush_updates_executes_pending_effects() {
use crate::reactive::runtime::set_scheduler;
use std::sync::{Arc, Mutex};
type ScheduledTasks = Arc<Mutex<Vec<Box<dyn FnOnce() + Send>>>>;
let scheduled_tasks: ScheduledTasks = Arc::new(Mutex::new(Vec::new()));
let tasks_clone = scheduled_tasks.clone();
set_scheduler(move |task| {
tasks_clone.lock().unwrap().push(task);
});
let signal = Signal::new(0);
let values = Rc::new(RefCell::new(alloc::vec::Vec::new()));
let values_clone = values.clone();
let signal_clone = signal.clone();
let _effect = Effect::new(move || {
values_clone.borrow_mut().push(signal_clone.get());
});
assert_eq!(*values.borrow(), alloc::vec![0]);
signal.set(42);
let tasks = std::mem::take(&mut *scheduled_tasks.lock().unwrap());
assert!(!tasks.is_empty(), "scheduler should have captured a task");
for task in tasks {
task();
}
assert_eq!(*values.borrow(), alloc::vec![0, 42]);
}
#[test]
#[serial]
fn new_with_deps_listed_dep_triggers_rerun() {
let s = Signal::new(0_i32);
let runs = Rc::new(RefCell::new(0_i32));
let runs_for_effect = runs.clone();
let s_for_effect = s.clone();
let deps = crate::reactive::deps::Deps::from_signals(&[s.id()]);
let _eff = Effect::new_with_deps::<_, fn()>(
move || {
let _ = s_for_effect.get();
*runs_for_effect.borrow_mut() += 1;
None
},
deps,
);
let initial = *runs.borrow();
s.set(1);
with_runtime(|rt| rt.flush_updates());
assert_eq!(
*runs.borrow(),
initial + 1,
"listed dep change must trigger re-run"
);
}
#[test]
#[serial]
fn new_with_deps_unlisted_signal_no_rerun() {
let listed = Signal::new(0_i32);
let unlisted = Signal::new(0_i32);
let runs = Rc::new(RefCell::new(0_i32));
let runs_for_effect = runs.clone();
let unlisted_for_effect = unlisted.clone();
let deps = crate::reactive::deps::Deps::from_signals(&[listed.id()]);
let _eff = Effect::new_with_deps::<_, fn()>(
move || {
let _ = unlisted_for_effect.get();
*runs_for_effect.borrow_mut() += 1;
None
},
deps,
);
let initial = *runs.borrow();
unlisted.set(99);
with_runtime(|rt| rt.flush_updates());
assert_eq!(
*runs.borrow(),
initial,
"unlisted Signal read must not subscribe (Option A core)"
);
}
#[test]
#[serial]
fn new_with_deps_cleanup_runs_before_rerun() {
let s = Signal::new(0_i32);
let log: Rc<RefCell<alloc::vec::Vec<&'static str>>> =
Rc::new(RefCell::new(alloc::vec::Vec::new()));
let log_for_effect = log.clone();
let s_for_effect = s.clone();
let deps = crate::reactive::deps::Deps::from_signals(&[s.id()]);
let _eff = Effect::new_with_deps(
move || {
let _ = s_for_effect.get();
log_for_effect.borrow_mut().push("run");
let log_inner = log_for_effect.clone();
Some(move || log_inner.borrow_mut().push("cleanup"))
},
deps,
);
s.set(1);
with_runtime(|rt| rt.flush_updates());
let recorded = log.borrow().clone();
assert_eq!(recorded, alloc::vec!["run", "cleanup", "run"]);
}
#[test]
#[serial(reactive_runtime)]
fn new_with_deps_cleanup_can_dispose_same_effect_without_reentrant_borrow() {
let s = Signal::new(0_i32);
let runs = Rc::new(RefCell::new(0_i32));
let effect_holder: Rc<RefCell<Option<Effect>>> = Rc::new(RefCell::new(None));
let runs_for_effect = runs.clone();
let holder_for_effect = effect_holder.clone();
let s_for_effect = s.clone();
let deps = crate::reactive::deps::Deps::from_signals(&[s.id()]);
let effect = Effect::new_with_deps(
move || {
let _ = s_for_effect.get();
*runs_for_effect.borrow_mut() += 1;
let holder_for_cleanup = holder_for_effect.clone();
Some(move || {
if let Some(effect) = holder_for_cleanup.borrow().as_ref() {
effect.dispose();
}
})
},
deps,
);
*effect_holder.borrow_mut() = Some(effect);
s.set(1);
with_runtime(|rt| rt.flush_updates());
assert_eq!(
*runs.borrow(),
2,
"Refs #5104: cleanup-triggered self-dispose must not panic before the rerun body"
);
s.set(2);
with_runtime(|rt| rt.flush_updates());
assert_eq!(
*runs.borrow(),
2,
"effect disposed during cleanup must not be reinserted"
);
let effect_to_drop = { effect_holder.borrow_mut().take() };
drop(effect_to_drop);
}
#[test]
#[serial(reactive_runtime)]
fn dispose_can_drop_nested_effect_function_without_reentrant_storage_borrow() {
let inner = Effect::new(|| {});
let outer = Effect::new(move || {
let _ = inner.id();
});
outer.dispose();
}
#[test]
#[serial(reactive_runtime)]
fn new_with_deps_effect_in_rc_survives_until_rc_dropped() {
let s = Signal::new(0_i32);
let runs = Rc::new(RefCell::new(0_i32));
let runs_for_effect = runs.clone();
let s_for_effect = s.clone();
let deps = crate::reactive::deps::Deps::from_signals(&[s.id()]);
let effect = Effect::new_with_deps::<_, fn()>(
move || {
let _ = s_for_effect.get();
*runs_for_effect.borrow_mut() += 1;
None
},
deps,
);
let guard = Rc::new(effect);
assert_eq!(*runs.borrow(), 1, "effect runs once on creation");
s.set(1);
with_runtime(|rt| rt.flush_updates());
assert_eq!(
*runs.borrow(),
2,
"effect held via Rc must re-run on dependency change"
);
s.set(2);
with_runtime(|rt| rt.flush_updates());
assert_eq!(*runs.borrow(), 3, "effect held via Rc must keep re-running");
drop(guard);
s.set(3);
with_runtime(|rt| rt.flush_updates());
assert_eq!(
*runs.borrow(),
3,
"disposed effect must not re-run after its Rc anchor is dropped"
);
}
}