Skip to main content

rust_elm/
runtime.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::Arc;
4use std::thread::{self, JoinHandle};
5use std::time::Duration;
6
7use crossbeam_channel::RecvTimeoutError;
8use parking_lot::Mutex;
9use tokio::runtime::Runtime as TokioRuntime;
10use tokio::task::JoinHandle as TokioJoinHandle;
11use tokio::time::timeout;
12
13use crate::bus::{Bus, BusSender};
14use crate::cmd::Cmd;
15use crate::effect::{
16    run_leaf, run_registered_env_task, run_registered_run, run_registered_task, Effect, EffectId,
17};
18use crate::env::Environment;
19use crate::error::EffectError;
20use crate::interp::flatten_effects;
21use crate::program::{Program, ReducerProgram};
22use crate::reducer::Reducer;
23use crate::store::{catch_reduce_panic, StoreBackend, StoreWorkUnwindGuard};
24use crate::sub::Sub;
25
26pub(crate) struct InterpreterState<M> {
27    pub cancel_tokens: Mutex<HashMap<EffectId, tokio::task::AbortHandle>>,
28    debounce_timers: Mutex<HashMap<EffectId, TokioJoinHandle<()>>>,
29    throttle_gates: Mutex<HashMap<EffectId, ThrottleGate<M>>>,
30}
31
32impl<M> InterpreterState<M> {
33    fn new() -> Arc<Self> {
34        Arc::new(Self {
35            cancel_tokens: Mutex::new(HashMap::new()),
36            debounce_timers: Mutex::new(HashMap::new()),
37            throttle_gates: Mutex::new(HashMap::new()),
38        })
39    }
40}
41
42struct ThrottleGate<M> {
43    latest: bool,
44    pending: Option<Effect<M>>,
45    timer: Option<TokioJoinHandle<()>>,
46}
47
48/// Live runtime — bus-driven update loop on a pinned thread with Tokio effect interpreter.
49pub struct Runtime<S, M> {
50    pub state: Arc<Mutex<S>>,
51    pub bus: Bus<M>,
52    pub env: Environment,
53    backend: StoreBackend<S, M>,
54    shutdown: Arc<AtomicBool>,
55    _thread: Option<JoinHandle<()>>,
56    _tokio: Arc<TokioRuntime>,
57}
58
59impl<S, M> Runtime<S, M>
60where
61    S: Send + Sync + 'static,
62    M: Send + 'static,
63{
64    pub fn from_program(program: Program<S, M>, env: Environment, bus_capacity: usize) -> Self {
65        let update = program.update;
66        Self::bootstrap(
67            program.init,
68            move |state, msg| update(state, msg),
69            program.subscriptions,
70            env,
71            bus_capacity,
72        )
73    }
74
75    pub fn from_reducer_program<R>(
76        program: ReducerProgram<R>,
77        env: Environment,
78        bus_capacity: usize,
79    ) -> Self
80    where
81        R: Reducer<State = S, Action = M> + Send + Sync + 'static,
82    {
83        let reducer = Arc::new(program.reducer);
84        let init = program.init;
85        let subscriptions = program.subscriptions;
86        Self::bootstrap(
87            init,
88            move |state, msg| reducer.reduce(state, msg),
89            subscriptions,
90            env,
91            bus_capacity,
92        )
93    }
94
95    fn bootstrap(
96        init: fn() -> (S, Cmd<M>),
97        update: impl Fn(&mut S, M) -> Cmd<M> + Send + Sync + 'static,
98        subscriptions: fn(&S) -> Sub<M>,
99        env: Environment,
100        bus_capacity: usize,
101    ) -> Self {
102        let (initial_state, init_cmd) = init();
103        let state = Arc::new(Mutex::new(initial_state));
104        let bus = Bus::new(bus_capacity);
105        let shutdown = Arc::new(AtomicBool::new(false));
106        let tokio = Arc::new(
107            TokioRuntime::new().expect("failed to create Tokio runtime for rust-elm"),
108        );
109        let interpreter = Arc::new(InterpreterState {
110            cancel_tokens: Mutex::new(HashMap::new()),
111            debounce_timers: Mutex::new(HashMap::new()),
112            throttle_gates: Mutex::new(HashMap::new()),
113        });
114        let backend = StoreBackend::new(state.clone(), bus.sender(), interpreter);
115        let tx = bus.sender();
116        let init_handles = tokio.block_on(interpret_effects_async(
117            init_cmd.into_effects(),
118            tx.clone(),
119            env.clone(),
120            tokio.handle().clone(),
121            backend.clone(),
122        ));
123        tokio.handle().spawn(async move {
124            for join in init_handles {
125                let _ = join.await;
126            }
127        });
128
129        let receiver = bus.receiver().clone();
130        let subs_fn = subscriptions;
131        let state_for_thread = state.clone();
132        let env_for_thread = env.clone();
133        let shutdown_for_thread = shutdown.clone();
134        let tokio_for_thread = tokio.clone();
135        let backend_for_thread = backend.clone();
136        let tx_for_thread = tx;
137
138        let thread = thread::spawn(move || {
139            let rt = tokio_for_thread;
140            while !shutdown_for_thread.load(Ordering::Relaxed) {
141                match receiver.recv_timeout(Duration::from_millis(50)) {
142                    Ok(msg) => {
143                        let mut unwind = StoreWorkUnwindGuard::new(&backend_for_thread);
144                        let cmd = {
145                            let mut guard = state_for_thread.lock();
146                            match catch_reduce_panic(&mut *guard, |s, a| update(s, a), msg) {
147                                Ok(cmd) => cmd,
148                                Err(_) => {
149                                    drop(guard);
150                                    backend_for_thread.notify_state();
151                                    continue;
152                                }
153                            }
154                        };
155                        unwind.disarm();
156                        backend_for_thread.notify_state();
157                        let handles = rt.block_on(interpret_effects_async(
158                            cmd.into_effects(),
159                            tx_for_thread.clone(),
160                            env_for_thread.clone(),
161                            rt.handle().clone(),
162                            backend_for_thread.clone(),
163                        ));
164                        if handles.is_empty() {
165                            backend_for_thread.end_store_work();
166                        } else {
167                            let backend_wait = backend_for_thread.clone();
168                            rt.handle().spawn(async move {
169                                for join in handles {
170                                    let _ = join.await;
171                                }
172                                backend_wait.end_store_work();
173                            });
174                        }
175
176                        let sub = {
177                            let guard = state_for_thread.lock();
178                            subs_fn(&guard)
179                        };
180                        let mut active_subs = HashSet::new();
181                        diff_subscriptions(&sub, &mut active_subs);
182                    }
183                    Err(RecvTimeoutError::Timeout) => continue,
184                    Err(RecvTimeoutError::Disconnected) => break,
185                }
186            }
187        });
188
189        Self {
190            state,
191            bus,
192            env,
193            backend,
194            shutdown,
195            _thread: Some(thread),
196            _tokio: tokio,
197        }
198    }
199
200    pub fn store(&self) -> crate::Store<S, M>
201    where
202        S: Clone + Send + Sync + 'static,
203    {
204        self.backend.store()
205    }
206
207    pub fn dispatch(&self, msg: M) {
208        let _ = self.bus.sender().send_blocking(msg);
209    }
210
211    pub fn sender(&self) -> BusSender<M> {
212        self.bus.sender()
213    }
214
215    pub fn cancel(&self, id: EffectId) {
216        if let Some(handle) = self.backend.interpreter.cancel_tokens.lock().remove(&id) {
217            handle.abort();
218        }
219    }
220
221    pub fn shutdown(self) {
222        self.shutdown.store(true, Ordering::Relaxed);
223        if let Some(handle) = self._thread {
224            let _ = handle.join();
225        }
226    }
227}
228
229fn dispatch_from_effect<S, M>(
230    backend: &StoreBackend<S, M>,
231    tx: &BusSender<M>,
232    msg: M,
233) where
234    S: Send + 'static,
235    M: Send + 'static,
236{
237    backend.begin_store_work();
238    let _ = tx.send_blocking(msg);
239}
240
241async fn interpret_effects_async<S, M>(
242    effects: Vec<Effect<M>>,
243    tx: BusSender<M>,
244    env: Environment,
245    handle: tokio::runtime::Handle,
246    backend: StoreBackend<S, M>,
247) -> Vec<TokioJoinHandle<()>>
248where
249    S: Send + 'static,
250    M: Send + 'static,
251{
252    let interpreter = backend.interpreter.clone();
253    let mut handles = Vec::new();
254    for effect in effects {
255        for leaf in flatten_effects(effect) {
256            handles.extend(spawn_effect(
257                leaf,
258                tx.clone(),
259                env.clone(),
260                handle.clone(),
261                interpreter.clone(),
262                backend.clone(),
263            ));
264        }
265    }
266    handles
267}
268
269fn spawn_effect<S, M>(
270    effect: Effect<M>,
271    tx: BusSender<M>,
272    env: Environment,
273    handle: tokio::runtime::Handle,
274    interpreter: Arc<InterpreterState<M>>,
275    backend: StoreBackend<S, M>,
276) -> Vec<TokioJoinHandle<()>>
277where
278    S: Send + 'static,
279    M: Send + 'static,
280{
281    let mut batch = Vec::new();
282    spawn_effect_inner(
283        effect,
284        tx,
285        env,
286        handle,
287        interpreter,
288        backend,
289        &mut batch,
290    );
291    batch
292}
293
294fn track_spawn<M>(
295    join: TokioJoinHandle<()>,
296    cancel_id: Option<EffectId>,
297    interpreter: &InterpreterState<M>,
298    batch: &mut Vec<TokioJoinHandle<()>>,
299) {
300    if let Some(id) = cancel_id {
301        interpreter
302            .cancel_tokens
303            .lock()
304            .insert(id, join.abort_handle());
305    }
306    batch.push(join);
307}
308
309fn spawn_effect_inner<S, M>(
310    effect: Effect<M>,
311    tx: BusSender<M>,
312    env: Environment,
313    handle: tokio::runtime::Handle,
314    interpreter: Arc<InterpreterState<M>>,
315    backend: StoreBackend<S, M>,
316    batch: &mut Vec<TokioJoinHandle<()>>,
317) where
318    S: Send + 'static,
319    M: Send + 'static,
320{
321    match effect {
322        Effect::None => {}
323        Effect::Cancel { id } => {
324            if let Some(old) = interpreter.cancel_tokens.lock().remove(&id) {
325                old.abort();
326            }
327        }
328        Effect::Task { id, run } => {
329            let tx = tx.clone();
330            let backend = backend.clone();
331            track_spawn(
332                handle.spawn(async move {
333                    if let Ok(msg) = run().await {
334                        dispatch_from_effect(&backend, &tx, msg);
335                    }
336                }),
337                Some(id),
338                &interpreter,
339                batch,
340            );
341        }
342        Effect::RegisteredTask { id } => {
343            let tx = tx.clone();
344            let backend = backend.clone();
345            track_spawn(
346                handle.spawn(async move {
347                    if let Ok(msg) = run_registered_task::<M>(id).await {
348                        dispatch_from_effect(&backend, &tx, msg);
349                    }
350                }),
351                Some(id),
352                &interpreter,
353                batch,
354            );
355        }
356        Effect::RegisteredRun { id } => {
357            let tx = tx.clone();
358            track_spawn(
359                handle.spawn(async move {
360                    let _ = run_registered_run::<M>(id, tx.clone()).await;
361                }),
362                Some(id),
363                &interpreter,
364                batch,
365            );
366        }
367        Effect::EnvTask { id, run } => {
368            let tx = tx.clone();
369            let env = env.clone();
370            let backend = backend.clone();
371            track_spawn(
372                handle.spawn(async move {
373                    if let Ok(msg) = run(&env).await {
374                        dispatch_from_effect(&backend, &tx, msg);
375                    }
376                }),
377                Some(id),
378                &interpreter,
379                batch,
380            );
381        }
382        Effect::RegisteredEnvTask { id } => {
383            let tx = tx.clone();
384            let env = env.clone();
385            let backend = backend.clone();
386            track_spawn(
387                handle.spawn(async move {
388                    if let Ok(msg) = run_registered_env_task::<M>(&env, id).await {
389                        dispatch_from_effect(&backend, &tx, msg);
390                    }
391                }),
392                Some(id),
393                &interpreter,
394                batch,
395            );
396        }
397        Effect::Batch(items) | Effect::Race(items) => {
398            for item in items {
399                spawn_effect_inner(
400                    item,
401                    tx.clone(),
402                    env.clone(),
403                    handle.clone(),
404                    interpreter.clone(),
405                    backend.clone(),
406                    batch,
407                );
408            }
409        }
410        Effect::Sequence(items) => {
411            let tx = tx.clone();
412            let env = env.clone();
413            let backend = backend.clone();
414            track_spawn(
415                handle.spawn(async move {
416                    for item in items {
417                        if let Ok(msg) = run_effect_once(item, env.clone()).await {
418                            dispatch_from_effect(&backend, &tx, msg);
419                        }
420                    }
421                }),
422                None,
423                &interpreter,
424                batch,
425            );
426        }
427        Effect::Cancellable {
428            id,
429            cancel_in_flight,
430            inner,
431        } => {
432            if cancel_in_flight {
433                if let Some(old) = interpreter.cancel_tokens.lock().remove(&id) {
434                    old.abort();
435                }
436            }
437            spawn_effect_inner(*inner, tx, env, handle, interpreter, backend, batch);
438        }
439        Effect::Debounce { id, duration, inner } => {
440            if let Some(old) = interpreter.debounce_timers.lock().remove(&id) {
441                old.abort();
442            }
443            let inner = *inner;
444            let tx = tx.clone();
445            let env = env.clone();
446            let handle_worker = handle.clone();
447            let interpreter_worker = interpreter.clone();
448            let backend_worker = backend.clone();
449            let join = handle.spawn(async move {
450                tokio::time::sleep(duration).await;
451                interpreter_worker.debounce_timers.lock().remove(&id);
452                spawn_effect(
453                    inner,
454                    tx,
455                    env,
456                    handle_worker,
457                    interpreter_worker,
458                    backend_worker,
459                );
460            });
461            interpreter.debounce_timers.lock().insert(id, join);
462        }
463        Effect::Throttle {
464            id,
465            duration,
466            latest,
467            inner,
468        } => {
469            let mut gates = interpreter.throttle_gates.lock();
470            let gate = gates.entry(id).or_insert(ThrottleGate {
471                latest,
472                pending: None,
473                timer: None,
474            });
475            gate.latest = latest;
476
477            if gate.timer.is_none() {
478                if latest {
479                    gate.pending = Some(*inner);
480                } else {
481                    let tx_now = tx.clone();
482                    let env_now = env.clone();
483                    let handle_now = handle.clone();
484                    let interpreter_now = interpreter.clone();
485                    let backend_now = backend.clone();
486                    batch.extend(spawn_effect(
487                        *inner,
488                        tx_now,
489                        env_now,
490                        handle_now,
491                        interpreter_now,
492                        backend_now,
493                    ));
494                }
495                let tx_timer = tx.clone();
496                let env_timer = env.clone();
497                let handle_timer = handle.clone();
498                let interpreter_timer = interpreter.clone();
499                let backend_timer = backend.clone();
500                let join = handle.spawn(async move {
501                    tokio::time::sleep(duration).await;
502                    let effect_to_run = {
503                        let mut gates = interpreter_timer.throttle_gates.lock();
504                        if let Some(g) = gates.get_mut(&id) {
505                            g.timer = None;
506                            if g.latest {
507                                g.pending.take()
508                            } else {
509                                None
510                            }
511                        } else {
512                            None
513                        }
514                    };
515                    if let Some(effect) = effect_to_run {
516                        spawn_effect(
517                            effect,
518                            tx_timer,
519                            env_timer,
520                            handle_timer,
521                            interpreter_timer,
522                            backend_timer,
523                        );
524                    } else {
525                        interpreter_timer.throttle_gates.lock().remove(&id);
526                    }
527                });
528                gate.timer = Some(join);
529            } else if latest {
530                gate.pending = Some(*inner);
531            }
532        }
533        Effect::Provide { env: layer, inner } => {
534            let scoped = env.scoped_with(layer);
535            spawn_effect_inner(*inner, tx, scoped, handle, interpreter, backend, batch);
536        }
537        Effect::Retry { attempts, inner } => {
538            let tx = tx.clone();
539            let env = env.clone();
540            let backend = backend.clone();
541            let inner = *inner;
542            track_spawn(
543                handle.spawn(async move {
544                    for _ in 0..attempts.max(1) {
545                        match run_effect_once(inner.clone(), env.clone()).await {
546                            Ok(msg) => {
547                                dispatch_from_effect(&backend, &tx, msg);
548                                break;
549                            }
550                            Err(_) => continue,
551                        }
552                    }
553                }),
554                None,
555                &interpreter,
556                batch,
557            );
558        }
559        Effect::Timeout { duration, inner } => {
560            let tx = tx.clone();
561            let env = env.clone();
562            let backend = backend.clone();
563            track_spawn(
564                handle.spawn(async move {
565                    match timeout(duration, run_effect_once(*inner, env)).await {
566                        Ok(Ok(msg)) => {
567                            dispatch_from_effect(&backend, &tx, msg);
568                        }
569                        _ => {}
570                    }
571                }),
572                None,
573                &interpreter,
574                batch,
575            );
576        }
577        Effect::Catch { inner, recover } => {
578            let tx = tx.clone();
579            let env = env.clone();
580            let handle_worker = handle.clone();
581            let backend_worker = backend.clone();
582            track_spawn(
583                handle.spawn(async move {
584                    match run_effect_once(*inner, env.clone()).await {
585                        Ok(msg) => {
586                            dispatch_from_effect(&backend_worker, &tx, msg);
587                        }
588                        Err(err) => {
589                            for join in interpret_effects_async(
590                                flatten_effects(recover(err)),
591                                tx,
592                                env,
593                                handle_worker.clone(),
594                                backend_worker,
595                            )
596                            .await
597                            {
598                                let _ = join.await;
599                            }
600                        }
601                    }
602                }),
603                None,
604                &interpreter,
605                batch,
606            );
607        }
608    }
609}
610
611async fn run_effect_once<M>(effect: Effect<M>, env: Environment) -> Result<M, EffectError>
612where
613    M: Send + 'static,
614{
615    run_leaf(effect, &env).await
616}
617
618fn diff_subscriptions<M>(sub: &Sub<M>, active: &mut HashSet<u64>) {
619    let mut seen = HashSet::new();
620    collect_sub_ids(sub, &mut seen);
621    active.retain(|id| seen.contains(id));
622    active.extend(seen);
623}
624
625fn collect_sub_ids<M>(sub: &Sub<M>, out: &mut HashSet<u64>) {
626    match sub {
627        Sub::None => {}
628        Sub::Tick { id, .. } | Sub::Stream { id, .. } | Sub::WebSocket { id, .. } => {
629            out.insert(*id);
630        }
631        Sub::MapMsg { inner, .. } => collect_sub_ids(inner, out),
632        Sub::Batch(items) => {
633            for item in items {
634                collect_sub_ids(item, out);
635            }
636        }
637    }
638}
639
640#[cfg(test)]
641mod tests {
642    use super::*;
643    use crate::effect::Effect;
644    use crate::panic_on_state_clone;
645
646    panic_on_state_clone! {
647        #[derive(Default)]
648        struct Counter {
649            n: i32,
650        }
651    }
652
653    fn init() -> (Counter, Cmd<i32>) {
654        (Counter::default(), Cmd::none())
655    }
656
657    fn update(s: &mut Counter, msg: i32) -> Cmd<i32> {
658        s.n += msg;
659        Cmd::none()
660    }
661
662    fn subs(_: &Counter) -> Sub<i32> {
663        Sub::none()
664    }
665
666    #[test]
667    fn runtime_applies_dispatched_messages() {
668        let program = Program::new(init, update, subs);
669        let runtime = Runtime::from_program(program, Environment::new(), 16);
670        runtime.dispatch(3);
671        runtime.dispatch(4);
672        std::thread::sleep(Duration::from_millis(200));
673        assert_eq!(runtime.state.lock().n, 7);
674        runtime.shutdown();
675    }
676
677    #[test]
678    fn runtime_runs_task_effects() {
679        fn update_with_effect(s: &mut Counter, msg: i32) -> Cmd<i32> {
680            if msg == 0 {
681                Cmd::single(Effect::task(1, || Box::pin(async { Ok(10) })))
682            } else {
683                s.n += msg;
684                Cmd::none()
685            }
686        }
687        let program = Program::new(init, update_with_effect, subs);
688        let runtime = Runtime::from_program(program, Environment::new(), 16);
689        runtime.dispatch(0);
690        std::thread::sleep(Duration::from_millis(200));
691        assert_eq!(runtime.state.lock().n, 10);
692        runtime.shutdown();
693    }
694
695    #[test]
696    fn runtime_catches_reduce_panic_and_unwinds_store_work() {
697        fn panicking_update(s: &mut Counter, msg: i32) -> Cmd<i32> {
698            s.n = 99;
699            if msg < 0 {
700                panic!("reduce panic");
701            }
702            s.n = msg;
703            Cmd::none()
704        }
705
706        let program = Program::new(init, panicking_update, subs);
707        let runtime = Runtime::from_program(program, Environment::new(), 16);
708        let store = runtime.store();
709        let task = store.send(-1);
710        assert!(task.finish().is_ok());
711        assert_eq!(runtime.state.lock().n, 99);
712
713        runtime.dispatch(4);
714        std::thread::sleep(Duration::from_millis(200));
715        assert_eq!(runtime.state.lock().n, 4);
716        runtime.shutdown();
717    }
718}