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