use super::computed::Computed;
use super::effect::Effect;
use super::signal::Signal;
pub struct ReactiveScope {
effects: Vec<Effect>,
children: Vec<ReactiveScope>,
cleanups: Vec<Box<dyn FnOnce()>>,
}
impl ReactiveScope {
#[must_use]
pub fn new() -> Self {
Self {
effects: Vec::new(),
children: Vec::new(),
cleanups: Vec::new(),
}
}
pub fn create_signal<T>(&self, value: T) -> Signal<T> {
Signal::new(value)
}
pub fn create_computed<T: Clone + 'static>(&self, f: impl Fn() -> T + 'static) -> Computed<T> {
Computed::new(f)
}
pub fn create_effect(&mut self, f: impl FnMut() + 'static) -> Effect {
let effect = Effect::new(f);
self.effects.push(effect.clone());
effect
}
pub fn on_cleanup(&mut self, f: impl FnOnce() + 'static) {
self.cleanups.push(Box::new(f));
}
pub fn child(&mut self) -> &mut ReactiveScope {
self.children.push(ReactiveScope::new());
let last = self.children.len() - 1;
&mut self.children[last]
}
pub fn effect_count(&self) -> usize {
self.effects.len()
}
pub fn child_count(&self) -> usize {
self.children.len()
}
}
impl Default for ReactiveScope {
fn default() -> Self {
Self::new()
}
}
impl Drop for ReactiveScope {
fn drop(&mut self) {
self.children.clear();
for effect in &self.effects {
effect.dispose();
}
while let Some(cleanup) = self.cleanups.pop() {
cleanup();
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::cell::Cell;
use std::rc::Rc;
#[test]
fn scope_disposes_effects_on_drop() {
let sig = Signal::new(0);
let count = Rc::new(Cell::new(0u32));
let effect;
{
let mut scope = ReactiveScope::new();
effect = scope.create_effect({
let sig = sig.clone();
let count = Rc::clone(&count);
move || {
let _ = sig.get();
count.set(count.get() + 1);
}
});
sig.subscribe(effect.as_subscriber());
assert_eq!(count.get(), 1);
sig.set(1);
assert_eq!(count.get(), 2);
}
assert!(!effect.is_active());
sig.set(2);
assert_eq!(count.get(), 2); }
#[test]
fn scope_runs_cleanups_on_drop() {
let order = Rc::new(RefCell::new(Vec::new()));
{
let mut scope = ReactiveScope::new();
scope.on_cleanup({
let order = Rc::clone(&order);
move || order.borrow_mut().push(1)
});
scope.on_cleanup({
let order = Rc::clone(&order);
move || order.borrow_mut().push(2)
});
}
assert_eq!(*order.borrow(), vec![2, 1]);
}
#[test]
fn nested_scope_dropped_before_parent_cleanup() {
let order = Rc::new(RefCell::new(Vec::new()));
{
let mut scope = ReactiveScope::new();
scope.on_cleanup({
let order = Rc::clone(&order);
move || order.borrow_mut().push("parent")
});
let child = scope.child();
child.on_cleanup({
let order = Rc::clone(&order);
move || order.borrow_mut().push("child")
});
}
assert_eq!(*order.borrow(), vec!["child", "parent"]);
}
#[test]
fn effect_in_dropped_scope_does_not_fire() {
let sig = Signal::new(0);
let count = Rc::new(Cell::new(0u32));
{
let mut scope = ReactiveScope::new();
let effect = scope.create_effect({
let sig = sig.clone();
let count = Rc::clone(&count);
move || {
let _ = sig.get();
count.set(count.get() + 1);
}
});
sig.subscribe(effect.as_subscriber());
}
sig.set(1);
assert_eq!(count.get(), 1);
}
#[test]
fn create_signal_through_scope() {
let scope = ReactiveScope::new();
let sig = scope.create_signal(42);
assert_eq!(sig.get(), 42);
}
#[test]
fn create_computed_through_scope() {
let scope = ReactiveScope::new();
let sig = Signal::new(3);
let doubled = scope.create_computed({
let sig = sig.clone();
move || sig.get() * 2
});
assert_eq!(doubled.get(), 6);
}
#[test]
fn scope_counts() {
let mut scope = ReactiveScope::new();
assert_eq!(scope.effect_count(), 0);
assert_eq!(scope.child_count(), 0);
scope.create_effect(|| {});
assert_eq!(scope.effect_count(), 1);
scope.child();
assert_eq!(scope.child_count(), 1);
}
#[test]
fn scope_can_be_moved() {
let mut scope = ReactiveScope::new();
scope.create_effect(|| {});
let scope2 = scope;
assert_eq!(scope2.effect_count(), 1);
}
#[test]
fn multiple_nested_scopes() {
let order = Rc::new(RefCell::new(Vec::new()));
{
let mut scope = ReactiveScope::new();
scope.on_cleanup({
let order = Rc::clone(&order);
move || order.borrow_mut().push("root")
});
let child1 = scope.child();
child1.on_cleanup({
let order = Rc::clone(&order);
move || order.borrow_mut().push("child1")
});
let child2 = scope.child();
child2.on_cleanup({
let order = Rc::clone(&order);
move || order.borrow_mut().push("child2")
});
}
let v = order.borrow().clone();
assert_eq!(v.len(), 3);
assert_eq!(v[0], "child1");
assert_eq!(v[1], "child2");
assert_eq!(v[2], "root");
}
use std::cell::RefCell;
}