Skip to main content

wayle_core/property/
mod.rs

1//! Single-producer, multi-consumer reactive values built on
2//! [`tokio::sync::watch`]. See [`Property`] for the main type.
3
4mod serde;
5mod stream;
6
7use std::sync::{
8    Arc,
9    atomic::{AtomicUsize, Ordering},
10};
11
12use futures::stream::Stream;
13use tokio::sync::{Notify, watch};
14use tokio_stream::wrappers::WatchStream;
15
16use self::stream::SubscribedStream;
17
18/// Stream of property value changes.
19pub type PropertyStream<T> = Box<dyn Stream<Item = T> + Send + Unpin>;
20
21/// A value you can `.get()` or `.watch()` for changes.
22///
23/// ```ignore
24/// let volume = device.volume.get();
25///
26/// let mut changes = device.volume.watch();
27/// while let Some(vol) = changes.next().await {
28///     println!("{vol:?}");
29/// }
30/// ```
31#[derive(Clone)]
32pub struct Property<T: Clone + Send + Sync + 'static> {
33    tx: watch::Sender<T>,
34    rx: watch::Receiver<T>,
35
36    subscriber_count: Arc<AtomicUsize>,
37    subscriber_notify: Arc<Notify>,
38}
39
40impl<T: Clone + Send + Sync + 'static> Property<T> {
41    /// Creates a property with an initial value.
42    ///
43    /// ```
44    /// use wayle_core::Property;
45    ///
46    /// let temperature = Property::new(22.5_f64);
47    /// assert_eq!(temperature.get(), 22.5);
48    /// ```
49    #[doc(hidden)]
50    pub fn new(initial: T) -> Self {
51        let (tx, rx) = watch::channel(initial);
52
53        Self {
54            tx,
55            rx,
56            subscriber_count: Arc::new(AtomicUsize::new(0)),
57            subscriber_notify: Arc::new(Notify::new()),
58        }
59    }
60
61    /// Updates the value. Watchers are only notified if the value actually changed.
62    ///
63    /// ```
64    /// use wayle_core::Property;
65    ///
66    /// let volume = Property::new(50_u32);
67    ///
68    /// volume.set(75);
69    /// assert_eq!(volume.get(), 75);
70    ///
71    /// // Setting the same value is a no-op (no watcher notification).
72    /// volume.set(75);
73    /// ```
74    #[doc(hidden)]
75    pub fn set(&self, new_value: T)
76    where
77        T: PartialEq,
78    {
79        self.tx.send_if_modified(|current| {
80            if *current != new_value {
81                *current = new_value;
82                return true;
83            }
84
85            false
86        });
87    }
88
89    /// Unconditional [`set`](Self::set). Always notifies watchers, and
90    /// doesn't require `PartialEq`.
91    ///
92    /// ```
93    /// use wayle_core::Property;
94    ///
95    /// let data = Property::new(vec![1, 2, 3]);
96    /// data.replace(vec![1, 2, 3]);
97    /// // Watchers fire even though the Vec is equal.
98    /// // Use `set` instead if you want to skip duplicates.
99    /// ```
100    pub fn replace(&self, new_value: T) {
101        self.tx.send_modify(|current| *current = new_value);
102    }
103
104    /// Snapshot of the current value (cloned).
105    ///
106    /// ```
107    /// use wayle_core::Property;
108    ///
109    /// let name = Property::new(String::from("default"));
110    /// assert_eq!(name.get(), "default");
111    /// ```
112    pub fn get(&self) -> T {
113        self.rx.borrow().clone()
114    }
115
116    /// Yields the current value immediately, then each subsequent change.
117    ///
118    /// Each call returns an independent stream. Multiple consumers
119    /// can watch the same property concurrently.
120    ///
121    /// ```no_run
122    /// use futures::stream::StreamExt;
123    /// use wayle_core::Property;
124    ///
125    /// # async fn example() {
126    /// let score = Property::new(0_u32);
127    ///
128    /// let mut stream = score.watch();
129    /// while let Some(points) = stream.next().await {
130    ///     println!("score: {points}");
131    /// }
132    /// # }
133    /// ```
134    pub fn watch(&self) -> impl Stream<Item = T> + Send + 'static {
135        SubscribedStream::new(
136            WatchStream::new(self.rx.clone()),
137            Arc::clone(&self.subscriber_count),
138            Arc::clone(&self.subscriber_notify),
139        )
140    }
141
142    /// Whether any [`.watch()`](Self::watch) streams are alive.
143    pub fn has_subscribers(&self) -> bool {
144        self.subscriber_count.load(Ordering::Acquire) > 0
145    }
146
147    /// Suspends until at least one consumer calls [`.watch()`](Self::watch).
148    ///
149    /// Pair with [`has_subscribers`](Self::has_subscribers) in a loop to
150    /// pause expensive work whenever nobody is listening, and resume
151    /// when someone subscribes again.
152    ///
153    /// ```no_run
154    /// use wayle_core::Property;
155    ///
156    /// # async fn example() {
157    /// let cpu_usage = Property::new(0.0_f64);
158    ///
159    /// loop {
160    ///     if !cpu_usage.has_subscribers() {
161    ///         cpu_usage.wait_for_subscribers().await;
162    ///     }
163    ///
164    ///     let usage = 42.0; // poll_cpu();
165    ///     cpu_usage.set(usage);
166    /// }
167    /// # }
168    /// ```
169    pub async fn wait_for_subscribers(&self) {
170        while !self.has_subscribers() {
171            self.subscriber_notify.notified().await;
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use std::task::Poll;
179
180    use futures::{poll, stream::StreamExt};
181
182    use super::*;
183
184    #[test]
185    fn set_updates_value() {
186        let property = Property::new(42);
187        property.set(100);
188
189        assert_eq!(property.get(), 100);
190    }
191
192    #[tokio::test]
193    async fn set_skips_notification_when_unchanged() {
194        let property = Property::new(42);
195        let mut stream = property.watch();
196
197        assert_eq!(stream.next().await, Some(42));
198
199        property.set(42);
200        assert_eq!(poll!(stream.next()), Poll::Pending);
201    }
202
203    #[tokio::test]
204    async fn notifies_watchers_on_change() {
205        let property = Property::new(1);
206        let mut stream = property.watch();
207
208        assert_eq!(stream.next().await, Some(1));
209
210        property.set(2);
211        assert_eq!(stream.next().await, Some(2));
212    }
213
214    #[test]
215    fn no_subscribers_initially() {
216        let property = Property::new(0);
217
218        assert!(!property.has_subscribers());
219    }
220
221    #[test]
222    fn tracks_subscriber_lifetime() {
223        let property = Property::new(0);
224
225        let stream = property.watch();
226        assert!(property.has_subscribers());
227
228        drop(stream);
229        assert!(!property.has_subscribers());
230    }
231
232    #[test]
233    fn tracks_multiple_subscribers() {
234        let property = Property::new(0);
235
236        let stream_a = property.watch();
237        let stream_b = property.watch();
238        let stream_c = property.watch();
239        assert!(property.has_subscribers());
240
241        drop(stream_a);
242        assert!(property.has_subscribers());
243
244        drop(stream_b);
245        assert!(property.has_subscribers());
246
247        drop(stream_c);
248        assert!(!property.has_subscribers());
249    }
250
251    #[test]
252    fn clones_share_subscriber_count() {
253        let property = Property::new(0);
254        let cloned = property.clone();
255
256        let _stream = cloned.watch();
257
258        assert!(property.has_subscribers());
259    }
260
261    #[tokio::test]
262    async fn wait_for_subscribers_resolves_on_first_watcher() {
263        let property = Property::new(0);
264        let waiting = property.clone();
265
266        let (ready_tx, ready_rx) = tokio::sync::oneshot::channel::<()>();
267
268        let waiter = tokio::spawn(async move {
269            let _ = ready_tx.send(());
270            waiting.wait_for_subscribers().await;
271        });
272
273        ready_rx.await.unwrap();
274        let _stream = property.watch();
275
276        waiter.await.unwrap();
277    }
278
279    #[tokio::test]
280    async fn wait_for_subscribers_returns_immediately_if_already_watched() {
281        let property = Property::new(0);
282        let _stream = property.watch();
283
284        property.wait_for_subscribers().await;
285    }
286}