Skip to main content

arora_simple_data_store/
lib.rs

1//! A simple hashmap-backed [`DataStore`].
2//!
3//! The lean, dependency-free reference implementation of
4//! [`arora_types::data::DataStore`]: a `HashMap` of path-keyed cells, std
5//! channels for change subscriptions. Cheaply cloneable — clones share the same
6//! storage, so the same blackboard can be handed to the HAL, the bridge, the
7//! behavior tree, and the engine at once. Richer backends (e.g. arora-ecbs) can
8//! implement the same trait.
9
10mod namespaced;
11pub use namespaced::NamespacedStore;
12
13use std::collections::HashMap;
14use std::sync::mpsc::{channel, Sender};
15use std::sync::{Arc, Mutex, RwLock};
16
17use arora_types::data::{DataError, DataStore, Key, Slot, State, StateChange, Subscription};
18use arora_types::value::Value;
19
20/// One key's storage cell, shared so a [`Slot`] keeps a direct reference to it.
21type Cell = Arc<RwLock<Option<Value>>>;
22
23#[derive(Default)]
24struct Inner {
25    cells: RwLock<HashMap<Key, Cell>>,
26    subscribers: Mutex<Vec<Sender<StateChange>>>,
27}
28
29impl Inner {
30    /// Broadcast a change to live subscribers, pruning ones whose receiver was
31    /// dropped.
32    fn notify(&self, change: StateChange) {
33        if change.is_empty() {
34            return;
35        }
36        let mut subs = self.subscribers.lock().unwrap();
37        subs.retain(|tx| tx.send(change.clone()).is_ok());
38    }
39}
40
41/// A simple hashmap-backed [`DataStore`]. Clone to share the same storage.
42#[derive(Clone, Default)]
43pub struct SimpleDataStore {
44    inner: Arc<Inner>,
45}
46
47impl SimpleDataStore {
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Get (or create) the storage cell for a key.
53    fn cell(&self, key: &Key) -> Cell {
54        if let Some(cell) = self.inner.cells.read().unwrap().get(key) {
55            return cell.clone();
56        }
57        self.inner
58            .cells
59            .write()
60            .unwrap()
61            .entry(key.clone())
62            .or_insert_with(|| Arc::new(RwLock::new(None)))
63            .clone()
64    }
65}
66
67impl DataStore for SimpleDataStore {
68    fn read(&self, keys: &[Key]) -> Vec<Option<Value>> {
69        let cells = self.inner.cells.read().unwrap();
70        keys.iter()
71            .map(|k| cells.get(k).and_then(|c| c.read().unwrap().clone()))
72            .collect()
73    }
74
75    fn write(&self, changes: StateChange) -> Result<(), DataError> {
76        // Observers see value CHANGES: a write that leaves a key at the value
77        // it already holds is dropped from the notification (and an unset of an
78        // absent key likewise). This is what keeps echo cycles damped — e.g. a
79        // HAL that mirrors actuation back as state produces one change, not a
80        // feedback loop. (`f32`/`f64` NaNs compare unequal, so NaN writes
81        // always notify.)
82        let mut effective = StateChange::new();
83        for (key, value) in &changes.set {
84            let cell = self.cell(key);
85            let mut current = cell.write().unwrap();
86            if current.as_ref() != value.as_ref() {
87                *current = value.clone();
88                effective.set.insert(key.clone(), value.clone());
89            }
90        }
91        for key in &changes.unset {
92            // Keep the cell (so any outstanding Slot stays valid); clear its value.
93            let cell = self.cell(key);
94            let mut current = cell.write().unwrap();
95            if current.is_some() {
96                *current = None;
97                effective.unset.insert(key.clone());
98            }
99        }
100        if !effective.is_empty() {
101            self.inner.notify(effective);
102        }
103        Ok(())
104    }
105
106    fn snapshot(&self) -> State {
107        let cells = self.inner.cells.read().unwrap();
108        let storage = cells
109            .iter()
110            .map(|(k, c)| (k.clone(), c.read().unwrap().clone()))
111            .collect();
112        State { storage }
113    }
114
115    fn slot(&self, key: &Key) -> Box<dyn Slot> {
116        Box::new(SimpleSlot {
117            cell: self.cell(key),
118            key: key.clone(),
119            inner: self.inner.clone(),
120        })
121    }
122
123    fn clone_box(&self) -> Box<dyn DataStore> {
124        Box::new(self.clone())
125    }
126
127    fn subscribe(&self) -> Subscription {
128        let (tx, rx) = channel();
129        // The current state, as the subscription's first change: whoever
130        // attaches sees everything the store holds, then stays current from
131        // what follows. Taken while holding the subscriber list so a
132        // concurrent write lands either in this snapshot or in a later
133        // change, never in neither.
134        let mut subscribers = self.inner.subscribers.lock().unwrap();
135        let mut initial = StateChange::new();
136        for (key, value) in self.snapshot().storage {
137            initial.set.insert(key, value);
138        }
139        let _ = tx.send(initial);
140        subscribers.push(tx);
141        Subscription::new(rx)
142    }
143}
144
145/// A direct handle to one key's cell — reads and writes hit the same storage as
146/// the store's `read`/`write` for that key, without re-resolving the path.
147struct SimpleSlot {
148    cell: Cell,
149    key: Key,
150    inner: Arc<Inner>,
151}
152
153impl Slot for SimpleSlot {
154    fn get(&self) -> Option<Value> {
155        self.cell.read().unwrap().clone()
156    }
157
158    fn set(&self, value: Option<Value>) -> Result<(), DataError> {
159        // Same change-only notification as `DataStore::write`: setting the
160        // value the cell already holds is a no-op for observers.
161        {
162            let mut current = self.cell.write().unwrap();
163            if *current == value {
164                return Ok(());
165            }
166            *current = value.clone();
167        }
168        self.inner.notify(StateChange {
169            set: HashMap::from([(self.key.clone(), value)]),
170            unset: Default::default(),
171        });
172        Ok(())
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn write_then_read() {
182        let store = SimpleDataStore::new();
183        store
184            .write(StateChange::set("a/b", Value::Boolean(true)))
185            .unwrap();
186        assert_eq!(
187            store.read(&[Key::from("a/b")]),
188            vec![Some(Value::Boolean(true))]
189        );
190        assert_eq!(store.read(&[Key::from("missing")]), vec![None]);
191    }
192
193    #[test]
194    fn slot_and_store_coincide() {
195        let store = SimpleDataStore::new();
196        let slot = store.slot(&Key::from("x"));
197        // write through the slot, read through the store
198        slot.set(Some(Value::Boolean(true))).unwrap();
199        assert_eq!(
200            store.read(&[Key::from("x")]),
201            vec![Some(Value::Boolean(true))]
202        );
203        // write through the store, read through the slot (same cell)
204        store
205            .write(StateChange::set("x", Value::Boolean(false)))
206            .unwrap();
207        assert_eq!(slot.get(), Some(Value::Boolean(false)));
208    }
209
210    #[test]
211    fn subscribe_delivers_changes_to_all() {
212        let store = SimpleDataStore::new();
213        let s1 = store.subscribe();
214        let s2 = store.subscribe();
215        s1.try_recv().expect("s1 opening state");
216        s2.try_recv().expect("s2 opening state");
217        store
218            .write(StateChange::set("k", Value::Boolean(true)))
219            .unwrap();
220        assert!(s1.try_recv().expect("s1 change").contains(&Key::from("k")));
221        assert!(s2.try_recv().expect("s2 change").contains(&Key::from("k")));
222    }
223
224    /// A subscription opens on everything the store already holds, so a
225    /// subscriber never has to read a snapshot separately and race the
226    /// changes that follow.
227    #[test]
228    fn subscribe_opens_on_the_current_state() {
229        let store = SimpleDataStore::new();
230        store
231            .write(StateChange::set("already", Value::Boolean(true)))
232            .unwrap();
233
234        let sub = store.subscribe();
235        let opening = sub.try_recv().expect("opening state");
236        assert!(opening.contains(&Key::from("already")));
237
238        store
239            .write(StateChange::set("later", Value::Boolean(false)))
240            .unwrap();
241        assert!(sub
242            .try_recv()
243            .expect("change")
244            .contains(&Key::from("later")));
245    }
246
247    #[test]
248    fn slot_set_notifies_subscribers() {
249        let store = SimpleDataStore::new();
250        let sub = store.subscribe();
251        sub.try_recv().expect("opening state");
252        store
253            .slot(&Key::from("y"))
254            .set(Some(Value::Boolean(true)))
255            .unwrap();
256        assert!(sub.try_recv().expect("change").contains(&Key::from("y")));
257    }
258
259    #[test]
260    fn snapshot_returns_all() {
261        let store = SimpleDataStore::new();
262        store
263            .write(StateChange::set("a", Value::Boolean(true)))
264            .unwrap();
265        store
266            .write(StateChange::set("b", Value::Boolean(false)))
267            .unwrap();
268        assert_eq!(store.snapshot().storage.len(), 2);
269    }
270
271    #[test]
272    fn clones_share_storage() {
273        let store = SimpleDataStore::new();
274        let other = store.clone();
275        store
276            .write(StateChange::set("shared", Value::Boolean(true)))
277            .unwrap();
278        assert_eq!(
279            other.read(&[Key::from("shared")]),
280            vec![Some(Value::Boolean(true))]
281        );
282    }
283}