use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::rc::Rc;
use super::context::SubscriberId;
use super::signal::Subscriber;
thread_local! {
static BATCH_DEPTH: Cell<u32> = const { Cell::new(0) };
static PENDING: RefCell<Vec<Rc<dyn Subscriber>>> = const { RefCell::new(Vec::new()) };
static PENDING_IDS: RefCell<HashSet<SubscriberId>> = RefCell::new(HashSet::new());
}
pub fn is_batching() -> bool {
BATCH_DEPTH.with(|d| d.get() > 0)
}
pub fn queue_subscriber(subscriber: &Rc<dyn Subscriber>) -> bool {
if !is_batching() {
return false;
}
let id = subscriber.id();
PENDING_IDS.with(|ids| {
let mut ids = ids.borrow_mut();
if ids.insert(id) {
PENDING.with(|pending| {
pending.borrow_mut().push(Rc::clone(subscriber));
});
}
});
true
}
pub fn batch(f: impl FnOnce()) {
BATCH_DEPTH.with(|d| d.set(d.get() + 1));
f();
BATCH_DEPTH.with(|d| {
let depth = d.get().saturating_sub(1);
d.set(depth);
if depth == 0 {
flush_pending();
}
});
}
fn flush_pending() {
let subscribers: Vec<Rc<dyn Subscriber>> =
PENDING.with(|pending| pending.borrow_mut().drain(..).collect());
PENDING_IDS.with(|ids| ids.borrow_mut().clear());
for sub in &subscribers {
sub.notify();
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::reactive::effect::Effect;
use crate::reactive::signal::Signal;
use std::cell::Cell;
#[test]
fn without_batch_effect_runs_on_every_set() {
let sig = Signal::new(0);
let count = Rc::new(Cell::new(0u32));
let effect = Effect::new({
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);
sig.set(2);
sig.set(3);
assert_eq!(count.get(), 4);
}
#[test]
fn with_batch_effect_runs_once() {
let sig = Signal::new(0);
let count = Rc::new(Cell::new(0u32));
let effect = Effect::new({
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);
batch(|| {
sig.set(1);
sig.set(2);
sig.set(3);
});
assert_eq!(count.get(), 2);
assert_eq!(sig.get(), 3);
}
#[test]
fn nested_batch_effects_run_after_outermost() {
let sig = Signal::new(0);
let count = Rc::new(Cell::new(0u32));
let effect = Effect::new({
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);
batch(|| {
sig.set(1);
batch(|| {
sig.set(2);
});
assert_eq!(count.get(), 1); });
assert_eq!(count.get(), 2);
}
#[test]
fn batch_with_multiple_signals() {
let a = Signal::new(0);
let b = Signal::new(0);
let count = Rc::new(Cell::new(0u32));
let effect = Effect::new({
let a = a.clone();
let b = b.clone();
let count = Rc::clone(&count);
move || {
let _ = a.get() + b.get();
count.set(count.get() + 1);
}
});
a.subscribe(effect.as_subscriber());
b.subscribe(effect.as_subscriber());
assert_eq!(count.get(), 1);
batch(|| {
a.set(1);
b.set(2);
});
assert_eq!(count.get(), 2);
}
#[test]
fn empty_batch_no_spurious_notifications() {
let count = Rc::new(Cell::new(0u32));
let sig = Signal::new(0);
let effect = Effect::new({
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);
batch(|| {
});
assert_eq!(count.get(), 1);
}
#[test]
fn is_batching_flag() {
assert!(!is_batching());
batch(|| {
assert!(is_batching());
});
assert!(!is_batching());
}
#[test]
fn batch_deduplicates_subscribers() {
let sig = Signal::new(0);
let count = Rc::new(Cell::new(0u32));
let effect = Effect::new({
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);
batch(|| {
sig.set(1);
sig.set(2);
sig.set(3);
});
assert_eq!(count.get(), 2);
}
}