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::reduce_panic::catch_reduce_panic;
17use crate::sub::Sub;
18
19use super::config::RuntimeConfig;
20use super::interpreter::{interpret_effects_async, InterpreterState};
21use super::store::{StoreBackend, StoreWorkUnwindGuard};
22use super::subscription;
23
24pub 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 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 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.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_catches_reduce_panic_and_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}