rs_store/
effect.rs

1use crate::Dispatcher;
2
3/// Represents a side effect that can be executed.
4///
5/// `Effect` is used to encapsulate actions that should be performed as a result of a state change.
6/// an effect can be either simple function or more complex thunk that require a dispatcher.
7pub enum Effect<Action> {
8    /// An action that should be dispatched.
9    Action(Action),
10    /// A task which will be executed asynchronously.
11    Task(Box<dyn FnOnce() + Send>),
12    /// A task that takes the dispatcher as an argument.
13    Thunk(Box<dyn FnOnce(Box<dyn Dispatcher<Action>>) + Send>),
14    /// A function which has a result.
15    /// The result is an Any type which can be downcasted to the expected type,
16    /// you should know the type and the String key can help.
17    /// The result default ignored, if you want to get the result of the function, you can use `EffectMiddleware` to receive the result.
18    Function(String, EffectFunction),
19}
20
21pub type EffectResult = Result<Box<dyn std::any::Any>, Box<dyn std::error::Error>>;
22pub type EffectFunction = Box<dyn FnOnce() -> EffectResult + Send>;
23
24/// EffectResultReceiver is a trait that can receive the result of an effect function.
25pub trait EffectResultReceiver {
26    fn receive(&self, key: String, result: EffectResult);
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32    use crate::{DispatchOp, Reducer, StoreBuilder, Subscriber};
33    use std::sync::{Arc, Mutex};
34    use std::thread;
35    use std::time::Duration;
36
37    // Test state and actions for Effect tests
38    #[derive(Debug, Clone, PartialEq)]
39    struct TestState {
40        value: i32,
41        messages: Vec<String>,
42    }
43
44    impl Default for TestState {
45        fn default() -> Self {
46            TestState {
47                value: 0,
48                messages: Vec::new(),
49            }
50        }
51    }
52
53    #[derive(Debug, Clone)]
54    enum TestAction {
55        SetValue(i32),
56        AddValue(i32),
57        AddMessage(String),
58        AsyncTask,
59        ThunkTask(i32),
60        FunctionTask(String),
61    }
62
63    // Test reducer that produces different types of effects
64    struct TestReducer;
65
66    impl Reducer<TestState, TestAction> for TestReducer {
67        fn reduce(
68            &self,
69            state: &TestState,
70            action: &TestAction,
71        ) -> DispatchOp<TestState, TestAction> {
72            match action {
73                TestAction::SetValue(value) => {
74                    let new_state = TestState {
75                        value: *value,
76                        messages: state.messages.clone(),
77                    };
78                    // Effect::Action - dispatch another action
79                    let effect =
80                        Effect::Action(TestAction::AddMessage(format!("Set to {}", value)));
81                    DispatchOp::Dispatch(new_state, Some(effect))
82                }
83                TestAction::AddValue(value) => {
84                    let new_state = TestState {
85                        value: state.value + value,
86                        messages: state.messages.clone(),
87                    };
88                    DispatchOp::Dispatch(new_state, None)
89                }
90                TestAction::AddMessage(msg) => {
91                    let mut new_messages = state.messages.clone();
92                    new_messages.push(msg.clone());
93                    let new_state = TestState {
94                        value: state.value,
95                        messages: new_messages,
96                    };
97                    DispatchOp::Dispatch(new_state, None)
98                }
99                TestAction::AsyncTask => {
100                    let new_state = TestState {
101                        value: state.value,
102                        messages: state.messages.clone(),
103                    };
104                    // Effect::Task - simple async task
105                    let effect = Effect::Task(Box::new(|| {
106                        thread::sleep(Duration::from_millis(10));
107                    }));
108                    DispatchOp::Dispatch(new_state, Some(effect))
109                }
110                TestAction::ThunkTask(value) => {
111                    let new_state = TestState {
112                        value: state.value,
113                        messages: state.messages.clone(),
114                    };
115                    // Effect::Thunk - task that uses dispatcher
116                    let value_clone = *value; // Clone the value to avoid lifetime issues
117                    let effect = Effect::Thunk(Box::new(move |dispatcher| {
118                        thread::sleep(Duration::from_millis(10));
119                        let _ = dispatcher.dispatch(TestAction::AddValue(value_clone));
120                    }));
121                    DispatchOp::Dispatch(new_state, Some(effect))
122                }
123                TestAction::FunctionTask(key) => {
124                    let new_state = TestState {
125                        value: state.value,
126                        messages: state.messages.clone(),
127                    };
128                    // Effect::Function - function that returns a result
129                    let key_clone = key.clone();
130                    let effect = Effect::Function(
131                        key_clone.clone(),
132                        Box::new(move || {
133                            thread::sleep(Duration::from_millis(10));
134                            Ok(Box::new(format!("Result for {}", key_clone))
135                                as Box<dyn std::any::Any>)
136                        }),
137                    );
138                    DispatchOp::Dispatch(new_state, Some(effect))
139                }
140            }
141        }
142    }
143
144    // Test subscriber to track state changes
145    #[derive(Default)]
146    struct TestSubscriber {
147        states: Arc<Mutex<Vec<TestState>>>,
148        actions: Arc<Mutex<Vec<TestAction>>>,
149    }
150
151    impl Subscriber<TestState, TestAction> for TestSubscriber {
152        fn on_notify(&self, state: &TestState, action: &TestAction) {
153            self.states.lock().unwrap().push(state.clone());
154            self.actions.lock().unwrap().push(action.clone());
155        }
156    }
157
158    impl TestSubscriber {
159        fn get_states(&self) -> Vec<TestState> {
160            self.states.lock().unwrap().clone()
161        }
162
163        fn get_actions(&self) -> Vec<TestAction> {
164            self.actions.lock().unwrap().clone()
165        }
166    }
167
168    #[test]
169    fn test_effect_action() {
170        // Test Effect::Action - simple action dispatch
171        println!("Testing Effect::Action");
172
173        let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
174            .with_name("test-action-effect".into())
175            .build()
176            .unwrap();
177
178        let subscriber = Arc::new(TestSubscriber::default());
179        store.add_subscriber(subscriber.clone()).unwrap();
180
181        // Dispatch action that produces Effect::Action
182        store.dispatch(TestAction::SetValue(42)).unwrap();
183
184        // Wait for effect to be processed
185        thread::sleep(Duration::from_millis(100));
186
187        // Stop store to ensure all effects are processed
188        store.stop().unwrap();
189
190        let states = subscriber.get_states();
191        let actions = subscriber.get_actions();
192
193        // Should have received the initial state and the SetValue action
194        assert!(states.len() >= 1);
195        assert!(actions.len() >= 1);
196
197        // The last state should have value 42
198        assert_eq!(states.last().unwrap().value, 42);
199
200        // Should have received both SetValue and AddMessage actions
201        assert!(actions.iter().any(|a| matches!(a, TestAction::SetValue(42))));
202        assert!(actions.iter().any(|a| matches!(a, TestAction::AddMessage(_))));
203
204        println!("Effect::Action test passed");
205    }
206
207    #[test]
208    fn test_effect_task() {
209        // Test Effect::Task - async task execution
210        println!("Testing Effect::Task");
211
212        let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
213            .with_name("test-task-effect".into())
214            .build()
215            .unwrap();
216
217        let subscriber = Arc::new(TestSubscriber::default());
218        store.add_subscriber(subscriber.clone()).unwrap();
219
220        // Dispatch action that produces Effect::Task
221        store.dispatch(TestAction::AsyncTask).unwrap();
222
223        // Wait for effect to be processed
224        thread::sleep(Duration::from_millis(100));
225
226        // Stop store to ensure all effects are processed
227        store.stop().unwrap();
228
229        let actions = subscriber.get_actions();
230
231        // Should have received the AsyncTask action
232        assert!(actions.iter().any(|a| matches!(a, TestAction::AsyncTask)));
233
234        println!("Effect::Task test passed");
235    }
236
237    #[test]
238    fn test_effect_thunk() {
239        // Test Effect::Thunk - task that uses dispatcher
240        println!("Testing Effect::Thunk");
241
242        let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
243            .with_name("test-thunk-effect".into())
244            .build()
245            .unwrap();
246
247        let subscriber = Arc::new(TestSubscriber::default());
248        store.add_subscriber(subscriber.clone()).unwrap();
249
250        // Dispatch action that produces Effect::Thunk
251        store.dispatch(TestAction::ThunkTask(10)).unwrap();
252
253        // Wait for effect to be processed
254        thread::sleep(Duration::from_millis(100));
255
256        // Stop store to ensure all effects are processed
257        store.stop().unwrap();
258
259        let states = subscriber.get_states();
260        let actions = subscriber.get_actions();
261
262        // Should have received the ThunkTask action
263        assert!(actions.iter().any(|a| matches!(a, TestAction::ThunkTask(10))));
264
265        // The thunk should have dispatched AddValue action
266        assert!(actions.iter().any(|a| matches!(a, TestAction::AddValue(10))));
267
268        // Final state should have value 10
269        assert_eq!(states.last().unwrap().value, 10);
270
271        println!("Effect::Thunk test passed");
272    }
273
274    #[test]
275    fn test_effect_function() {
276        // Test Effect::Function - function that returns a result
277        println!("Testing Effect::Function");
278
279        let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
280            .with_name("test-function-effect".into())
281            .build()
282            .unwrap();
283
284        let subscriber = Arc::new(TestSubscriber::default());
285        store.add_subscriber(subscriber.clone()).unwrap();
286
287        // Dispatch action that produces Effect::Function
288        store.dispatch(TestAction::FunctionTask("test-key".to_string())).unwrap();
289
290        // Wait for effect to be processed
291        thread::sleep(Duration::from_millis(100));
292
293        // Stop store to ensure all effects are processed
294        store.stop().unwrap();
295
296        let actions = subscriber.get_actions();
297
298        // Should have received the FunctionTask action
299        assert!(actions.iter().any(|a| matches!(a, TestAction::FunctionTask(_))));
300
301        println!("Effect::Function test passed");
302    }
303
304    #[test]
305    fn test_effect_chain() {
306        // Test chaining multiple effects
307        println!("Testing Effect chaining");
308
309        let store = StoreBuilder::new_with_reducer(TestState::default(), Box::new(TestReducer))
310            .with_name("test-effect-chain".into())
311            .build()
312            .unwrap();
313
314        let subscriber = Arc::new(TestSubscriber::default());
315        store.add_subscriber(subscriber.clone()).unwrap();
316
317        // Dispatch multiple actions with effects
318        store.dispatch(TestAction::SetValue(5)).unwrap();
319        store.dispatch(TestAction::ThunkTask(3)).unwrap();
320        store.dispatch(TestAction::AsyncTask).unwrap();
321
322        // Wait for all effects to be processed
323        thread::sleep(Duration::from_millis(200));
324
325        // Stop store to ensure all effects are processed
326        store.stop().unwrap();
327
328        let actions = subscriber.get_actions();
329
330        // Should have multiple actions
331        assert!(actions.len() >= 3);
332
333        // Should have SetValue, ThunkTask, and AsyncTask
334        assert!(actions.iter().any(|a| matches!(a, TestAction::SetValue(5))));
335        assert!(actions.iter().any(|a| matches!(a, TestAction::ThunkTask(3))));
336        assert!(actions.iter().any(|a| matches!(a, TestAction::AsyncTask)));
337
338        // Should have AddValue from thunk
339        assert!(actions.iter().any(|a| matches!(a, TestAction::AddValue(3))));
340
341        // Should have AddMessage from SetValue effect
342        assert!(actions.iter().any(|a| matches!(a, TestAction::AddMessage(_))));
343
344        println!("Effect chaining test passed");
345    }
346}