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}