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}