Skip to main content

rama_utils/
reactive.rs

1//! A lock-free-read value paired with a race-free change signal.
2
3use std::marker::PhantomData;
4use std::sync::atomic::{AtomicUsize, Ordering};
5use tokio::sync::watch;
6
7/// A value that can be stored in a [`Reactive`] as a `usize`.
8pub trait ReactiveRepr: Copy {
9    /// Encode the value into the backing `usize`.
10    fn to_usize(self) -> usize;
11    /// Decode the value from the backing `usize`.
12    fn from_usize(value: usize) -> Self;
13}
14
15impl ReactiveRepr for usize {
16    #[inline]
17    fn to_usize(self) -> usize {
18        self
19    }
20    #[inline]
21    fn from_usize(value: usize) -> Self {
22        value
23    }
24}
25
26/// A [`ReactiveRepr`] value with **lock-free reads** and a **race-free typed
27/// change signal**.
28///
29/// [`Reactive::get`] is a plain atomic load. [`Reactive::watch`] hands out a
30/// [`Changed`] whose [`Changed::changed`] awaits the next change and returns the
31/// new value. tokio's `watch` handles the wakeup race, versioning, and multiple
32/// independent subscribers.
33///
34/// The value is held in the atomic (for lock-free reads) *and* carried on the
35/// `watch` (so `changed()` can return it without a separate read). `set` stores
36/// the atomic and then `send`s, which is a no-op when nobody is subscribed, so
37/// an idle value costs nothing beyond the atomic store.
38#[derive(Debug)]
39pub struct Reactive<T> {
40    value: AtomicUsize,
41    signal: watch::Sender<usize>,
42    _repr: PhantomData<fn() -> T>,
43}
44
45impl<T: ReactiveRepr> Reactive<T> {
46    /// Create a new [`Reactive`] holding `value`.
47    #[must_use]
48    pub fn new(value: T) -> Self {
49        let bits = value.to_usize();
50        // Drop the initial receiver: no watchers until someone calls `watch`.
51        let (signal, _) = watch::channel(bits);
52        Self {
53            value: AtomicUsize::new(bits),
54            signal,
55            _repr: PhantomData,
56        }
57    }
58
59    /// Read the current value (lock-free).
60    #[must_use]
61    pub fn get(&self) -> T {
62        T::from_usize(self.value.load(Ordering::Acquire))
63    }
64
65    /// Store `value` and wake watchers.
66    pub fn set(&self, value: T) {
67        let bits = value.to_usize();
68        // Publish to the atomic first (lock-free reads see it immediately), then
69        // signal. `send` is a no-op when there are no watchers, so an idle value
70        // pays only the atomic store.
71        self.value.store(bits, Ordering::Release);
72        // `send` errors only when there are no receivers, that's the idle case we
73        // intentionally treat as a no-op.
74        let _unused = self.signal.send(bits);
75    }
76
77    /// Subscribe to changes. Hold the returned [`Changed`] and loop
78    /// [`Changed::changed`] to observe every change, its `watch` cursor is
79    /// persistent, so no edge is missed (unlike re-subscribing per await).
80    #[must_use]
81    pub fn watch(&self) -> Changed<T> {
82        Changed {
83            rx: self.signal.subscribe(),
84            _repr: PhantomData,
85        }
86    }
87}
88
89impl<T: ReactiveRepr + Default> Default for Reactive<T> {
90    fn default() -> Self {
91        Self::new(T::default())
92    }
93}
94
95/// Change subscription handed out by [`Reactive::watch`].
96///
97/// Holds a persistent `watch` cursor, so looping [`Changed::changed`] observes
98/// every change (coalescing to the latest value) without missing edges.
99#[derive(Debug, Clone)]
100pub struct Changed<T> {
101    rx: watch::Receiver<usize>,
102    _repr: PhantomData<fn() -> T>,
103}
104
105impl<T: ReactiveRepr> Changed<T> {
106    /// Wait for the next change and return the new value. Returns `None` once the
107    /// source [`Reactive`] is gone (all references dropped), so a forwarding loop
108    /// can terminate.
109    pub async fn changed(&mut self) -> Option<T> {
110        self.rx.changed().await.ok()?;
111        Some(T::from_usize(*self.rx.borrow_and_update()))
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use std::sync::Arc;
119
120    #[test]
121    fn get_set_roundtrip() {
122        let r = Reactive::<usize>::new(3);
123        assert_eq!(r.get(), 3);
124        r.set(7);
125        assert_eq!(r.get(), 7);
126    }
127
128    #[tokio::test]
129    async fn changed_yields_the_new_value() {
130        let r = Arc::new(Reactive::<usize>::new(0));
131        let mut w = r.watch();
132
133        let handle = tokio::spawn(async move { w.changed().await });
134
135        // give the watcher a chance to park, then change the value
136        tokio::task::yield_now().await;
137        r.set(42);
138
139        assert_eq!(handle.await.unwrap(), Some(42));
140    }
141
142    #[tokio::test]
143    async fn changed_returns_none_once_source_dropped() {
144        let r = Reactive::<usize>::new(0);
145        let mut w = r.watch();
146        drop(r);
147        assert_eq!(
148            w.changed().await,
149            None,
150            "no source left: should report closed"
151        );
152    }
153
154    #[tokio::test]
155    async fn set_without_watchers_is_a_noop_send() {
156        // No watcher subscribed: `set` must still update the value (via the
157        // atomic) without erroring or blocking.
158        let r = Reactive::<usize>::new(1);
159        r.set(2);
160        assert_eq!(r.get(), 2);
161        // A watcher subscribing afterwards sees the current value and future changes.
162        let mut w = r.watch();
163        r.set(3);
164        assert_eq!(w.changed().await, Some(3));
165    }
166}