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>;
thread_local! {
static EFFECT_FUNCTIONS: RefCell<BTreeMap<NodeId, EffectFn>> = 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>>,
}
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,
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 }
}
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,
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 }
}
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, f);
});
}
}
}
let mut guard = EffectFnGuard {
effect_id,
effect_fn: EFFECT_FUNCTIONS.with(|storage| storage.borrow_mut().remove(&effect_id)),
};
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 _ = try_with_runtime(|rt| rt.remove_node(self.id));
let _ = EFFECT_FUNCTIONS.try_with(|storage| {
storage.borrow_mut().remove(&self.id);
});
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]);
}
}