use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use adaptite::{Reactor, Source};
use runite::{queue_macrotask, run};
struct ReactiveMap<K, V> {
reactor: Reactor,
entries: RefCell<HashMap<K, V>>,
key_sources: RefCell<HashMap<K, Source>>,
structure: Source,
}
impl<K: Clone + Eq + std::hash::Hash, V: Clone> ReactiveMap<K, V> {
fn new(reactor: &Reactor) -> Self {
Self {
reactor: reactor.clone(),
entries: RefCell::new(HashMap::new()),
key_sources: RefCell::new(HashMap::new()),
structure: reactor.source(),
}
}
fn key_source(&self, key: &K) -> Source {
self.key_sources
.borrow_mut()
.entry(key.clone())
.or_insert_with(|| self.reactor.source())
.clone()
}
fn get(&self, key: &K) -> Option<V> {
self.key_source(key).observe();
self.entries.borrow().get(key).cloned()
}
fn len(&self) -> usize {
self.structure.observe();
self.entries.borrow().len()
}
fn insert(&self, key: K, value: V) {
let existed = self
.entries
.borrow_mut()
.insert(key.clone(), value)
.is_some();
self.key_source(&key).trigger();
if !existed {
self.structure.trigger();
}
}
fn remove(&self, key: &K) {
if self.entries.borrow_mut().remove(key).is_some() {
self.key_source(key).trigger();
self.structure.trigger();
}
}
fn collect_garbage(&self) {
self.key_sources
.borrow_mut()
.retain(|_, source| source.is_observed());
}
fn source_count(&self) -> usize {
self.key_sources.borrow().len()
}
}
type Phase = (&'static str, [usize; 4]);
#[test]
fn observers_depend_on_individual_keys_not_the_whole_map() {
let phases: Rc<RefCell<Vec<Phase>>> = Rc::new(RefCell::new(Vec::new()));
queue_macrotask({
let phases = Rc::clone(&phases);
move || {
let counts = Rc::new(RefCell::new(HashMap::<&str, usize>::new()));
let bump = |counts: &Rc<RefCell<HashMap<&str, usize>>>, who: &'static str| {
*counts.borrow_mut().entry(who).or_default() += 1;
};
let reactor = Reactor::new();
let map = Rc::new(ReactiveMap::<String, i64>::new(&reactor));
map.insert("a".into(), 1);
map.insert("b".into(), 2);
{
let map = Rc::clone(&map);
let counts = Rc::clone(&counts);
reactor
.effect(move || {
let _ = map.get(&"a".into());
bump(&counts, "a");
})
.leak()
};
{
let map = Rc::clone(&map);
let counts = Rc::clone(&counts);
reactor
.effect(move || {
let _ = map.get(&"b".into());
bump(&counts, "b");
})
.leak()
};
{
let map = Rc::clone(&map);
let counts = Rc::clone(&counts);
reactor
.effect(move || {
let _ = map.get(&"c".into());
bump(&counts, "c");
})
.leak()
};
{
let map = Rc::clone(&map);
let counts = Rc::clone(&counts);
reactor
.effect(move || {
let _ = map.len();
bump(&counts, "len");
})
.leak()
};
let record = |label: &'static str| {
let counts = counts.borrow();
let read = |who: &str| counts.get(who).copied().unwrap_or(0);
phases
.borrow_mut()
.push((label, [read("a"), read("b"), read("c"), read("len")]));
};
reactor.flush_now();
record("baseline");
map.insert("b".into(), 20);
reactor.flush_now();
record("update existing key b");
map.insert("c".into(), 3);
reactor.flush_now();
record("insert absent key c");
map.remove(&"a".into());
reactor.flush_now();
record("remove key a");
}
});
run();
let phases = phases.borrow();
assert!(
!phases.is_empty(),
"the macrotask recorded nothing: it panicked before the first observation, and runite \
swallowed the panic"
);
assert_eq!(
&phases[..],
&[
("baseline", [1, 1, 1, 1]),
("update existing key b", [1, 2, 1, 1]),
("insert absent key c", [1, 2, 2, 2]),
("remove key a", [2, 2, 2, 3]),
],
"each mutation must re-run exactly the observers whose footprint it touches"
);
}
#[test]
fn per_key_sources_are_garbage_collected_once_unobserved() {
let counts: Rc<RefCell<Vec<(&'static str, usize)>>> = Rc::new(RefCell::new(Vec::new()));
queue_macrotask({
let counts = Rc::clone(&counts);
move || {
let reactor = Reactor::new();
let map = Rc::new(ReactiveMap::<u32, u32>::new(&reactor));
for key in 0..100 {
map.insert(key, key);
}
let broad = reactor.effect({
let map = Rc::clone(&map);
move || {
for key in 0..100 {
let _ = map.get(&key);
}
}
});
let narrow = reactor.effect({
let map = Rc::clone(&map);
move || {
let _ = map.get(&0);
}
});
reactor.flush_now();
let record =
|label: &'static str, count: usize| counts.borrow_mut().push((label, count));
record("after the first flush", map.source_count());
map.collect_garbage();
record(
"after collecting while every key is observed",
map.source_count(),
);
broad.dispose();
map.collect_garbage();
record(
"after collecting with only key 0 observed",
map.source_count(),
);
narrow.leak();
}
});
run();
let counts = counts.borrow();
assert!(
!counts.is_empty(),
"the macrotask recorded nothing: it panicked before the first count, and runite swallowed \
the panic"
);
assert_eq!(
&counts[..],
&[
("after the first flush", 100),
("after collecting while every key is observed", 100),
("after collecting with only key 0 observed", 1),
],
"sources without observers must be collectable via is_observed, and observed ones must not"
);
}