Skip to main content

rust_elm/runtime/
engine.rs

1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::Arc;
3use std::thread::{self, JoinHandle};
4use std::time::Duration;
5
6use crossbeam_channel::RecvTimeoutError;
7use parking_lot::Mutex;
8use tokio::runtime::{Builder as TokioRuntimeBuilder, Runtime as TokioRuntime};
9
10use crate::bus::{Bus, BusSender};
11use crate::cmd::Cmd;
12use crate::effect::EffectId;
13use crate::env::Environment;
14use crate::program::{Program, ReducerProgram};
15use crate::reducer::Reducer;
16use crate::safe_reducer::safe_reduce_update;
17use crate::sub::Sub;
18
19use super::config::RuntimeConfig;
20use super::interpreter::{interpret_effects_async, InterpreterState};
21use super::store::{StoreBackend, StoreWork, StoreWorkUnwindGuard};
22use super::subscription;
23
24/// Live runtime — bus-driven update loop on a pinned thread with Tokio effect interpreter.
25pub struct Runtime<S, M> {
26    pub state: Arc<Mutex<S>>,
27    pub bus: Bus<M>,
28    pub env: Environment,
29    backend: StoreBackend<S, M>,
30    shutdown: Arc<AtomicBool>,
31    _thread: Option<JoinHandle<()>>,
32    _tokio: Arc<TokioRuntime>,
33    _subscriptions: Arc<subscription::SubscriptionHandles>,
34}
35
36impl<S, M> Runtime<S, M>
37where
38    S: Send + Sync + 'static,
39    M: Send + 'static,
40{
41    pub fn from_program(program: Program<S, M>, env: Environment, config: RuntimeConfig) -> Self {
42        Self::bootstrap(
43            program.init,
44            program.update,
45            program.subscriptions,
46            env,
47            config,
48        )
49    }
50
51    pub fn from_reducer_program<R>(
52        program: ReducerProgram<R>,
53        env: Environment,
54        config: RuntimeConfig,
55    ) -> Self
56    where
57        R: Reducer<State = S, Action = M> + Send + Sync + 'static,
58    {
59        let reducer = Arc::new(program.reducer);
60        let init = program.init;
61        let subscriptions = program.subscriptions;
62        Self::bootstrap(
63            init,
64            move |state, msg| reducer.reduce(state, msg),
65            subscriptions,
66            env,
67            config,
68        )
69    }
70
71    fn bootstrap(
72        init: fn() -> (S, Cmd<M>),
73        update: impl Fn(&mut S, M) -> Cmd<M> + Send + Sync + 'static,
74        subscriptions: fn(&S) -> Sub<M>,
75        env: Environment,
76        config: RuntimeConfig,
77    ) -> Self {
78        let RuntimeConfig {
79            bus_capacity,
80            worker_threads,
81            thread_name,
82        } = config;
83        let (initial_state, init_cmd) = init();
84        let state = Arc::new(Mutex::new(initial_state));
85        let bus = Bus::new(bus_capacity);
86        let shutdown = Arc::new(AtomicBool::new(false));
87        let tokio = Arc::new(
88            TokioRuntimeBuilder::new_multi_thread()
89                .worker_threads(worker_threads.max(1))
90                .thread_name(thread_name)
91                .enable_all()
92                .build()
93                .expect("failed to create Tokio runtime for rust-elm"),
94        );
95        let interpreter = InterpreterState::new();
96        let sub_handles = Arc::new(subscription::SubscriptionHandles::new());
97        let backend = StoreBackend::new(state.clone(), bus.sender(), interpreter);
98        let tx = bus.sender();
99        let init_handles = tokio.block_on(interpret_effects_async(
100            init_cmd.into_effects(),
101            tx.clone(),
102            env.clone(),
103            tokio.handle().clone(),
104            backend.clone(),
105        ));
106        tokio.handle().spawn(async move {
107            for join in init_handles {
108                let _ = join.await;
109            }
110        });
111
112        let receiver = bus.receiver().clone();
113        let subs_fn = subscriptions;
114        let state_for_thread = state.clone();
115        let env_for_thread = env.clone();
116        let shutdown_for_thread = shutdown.clone();
117        let tokio_for_thread = tokio.clone();
118        let backend_for_thread = backend.clone();
119        let tx_for_thread = tx;
120        let subs_registry = sub_handles.clone();
121
122        let thread = thread::Builder::new()
123            .name(thread_name.to_string())
124            .spawn(move || {
125                let rt = tokio_for_thread;
126                let sync_subs = |state: &S| {
127                    let sub = subs_fn(state);
128                    subscription::sync_subscriptions(
129                        &sub,
130                        &subs_registry,
131                        rt.handle().clone(),
132                        tx_for_thread.clone(),
133                        backend_for_thread.clone(),
134                        shutdown_for_thread.clone(),
135                    );
136                };
137
138                sync_subs(&state_for_thread.lock());
139
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 safe_reduce_update(&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                            sync_subs(&state_for_thread.lock());
177                        }
178                        Err(RecvTimeoutError::Timeout) => continue,
179                        Err(RecvTimeoutError::Disconnected) => break,
180                    }
181                }
182            })
183            .expect("failed to spawn rust-elm reducer thread");
184
185        Self {
186            state,
187            bus,
188            env,
189            backend,
190            shutdown,
191            _thread: Some(thread),
192            _tokio: tokio,
193            _subscriptions: sub_handles,
194        }
195    }
196
197    pub fn store(&self) -> crate::Store<S, M>
198    where
199        S: Clone + Send + Sync + 'static,
200    {
201        self.backend.store()
202    }
203
204    pub fn dispatch(&self, msg: M) {
205        let _ = self.bus.sender().send_blocking(msg);
206    }
207
208    pub fn sender(&self) -> BusSender<M> {
209        self.bus.sender()
210    }
211
212    pub fn cancel(&self, id: EffectId) {
213        if let Some(handle) = self.backend.hub.interpreter.cancel_tokens.lock().remove(&id) {
214            handle.abort();
215        }
216    }
217
218    pub fn shutdown(self) {
219        self.shutdown.store(true, Ordering::Relaxed);
220        self._subscriptions.abort_all();
221        if let Some(handle) = self._thread {
222            let _ = handle.join();
223        }
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use crate::effect::Effect;
231    use crate::panic_on_state_clone;
232
233    panic_on_state_clone! {
234        #[derive(Default)]
235        struct Counter {
236            n: i32,
237        }
238    }
239
240    fn init() -> (Counter, Cmd<i32>) {
241        (Counter::default(), Cmd::none())
242    }
243
244    fn update(s: &mut Counter, msg: i32) -> Cmd<i32> {
245        s.n += msg;
246        Cmd::none()
247    }
248
249    fn subs(_: &Counter) -> Sub<i32> {
250        Sub::none()
251    }
252
253    #[test]
254    fn runtime_tick_subscription_dispatches() {
255        fn subs(_: &Counter) -> Sub<i32> {
256            Sub::tick(7, Duration::from_millis(30), || 1)
257        }
258
259        let program = Program::new(init, update, subs);
260        let runtime = Runtime::from_program(program, Environment::new(), RuntimeConfig::new(16));
261        std::thread::sleep(Duration::from_millis(200));
262        assert!(runtime.state.lock().n >= 1);
263        runtime.shutdown();
264    }
265
266    #[test]
267    fn runtime_applies_dispatched_messages() {
268        let program = Program::new(init, update, subs);
269        let runtime = Runtime::from_program(program, Environment::new(), RuntimeConfig::new(16));
270        runtime.dispatch(3);
271        runtime.dispatch(4);
272        std::thread::sleep(Duration::from_millis(200));
273        assert_eq!(runtime.state.lock().n, 7);
274        runtime.shutdown();
275    }
276
277    #[test]
278    fn runtime_runs_task_effects() {
279        fn update_with_effect(s: &mut Counter, msg: i32) -> Cmd<i32> {
280            if msg == 0 {
281                Cmd::single(Effect::task(1, || Box::pin(async { Ok(10) })))
282            } else {
283                s.n += msg;
284                Cmd::none()
285            }
286        }
287        let program = Program::new(init, update_with_effect, subs);
288        let runtime = Runtime::from_program(program, Environment::new(), RuntimeConfig::new(16));
289        runtime.dispatch(0);
290        std::thread::sleep(Duration::from_millis(200));
291        assert_eq!(runtime.state.lock().n, 10);
292        runtime.shutdown();
293    }
294
295    #[test]
296    fn runtime_safe_reduce_update_unwinds_store_work() {
297        fn panicking_update(s: &mut Counter, msg: i32) -> Cmd<i32> {
298            s.n = 99;
299            if msg < 0 {
300                panic!("reduce panic");
301            }
302            s.n = msg;
303            Cmd::none()
304        }
305
306        let program = Program::new(init, panicking_update, subs);
307        let runtime = Runtime::from_program(program, Environment::new(), RuntimeConfig::new(16));
308        let store = runtime.store();
309        let task = store.send(-1);
310        assert!(task.finish().is_ok());
311        assert_eq!(runtime.state.lock().n, 99);
312
313        runtime.dispatch(4);
314        std::thread::sleep(Duration::from_millis(200));
315        assert_eq!(runtime.state.lock().n, 4);
316        runtime.shutdown();
317    }
318}