alien-signals 0.1.4

Rust port of alien-signals 👾 the lightest signal library
Documentation
use alien_signals::{Computed, Effect, EffectScope, Signal};

#[test]
fn should_clear_subscription_when_untracked_by_all_subscribers() {
    let b_run_times = std::rc::Rc::new(std::sync::Mutex::new(0));

    let a = Signal::new(0);
    let b = Computed::new({
        let b_run_times = b_run_times.clone();
        move |_| {
            *b_run_times.lock().unwrap() += 1;
            a.get() * 2
        }
    });
    let effect = Effect::new(move || {
        println!("[effect] b: {}", b.get());
    });

    assert_eq!(*b_run_times.lock().unwrap(), 1);
    a.set(2);
    assert_eq!(*b_run_times.lock().unwrap(), 2);
    effect.dispose();
    a.set(3);
    assert_eq!(*b_run_times.lock().unwrap(), 2);
}

#[test]
fn should_not_run_untracked_inner_effect() {
    let a = Signal::new(3);
    let b = Computed::new(move |_| a.get() > 0);
    Effect::new(move || {
        if b.get() {
            Effect::new(move || {
                if a.get() == 0 {
                    panic!("bad");
                }
            });
        }
    });

    a.set(2);
    a.set(1);
    a.set(0);
}

#[test]
fn should_run_outer_effect_first() {
    let a = Signal::new(1);
    let b = Signal::new(1);

    Effect::new(move || {
        if a.get() > 0 {
            Effect::new(move || {
                let _ = b.get();
                if a.get() == 0 {
                    panic!("bad");
                }
            });
        }
    });

    alien_signals::start_batch();
    b.set(0);
    a.set(0);
    alien_signals::end_batch();
}

#[test]
fn should_not_trigger_inner_effect_when_resolve_maybe_dirty() {
    let a = Signal::new(0);
    let b = Computed::new(move |_| a.get() % 2);

    let inner_trigger_times = std::rc::Rc::new(std::sync::Mutex::new(0));

    Effect::new(move || {
        let inner_trigger_times = inner_trigger_times.clone();
        Effect::new(move || {
            let _ = b.get();
            *inner_trigger_times.lock().unwrap() += 1;
            if *inner_trigger_times.lock().unwrap() >= 2 {
                panic!("bad");
            }
        });
    });

    a.set(2);
}

#[test]
fn should_notify_inner_effects_in_the_same_order_as_non_inner_effects() {
    let a = Signal::new(0);
    let b = Signal::new(0);
    let c = Computed::new(move |_| a.get() - b.get());
    let order1 = std::rc::Rc::new(std::sync::Mutex::new(vec![]));
    let order2 = std::rc::Rc::new(std::sync::Mutex::new(vec![]));
    let order3 = std::rc::Rc::new(std::sync::Mutex::new(vec![]));

    Effect::new({
        let order1 = order1.clone();
        move || {
            order1.lock().unwrap().push("effect1");
            let _ = a.get();
        }
    });
    Effect::new({
        let order1 = order1.clone();
        move || {
            order1.lock().unwrap().push("effect2");
            let _ = a.get();
            let _ = b.get();
        }
    });

    Effect::new({
        let _ = c.get();
        let order2 = order2.clone();
        move || {
            Effect::new({
                let order2 = order2.clone();
                move || {
                    order2.lock().unwrap().push("effect1");
                    let _ = a.get();
                }
            });
            Effect::new({
                let order2 = order2.clone();
                move || {
                    order2.lock().unwrap().push("effect2");
                    let _ = a.get();
                    let _ = b.get();
                }
            });
        }
    });

    EffectScope::new({
        let order3 = order3.clone();
        move || {
            Effect::new({
                let order3 = order3.clone();
                move || {
                    order3.lock().unwrap().push("effect1");
                    let _ = a.get();
                }
            });
            Effect::new({
                let order3 = order3.clone();
                move || {
                    order3.lock().unwrap().push("effect2");
                    let _ = a.get();
                    let _ = b.get();
                }
            });
        }
    });

    order1.lock().unwrap().clear();
    order2.lock().unwrap().clear();
    order3.lock().unwrap().clear();

    alien_signals::start_batch();
    b.set(1);
    a.set(1);
    alien_signals::end_batch();

    assert_eq!(order1.lock().unwrap().as_slice(), ["effect2", "effect1"]);
    assert_eq!(
        order2.lock().unwrap().as_slice(),
        order1.lock().unwrap().as_slice()
    );
    assert_eq!(
        order3.lock().unwrap().as_slice(),
        order1.lock().unwrap().as_slice()
    );
}

