1use std::io;
7use std::marker::PhantomData;
8use std::time::Duration;
9
10use ratatui::backend::Backend;
11use ratatui::layout::Rect;
12use ratatui::{Frame, Terminal};
13use tokio::sync::mpsc;
14use tokio_util::sync::CancellationToken;
15
16use crate::bus::{process_raw_event, spawn_event_poller, EventOutcome, RawEvent};
17use crate::event::EventKind;
18use crate::store::{
19 DispatchError, Middleware, NoEffect, Reducer, ReducerResult, Store, StoreWithMiddleware,
20};
21use crate::Action;
22
23#[cfg(feature = "subscriptions")]
24use crate::subscriptions::Subscriptions;
25#[cfg(feature = "tasks")]
26use crate::tasks::TaskManager;
27
28#[derive(Debug, Clone, Copy)]
30pub struct PollerConfig {
31 pub poll_timeout: Duration,
33 pub loop_sleep: Duration,
35}
36
37impl Default for PollerConfig {
38 fn default() -> Self {
39 Self {
40 poll_timeout: Duration::from_millis(10),
41 loop_sleep: Duration::from_millis(16),
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, Default)]
48pub struct RenderContext {
49 pub debug_enabled: bool,
51}
52
53impl RenderContext {
54 pub fn is_focused(self) -> bool {
56 !self.debug_enabled
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum DispatchErrorPolicy {
63 Continue,
65 Render,
70 Stop,
72}
73
74fn apply_dispatch_error_policy(
75 handler: &mut dyn FnMut(&DispatchError) -> DispatchErrorPolicy,
76 error: DispatchError,
77 should_render: &mut bool,
78) -> bool {
79 match handler(&error) {
80 DispatchErrorPolicy::Continue => false,
81 DispatchErrorPolicy::Render => {
82 *should_render = true;
83 false
84 }
85 DispatchErrorPolicy::Stop => true,
86 }
87}
88
89#[cfg(feature = "debug")]
90pub trait DebugAdapter<S, A>: 'static {
91 fn render(
92 &mut self,
93 frame: &mut Frame,
94 state: &S,
95 render_ctx: RenderContext,
96 render_fn: &mut dyn FnMut(&mut Frame, Rect, &S, RenderContext),
97 );
98
99 fn handle_event(
100 &mut self,
101 event: &EventKind,
102 state: &S,
103 action_tx: &mpsc::UnboundedSender<A>,
104 ) -> Option<bool>;
105
106 fn log_action(&mut self, action: &A);
107 fn is_enabled(&self) -> bool;
108
109 #[cfg(feature = "tasks")]
110 fn with_task_manager(self, _tasks: &TaskManager<A>) -> Self
111 where
112 Self: Sized,
113 {
114 self
115 }
116
117 #[cfg(feature = "subscriptions")]
118 fn with_subscriptions(self, _subscriptions: &Subscriptions<A>) -> Self
119 where
120 Self: Sized,
121 {
122 self
123 }
124}
125
126pub(crate) fn draw_frame<S: 'static, A, B, F>(
127 shell: &mut RuntimeShell<S, A>,
128 state: &S,
129 terminal: &mut Terminal<B>,
130 mut render: F,
131) -> io::Result<()>
132where
133 A: Action,
134 B: Backend,
135 F: FnMut(&mut Frame, Rect, &S, RenderContext),
136{
137 let render_ctx = shell.render_ctx();
138 terminal.draw(|frame| {
139 #[cfg(feature = "debug")]
140 if let Some(debug) = shell.debug.as_mut() {
141 let mut rf =
142 |f: &mut Frame, area: Rect, s: &S, ctx: RenderContext| render(f, area, s, ctx);
143 debug.render(frame, state, render_ctx, &mut rf);
144 } else {
145 render(frame, frame.area(), state, render_ctx);
146 }
147 #[cfg(not(feature = "debug"))]
148 {
149 render(frame, frame.area(), state, render_ctx);
150 }
151 })?;
152 shell.should_render = false;
153 Ok(())
154}
155
156pub(crate) struct RuntimeShell<S, A: Action> {
157 pub(crate) action_tx: mpsc::UnboundedSender<A>,
158 pub(crate) action_rx: mpsc::UnboundedReceiver<A>,
159 pub(crate) poller_config: PollerConfig,
160 #[cfg(feature = "debug")]
161 pub(crate) debug: Option<Box<dyn DebugAdapter<S, A>>>,
162 pub(crate) dispatch_error_handler: Box<dyn FnMut(&DispatchError) -> DispatchErrorPolicy>,
163 pub(crate) should_render: bool,
164 _state: PhantomData<S>,
165}
166
167impl<S: 'static, A: Action> RuntimeShell<S, A> {
168 pub(crate) fn new() -> Self {
169 let (action_tx, action_rx) = mpsc::unbounded_channel();
170 Self {
171 action_tx,
172 action_rx,
173 poller_config: PollerConfig::default(),
174 #[cfg(feature = "debug")]
175 debug: None,
176 dispatch_error_handler: Box::new(|_| DispatchErrorPolicy::Stop),
177 should_render: true,
178 _state: PhantomData,
179 }
180 }
181
182 pub(crate) fn enqueue(&self, action: A) {
183 let _ = self.action_tx.send(action);
184 }
185
186 pub(crate) fn action_tx_clone(&self) -> mpsc::UnboundedSender<A> {
187 self.action_tx.clone()
188 }
189
190 pub(crate) fn render_ctx(&self) -> RenderContext {
191 RenderContext {
192 debug_enabled: {
193 #[cfg(feature = "debug")]
194 {
195 self.debug.as_ref().is_some_and(|d| d.is_enabled())
196 }
197 #[cfg(not(feature = "debug"))]
198 {
199 false
200 }
201 },
202 }
203 }
204
205 #[allow(unused_variables)]
206 pub(crate) fn debug_intercept_event(&mut self, event: &EventKind, state: &S) -> Option<bool> {
207 #[cfg(feature = "debug")]
208 if let Some(debug) = self.debug.as_mut() {
209 return debug.handle_event(event, state, &self.action_tx);
210 }
211 None
212 }
213
214 #[allow(unused_variables)]
215 pub(crate) fn debug_log_action(&mut self, action: &A) {
216 #[cfg(feature = "debug")]
217 if let Some(debug) = self.debug.as_mut() {
218 debug.log_action(action);
219 }
220 }
221
222 pub(crate) fn enqueue_outcome(&mut self, outcome: EventOutcome<A>) {
223 if outcome.needs_render {
224 self.should_render = true;
225 }
226 for action in outcome.actions {
227 let _ = self.action_tx.send(action);
228 }
229 }
230
231 pub(crate) fn spawn_poller(&self) -> (mpsc::UnboundedReceiver<RawEvent>, CancellationToken) {
232 let (event_tx, event_rx) = mpsc::unbounded_channel::<RawEvent>();
233 let cancel_token = CancellationToken::new();
234 let _handle = spawn_event_poller(
235 event_tx,
236 self.poller_config.poll_timeout,
237 self.poller_config.loop_sleep,
238 cancel_token.clone(),
239 );
240 (event_rx, cancel_token)
241 }
242
243 pub(crate) fn apply_error_policy(&mut self, error: DispatchError) -> bool {
244 apply_dispatch_error_policy(
245 self.dispatch_error_handler.as_mut(),
246 error,
247 &mut self.should_render,
248 )
249 }
250
251 pub(crate) fn process_event<FMap, R>(&mut self, raw_event: RawEvent, state: &S, map: FMap)
252 where
253 FMap: FnOnce(&EventKind, &S) -> R,
254 R: Into<EventOutcome<A>>,
255 {
256 let event = process_raw_event(raw_event);
257 if let Some(needs_render) = self.debug_intercept_event(&event, state) {
258 self.should_render = needs_render;
259 return;
260 }
261 self.enqueue_outcome(map(&event, state).into());
262 }
263}
264
265pub trait RuntimeStore<S, A: Action, E = NoEffect> {
267 fn dispatch(&mut self, action: A) -> ReducerResult<E>;
269 fn try_dispatch(&mut self, action: A) -> Result<ReducerResult<E>, DispatchError> {
273 Ok(self.dispatch(action))
274 }
275 fn state(&self) -> &S;
277}
278
279impl<S, A: Action, E> RuntimeStore<S, A, E> for Store<S, A, E> {
280 fn dispatch(&mut self, action: A) -> ReducerResult<E> {
281 Store::dispatch(self, action)
282 }
283
284 fn state(&self) -> &S {
285 Store::state(self)
286 }
287}
288
289impl<S, A: Action, E, M: Middleware<S, A>> RuntimeStore<S, A, E>
290 for StoreWithMiddleware<S, A, E, M>
291{
292 fn dispatch(&mut self, action: A) -> ReducerResult<E> {
293 StoreWithMiddleware::dispatch(self, action)
294 }
295
296 fn try_dispatch(&mut self, action: A) -> Result<ReducerResult<E>, DispatchError> {
297 StoreWithMiddleware::try_dispatch(self, action)
298 }
299
300 fn state(&self) -> &S {
301 StoreWithMiddleware::state(self)
302 }
303}
304
305#[doc(hidden)]
307#[derive(Debug, Clone, Copy, Default)]
308pub struct Direct;
309
310pub struct Runtime<S, A: Action, E = NoEffect, Routing = Direct, St = Store<S, A, E>>
312where
313 St: RuntimeStore<S, A, E>,
314{
315 pub(crate) store: St,
316 pub(crate) shell: RuntimeShell<S, A>,
317 pub(crate) routing: Routing,
318 #[cfg(feature = "tasks")]
319 pub(crate) tasks: TaskManager<A>,
320 #[cfg(feature = "subscriptions")]
321 pub(crate) subscriptions: Subscriptions<A>,
322 pub(crate) action_broadcast: tokio::sync::broadcast::Sender<String>,
323 pub(crate) _effect: PhantomData<E>,
324}
325
326impl<S: 'static, A: Action, E> Runtime<S, A, E, Direct, Store<S, A, E>> {
327 pub fn new(state: S, reducer: Reducer<S, A, E>) -> Self {
329 Self::from_store(Store::new(state, reducer))
330 }
331}
332
333impl<S: 'static, A: Action, E, St: RuntimeStore<S, A, E>> Runtime<S, A, E, Direct, St> {
334 pub fn from_store(store: St) -> Self {
336 Self::from_store_with_routing(store, Direct)
337 }
338}
339
340impl<S: 'static, A: Action, E, Routing, St> Runtime<S, A, E, Routing, St>
341where
342 St: RuntimeStore<S, A, E>,
343{
344 pub(crate) fn from_store_with_routing(store: St, routing: Routing) -> Self {
345 let shell = RuntimeShell::new();
346 let (action_broadcast, _) = tokio::sync::broadcast::channel(64);
347
348 #[cfg(feature = "tasks")]
349 let tasks = TaskManager::new(shell.action_tx.clone());
350 #[cfg(feature = "subscriptions")]
351 let subscriptions = Subscriptions::new(shell.action_tx.clone());
352
353 Self {
354 store,
355 shell,
356 routing,
357 #[cfg(feature = "tasks")]
358 tasks,
359 #[cfg(feature = "subscriptions")]
360 subscriptions,
361 action_broadcast,
362 _effect: PhantomData,
363 }
364 }
365
366 #[cfg(feature = "debug")]
368 pub fn with_debug<D>(mut self, debug: D) -> Self
369 where
370 D: DebugAdapter<S, A>,
371 {
372 let debug = {
373 let debug = debug;
374 #[cfg(feature = "tasks")]
375 let debug = debug.with_task_manager(&self.tasks);
376 #[cfg(feature = "subscriptions")]
377 let debug = debug.with_subscriptions(&self.subscriptions);
378 debug
379 };
380 self.shell.debug = Some(Box::new(debug));
381 self
382 }
383
384 pub fn with_event_poller(mut self, config: PollerConfig) -> Self {
386 self.shell.poller_config = config;
387 self
388 }
389
390 pub fn with_dispatch_error_handler<F>(mut self, handler: F) -> Self
396 where
397 F: FnMut(&DispatchError) -> DispatchErrorPolicy + 'static,
398 {
399 self.shell.dispatch_error_handler = Box::new(handler);
400 self
401 }
402
403 pub fn subscribe_actions(&self) -> tokio::sync::broadcast::Receiver<String> {
405 self.action_broadcast.subscribe()
406 }
407
408 pub fn enqueue(&self, action: A) {
410 self.shell.enqueue(action);
411 }
412
413 pub fn action_tx(&self) -> mpsc::UnboundedSender<A> {
415 self.shell.action_tx_clone()
416 }
417
418 pub fn state(&self) -> &S {
420 self.store.state()
421 }
422
423 #[cfg(feature = "tasks")]
425 pub fn tasks(&mut self) -> &mut TaskManager<A> {
426 &mut self.tasks
427 }
428
429 #[cfg(feature = "subscriptions")]
431 pub fn subscriptions(&mut self) -> &mut Subscriptions<A> {
432 &mut self.subscriptions
433 }
434
435 #[cfg(all(feature = "tasks", feature = "subscriptions"))]
436 pub(crate) fn effect_context(&mut self) -> EffectContext<'_, A> {
437 EffectContext {
438 action_tx: &self.shell.action_tx,
439 tasks: &mut self.tasks,
440 subscriptions: &mut self.subscriptions,
441 }
442 }
443
444 #[cfg(all(feature = "tasks", not(feature = "subscriptions")))]
445 pub(crate) fn effect_context(&mut self) -> EffectContext<'_, A> {
446 EffectContext {
447 action_tx: &self.shell.action_tx,
448 tasks: &mut self.tasks,
449 }
450 }
451
452 #[cfg(all(not(feature = "tasks"), feature = "subscriptions"))]
453 pub(crate) fn effect_context(&mut self) -> EffectContext<'_, A> {
454 EffectContext {
455 action_tx: &self.shell.action_tx,
456 subscriptions: &mut self.subscriptions,
457 }
458 }
459
460 #[cfg(all(not(feature = "tasks"), not(feature = "subscriptions")))]
461 pub(crate) fn effect_context(&mut self) -> EffectContext<'_, A> {
462 EffectContext {
463 action_tx: &self.shell.action_tx,
464 }
465 }
466
467 fn broadcast_action(&self, action: &A) {
468 if self.action_broadcast.receiver_count() > 0 {
469 let _ = self.action_broadcast.send(action.name().to_string());
470 }
471 }
472
473 pub(crate) fn cleanup(&mut self, cancel_token: CancellationToken) {
474 cancel_token.cancel();
475 #[cfg(feature = "subscriptions")]
476 self.subscriptions.cancel_all();
477 #[cfg(feature = "tasks")]
478 self.tasks.cancel_all();
479 }
480
481 pub(crate) fn dispatch_and_handle_effects(
482 &mut self,
483 action: A,
484 handle_effect: &mut impl FnMut(E, &mut EffectContext<A>),
485 ) -> bool {
486 self.broadcast_action(&action);
487 match self.store.try_dispatch(action) {
488 Ok(result) => {
489 if result.has_effects() {
490 let mut ctx = self.effect_context();
491 for effect in result.effects {
492 handle_effect(effect, &mut ctx);
493 }
494 }
495 self.shell.should_render = result.changed;
496 false
497 }
498 Err(error) => self.shell.apply_error_policy(error),
499 }
500 }
501}
502
503impl<S: 'static, A: Action, Routing, St> Runtime<S, A, NoEffect, Routing, St>
504where
505 St: RuntimeStore<S, A, NoEffect>,
506{
507 pub(crate) fn dispatch_action(&mut self, action: A) -> bool {
508 self.broadcast_action(&action);
509 match self.store.try_dispatch(action) {
510 Ok(result) => {
511 self.shell.should_render = result.changed;
512 false
513 }
514 Err(error) => self.shell.apply_error_policy(error),
515 }
516 }
517}
518
519impl<S: 'static, A: Action, St> Runtime<S, A, NoEffect, Direct, St>
520where
521 St: RuntimeStore<S, A, NoEffect>,
522{
523 pub async fn run<B, FRender, FEvent, FQuit, R>(
525 &mut self,
526 terminal: &mut Terminal<B>,
527 mut render: FRender,
528 mut map_event: FEvent,
529 mut should_quit: FQuit,
530 ) -> io::Result<()>
531 where
532 B: Backend,
533 FRender: FnMut(&mut Frame, Rect, &S, RenderContext),
534 FEvent: FnMut(&EventKind, &S) -> R,
535 R: Into<EventOutcome<A>>,
536 FQuit: FnMut(&A) -> bool,
537 {
538 let (mut event_rx, cancel_token) = self.shell.spawn_poller();
539
540 loop {
541 if self.shell.should_render {
542 draw_frame(&mut self.shell, self.store.state(), terminal, &mut render)?;
543 }
544
545 tokio::select! {
546 Some(raw_event) = event_rx.recv() => {
547 self.shell.process_event(raw_event, self.store.state(), &mut map_event);
548 }
549
550 Some(action) = self.shell.action_rx.recv() => {
551 if should_quit(&action) {
552 break;
553 }
554 self.shell.debug_log_action(&action);
555 if self.dispatch_action(action) {
556 break;
557 }
558 }
559
560 else => { break; }
561 }
562 }
563
564 self.cleanup(cancel_token);
565 Ok(())
566 }
567}
568
569impl<S: 'static, A: Action, E, St> Runtime<S, A, E, Direct, St>
570where
571 St: RuntimeStore<S, A, E>,
572{
573 pub async fn run_with_effects<B, FRender, FEvent, FQuit, FEffect, R>(
576 &mut self,
577 terminal: &mut Terminal<B>,
578 mut render: FRender,
579 mut map_event: FEvent,
580 mut should_quit: FQuit,
581 mut handle_effect: FEffect,
582 ) -> io::Result<()>
583 where
584 B: Backend,
585 FRender: FnMut(&mut Frame, Rect, &S, RenderContext),
586 FEvent: FnMut(&EventKind, &S) -> R,
587 R: Into<EventOutcome<A>>,
588 FQuit: FnMut(&A) -> bool,
589 FEffect: FnMut(E, &mut EffectContext<A>),
590 {
591 let (mut event_rx, cancel_token) = self.shell.spawn_poller();
592
593 loop {
594 if self.shell.should_render {
595 draw_frame(&mut self.shell, self.store.state(), terminal, &mut render)?;
596 }
597
598 tokio::select! {
599 Some(raw_event) = event_rx.recv() => {
600 self.shell.process_event(raw_event, self.store.state(), &mut map_event);
601 }
602
603 Some(action) = self.shell.action_rx.recv() => {
604 if should_quit(&action) {
605 break;
606 }
607 self.shell.debug_log_action(&action);
608 if self.dispatch_and_handle_effects(action, &mut handle_effect) {
609 break;
610 }
611 }
612
613 else => { break; }
614 }
615 }
616
617 self.cleanup(cancel_token);
618 Ok(())
619 }
620}
621
622pub struct EffectContext<'a, A: Action> {
624 action_tx: &'a mpsc::UnboundedSender<A>,
625 #[cfg(feature = "tasks")]
626 tasks: &'a mut TaskManager<A>,
627 #[cfg(feature = "subscriptions")]
628 subscriptions: &'a mut Subscriptions<A>,
629}
630
631impl<'a, A: Action> EffectContext<'a, A> {
632 pub fn emit(&self, action: A) {
634 let _ = self.action_tx.send(action);
635 }
636
637 pub fn action_tx(&self) -> &mpsc::UnboundedSender<A> {
639 self.action_tx
640 }
641
642 #[cfg(feature = "tasks")]
644 pub fn tasks(&mut self) -> &mut TaskManager<A> {
645 self.tasks
646 }
647
648 #[cfg(feature = "subscriptions")]
650 pub fn subscriptions(&mut self) -> &mut Subscriptions<A> {
651 self.subscriptions
652 }
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658 use crate::store::DispatchLimits;
659 use std::collections::VecDeque;
660
661 #[derive(Clone, Debug)]
662 enum TestAction {
663 Increment,
664 }
665
666 impl Action for TestAction {
667 fn name(&self) -> &'static str {
668 match self {
669 TestAction::Increment => "Increment",
670 }
671 }
672 }
673
674 #[derive(Default)]
675 struct TestState {
676 count: usize,
677 }
678
679 fn reducer(state: &mut TestState, _action: TestAction) -> ReducerResult {
680 state.count += 1;
681 ReducerResult::changed()
682 }
683
684 fn effect_reducer(state: &mut TestState, _action: TestAction) -> ReducerResult<()> {
685 state.count += 1;
686 ReducerResult::changed()
687 }
688
689 struct LoopMiddleware;
690
691 impl Middleware<TestState, TestAction> for LoopMiddleware {
692 fn before(&mut self, _action: &TestAction, _state: &TestState) -> bool {
693 true
694 }
695
696 fn after(
697 &mut self,
698 _action: &TestAction,
699 _state_changed: bool,
700 _state: &TestState,
701 ) -> Vec<TestAction> {
702 vec![TestAction::Increment]
703 }
704 }
705
706 struct MockStore<E> {
707 state: TestState,
708 queued_results: VecDeque<Result<ReducerResult<E>, DispatchError>>,
709 }
710
711 impl<E> MockStore<E> {
712 fn from_results(
713 results: impl IntoIterator<Item = Result<ReducerResult<E>, DispatchError>>,
714 ) -> Self {
715 Self {
716 state: TestState::default(),
717 queued_results: results.into_iter().collect(),
718 }
719 }
720 }
721
722 impl<E> RuntimeStore<TestState, TestAction, E> for MockStore<E> {
723 fn dispatch(&mut self, _action: TestAction) -> ReducerResult<E> {
724 ReducerResult::unchanged()
725 }
726
727 fn try_dispatch(&mut self, _action: TestAction) -> Result<ReducerResult<E>, DispatchError> {
728 self.queued_results
729 .pop_front()
730 .expect("test configured with at least one dispatch result")
731 }
732
733 fn state(&self) -> &TestState {
734 &self.state
735 }
736 }
737
738 fn test_error() -> DispatchError {
739 DispatchError::DepthExceeded {
740 max_depth: 1,
741 action: "Increment",
742 }
743 }
744
745 #[test]
746 fn runtime_continue_policy_keeps_running_without_render() {
747 let store: MockStore<NoEffect> = MockStore::from_results([Err(test_error())]);
748 let mut runtime = Runtime::from_store(store)
749 .with_dispatch_error_handler(|_| DispatchErrorPolicy::Continue);
750 runtime.shell.should_render = false;
751
752 let should_stop = runtime.dispatch_action(TestAction::Increment);
753
754 assert!(!should_stop);
755 assert!(!runtime.shell.should_render);
756 }
757
758 #[test]
759 fn runtime_render_policy_forces_render() {
760 let store: MockStore<NoEffect> = MockStore::from_results([Err(test_error())]);
761 let mut runtime =
762 Runtime::from_store(store).with_dispatch_error_handler(|_| DispatchErrorPolicy::Render);
763 runtime.shell.should_render = false;
764
765 let should_stop = runtime.dispatch_action(TestAction::Increment);
766
767 assert!(!should_stop);
768 assert!(runtime.shell.should_render);
769 }
770
771 #[test]
772 fn runtime_stop_policy_breaks_loop() {
773 let store: MockStore<NoEffect> = MockStore::from_results([Err(test_error())]);
774 let mut runtime =
775 Runtime::from_store(store).with_dispatch_error_handler(|_| DispatchErrorPolicy::Stop);
776 runtime.shell.should_render = false;
777
778 let should_stop = runtime.dispatch_action(TestAction::Increment);
779
780 assert!(should_stop);
781 assert!(!runtime.shell.should_render);
782 }
783
784 #[test]
785 fn runtime_effect_continue_policy_keeps_running_without_render() {
786 let store: MockStore<()> = MockStore::from_results([Err(test_error())]);
787 let mut runtime = Runtime::from_store(store)
788 .with_dispatch_error_handler(|_| DispatchErrorPolicy::Continue);
789 runtime.shell.should_render = false;
790
791 let should_stop =
792 runtime.dispatch_and_handle_effects(TestAction::Increment, &mut |_effect, _ctx| {});
793
794 assert!(!should_stop);
795 assert!(!runtime.shell.should_render);
796 }
797
798 #[test]
799 fn runtime_effect_render_policy_forces_render() {
800 let store: MockStore<()> = MockStore::from_results([Err(test_error())]);
801 let mut runtime =
802 Runtime::from_store(store).with_dispatch_error_handler(|_| DispatchErrorPolicy::Render);
803 runtime.shell.should_render = false;
804
805 let should_stop =
806 runtime.dispatch_and_handle_effects(TestAction::Increment, &mut |_effect, _ctx| {});
807
808 assert!(!should_stop);
809 assert!(runtime.shell.should_render);
810 }
811
812 #[test]
813 fn runtime_effect_stop_policy_breaks_loop() {
814 let store: MockStore<()> = MockStore::from_results([Err(test_error())]);
815 let mut runtime =
816 Runtime::from_store(store).with_dispatch_error_handler(|_| DispatchErrorPolicy::Stop);
817 runtime.shell.should_render = false;
818
819 let should_stop =
820 runtime.dispatch_and_handle_effects(TestAction::Increment, &mut |_effect, _ctx| {});
821
822 assert!(should_stop);
823 assert!(!runtime.shell.should_render);
824 }
825
826 #[test]
827 fn runtime_uses_try_dispatch_for_middleware_overflow() {
828 let store = StoreWithMiddleware::new(TestState::default(), reducer, LoopMiddleware)
829 .with_dispatch_limits(DispatchLimits {
830 max_depth: 1,
831 max_actions: 100,
832 });
833 let mut runtime =
834 Runtime::from_store(store).with_dispatch_error_handler(|_| DispatchErrorPolicy::Stop);
835 runtime.shell.should_render = false;
836
837 let should_stop = runtime.dispatch_action(TestAction::Increment);
838
839 assert!(should_stop);
840 assert_eq!(runtime.state().count, 1);
841 }
842
843 #[test]
844 fn runtime_effect_uses_try_dispatch_for_middleware_overflow() {
845 let store = StoreWithMiddleware::new(TestState::default(), effect_reducer, LoopMiddleware)
846 .with_dispatch_limits(DispatchLimits {
847 max_depth: 1,
848 max_actions: 100,
849 });
850 let mut runtime =
851 Runtime::from_store(store).with_dispatch_error_handler(|_| DispatchErrorPolicy::Stop);
852 runtime.shell.should_render = false;
853
854 let should_stop =
855 runtime.dispatch_and_handle_effects(TestAction::Increment, &mut |_effect, _ctx| {});
856
857 assert!(should_stop);
858 assert_eq!(runtime.state().count, 1);
859 }
860}