Skip to main content

amethystate/reactive/
cell.rs

1use crate::reactive::error::WriteResult;
2use crate::reactive::watch::{Immediate, Watch, Watchable};
3use amethystate_core::{Signal, SignalSubscription};
4use std::sync::Arc;
5use uuid::Uuid;
6
7type Writer<T> = Arc<dyn Fn(T) -> WriteResult<()> + Send + Sync>;
8
9/// How a cell commits, for the primitives that have somewhere to commit to.
10/// A cell holding a value in memory has none, and needs none.
11#[derive(Clone)]
12pub(crate) struct CellCommit {
13    pub(crate) now: Arc<dyn Fn() -> WriteResult<()> + Send + Sync>,
14    pub(crate) start: Arc<dyn Fn() -> crate::store::Commit + Send + Sync>,
15}
16
17/// A reactive value you can read, write and subscribe to, with the primitive
18/// behind it erased - along with its type parameters, so fields, map entries
19/// and in-memory values share one type and one collection.
20///
21/// Writes always reach where the value lives.
22pub struct ReactiveCell<T> {
23    cache: Signal<T>,
24    writer: Writer<T>,
25    origin: Uuid,
26    commit: Option<CellCommit>,
27    _keepalive: Option<Arc<dyn Send + Sync>>,
28}
29
30impl<T> Clone for ReactiveCell<T> {
31    fn clone(&self) -> Self {
32        Self {
33            cache: self.cache.clone(),
34            writer: Arc::clone(&self.writer),
35            origin: self.origin,
36            commit: self.commit.clone(),
37            _keepalive: self._keepalive.clone(),
38        }
39    }
40}
41
42impl<T> ReactiveCell<T>
43where
44    T: Clone + Send + Sync + 'static,
45{
46    /// `keepalive` must hold the subscription feeding `cache` unless something
47    /// captured by `writer` already does.
48    pub(crate) fn from_parts(
49        cache: Signal<T>,
50        writer: Writer<T>,
51        origin: Uuid,
52        commit: Option<CellCommit>,
53        keepalive: Option<Arc<dyn Send + Sync>>,
54    ) -> Self {
55        Self {
56            cache,
57            writer,
58            origin,
59            commit,
60            _keepalive: keepalive,
61        }
62    }
63
64    /// An in-memory cell, for reactive values that are not persisted.
65    pub fn new(initial: T) -> Self {
66        let cache = Signal::new(initial);
67        let sink = cache.clone();
68
69        Self {
70            cache,
71            writer: Arc::new(move |value| {
72                sink.set(value);
73                Ok(())
74            }),
75            origin: Uuid::new_v4(),
76            commit: None,
77            _keepalive: None,
78        }
79    }
80
81    pub fn get(&self) -> T {
82        self.cache.get()
83    }
84
85    /// The same writes, each returning only once the value is on disk.
86    ///
87    /// A cell over a value held in memory has nothing to commit to, and its
88    /// writes here behave as the plain ones do.
89    pub fn durable(&self) -> crate::store::Durable<'_, Self> {
90        crate::store::Durable(self)
91    }
92
93    /// Fails if the write is refused - by an interceptor, or by the store.
94    /// The cache is left to the store subscription, so a refused write never
95    /// shows up in `get()`.
96    pub fn set(&self, value: T) -> WriteResult<()> {
97        (self.writer)(value)
98    }
99
100    /// Read-modify-write, not atomic.
101    pub fn update<F>(&self, f: F) -> WriteResult<T>
102    where
103        F: FnOnce(T) -> T,
104    {
105        let next = f(self.get());
106        self.set(next.clone())?;
107        Ok(next)
108    }
109
110    /// Same caveat as [`ReactiveCell::update`]: read-modify-write, not atomic.
111    pub fn modify<F>(&self, f: F) -> WriteResult<()>
112    where
113        F: FnOnce(&mut T),
114    {
115        let mut value = self.get();
116        f(&mut value);
117        self.set(value)
118    }
119
120    #[track_caller]
121    pub fn subscribe<F>(&self, callback: F) -> SignalSubscription
122    where
123        F: Fn(&T) + Send + Sync + 'static,
124    {
125        self.cache.subscribe(callback)
126    }
127
128    /// Subscribes with the provenance of each change, so a subscriber can tell
129    /// its own writes from everyone else's.
130    #[track_caller]
131    pub fn subscribe_with_source<F>(&self, callback: F) -> SignalSubscription
132    where
133        F: Fn(&T, Option<Uuid>) + Send + Sync + 'static,
134    {
135        self.cache.subscribe_with_source(callback)
136    }
137
138    /// Configures a subscription: filtering, provenance, and where the callback
139    /// runs. See [`Watch`].
140    pub fn subscription_with(&self) -> Watch<Self, Immediate> {
141        Watch::new(self.clone())
142    }
143}
144
145impl<T> Watchable for ReactiveCell<T>
146where
147    T: Clone + Send + Sync + 'static,
148{
149    type Item = T;
150
151    fn watch_id(&self) -> Uuid {
152        self.origin
153    }
154
155    fn watch_raw<F>(&self, callback: F) -> SignalSubscription
156    where
157        F: Fn(&T, Option<Uuid>) + Send + Sync + 'static,
158    {
159        self.cache.subscribe_with_source(callback)
160    }
161}
162
163impl<T> amethystate_core::pipeline::Reactive<T> for ReactiveCell<T>
164where
165    T: Clone + Send + Sync + 'static,
166{
167    fn get(&self) -> T {
168        self.get()
169    }
170
171    fn subscribe_with_source<F>(&self, callback: F) -> SignalSubscription
172    where
173        F: for<'a> Fn(&'a T, Option<Uuid>) + Send + Sync + 'static,
174    {
175        self.cache.subscribe_with_source(callback)
176    }
177
178    fn keepalive(&self) -> Option<Arc<dyn Send + Sync>> {
179        self._keepalive.clone()
180    }
181}
182
183impl<T: std::fmt::Debug + Clone + Send + Sync + 'static> std::fmt::Debug for ReactiveCell<T> {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        f.debug_struct("ReactiveCell")
186            .field("value", &self.get())
187            .finish_non_exhaustive()
188    }
189}
190
191impl<T> crate::store::Durable<'_, ReactiveCell<T>>
192where
193    T: Clone + Send + Sync + 'static,
194{
195    fn commit(&self) -> WriteResult<()> {
196        match &self.0.commit {
197            Some(commit) => (commit.now)(),
198            None => Ok(()),
199        }
200    }
201
202    async fn commit_async(&self) -> WriteResult<()> {
203        let pending = self.0.commit.as_ref().map(|commit| (commit.start)());
204        match pending {
205            Some(commit) => Ok(commit.await?),
206            None => Ok(()),
207        }
208    }
209
210    pub fn set(&self, value: T) -> WriteResult<()> {
211        self.0.set(value)?;
212        self.commit()
213    }
214
215    pub async fn set_async(&self, value: T) -> WriteResult<()> {
216        self.0.set(value)?;
217        self.commit_async().await
218    }
219
220    pub fn update<F>(&self, f: F) -> WriteResult<T>
221    where
222        F: FnOnce(T) -> T,
223    {
224        let value = self.0.update(f)?;
225        self.commit()?;
226        Ok(value)
227    }
228
229    pub async fn update_async<F>(&self, f: F) -> WriteResult<T>
230    where
231        F: FnOnce(T) -> T,
232    {
233        let value = self.0.update(f)?;
234        self.commit_async().await?;
235        Ok(value)
236    }
237
238    pub fn modify<F>(&self, f: F) -> WriteResult<()>
239    where
240        F: FnOnce(&mut T),
241    {
242        self.0.modify(f)?;
243        self.commit()
244    }
245
246    pub async fn modify_async<F>(&self, f: F) -> WriteResult<()>
247    where
248        F: FnOnce(&mut T),
249    {
250        self.0.modify(f)?;
251        self.commit_async().await
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use crate::DefaultStore;
259    use crate::store::{StateScope, Store};
260    use crate::test_utils::unique_store;
261    use std::sync::Mutex;
262    use std::sync::atomic::{AtomicUsize, Ordering};
263
264    struct UiScope;
265    impl StateScope for UiScope {
266        const PREFIX: &'static str = "ui";
267    }
268
269    fn stored_field(
270        store: &DefaultStore,
271        name: &'static str,
272        default: u64,
273    ) -> crate::WritableField<u64, DefaultStore> {
274        crate::store::field::<UiScope, u64, DefaultStore>(store, name, default, Uuid::new_v4())
275            .expect("field")
276    }
277
278    #[test]
279    fn volatile_cell_round_trips() {
280        let cell = ReactiveCell::new(7u64);
281        assert_eq!(cell.get(), 7);
282
283        cell.set(9).expect("volatile write cannot fail");
284        assert_eq!(cell.get(), 9);
285    }
286
287    #[test]
288    fn field_cell_writes_reach_the_store() {
289        let store = unique_store("write-through");
290        let field = stored_field(&store, "width", 110);
291        let cell = field.cell();
292
293        cell.set(200).expect("write should reach the store");
294
295        assert_eq!(store.get::<u64>("ui.width").unwrap(), Some(200));
296        assert_eq!(cell.get(), 200, "cache must reflect the committed value");
297        assert_eq!(field.get(), 200, "field and cell share one cache");
298    }
299
300    #[test]
301    fn cell_outlives_the_field_it_came_from() {
302        // The cache is fed by the store subscription, which the field owns. If
303        // the cell did not keep that alive, `get()` would go on answering with
304        // a value that stopped being updated - a silent lie rather than a
305        // failure. Here the writer holds the field, so dropping our handle
306        // changes nothing.
307        let store = unique_store("outlive");
308        let cell = {
309            let field = stored_field(&store, "height", 10);
310            field.cell()
311        };
312
313        store.set("ui.height", &42u64).expect("external write");
314
315        assert_eq!(
316            cell.get(),
317            42,
318            "cache must still be fed after the field is dropped"
319        );
320    }
321
322    #[test]
323    fn subscribers_see_writes_from_anywhere() {
324        let store = unique_store("subscribe");
325        let field = stored_field(&store, "depth", 1);
326        let cell = field.cell();
327
328        let seen = Arc::new(Mutex::new(Vec::new()));
329        let cap = seen.clone();
330        let _sub = cell.subscribe(move |v: &u64| cap.lock().unwrap().push(*v));
331
332        cell.set(2).unwrap();
333        field.set(3).unwrap();
334        store.set("ui.depth", &4u64).unwrap();
335
336        assert_eq!(*seen.lock().unwrap(), vec![2, 3, 4]);
337    }
338
339    #[test]
340    fn write_through_cell_fires_once_per_write() {
341        // The bug that started this refactor: a cell with two writers - a local
342        // optimistic set plus the store round-trip - raised subscribers twice
343        // for one write. With the store as the only writer, one write is one
344        // notification.
345        let store = unique_store("single-fire");
346        let cell = stored_field(&store, "fires", 0).cell();
347
348        let fires = Arc::new(AtomicUsize::new(0));
349        let cap = fires.clone();
350        let _sub = cell.subscribe(move |_: &u64| {
351            cap.fetch_add(1, Ordering::SeqCst);
352        });
353
354        cell.set(1).unwrap();
355
356        assert_eq!(fires.load(Ordering::SeqCst), 1, "one write, one fire");
357    }
358
359    #[test]
360    fn rejected_write_reports_and_leaves_the_cache_alone() {
361        // An interceptor refusing the write must surface as an error, and the
362        // cell must keep reporting what is actually stored - not the value
363        // somebody tried to write.
364        let store = unique_store("rejected");
365        let field = stored_field(&store, "guarded", 5);
366        let _guard = field.intercept(|_change| None);
367        let cell = field.cell();
368
369        let result = cell.set(99);
370
371        assert!(result.is_err(), "rejected write must not report success");
372        assert_eq!(
373            cell.get(),
374            5,
375            "cache must not hold a value the store refused"
376        );
377        assert_eq!(store.get::<u64>("ui.guarded").unwrap(), Some(5));
378    }
379
380    #[test]
381    fn clones_share_one_value() {
382        let cell = ReactiveCell::new(1u64);
383        let twin = cell.clone();
384
385        twin.set(2).unwrap();
386
387        assert_eq!(cell.get(), 2, "clones are handles onto the same cell");
388    }
389}