use std::cell::{Cell as StdCell, RefCell};
use std::collections::HashMap;
use std::hash::Hash;
use std::marker::PhantomData;
use std::rc::Rc;
use crate::Context;
use crate::cell::Computed;
use crate::cell::Source;
use crate::context::{Compute, ComputeOps};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryKind {
Cell,
Slot,
}
mod sealed {
pub trait Sealed {}
}
pub trait MapHandle<V>: sealed::Sealed + Copy + 'static {
const KIND: EntryKind;
fn materialize(ctx: &Context, compute: impl Fn(&Compute) -> V + 'static) -> Self
where
V: PartialEq + Clone + 'static;
fn observe<C: ComputeOps>(self, ctx: &C) -> V
where
V: Clone + 'static;
fn clear_dependents(self, ctx: &Context);
}
impl<V> sealed::Sealed for Source<V> {}
impl<V: 'static> MapHandle<V> for Source<V> {
const KIND: EntryKind = EntryKind::Cell;
fn materialize(ctx: &Context, compute: impl Fn(&Compute) -> V + 'static) -> Self
where
V: PartialEq + Clone + 'static,
{
ctx.source(ctx.eval_detached(compute))
}
fn observe<C: ComputeOps>(self, ctx: &C) -> V
where
V: Clone + 'static,
{
self.get(ctx)
}
fn clear_dependents(self, ctx: &Context) {
Source::clear_dependents(&self, ctx);
}
}
impl<V> sealed::Sealed for Computed<V> {}
impl<V: 'static> MapHandle<V> for Computed<V> {
const KIND: EntryKind = EntryKind::Slot;
fn materialize(ctx: &Context, compute: impl Fn(&Compute) -> V + 'static) -> Self
where
V: PartialEq + Clone + 'static,
{
ctx.computed(compute)
}
fn observe<C: ComputeOps>(self, ctx: &C) -> V
where
V: Clone + 'static,
{
self.get(ctx)
}
fn clear_dependents(self, ctx: &Context) {
self.clear(ctx);
}
}
pub struct ReactiveMap<K, V, H> {
inner: Rc<ReactiveMapInner<K, H>>,
_marker: PhantomData<V>,
}
struct ReactiveMapInner<K, H> {
entries: RefCell<HashMap<K, H>>,
order: RefCell<Vec<K>>,
membership: Source<u64>,
version: StdCell<u64>,
order_signal: Source<u64>,
order_version: StdCell<u64>,
}
impl<K, V, H> Clone for ReactiveMap<K, V, H> {
fn clone(&self) -> Self {
Self {
inner: Rc::clone(&self.inner),
_marker: PhantomData,
}
}
}
impl<K, V, H> ReactiveMap<K, V, H>
where
K: Eq + Hash + Clone + 'static,
V: PartialEq + Clone + 'static,
H: MapHandle<V>,
{
pub fn new(ctx: &Context) -> Self {
Self {
inner: Rc::new(ReactiveMapInner {
entries: RefCell::new(HashMap::new()),
order: RefCell::new(Vec::new()),
membership: ctx.source(0u64),
version: StdCell::new(0),
order_signal: ctx.source(0u64),
order_version: StdCell::new(0),
}),
_marker: PhantomData,
}
}
fn bump_order(&self, ctx: &Context) {
let next = self.inner.order_version.get().wrapping_add(1);
self.inner.order_version.set(next);
ctx.set(&self.inner.order_signal, next);
}
fn bump_membership(&self, ctx: &Context) {
let next = self.inner.version.get().wrapping_add(1);
self.inner.version.set(next);
ctx.set(&self.inner.membership, next);
self.bump_order(ctx);
}
fn mint_with(&self, ctx: &Context, key: K, compute: impl Fn(&Compute) -> V + 'static) -> H {
if let Some(handle) = self.inner.entries.borrow().get(&key).copied() {
return handle; }
let handle = H::materialize(ctx, compute);
self.inner.entries.borrow_mut().insert(key.clone(), handle);
self.inner.order.borrow_mut().push(key);
self.bump_membership(ctx);
handle
}
pub fn get_or_insert_with(
&self,
ctx: &Context,
key: K,
factory: impl Fn(&K) -> V + 'static,
) -> V {
if let Some(handle) = self.inner.entries.borrow().get(&key).copied() {
return handle.observe(ctx);
}
let k = key.clone();
let handle = self.mint_with(ctx, key, move |_ctx| factory(&k));
handle.observe(ctx)
}
pub fn handle(&self, key: &K) -> Option<H> {
self.inner.entries.borrow().get(key).copied()
}
pub fn get<C: ComputeOps>(&self, ctx: &C, key: &K) -> Option<V> {
let handle = self.inner.entries.borrow().get(key).copied();
handle.map(|h| h.observe(ctx))
}
pub fn remove(&self, ctx: &Context, key: &K) -> bool {
let removed = self.inner.entries.borrow_mut().remove(key);
let Some(handle) = removed else {
return false;
};
self.inner.order.borrow_mut().retain(|k| k != key);
handle.clear_dependents(ctx);
self.bump_membership(ctx);
true
}
pub fn keys<C: ComputeOps>(&self, ctx: &C) -> Vec<K> {
let _ = self.inner.order_signal.get(ctx);
self.inner.order.borrow().clone()
}
pub fn present_keys(&self) -> Vec<K> {
self.inner.order.borrow().clone()
}
pub fn present_count(&self) -> usize {
self.inner.order.borrow().len()
}
pub fn is_present(&self, key: &K) -> bool {
self.inner.entries.borrow().contains_key(key)
}
pub fn position(&self, key: &K) -> Option<usize> {
self.inner.order.borrow().iter().position(|k| k == key)
}
pub fn move_to(&self, ctx: &Context, key: &K, index: usize) -> bool {
let mut order = self.inner.order.borrow_mut();
let Some(from) = order.iter().position(|k| k == key) else {
return false;
};
let to = index.min(order.len().saturating_sub(1));
if from == to {
return true; }
let k = order.remove(from);
order.insert(to, k);
drop(order);
self.bump_order(ctx);
true
}
pub fn move_before(&self, ctx: &Context, key: &K, anchor: &K) -> bool {
let Some(anchor_idx) = self.position(anchor) else {
return false;
};
let from = match self.position(key) {
Some(i) => i,
None => return false,
};
let target = if from < anchor_idx {
anchor_idx - 1
} else {
anchor_idx
};
self.move_to(ctx, key, target)
}
pub fn move_after(&self, ctx: &Context, key: &K, anchor: &K) -> bool {
let Some(anchor_idx) = self.position(anchor) else {
return false;
};
let from = match self.position(key) {
Some(i) => i,
None => return false,
};
let target = if from <= anchor_idx {
anchor_idx
} else {
anchor_idx + 1
};
self.move_to(ctx, key, target)
}
pub fn len<C: ComputeOps>(&self, ctx: &C) -> usize {
let _ = self.inner.membership.get(ctx);
self.inner.order.borrow().len()
}
pub fn is_empty<C: ComputeOps>(&self, ctx: &C) -> bool {
self.len(ctx) == 0
}
pub fn contains_key<C: ComputeOps>(&self, ctx: &C, key: &K) -> bool {
let _ = self.inner.membership.get(ctx);
self.inner.entries.borrow().contains_key(key)
}
pub fn len_untracked(&self) -> usize {
self.inner.order.borrow().len()
}
pub fn entry_kind(&self) -> EntryKind {
H::KIND
}
}
pub type CellMap<K, V> = ReactiveMap<K, V, Source<V>>;
pub type SlotMap<K, V> = ReactiveMap<K, V, Computed<V>>;
impl<K, V> ReactiveMap<K, V, Source<V>>
where
K: Eq + Hash + Clone + 'static,
V: PartialEq + Clone + 'static,
{
pub fn entry_with(&self, ctx: &Context, key: K, default: impl FnOnce() -> V) -> Source<V> {
if let Some(handle) = self.inner.entries.borrow().get(&key).copied() {
return handle;
}
let value = default();
self.mint_with(ctx, key, move |_ctx| value.clone())
}
pub fn entry(&self, ctx: &Context, key: K, default: V) -> Source<V> {
self.entry_with(ctx, key, || default)
}
pub fn set(&self, ctx: &Context, key: K, value: V) {
if let Some(handle) = self.inner.entries.borrow().get(&key).copied() {
handle.set(ctx, value);
return;
}
self.entry_with(ctx, key, || value);
}
}
impl<K, V> ReactiveMap<K, V, Computed<V>>
where
K: Eq + Hash + Clone + 'static,
V: PartialEq + Clone + 'static,
{
pub fn materialize_all(
&self,
ctx: &Context,
keys: impl IntoIterator<Item = K>,
factory: impl Fn(&K) -> V + 'static,
) {
let factory = Rc::new(factory);
for key in keys {
let f = Rc::clone(&factory);
self.get_or_insert_with(ctx, key, move |k| f(k));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entry_caches_one_cell_per_key() {
let ctx = Context::new();
let map: CellMap<&str, i32> = CellMap::new(&ctx);
let a1 = map.entry(&ctx, "a", 1);
let a2 = map.entry(&ctx, "a", 999);
assert_eq!(a1.id, a2.id);
assert_eq!(a1.get(&ctx), 1);
assert_eq!(map.len_untracked(), 1);
}
#[test]
fn get_or_insert_with_mints_once_then_returns_existing() {
let ctx = Context::new();
let map: CellMap<&str, i32> = CellMap::new(&ctx);
let calls = Rc::new(StdCell::new(0));
assert_eq!(
map.get_or_insert_with(&ctx, "a", {
let calls = Rc::clone(&calls);
move |_| {
calls.set(calls.get() + 1);
7
}
}),
7
);
assert_eq!(map.len_untracked(), 1);
assert_eq!(
map.get_or_insert_with(&ctx, "a", {
let calls = Rc::clone(&calls);
move |_| {
calls.set(calls.get() + 1);
999
}
}),
7
);
assert_eq!(calls.get(), 1);
map.set(&ctx, "a", 42);
assert_eq!(map.get_or_insert_with(&ctx, "a", |_| 0), 42);
}
#[test]
fn membership_is_reactive_but_value_changes_are_not() {
let ctx = Context::new();
let map: CellMap<&str, i32> = CellMap::new(&ctx);
let a = map.entry(&ctx, "a", 1);
map.entry(&ctx, "b", 2);
let count = ctx.computed({
let map = map.clone();
move |ctx| map.len(ctx)
});
assert_eq!(ctx.get(&count), 2);
a.set(&ctx, 100);
assert!(ctx.is_set(&count), "membership reader stayed cached");
assert_eq!(ctx.get(&count), 2);
map.entry(&ctx, "c", 3);
assert_eq!(ctx.get(&count), 3);
assert!(map.remove(&ctx, &"b"));
assert_eq!(ctx.get(&count), 2);
assert_eq!(map.keys(&ctx), vec!["a", "c"]);
}
#[test]
fn per_entry_reads_are_independent() {
let ctx = Context::new();
let map: CellMap<&str, i32> = CellMap::new(&ctx);
let a = map.entry(&ctx, "a", 1);
let b = map.entry(&ctx, "b", 2);
let view_a = ctx.computed({
let map = map.clone();
move |ctx| map.get(ctx, &"a").unwrap_or(0) * 10
});
assert_eq!(ctx.get(&view_a), 10);
b.set(&ctx, 222);
assert!(ctx.is_set(&view_a), "sibling change must not invalidate");
assert_eq!(ctx.get(&view_a), 10);
a.set(&ctx, 5);
assert_eq!(ctx.get(&view_a), 50);
}
#[test]
fn slot_map_mints_lazily_and_caches() {
let ctx = Context::new();
let fam: SlotMap<u32, u32> = SlotMap::new(&ctx);
assert_eq!(fam.present_count(), 0);
assert_eq!(fam.get_or_insert_with(&ctx, 7, |&k| k * 2), 14);
assert_eq!(fam.present_count(), 1);
assert!(fam.is_present(&7));
let h = fam.handle(&7).unwrap();
assert_eq!(h.get(&ctx), 14);
assert_eq!(fam.get_or_insert_with(&ctx, 7, |&k| k * 999), 14);
}
#[test]
fn slot_map_materialize_all_is_eager() {
let ctx = Context::new();
let fam: SlotMap<u32, u32> = SlotMap::new(&ctx);
fam.materialize_all(&ctx, [0u32, 1, 2, 5, 9], |&k| k * 3);
assert_eq!(fam.present_count(), 5);
for k in [0u32, 1, 2, 5, 9] {
assert!(fam.is_present(&k));
}
assert_eq!(fam.get(&ctx, &5), Some(15));
assert_eq!(fam.entry_kind(), EntryKind::Slot);
}
#[test]
fn move_to_reorders_keys_and_keeps_cell_identity() {
let ctx = Context::new();
let map: CellMap<&str, i32> = CellMap::new(&ctx);
let a = map.entry(&ctx, "a", 1);
map.entry(&ctx, "b", 2);
map.entry(&ctx, "c", 3);
assert_eq!(map.keys(&ctx), vec!["a", "b", "c"]);
assert!(map.move_to(&ctx, &"c", 0));
assert_eq!(map.keys(&ctx), vec!["c", "a", "b"]);
assert_eq!(map.handle(&"a").unwrap().id, a.id);
assert_eq!(map.get(&ctx, &"a"), Some(1));
assert_eq!(map.get(&ctx, &"c"), Some(3));
assert!(!map.move_to(&ctx, &"z", 0));
assert_eq!(map.keys(&ctx), vec!["c", "a", "b"]);
}
#[test]
fn pure_move_invalidates_order_but_not_membership_readers() {
let ctx = Context::new();
let map: CellMap<&str, i32> = CellMap::new(&ctx);
map.entry(&ctx, "a", 1);
map.entry(&ctx, "b", 2);
map.entry(&ctx, "c", 3);
let order_reader = ctx.computed({
let map = map.clone();
move |ctx| map.keys(ctx).join(",")
});
let count = ctx.computed({
let map = map.clone();
move |ctx| map.len(ctx)
});
let has_b = ctx.computed({
let map = map.clone();
move |ctx| map.contains_key(ctx, &"b")
});
assert_eq!(ctx.get(&order_reader), "a,b,c");
assert_eq!(ctx.get(&count), 3);
assert!(ctx.get(&has_b));
assert!(map.move_to(&ctx, &"a", 2));
assert_eq!(ctx.get(&order_reader), "b,c,a");
assert!(
ctx.is_set(&count),
"len reader must stay cached on pure move"
);
assert!(
ctx.is_set(&has_b),
"contains_key reader must stay cached on pure move"
);
assert_eq!(ctx.get(&count), 3);
}
#[test]
fn move_to_is_noop_when_position_unchanged() {
let ctx = Context::new();
let map: CellMap<&str, i32> = CellMap::new(&ctx);
map.entry(&ctx, "a", 1);
map.entry(&ctx, "b", 2);
let order_reader = ctx.computed({
let map = map.clone();
move |ctx| map.keys(ctx).join(",")
});
assert_eq!(ctx.get(&order_reader), "a,b");
assert!(map.move_to(&ctx, &"a", 0));
assert!(
ctx.is_set(&order_reader),
"no-op move must not invalidate keys readers"
);
assert!(map.move_to(&ctx, &"a", 99));
assert_eq!(ctx.get(&order_reader), "b,a");
}
#[test]
fn move_before_and_after_place_relative_to_anchor() {
let ctx = Context::new();
let map: CellMap<i32, i32> = CellMap::new(&ctx);
for k in 0..4 {
map.entry(&ctx, k, k * 10);
}
assert_eq!(map.keys(&ctx), vec![0, 1, 2, 3]);
assert!(map.move_before(&ctx, &3, &1));
assert_eq!(map.keys(&ctx), vec![0, 3, 1, 2]);
assert!(map.move_after(&ctx, &0, &2));
assert_eq!(map.keys(&ctx), vec![3, 1, 2, 0]);
assert!(!map.move_before(&ctx, &3, &99));
assert!(!map.move_after(&ctx, &99, &2));
}
#[test]
fn contains_key_tracks_membership() {
let ctx = Context::new();
let map: CellMap<i32, i32> = CellMap::new(&ctx);
let has_5 = ctx.computed({
let map = map.clone();
move |ctx| map.contains_key(ctx, &5)
});
assert!(!ctx.get(&has_5));
map.entry(&ctx, 5, 50);
assert!(ctx.get(&has_5));
}
}