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