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