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}