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#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct RuntimeConfig {
52 pub bus_capacity: usize,
54 pub worker_threads: usize,
56 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 pub fn new(bus_capacity: usize) -> Self {
75 Self {
76 bus_capacity,
77 ..Self::default()
78 }
79 }
80}
81
82pub 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}