rs_store/
reducer.rs

1use crate::effect::Effect;
2use std::sync::Arc;
3
4/// determine if the action should be dispatched or not
5pub enum DispatchOp<State, Action> {
6    /// Dispatch new state with effects
7    /// since 3.0.0
8    Dispatch(State, Vec<Effect<Action>>),
9    /// Keep new state but do not dispatch
10    Keep(State, Vec<Effect<Action>>),
11}
12
13/// Reducer reduces the state based on the action.
14///
15/// ## Parameters
16/// - `state`: Current state (reference)
17/// - `action`: Action to process (reference)
18///
19/// ## Returns
20/// - [`DispatchOp<State, Action>`]: result that contains the next state and any effects to run.
21pub trait Reducer<State, Action>
22where
23    State: Send + Sync + Clone,
24    Action: Send + Sync + Clone + 'static,
25{
26    fn reduce(&self, state: &State, action: &Action) -> DispatchOp<State, Action>;
27}
28
29/// ReducerChain chains reducers together sequentially.
30/// The first reducer in the vector becomes the first reducer in the chain.
31/// Execution order: first reducer -> second reducer -> ... -> last reducer
32pub struct ReducerChain<State, Action>
33where
34    State: Send + Sync + Clone,
35    Action: Send + Sync + 'static,
36{
37    reducer: Arc<dyn Reducer<State, Action> + Send + Sync>,
38    next: Option<Box<ReducerChain<State, Action>>>,
39}
40
41impl<State, Action> ReducerChain<State, Action>
42where
43    State: Send + Sync + Clone,
44    Action: Send + Sync + 'static,
45{
46    /// Create a new reducer chain with a single reducer
47    pub fn new(reducer: Arc<dyn Reducer<State, Action> + Send + Sync>) -> Self {
48        Self {
49            reducer,
50            next: None,
51        }
52    }
53
54    /// Chain another reducer to this chain
55    pub fn chain(mut self, reducer: Arc<dyn Reducer<State, Action> + Send + Sync>) -> Self {
56        if let Some(ref mut next) = self.next {
57            // Recursively chain to the end
58            *next = Box::new(next.as_ref().clone().chain(reducer));
59        } else {
60            // add at tail of the chain
61            self.next = Some(Box::new(ReducerChain::new(reducer)));
62        }
63        self
64    }
65
66    /// Create a reducer chain from a vector of reducers
67    /// The first reducer in the vector becomes the first reducer in the chain.
68    pub fn from_vec(reducers: Vec<Box<dyn Reducer<State, Action> + Send + Sync>>) -> Option<Self> {
69        if reducers.is_empty() {
70            return None;
71        }
72
73        let mut iter = reducers.into_iter();
74        let mut tail = ReducerChain::new(Arc::from(iter.next()?));
75
76        for reducer in iter {
77            tail = tail.chain(Arc::from(reducer));
78        }
79
80        Some(tail)
81    }
82}
83
84// Implement Clone for ReducerChain to support recursive chaining
85impl<State, Action> Clone for ReducerChain<State, Action>
86where
87    State: Send + Sync + Clone,
88    Action: Send + Sync + 'static,
89{
90    fn clone(&self) -> Self {
91        Self {
92            reducer: self.reducer.clone(),
93            next: self.next.as_ref().map(|n| Box::new(n.as_ref().clone())),
94        }
95    }
96}
97
98impl<State, Action> Reducer<State, Action> for ReducerChain<State, Action>
99where
100    State: Send + Sync + Clone,
101    Action: Send + Sync + Clone + 'static,
102{
103    fn reduce(&self, state: &State, action: &Action) -> DispatchOp<State, Action> {
104        // Execute current reducer
105        let mut result = self.reducer.reduce(state, action);
106
107        // Continue with next reducer if exists
108        if let Some(ref next) = self.next {
109            match result {
110                DispatchOp::Dispatch(current_state, current_effects) => {
111                    result = next.reduce(&current_state, action);
112                    // Merge effects from both reducers
113                    match result {
114                        DispatchOp::Dispatch(next_state, mut next_effects) => {
115                            next_effects.extend(current_effects);
116                            DispatchOp::Dispatch(next_state, next_effects)
117                        }
118                        DispatchOp::Keep(next_state, mut next_effects) => {
119                            next_effects.extend(current_effects);
120                            DispatchOp::Keep(next_state, next_effects)
121                        }
122                    }
123                }
124                DispatchOp::Keep(current_state, current_effects) => {
125                    result = next.reduce(&current_state, action);
126                    // Merge effects from both reducers
127                    match result {
128                        DispatchOp::Dispatch(next_state, mut next_effects) => {
129                            next_effects.extend(current_effects);
130                            DispatchOp::Dispatch(next_state, next_effects)
131                        }
132                        DispatchOp::Keep(next_state, mut next_effects) => {
133                            next_effects.extend(current_effects);
134                            DispatchOp::Keep(next_state, next_effects)
135                        }
136                    }
137                }
138            }
139        } else {
140            result
141        }
142    }
143}
144
145/// FnReducer is a reducer that is created from a function.
146///
147/// The function signature should match: `Fn(&State, &Action) -> DispatchOp<State, Action>`
148pub struct FnReducer<F, State, Action>
149where
150    F: Fn(&State, &Action) -> DispatchOp<State, Action>,
151    State: Send + Sync + Clone,
152    Action: Send + Sync + Clone + 'static,
153{
154    func: F,
155    _marker: std::marker::PhantomData<(State, Action)>,
156}
157
158impl<F, State, Action> Reducer<State, Action> for FnReducer<F, State, Action>
159where
160    F: Fn(&State, &Action) -> DispatchOp<State, Action>,
161    State: Send + Sync + Clone,
162    Action: Send + Sync + Clone + 'static,
163{
164    fn reduce(&self, state: &State, action: &Action) -> DispatchOp<State, Action> {
165        (self.func)(state, action)
166    }
167}
168
169impl<F, State, Action> From<F> for FnReducer<F, State, Action>
170where
171    F: Fn(&State, &Action) -> DispatchOp<State, Action>,
172    State: Send + Sync + Clone,
173    Action: Send + Sync + Clone + 'static,
174{
175    fn from(func: F) -> Self {
176        Self {
177            func,
178            _marker: std::marker::PhantomData,
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::subscriber::Subscriber;
187    use crate::StoreBuilder;
188    use std::sync::{Arc, Mutex};
189    use std::thread;
190
191    struct TestSubscriber {
192        state_changes: Arc<Mutex<Vec<i32>>>,
193    }
194
195    impl Subscriber<i32, i32> for TestSubscriber {
196        fn on_notify(&self, state: &i32, _action: &i32) {
197            self.state_changes.lock().unwrap().push(*state);
198        }
199    }
200
201    #[test]
202    fn test_store_continues_after_reducer_panic() {
203        // given
204
205        // A reducer that panics on specific action value
206        struct PanicOnValueReducer {
207            panic_on: i32,
208        }
209
210        impl Reducer<i32, i32> for PanicOnValueReducer {
211            fn reduce(&self, state: &i32, action: &i32) -> DispatchOp<i32, i32> {
212                if *action == self.panic_on {
213                    // Catch the panic and return current state
214                    let result = std::panic::catch_unwind(|| {
215                        panic!("Intentional panic on action {}", action);
216                    });
217                    // keep state if panic
218                    if result.is_err() {
219                        return DispatchOp::Keep(state.clone(), vec![]);
220                    }
221                }
222                // Normal operation for other actions
223                DispatchOp::Dispatch(state + action, vec![])
224            }
225        }
226
227        // Create store with our test reducer
228        let reducer = Box::new(PanicOnValueReducer { panic_on: 42 });
229        let store = StoreBuilder::new_with_reducer(0, reducer).build().unwrap();
230
231        // Track state changes
232        let state_changes = Arc::new(Mutex::new(Vec::new()));
233        let state_changes_clone = state_changes.clone();
234
235        let subscriber = Arc::new(TestSubscriber {
236            state_changes: state_changes_clone,
237        });
238        store.add_subscriber(subscriber).unwrap();
239
240        // then
241        // Test sequence of actions
242        store.dispatch(1).unwrap(); // Should work: 0 -> 1
243        store.dispatch(42).unwrap(); // Should panic but be caught: stays at 1
244        store.dispatch(2).unwrap(); // Should work: 1 -> 3
245
246        // Give time for all actions to be processed
247        match store.stop() {
248            Ok(_) => println!("store stopped"),
249            Err(e) => {
250                panic!("store stop failed  : {:?}", e);
251            }
252        }
253
254        // then
255        // Verify final state
256        assert_eq!(store.get_state(), 3);
257        // Verify state change history
258        let changes = state_changes.lock().unwrap();
259        assert_eq!(&*changes, &vec![1, 3]); // Should only have non-panic state changes
260    }
261
262    #[test]
263    fn test_multiple_reducers_continue_after_panic() {
264        // given
265        struct PanicReducer;
266        struct NormalReducer;
267
268        impl Reducer<i32, i32> for PanicReducer {
269            fn reduce(&self, state: &i32, action: &i32) -> DispatchOp<i32, i32> {
270                let result = std::panic::catch_unwind(|| {
271                    panic!("Always panic!");
272                });
273                // keep state if panic
274                if result.is_err() {
275                    return DispatchOp::Keep(state.clone(), vec![]);
276                }
277                DispatchOp::Dispatch(state + action, vec![])
278            }
279        }
280
281        impl Reducer<i32, i32> for NormalReducer {
282            fn reduce(&self, state: &i32, action: &i32) -> DispatchOp<i32, i32> {
283                DispatchOp::Dispatch(state + action, vec![])
284            }
285        }
286
287        // Create store with both reducers
288        let store = StoreBuilder::new(0)
289            .with_reducer(Box::new(PanicReducer))
290            .add_reducer(Box::new(NormalReducer))
291            .build()
292            .unwrap();
293
294        // when
295        // Dispatch actions
296        store.dispatch(1).unwrap();
297        store.dispatch(2).unwrap();
298
299        match store.stop() {
300            Ok(_) => println!("store stopped"),
301            Err(e) => {
302                panic!("store stop failed  : {:?}", e);
303            }
304        }
305
306        // then
307        // Even though PanicReducer panics, NormalReducer should still process actions
308        assert_eq!(store.get_state(), 3);
309    }
310
311    #[test]
312    fn test_fn_reducer_basic() {
313        // given
314        let reducer = FnReducer::from(|state: &i32, action: &i32| {
315            DispatchOp::Dispatch(state + action, vec![])
316        });
317        let store = StoreBuilder::new_with_reducer(0, Box::new(reducer)).build().unwrap();
318
319        // when
320        store.dispatch(5).unwrap();
321        store.dispatch(3).unwrap();
322        match store.stop() {
323            Ok(_) => println!("store stopped"),
324            Err(e) => {
325                panic!("store stop failed  : {:?}", e);
326            }
327        }
328
329        // then
330        assert_eq!(store.get_state(), 8); // 0 + 5 + 3 = 8
331    }
332
333    #[test]
334    fn test_fn_reducer_with_effect() {
335        // given
336        #[derive(Clone, Debug)]
337        enum Action {
338            AddWithEffect(i32),
339            Add(i32),
340        }
341
342        let reducer = FnReducer::from(|state: &i32, action: &Action| {
343            match action {
344                Action::AddWithEffect(i) => {
345                    let new_state = state + i;
346                    let effect = Effect::Action(Action::Add(40)); // Effect that adds 40 more
347                    DispatchOp::Dispatch(new_state, vec![effect])
348                }
349                Action::Add(i) => {
350                    let new_state = state + i;
351                    DispatchOp::Dispatch(new_state, vec![])
352                }
353            }
354        });
355        let store = StoreBuilder::new_with_reducer(0, Box::new(reducer)).build().unwrap();
356
357        // when
358        store.dispatch(Action::AddWithEffect(2)).unwrap();
359        thread::sleep(std::time::Duration::from_millis(1000)); // Wait for effect to be processed
360        match store.stop() {
361            Ok(_) => println!("store stopped"),
362            Err(e) => {
363                panic!("store stop failed  : {:?}", e);
364            }
365        }
366
367        // then
368        // Initial state(0) + action(2) + effect(40) = 42
369        assert_eq!(store.get_state(), 42);
370    }
371
372    #[test]
373    fn test_fn_reducer_keep_state() {
374        // given
375        let reducer = FnReducer::from(|state: &i32, action: &i32| {
376            if *action < 0 {
377                // Keep current state for negative actions
378                DispatchOp::Keep(state.clone(), vec![])
379            } else {
380                DispatchOp::Dispatch(state + action, vec![])
381            }
382        });
383        let store = StoreBuilder::new_with_reducer(0, Box::new(reducer)).build().unwrap();
384
385        // Track state changes
386        let state_changes = Arc::new(Mutex::new(Vec::new()));
387        let state_changes_clone = state_changes.clone();
388
389        let subscriber = Arc::new(TestSubscriber {
390            state_changes: state_changes_clone,
391        });
392        store.add_subscriber(subscriber).unwrap();
393
394        // when
395        store.dispatch(5).unwrap(); // Should change state
396        store.dispatch(-3).unwrap(); // Should keep state
397        store.dispatch(2).unwrap(); // Should change state
398        match store.stop() {
399            Ok(_) => println!("store stopped"),
400            Err(e) => {
401                panic!("store stop failed  : {:?}", e);
402            }
403        }
404
405        // then
406        assert_eq!(store.get_state(), 7); // 0 + 5 + 2 = 7
407        let changes = state_changes.lock().unwrap();
408        assert_eq!(&*changes, &vec![5, 7]); // Only two state changes should be recorded
409    }
410
411    #[test]
412    fn test_multiple_fn_reducers() {
413        // given
414        let add_reducer = FnReducer::from(|state: &i32, action: &i32| {
415            DispatchOp::Dispatch(state + action, vec![])
416        });
417        let double_reducer =
418            FnReducer::from(|state: &i32, _action: &i32| DispatchOp::Dispatch(state * 2, vec![]));
419
420        let store = StoreBuilder::new(0)
421            .with_reducer(Box::new(add_reducer))
422            .add_reducer(Box::new(double_reducer))
423            .build()
424            .unwrap();
425
426        // when
427        store.dispatch(3).unwrap(); // (((0)  +3) *2) = 6
428        store.dispatch(15).unwrap(); // (((6) +15) *2) = 42
429        match store.stop() {
430            Ok(_) => println!("store stopped"),
431            Err(e) => {
432                panic!("store stop failed  : {:?}", e);
433            }
434        }
435
436        // then
437        assert_eq!(store.get_state(), 42);
438    }
439}