Skip to main content

frus_shell/
subscription.rs

1//! [`Subscription`]: the **continuous sources** of messages an application
2//! declares — the streaming counterpart to [`crate::Command`], which is one-shot.
3//!
4//! An app declares its subscriptions **as a function of its state**, and the
5//! framework **diffs** them every cycle, starting the new ones and stopping those
6//! that were withdrawn. That works thanks to a stable **id** per subscription, the
7//! hash of its recipe.
8
9use std::time::Duration;
10
11use web_time::Instant;
12
13/// A timer's message factory, called at every interval.
14type TimerFn<Msg> = Box<dyn Fn(Instant) -> Msg + Send>;
15
16/// What a subscription is.
17pub(crate) enum Kind<Msg> {
18    /// Emits a message at a regular interval.
19    Every {
20        interval: Duration,
21        make: TimerFn<Msg>,
22    },
23}
24
25/// A single subscription: its id, used by the diff, and its nature.
26pub(crate) struct Entry<Msg> {
27    pub(crate) id: u64,
28    pub(crate) kind: Kind<Msg>,
29}
30
31/// A set of continuous message sources, possibly empty.
32pub struct Subscription<Msg> {
33    entries: Vec<Entry<Msg>>,
34}
35
36/// An `every`'s stable id: the hash of its recipe, the kind plus the duration in ms.
37fn every_id(interval: Duration) -> u64 {
38    use std::hash::{Hash, Hasher};
39    let mut hasher = std::collections::hash_map::DefaultHasher::new();
40    "every".hash(&mut hasher);
41    interval.as_millis().hash(&mut hasher);
42    hasher.finish()
43}
44
45impl<Msg: Send + 'static> Subscription<Msg> {
46    /// No subscription at all.
47    pub fn none() -> Self {
48        Self {
49            entries: Vec::new(),
50        }
51    }
52
53    /// Groups several subscriptions together.
54    pub fn batch(subscriptions: impl IntoIterator<Item = Subscription<Msg>>) -> Self {
55        let mut entries = Vec::new();
56        for subscription in subscriptions {
57            entries.extend(subscription.entries);
58        }
59        Self { entries }
60    }
61
62    /// Emits a message every `interval`; the tick's `Instant` is passed along.
63    pub fn every(interval: Duration, make: impl Fn(Instant) -> Msg + Send + 'static) -> Self {
64        Self {
65            entries: vec![Entry {
66                id: every_id(interval),
67                kind: Kind::Every {
68                    interval,
69                    make: Box::new(make),
70                },
71            }],
72        }
73    }
74
75    /// [`Subscription::every`] under an identity of the caller's: two timers of the same period
76    /// are two subscriptions when their keys differ, and one that keeps its key from one cycle
77    /// to the next keeps running.
78    pub fn every_keyed(
79        key: u64,
80        interval: Duration,
81        make: impl Fn(Instant) -> Msg + Send + 'static,
82    ) -> Self {
83        let id = {
84            use std::hash::{Hash, Hasher};
85            let mut hasher = std::collections::hash_map::DefaultHasher::new();
86            "every-keyed".hash(&mut hasher);
87            key.hash(&mut hasher);
88            interval.as_millis().hash(&mut hasher);
89            hasher.finish()
90        };
91        Self {
92            entries: vec![Entry {
93                id,
94                kind: Kind::Every {
95                    interval,
96                    make: Box::new(make),
97                },
98            }],
99        }
100    }
101
102    /// `true` when there is no subscription.
103    pub fn is_empty(&self) -> bool {
104        self.entries.is_empty()
105    }
106
107    /// The subscriptions' ids, for inspection and testing.
108    pub fn ids(&self) -> Vec<u64> {
109        self.entries.iter().map(|entry| entry.id).collect()
110    }
111
112    /// Takes the entries out, for the framework to diff.
113    pub(crate) fn into_entries(self) -> Vec<Entry<Msg>> {
114        self.entries
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn none_is_empty() {
124        assert!(Subscription::<u32>::none().is_empty());
125    }
126
127    #[test]
128    fn every_id_is_stable_per_duration() {
129        let a = Subscription::every(Duration::from_secs(1), |_| 0u32);
130        let b = Subscription::every(Duration::from_secs(1), |_| 0u32);
131        let c = Subscription::every(Duration::from_secs(2), |_| 0u32);
132        assert_eq!(a.ids(), b.ids(), "same duration → same id");
133        assert_ne!(a.ids(), c.ids(), "different duration → different id");
134    }
135
136    #[test]
137    fn a_key_tells_two_timers_of_one_period_apart() {
138        let period = Duration::from_secs(1);
139        let a = Subscription::every_keyed(1, period, |_| 0u32);
140        let b = Subscription::every_keyed(2, period, |_| 0u32);
141        let again = Subscription::every_keyed(1, period, |_| 0u32);
142        assert_ne!(a.ids(), b.ids());
143        assert_eq!(
144            a.ids(),
145            again.ids(),
146            "and one that keeps its key keeps its identity"
147        );
148        assert_ne!(a.ids(), Subscription::every(period, |_| 0u32).ids());
149    }
150
151    #[test]
152    fn batch_combines_entries() {
153        let combined = Subscription::batch([
154            Subscription::every(Duration::from_secs(1), |_| 0u32),
155            Subscription::none(),
156            Subscription::every(Duration::from_secs(2), |_| 0u32),
157        ]);
158        assert_eq!(combined.ids().len(), 2);
159    }
160}