Skip to main content

rust_elm/
runtime.rs

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