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::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::error::{build_tokio, RuntimeError};
21use super::interpreter::{interpret_effects_async, InterpreterState};
22use super::store::{StoreBackend, StoreWork, StoreWorkUnwindGuard};
23use super::subscription;
24
25/// Live runtime — bus-driven update loop on a pinned thread with Tokio effect interpreter.
26pub struct Runtime<S, M> {
27    pub state: Arc<Mutex<S>>,
28    pub bus: Bus<M>,
29    pub env: Environment,
30    backend: StoreBackend<S, M>,
31    shutdown: Arc<AtomicBool>,
32    _thread: Option<JoinHandle<()>>,
33    _tokio: Arc<TokioRuntime>,
34    _subscriptions: Arc<subscription::SubscriptionHandles>,
35}
36
37impl<S, M> Runtime<S, M>
38where
39    S: Send + Sync + 'static,
40    M: Send + 'static,
41{
42    pub fn from_program(
43        program: Program<S, M>,
44        env: Environment,
45        config: RuntimeConfig,
46    ) -> Result<Self, RuntimeError> {
47        Self::bootstrap(
48            program.init,
49            program.update,
50            program.subscriptions,
51            env,
52            config,
53        )
54    }
55
56    pub fn from_reducer_program<R>(
57        program: ReducerProgram<R>,
58        env: Environment,
59        config: RuntimeConfig,
60    ) -> Result<Self, RuntimeError>
61    where
62        R: Reducer<State = S, Action = M> + Send + Sync + 'static,
63    {
64        let reducer = Arc::new(program.reducer);
65        let init = program.init;
66        let subscriptions = program.subscriptions;
67        Self::bootstrap(
68            init,
69            move |state, msg| reducer.reduce(state, msg),
70            subscriptions,
71            env,
72            config,
73        )
74    }
75
76    fn bootstrap(
77        init: fn() -> (S, Cmd<M>),
78        update: impl Fn(&mut S, M) -> Cmd<M> + Send + Sync + 'static,
79        subscriptions: fn(&S) -> Sub<M>,
80        env: Environment,
81        config: RuntimeConfig,
82    ) -> Result<Self, RuntimeError> {
83        let RuntimeConfig {
84            bus_capacity,
85            worker_threads,
86            thread_name,
87        } = config;
88        let (initial_state, init_cmd) = init();
89        let state = Arc::new(Mutex::new(initial_state));
90        let bus = Bus::new(bus_capacity);
91        let shutdown = Arc::new(AtomicBool::new(false));
92        let tokio = build_tokio(worker_threads, thread_name)?;
93        let interpreter = InterpreterState::new();
94        let sub_handles = Arc::new(subscription::SubscriptionHandles::new());
95        let backend = StoreBackend::new(state.clone(), bus.sender(), interpreter);
96        let tx = bus.sender();
97        let init_handles = tokio.block_on(interpret_effects_async(
98            init_cmd.into_effects(),
99            tx.clone(),
100            env.clone(),
101            tokio.handle().clone(),
102            backend.clone(),
103        ));
104        tokio.handle().spawn(async move {
105            for join in init_handles {
106                let _ = join.await;
107            }
108        });
109
110        let receiver = bus.receiver().clone();
111        let subs_fn = subscriptions;
112        let state_for_thread = state.clone();
113        let env_for_thread = env.clone();
114        let shutdown_for_thread = shutdown.clone();
115        let tokio_for_thread = tokio.clone();
116        let backend_for_thread = backend.clone();
117        let tx_for_thread = tx;
118        let subs_registry = sub_handles.clone();
119
120        let thread = thread::Builder::new()
121            .name(thread_name.to_string())
122            .spawn(move || {
123                let rt = tokio_for_thread;
124                let sync_subs = |state: &S| {
125                    let sub = subs_fn(state);
126                    subscription::sync_subscriptions(
127                        &sub,
128                        &subs_registry,
129                        rt.handle().clone(),
130                        tx_for_thread.clone(),
131                        backend_for_thread.clone(),
132                        shutdown_for_thread.clone(),
133                    );
134                };
135
136                sync_subs(&state_for_thread.lock());
137
138                while !shutdown_for_thread.load(Ordering::Relaxed) {
139                    match receiver.recv_timeout(Duration::from_millis(50)) {
140                        Ok(msg) => {
141                            let mut unwind = StoreWorkUnwindGuard::new(&backend_for_thread);
142                            let cmd = {
143                                let mut guard = state_for_thread.lock();
144                                match safe_reduce_update(&mut *guard, |s, a| update(s, a), msg) {
145                                    Ok(cmd) => cmd,
146                                    Err(_) => {
147                                        drop(guard);
148                                        backend_for_thread.notify_state();
149                                        continue;
150                                    }
151                                }
152                            };
153                            unwind.disarm();
154                            backend_for_thread.notify_state();
155                            let handles = rt.block_on(interpret_effects_async(
156                                cmd.into_effects(),
157                                tx_for_thread.clone(),
158                                env_for_thread.clone(),
159                                rt.handle().clone(),
160                                backend_for_thread.clone(),
161                            ));
162                            if handles.is_empty() {
163                                backend_for_thread.end_store_work();
164                            } else {
165                                let backend_wait = backend_for_thread.clone();
166                                rt.handle().spawn(async move {
167                                    for join in handles {
168                                        let _ = join.await;
169                                    }
170                                    backend_wait.end_store_work();
171                                });
172                            }
173
174                            sync_subs(&state_for_thread.lock());
175                        }
176                        Err(RecvTimeoutError::Timeout) => continue,
177                        Err(RecvTimeoutError::Disconnected) => break,
178                    }
179                }
180            })
181            .map_err(RuntimeError::ThreadSpawn)?;
182
183        Ok(Self {
184            state,
185            bus,
186            env,
187            backend,
188            shutdown,
189            _thread: Some(thread),
190            _tokio: tokio,
191            _subscriptions: sub_handles,
192        })
193    }
194
195    pub fn store(&self) -> crate::Store<S, M>
196    where
197        S: Clone + Send + Sync + 'static,
198    {
199        self.backend.store()
200    }
201
202    pub fn dispatch(&self, msg: M) {
203        let _ = self.bus.sender().send_blocking(msg);
204    }
205
206    pub fn sender(&self) -> BusSender<M> {
207        self.bus.sender()
208    }
209
210    pub fn cancel(&self, id: EffectId) {
211        if let Some(handle) = self.backend.hub.interpreter.cancel_tokens.lock().remove(&id) {
212            handle.abort();
213        }
214    }
215
216    pub fn shutdown(mut self) {
217        super::lifecycle::join_reducer_thread(&self.shutdown, &self._subscriptions, &mut self._thread);
218    }
219}
220
221impl<S, M> Drop for Runtime<S, M> {
222    fn drop(&mut self) {
223        super::lifecycle::join_reducer_thread(&self.shutdown, &self._subscriptions, &mut self._thread);
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    fn boot(program: Program<Counter, i32>) -> Runtime<Counter, i32> {
254        match Runtime::from_program(program, Environment::new(), RuntimeConfig::new(16)) {
255            Ok(runtime) => runtime,
256            Err(err) => panic!("test runtime bootstrap failed: {err}"),
257        }
258    }
259
260    #[test]
261    fn runtime_tick_subscription_dispatches() {
262        fn subs(_: &Counter) -> Sub<i32> {
263            Sub::tick(7, Duration::from_millis(30), || 1)
264        }
265
266        let program = Program::new(init, update, subs);
267        let runtime = boot(program);
268        std::thread::sleep(Duration::from_millis(200));
269        assert!(runtime.state.lock().n >= 1);
270        runtime.shutdown();
271    }
272
273    #[test]
274    fn runtime_applies_dispatched_messages() {
275        let program = Program::new(init, update, subs);
276        let runtime = boot(program);
277        runtime.dispatch(3);
278        runtime.dispatch(4);
279        std::thread::sleep(Duration::from_millis(200));
280        assert_eq!(runtime.state.lock().n, 7);
281        runtime.shutdown();
282    }
283
284    #[test]
285    fn runtime_runs_task_effects() {
286        fn update_with_effect(s: &mut Counter, msg: i32) -> Cmd<i32> {
287            if msg == 0 {
288                Cmd::single(Effect::task(1, || Box::pin(async { Ok(10) })))
289            } else {
290                s.n += msg;
291                Cmd::none()
292            }
293        }
294        let program = Program::new(init, update_with_effect, subs);
295        let runtime = boot(program);
296        runtime.dispatch(0);
297        std::thread::sleep(Duration::from_millis(200));
298        assert_eq!(runtime.state.lock().n, 10);
299        runtime.shutdown();
300    }
301
302    #[test]
303    fn runtime_safe_reduce_update_unwinds_store_work() {
304        fn panicking_update(s: &mut Counter, msg: i32) -> Cmd<i32> {
305            s.n = 99;
306            if msg < 0 {
307                panic!("reduce panic");
308            }
309            s.n = msg;
310            Cmd::none()
311        }
312
313        let program = Program::new(init, panicking_update, subs);
314        let runtime = boot(program);
315        let store = runtime.store();
316        let task = store.send(-1);
317        assert!(task.finish().is_ok());
318        assert_eq!(runtime.state.lock().n, 99);
319
320        runtime.dispatch(4);
321        std::thread::sleep(Duration::from_millis(200));
322        assert_eq!(runtime.state.lock().n, 4);
323        runtime.shutdown();
324    }
325}