use std::cell::{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()
}
}
#[test]
fn observers_depend_on_individual_keys_not_the_whole_map() {
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;
};
queue_macrotask({
let counts = Rc::clone(&counts);
move || {
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()
};
reactor.flush_now();
let baseline = counts.borrow().clone();
assert!(baseline.values().all(|&count| count == 1));
map.insert("b".into(), 20);
reactor.flush_now();
assert_eq!(counts.borrow()["a"], 1);
assert_eq!(counts.borrow()["b"], 2);
assert_eq!(counts.borrow()["c"], 1);
assert_eq!(counts.borrow()["len"], 1);
map.insert("c".into(), 3);
reactor.flush_now();
assert_eq!(counts.borrow()["a"], 1);
assert_eq!(counts.borrow()["b"], 2);
assert_eq!(counts.borrow()["c"], 2);
assert_eq!(counts.borrow()["len"], 2);
map.remove(&"a".into());
reactor.flush_now();
assert_eq!(counts.borrow()["a"], 2);
assert_eq!(counts.borrow()["b"], 2);
assert_eq!(counts.borrow()["c"], 2);
assert_eq!(counts.borrow()["len"], 3);
}
});
run();
}
#[test]
fn per_key_sources_are_garbage_collected_once_unobserved() {
let final_sources = Rc::new(Cell::new(usize::MAX));
queue_macrotask({
let final_sources = Rc::clone(&final_sources);
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();
assert_eq!(map.source_count(), 100);
map.collect_garbage();
assert_eq!(map.source_count(), 100, "all keys are still observed");
broad.dispose();
map.collect_garbage();
final_sources.set(map.source_count());
narrow.leak();
}
});
run();
assert_eq!(
final_sources.get(),
1,
"sources without observers must be collectable via is_observed"
);
}