Skip to main content

rust_elm/runtime/subscription/
mod.rs

1//! Subscription interpreter — starts/stops long-lived Tokio sources declared by [`Sub`].
2mod websocket;
3
4use std::collections::{HashMap, HashSet};
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::Arc;
7
8use tokio::runtime::Handle;
9use tokio::task::AbortHandle;
10use tokio::time::{interval, MissedTickBehavior};
11
12use crate::bus::BusSender;
13use crate::store::StoreWork;
14use crate::sub::Sub;
15
16pub(crate) struct SubscriptionHandles {
17    pub handles: parking_lot::Mutex<HashMap<u64, AbortHandle>>,
18}
19
20impl SubscriptionHandles {
21    pub fn new() -> Self {
22        Self {
23            handles: parking_lot::Mutex::new(HashMap::new()),
24        }
25    }
26
27    pub fn abort_all(&self) {
28        self.handles
29            .lock()
30            .drain()
31            .for_each(|(_, handle)| handle.abort());
32    }
33}
34
35/// Reconcile running subscription tasks with the desired [`Sub`] tree.
36pub(crate) fn sync_subscriptions<S, M, B>(
37    sub: &Sub<M>,
38    registry: &SubscriptionHandles,
39    handle: Handle,
40    tx: BusSender<M>,
41    backend: B,
42    shutdown: Arc<AtomicBool>,
43) where
44    S: Send + 'static,
45    M: Send + 'static,
46    B: StoreWork<S, M> + Clone + Send + Sync + 'static,
47{
48    let mut desired = HashSet::new();
49    collect_sub_ids(sub, &mut desired);
50
51    let mut active = registry.handles.lock();
52    active.retain(|id, abort| {
53        if desired.contains(id) {
54            true
55        } else {
56            abort.abort();
57            false
58        }
59    });
60
61    spawn_subscriptions(
62        sub,
63        &mut active,
64        handle,
65        tx,
66        backend,
67        shutdown,
68    );
69}
70
71fn spawn_subscriptions<S, M, B>(
72    sub: &Sub<M>,
73    active: &mut HashMap<u64, AbortHandle>,
74    handle: Handle,
75    tx: BusSender<M>,
76    backend: B,
77    shutdown: Arc<AtomicBool>,
78) where
79    S: Send + 'static,
80    M: Send + 'static,
81    B: StoreWork<S, M> + Clone + Send + Sync + 'static,
82{
83    match sub {
84        Sub::None => {}
85        Sub::Tick { id, every, produce } => {
86            spawn_if_new(*id, active, || {
87                spawn_tick(*every, *produce, handle, tx, backend, shutdown)
88            });
89        }
90        Sub::Stream { id, name, every, produce } => {
91            spawn_if_new(*id, active, || {
92                spawn_stream(name, *every, *produce, handle, tx, backend, shutdown)
93            });
94        }
95        Sub::WebSocket { id, url, every, produce } => {
96            spawn_if_new(*id, active, || {
97                websocket::spawn_websocket(url, *every, *produce, handle, tx, backend, shutdown)
98            });
99        }
100        Sub::MapMsg { inner, map } => spawn_unit_subscriptions(
101            inner.as_ref(),
102            *map,
103            active,
104            handle,
105            tx,
106            backend,
107            shutdown,
108        ),
109        Sub::Batch(items) => {
110            for item in items {
111                spawn_subscriptions(
112                    item,
113                    active,
114                    handle.clone(),
115                    tx.clone(),
116                    backend.clone(),
117                    shutdown.clone(),
118                );
119            }
120        }
121    }
122}
123
124fn spawn_unit_subscriptions<S, M, B>(
125    sub: &Sub<()>,
126    map: fn(()) -> M,
127    active: &mut HashMap<u64, AbortHandle>,
128    handle: Handle,
129    tx: BusSender<M>,
130    backend: B,
131    shutdown: Arc<AtomicBool>,
132) where
133    S: Send + 'static,
134    M: Send + 'static,
135    B: StoreWork<S, M> + Clone + Send + Sync + 'static,
136{
137    match sub {
138        Sub::None => {}
139        Sub::Tick { id, every, .. } => {
140            spawn_if_new(*id, active, || {
141                spawn_tick_mapped(*every, map, handle, tx, backend, shutdown)
142            });
143        }
144        Sub::Stream { id, name, every, .. } => {
145            spawn_if_new(*id, active, || {
146                spawn_stream_mapped(name, *every, map, handle, tx, backend, shutdown)
147            });
148        }
149        Sub::WebSocket { id, url, every, .. } => {
150            spawn_if_new(*id, active, || {
151                websocket::spawn_websocket_mapped(url, *every, map, handle, tx, backend, shutdown)
152            });
153        }
154        Sub::MapMsg { .. } => {}
155        Sub::Batch(items) => {
156            for item in items {
157                spawn_unit_subscriptions(
158                    item,
159                    map,
160                    active,
161                    handle.clone(),
162                    tx.clone(),
163                    backend.clone(),
164                    shutdown.clone(),
165                );
166            }
167        }
168    }
169}
170
171fn spawn_if_new(id: u64, active: &mut HashMap<u64, AbortHandle>, spawn: impl FnOnce() -> AbortHandle) {
172    if active.contains_key(&id) {
173        return;
174    }
175    active.insert(id, spawn());
176}
177
178fn spawn_tick<S, M, B>(
179    every: std::time::Duration,
180    produce: fn() -> M,
181    handle: Handle,
182    tx: BusSender<M>,
183    backend: B,
184    shutdown: Arc<AtomicBool>,
185) -> AbortHandle
186where
187    S: Send + 'static,
188    M: Send + 'static,
189    B: StoreWork<S, M> + Clone + Send + Sync + 'static,
190{
191    handle
192        .spawn(async move {
193            let mut ticker = interval(every);
194            ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
195            ticker.tick().await;
196            loop {
197                if shutdown.load(Ordering::Relaxed) {
198                    break;
199                }
200                ticker.tick().await;
201                if shutdown.load(Ordering::Relaxed) {
202                    break;
203                }
204                crate::runtime::dispatch::dispatch_from_subscription(&backend, &tx, produce());
205            }
206        })
207        .abort_handle()
208}
209
210fn spawn_tick_mapped<S, M, B>(
211    every: std::time::Duration,
212    map: fn(()) -> M,
213    handle: Handle,
214    tx: BusSender<M>,
215    backend: B,
216    shutdown: Arc<AtomicBool>,
217) -> AbortHandle
218where
219    S: Send + 'static,
220    M: Send + 'static,
221    B: StoreWork<S, M> + Clone + Send + Sync + 'static,
222{
223    handle
224        .spawn(async move {
225            let mut ticker = interval(every);
226            ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
227            ticker.tick().await;
228            loop {
229                if shutdown.load(Ordering::Relaxed) {
230                    break;
231                }
232                ticker.tick().await;
233                if shutdown.load(Ordering::Relaxed) {
234                    break;
235                }
236                crate::runtime::dispatch::dispatch_from_subscription(&backend, &tx, map(()));
237            }
238        })
239        .abort_handle()
240}
241
242fn spawn_stream<S, M, B>(
243    _name: &'static str,
244    every: std::time::Duration,
245    produce: fn() -> M,
246    handle: Handle,
247    tx: BusSender<M>,
248    backend: B,
249    shutdown: Arc<AtomicBool>,
250) -> AbortHandle
251where
252    S: Send + 'static,
253    M: Send + 'static,
254    B: StoreWork<S, M> + Clone + Send + Sync + 'static,
255{
256    handle
257        .spawn(async move {
258            let mut ticker = interval(every);
259            ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
260            loop {
261                if shutdown.load(Ordering::Relaxed) {
262                    break;
263                }
264                ticker.tick().await;
265                if shutdown.load(Ordering::Relaxed) {
266                    break;
267                }
268                crate::runtime::dispatch::dispatch_from_subscription(&backend, &tx, produce());
269            }
270        })
271        .abort_handle()
272}
273
274fn spawn_stream_mapped<S, M, B>(
275    _name: &'static str,
276    every: std::time::Duration,
277    map: fn(()) -> M,
278    handle: Handle,
279    tx: BusSender<M>,
280    backend: B,
281    shutdown: Arc<AtomicBool>,
282) -> AbortHandle
283where
284    S: Send + 'static,
285    M: Send + 'static,
286    B: StoreWork<S, M> + Clone + Send + Sync + 'static,
287{
288    handle
289        .spawn(async move {
290            let mut ticker = interval(every);
291            ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
292            loop {
293                if shutdown.load(Ordering::Relaxed) {
294                    break;
295                }
296                ticker.tick().await;
297                if shutdown.load(Ordering::Relaxed) {
298                    break;
299                }
300                crate::runtime::dispatch::dispatch_from_subscription(&backend, &tx, map(()));
301            }
302        })
303        .abort_handle()
304}
305
306pub(crate) fn collect_sub_ids<M>(sub: &Sub<M>, out: &mut HashSet<u64>) {
307    match sub {
308        Sub::None => {}
309        Sub::Tick { id, .. }
310        | Sub::Stream { id, .. }
311        | Sub::WebSocket { id, .. } => {
312            out.insert(*id);
313        }
314        Sub::MapMsg { inner, .. } => collect_sub_ids(inner, out),
315        Sub::Batch(items) => {
316            for item in items {
317                collect_sub_ids(item, out);
318            }
319        }
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use std::time::Duration;
327
328    fn unit() {}
329
330    fn mapped(_: ()) -> i32 {
331        7
332    }
333
334    #[test]
335    fn collect_ids_from_batch() {
336        fn zero() -> i32 {
337            0
338        }
339        let sub = Sub::batch([
340            Sub::tick(1, Duration::from_secs(1), zero),
341            Sub::stream(2, "events", Duration::from_secs(1), zero),
342        ]);
343        let mut ids = HashSet::new();
344        collect_sub_ids(&sub, &mut ids);
345        assert_eq!(ids, HashSet::from([1, 2]));
346    }
347
348    #[test]
349    fn collect_ids_from_map_msg() {
350        let sub = Sub::MapMsg {
351            inner: Box::new(Sub::tick(9, Duration::from_millis(1), unit)),
352            map: mapped,
353        };
354        let mut ids = HashSet::new();
355        collect_sub_ids(&sub, &mut ids);
356        assert_eq!(ids, HashSet::from([9]));
357    }
358}