oxide_mvu/
effect.rs

1//! Declarative effect system for describing deferred event processing.
2
3#[cfg(feature = "no_std")]
4use alloc::boxed::Box;
5#[cfg(feature = "no_std")]
6use alloc::vec::Vec;
7
8use core::future::Future;
9use core::pin::Pin;
10
11use crate::Emitter;
12
13/// Declarative description of events to be processed.
14///
15/// Effects allow you to describe asynchronous or deferred work that will
16/// produce events. They are returned from [`MvuLogic::init`](crate::MvuLogic::init)
17/// and [`MvuLogic::update`](crate::MvuLogic::update) with the new model state.
18///
19/// # Example
20///
21/// ```rust
22/// use oxide_mvu::Effect;
23///
24/// #[derive(Clone)]
25/// enum Event {
26///     LoadData,
27///     DataLoaded(String),
28/// }
29///
30/// // Trigger a follow-up event
31/// let effect = Effect::just(Event::LoadData);
32///
33/// // Combine multiple effects
34/// let effect = Effect::batch(vec![
35///     Effect::just(Event::LoadData),
36///     Effect::just(Event::DataLoaded("cached".to_string())),
37/// ]);
38///
39/// // No side effects
40/// let effect: Effect<Event> = Effect::none();
41/// ```
42pub struct Effect<Event: Send>(Box<dyn FnOnceBox<Event> + Send>);
43
44impl<Event: Send + 'static> Effect<Event> {
45    /// Execute the effect, consuming it and returning a future.
46    ///
47    /// The returned future will be spawned on your async runtime using the provided spawner.
48    pub fn execute(self, emitter: &Emitter<Event>) -> Pin<Box<dyn Future<Output = ()> + Send>> {
49        self.0.call_box(emitter)
50    }
51
52    /// Create an empty effect.
53    ///
54    /// This is private - use [`Effect::none()`] instead.
55    fn new() -> Self {
56        fn empty_fn<Event: Send>(_: &Emitter<Event>) -> Pin<Box<dyn Future<Output = ()> + Send>> {
57            Box::pin(async {})
58        }
59        Self(Box::new(empty_fn))
60    }
61
62    /// Create an effect that just emits a single event.
63    ///
64    /// Useful for triggering immediate follow-up events.
65    ///
66    /// # Example
67    ///
68    /// ```rust
69    /// use oxide_mvu::Effect;
70    ///
71    /// #[derive(Clone)]
72    /// enum Event { Refresh }
73    ///
74    /// let effect = Effect::just(Event::Refresh);
75    /// ```
76    pub fn just(event: Event) -> Self
77    where
78        Event: Send + 'static,
79    {
80        Self(Box::new(move |emitter: &Emitter<Event>| {
81            let emitter = emitter.clone();
82            Box::pin(async move { emitter.emit(event) }) as Pin<Box<dyn Future<Output = ()> + Send>>
83        }))
84    }
85
86    /// Create an empty effect.
87    ///
88    /// Prefer this when semantically indicating "no side effects".
89    ///
90    /// # Example
91    ///
92    /// ```rust
93    /// use oxide_mvu::Effect;
94    ///
95    /// #[derive(Clone)]
96    /// enum Event { Increment }
97    ///
98    /// let effect: Effect<Event> = Effect::none();
99    /// ```
100    pub fn none() -> Self {
101        Self::new()
102    }
103
104    /// Combine multiple effects into a single effect.
105    ///
106    /// All events from all effects will be queued for processing.
107    ///
108    /// # Example
109    ///
110    /// ```rust
111    /// use oxide_mvu::Effect;
112    ///
113    /// #[derive(Clone)]
114    /// enum Event { A, B, C }
115    ///
116    /// let combined = Effect::batch(vec![
117    ///     Effect::just(Event::A),
118    ///     Effect::just(Event::B),
119    ///     Effect::just(Event::C),
120    /// ]);
121    /// ```
122    pub fn batch(effects: Vec<Effect<Event>>) -> Self {
123        Self(Box::new(move |emitter: &Emitter<Event>| {
124            let emitter = emitter.clone();
125            Box::pin(async move {
126                for effect in effects {
127                    effect.execute(&emitter).await;
128                }
129            }) as Pin<Box<dyn Future<Output = ()> + Send>>
130        }))
131    }
132
133    /// Create an effect from an async function using a runtime-agnostic spawner.
134    ///
135    /// This allows you to use async/await syntax with any async runtime (tokio,
136    /// async-std, smol, etc.) by providing a spawner function that knows how to
137    /// execute futures on your chosen runtime.
138    ///
139    /// The async function receives a cloned `Emitter` that can be used to emit
140    /// events when the async work completes.
141    ///
142    /// # Arguments
143    ///
144    /// * `spawner` - A function that spawns the future on your async runtime
145    /// * `f` - An async function that receives an Emitter and returns a Future
146    ///
147    /// # Example with tokio
148    ///
149    /// ```rust,no_run
150    /// use oxide_mvu::Effect;
151    /// use std::time::Duration;
152    ///
153    /// #[derive(Clone)]
154    /// enum Event {
155    ///     FetchData,
156    ///     DataLoaded(String),
157    ///     DataFailed(String),
158    /// }
159    ///
160    /// async fn fetch_from_api() -> Result<String, String> {
161    ///     // Await some async operation...
162    ///     Ok("data from API".to_string())
163    /// }
164    ///
165    /// let effect = Effect::from_async(
166    ///     |emitter| async move {
167    ///         match fetch_from_api().await {
168    ///             Ok(data) => emitter.emit(Event::DataLoaded(data)),
169    ///             Err(err) => emitter.emit(Event::DataFailed(err)),
170    ///         }
171    ///     }
172    /// );
173    /// ```
174    ///
175    /// # Example with async-std
176    ///
177    /// ```rust,no_run
178    /// use oxide_mvu::Effect;
179    ///
180    /// #[derive(Clone)]
181    /// enum Event { TimerAlert }
182    ///
183    /// let await_timer_effect = Effect::from_async(
184    ///     |emitter| async move {
185    ///         // Await timer
186    ///         emitter.emit(Event::TimerAlert);
187    ///     }
188    /// );
189    /// ```
190    pub fn from_async<F, Fut>(f: F) -> Self
191    where
192        F: FnOnce(Emitter<Event>) -> Fut + Send + 'static,
193        Fut: Future<Output = ()> + Send + 'static,
194    {
195        Self(Box::new(move |emitter: &Emitter<Event>| {
196            let future = f(emitter.clone());
197            Box::pin(future) as Pin<Box<dyn Future<Output = ()> + Send>>
198        }))
199    }
200}
201
202trait FnOnceBox<Event: Send> {
203    fn call_box(
204        self: Box<Self>,
205        emitter: &Emitter<Event>,
206    ) -> Pin<Box<dyn Future<Output = ()> + Send>>;
207}
208
209impl<F, Event: Send> FnOnceBox<Event> for F
210where
211    F: for<'a> FnOnce(&'a Emitter<Event>) -> Pin<Box<dyn Future<Output = ()> + Send>>,
212{
213    fn call_box(
214        self: Box<Self>,
215        emitter: &Emitter<Event>,
216    ) -> Pin<Box<dyn Future<Output = ()> + Send>> {
217        (*self)(emitter)
218    }
219}