frus_shell/
subscription.rs1use std::time::Duration;
10
11use web_time::Instant;
12
13type TimerFn<Msg> = Box<dyn Fn(Instant) -> Msg + Send>;
15
16pub(crate) enum Kind<Msg> {
18 Every {
20 interval: Duration,
21 make: TimerFn<Msg>,
22 },
23}
24
25pub(crate) struct Entry<Msg> {
27 pub(crate) id: u64,
28 pub(crate) kind: Kind<Msg>,
29}
30
31pub struct Subscription<Msg> {
33 entries: Vec<Entry<Msg>>,
34}
35
36fn 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 pub fn none() -> Self {
48 Self {
49 entries: Vec::new(),
50 }
51 }
52
53 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 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 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 pub fn is_empty(&self) -> bool {
104 self.entries.is_empty()
105 }
106
107 pub fn ids(&self) -> Vec<u64> {
109 self.entries.iter().map(|entry| entry.id).collect()
110 }
111
112 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}