#[test]
fn should_custom_effect_support_batch() {
    fn batch_effect<F: Fn() + 'static>(f: F) -> Effect {
        Effect::new(move || {
            alien_signals::start_batch();
            f();
            alien_signals::end_batch();
        })
    }

    let logs = std::rc::Rc::new(std::sync::Mutex::new(vec![]));
    let a = Signal::new(0);
    let b = Signal::new(0);

    let aa = Computed::new({
        let logs = logs.clone();
        move |_| {
            logs.lock().unwrap().push("aa-0");
            if a.get() == 0 {
                b.set(1);
            }
            logs.lock().unwrap().push("aa-1");
        }
    });
    let bb = Computed::new({
        let logs = logs.clone();
        move |_| {
            logs.lock().unwrap().push("bb");
            b.get()
        }
    });

    batch_effect(move || {
        bb.get();
    });
    batch_effect(move || {
        aa.get();
    });

    assert_eq!(
        logs.lock().unwrap().as_slice(),
        ["bb", "aa-0", "aa-1", "bb"]
    );
}

#[test]
fn should_duplicate_subscribers_do_not_affect_the_notify_order() {
    let src1 = Signal::new(0);
    let src2 = Signal::new(0);
    let order = std::rc::Rc::new(std::sync::Mutex::new(vec![]));

    Effect::new({
        let order = order.clone();
        move || {
            order.lock().unwrap().push("a");
            let current_sub = alien_signals::set_active_sub(None);
            let is_one = src2.get() == 1;
            alien_signals::set_active_sub(current_sub);
            if is_one {
                let _ = src1.get();
            }
            let _ = src2.get();
            let _ = src1.get();
        }
    });
    Effect::new({
        let order = order.clone();
        move || {
            order.lock().unwrap().push("b");
            let _ = src1.get();
        }
    });
    src2.set(1);

    order.lock().unwrap().clear();
    src1.set(src1.get() + 1);

    assert_eq!(order.lock().unwrap().as_slice(), ["a", "b"]);
}

#[test]
fn should_handle_side_effect_with_inner_effects() {
    let a = Signal::new(0);
    let b = Signal::new(0);
    let order = std::rc::Rc::new(std::sync::Mutex::new(vec![]));

    Effect::new({
        let order = order.clone();
        move || {
            Effect::new({
                let order = order.clone();
                move || {
                    let _ = a.get();
                    order.lock().unwrap().push("a");
                }
            });
            Effect::new({
                let order = order.clone();
                move || {
                    let _ = b.get();
                    order.lock().unwrap().push("b");
                }
            });
            assert_eq!(order.lock().unwrap().as_slice(), ["a", "b"]);

            order.lock().unwrap().clear();
            b.set(1);
            a.set(1);
            assert_eq!(order.lock().unwrap().as_slice(), ["b", "a"]);
        }
    });
}

#[test]
fn should_handle_flags_are_indirectly_updated_dyring_check_dirty() {
    let a = Signal::new(false);
    let b = Computed::new(move |_| a.get());
    let c = Computed::new(move |_| {
        let _ = b.get();
        0
    });
    let d = Computed::new(move |_| {
        let _ = c.get();
        b.get()
    });

    let triggers = std::rc::Rc::new(std::sync::Mutex::new(0));

    Effect::new({
        let triggers = triggers.clone();
        move || {
            let _ = d.get();
            *triggers.lock().unwrap() += 1;
        }
    });
    assert_eq!(*triggers.lock().unwrap(), 1);
    a.set(true);
    assert_eq!(*triggers.lock().unwrap(), 2);
}

#[test]
fn should_handle_effect_recursion_for_the_first_execution() {
    let src1 = Signal::new(0);
    let src2 = Signal::new(0);

    let triggers1 = std::rc::Rc::new(std::sync::Mutex::new(0));
    let triggers2 = std::rc::Rc::new(std::sync::Mutex::new(0));

    Effect::new({
        let triggers1 = triggers1.clone();
        move || {
            *triggers1.lock().unwrap() += 1;
            src1.set(i32::min(src1.get() + 1, 5));
        }
    });
    Effect::new({
        let triggers2 = triggers2.clone();
        move || {
            *triggers2.lock().unwrap() += 1;
            src2.set(i32::min(src2.get() + 1, 5));
            let _ = src2.get();
        }
    });

    assert_eq!(*triggers1.lock().unwrap(), 1);
    assert_eq!(*triggers2.lock().unwrap(), 1);
}

#[test]
fn should_support_custom_recurse_effect() {
    let src = Signal::new(0);

    let triggers = std::rc::Rc::new(std::sync::Mutex::new(0));

    Effect::new({
        let triggers = triggers.clone();
        move || {
            alien_signals::get_active_sub()
                .unwrap()
                .remove_flags(alien_signals::Flags::RECURSED_CHECK);
            *triggers.lock().unwrap() += 1;
            src.set(i32::min(src.get() + 1, 5));
        }
    });

    assert_eq!(*triggers.lock().unwrap(), 6);
}