Skip to main content

urushi_tui_app/
effect.rs

1//! What an `update` asks the runtime to do.
2
3use std::fmt;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::time::{Duration, Instant};
8
9use urushi::Key;
10
11/// Blocking work an effect carries.
12pub(crate) type Work<Message> = Box<dyn FnOnce() -> Message + Send + 'static>;
13
14/// What a delayed effect turns its firing time into.
15pub(crate) type Fire<Message> = Box<dyn FnOnce(Instant) -> Message + Send + 'static>;
16
17/// A future an effect carries.
18pub(crate) type BoxFuture<Message> = Pin<Box<dyn Future<Output = Message> + Send + 'static>>;
19
20/// A function the runtime may call for many values.
21pub(crate) type Mapper<From, To> = Arc<dyn Fn(From) -> To + Send + Sync + 'static>;
22
23/// Work the runtime performs on behalf of an `update`, whose result returns as
24/// a message.
25///
26/// An effect is a value. It runs nothing when it is built, it cannot touch the
27/// model, and it names no executor: which thread runs a closure and what polls
28/// a future is the runtime's, behind a boundary an application never sees.
29/// Building one in a test therefore costs nothing and needs no terminal.
30///
31/// ```
32/// use urushi_tui_app::Effect;
33///
34/// enum Message {
35///     Loaded(usize),
36/// }
37///
38/// let effect = Effect::perform(|| Message::Loaded(1));
39/// ```
40///
41/// # Which constructor
42///
43/// [`perform`](Effect::perform) carries blocking work and
44/// [`future`](Effect::future) carries a future, so an application whose I/O is
45/// already asynchronous does not wrap it in a thread and one whose work is
46/// blocking does not reach for an executor by name. The `_latest` forms of each
47/// carry a [`Key`]: starting one replaces an unfinished effect running under
48/// the same key, which is the one staleness the runtime knows about on its own.
49/// Every other reason a completion no longer applies is the application's to
50/// check in `update`.
51///
52/// [`after`](Effect::after) is the one-shot timer: it waits and then sends its
53/// message, reading the runtime's clock rather than an executor the application
54/// brought. [`after_latest`](Effect::after_latest) is the same under a key, so a
55/// second one restarts the wait rather than adding a second timer — which is
56/// what debouncing is. A timer that repeats is
57/// [`Subscription::interval`](super::Subscription::interval) instead, because it
58/// lives for as long as the application declares it.
59///
60/// [`batch`](Effect::batch) starts several effects concurrently and promises
61/// nothing about the order their completions arrive in. There is no sequencing
62/// combinator: work that must follow other work is returned from the `update`
63/// that receives the first completion.
64pub struct Effect<Message> {
65    kind: EffectKind<Message>,
66}
67
68/// The shape of an [`Effect`], as the runtime reads it.
69pub(crate) enum EffectKind<Message> {
70    /// Nothing to do.
71    None,
72    /// Stop the runtime.
73    Shutdown,
74    /// Blocking work, replaceable when it carries a key.
75    Perform {
76        key: Option<Key>,
77        work: Work<Message>,
78    },
79    /// A future, replaceable when it carries a key.
80    Future {
81        key: Option<Key>,
82        future: BoxFuture<Message>,
83    },
84    /// A message to send once `delay` has passed, replaceable when it carries a
85    /// key.
86    After {
87        key: Option<Key>,
88        delay: Duration,
89        fire: Fire<Message>,
90    },
91    /// Several effects, started concurrently.
92    Batch(Vec<Effect<Message>>),
93}
94
95impl<Message> Effect<Message> {
96    /// An effect that does nothing.
97    pub fn none() -> Self {
98        Self {
99            kind: EffectKind::None,
100        }
101    }
102
103    /// The request to stop the runtime.
104    ///
105    /// The runtime reads the request from `update`'s return value rather than
106    /// delivering it, so it never enters admission and is not a message. On it,
107    /// no sibling effect in the same return value starts, the completions of
108    /// effects still in flight are discarded, every subscription stops, the
109    /// terminal session is restored, and the entry point returns the final
110    /// model. Work that must finish before the application exits is an ordinary
111    /// effect whose completion is the `update` that returns this.
112    pub fn shutdown() -> Self {
113        Self {
114            kind: EffectKind::Shutdown,
115        }
116    }
117
118    /// Blocking work, run off the thread that runs `update`.
119    pub fn perform<F>(work: F) -> Self
120    where
121        F: FnOnce() -> Message + Send + 'static,
122    {
123        Self {
124            kind: EffectKind::Perform {
125                key: None,
126                work: Box::new(work),
127            },
128        }
129    }
130
131    /// Blocking work that replaces any unfinished work running under `key`.
132    pub fn perform_latest<F>(key: impl Into<Key>, work: F) -> Self
133    where
134        F: FnOnce() -> Message + Send + 'static,
135    {
136        Self {
137            kind: EffectKind::Perform {
138                key: Some(key.into()),
139                work: Box::new(work),
140            },
141        }
142    }
143
144    /// Asynchronous work.
145    pub fn future<F>(future: F) -> Self
146    where
147        F: Future<Output = Message> + Send + 'static,
148    {
149        Self {
150            kind: EffectKind::Future {
151                key: None,
152                future: Box::pin(future),
153            },
154        }
155    }
156
157    /// Asynchronous work that replaces any unfinished work running under `key`.
158    pub fn future_latest<F>(key: impl Into<Key>, future: F) -> Self
159    where
160        F: Future<Output = Message> + Send + 'static,
161    {
162        Self {
163            kind: EffectKind::Future {
164                key: Some(key.into()),
165                future: Box::pin(future),
166            },
167        }
168    }
169
170    /// A message sent once `delay` has passed, timed by the runtime's clock.
171    ///
172    /// The clock is the runtime's, which is what keeps a waiting application
173    /// free of an executor of its own and what lets a test drive the wait by
174    /// hand instead of sleeping.
175    pub fn after<F>(delay: Duration, f: F) -> Self
176    where
177        F: FnOnce(Instant) -> Message + Send + 'static,
178    {
179        Self {
180            kind: EffectKind::After {
181                key: None,
182                delay,
183                fire: Box::new(f),
184            },
185        }
186    }
187
188    /// A delayed message that replaces any unfired timer under `key`.
189    ///
190    /// Replacing a timer restarts its wait, so an `update` that returns this on
191    /// every keystroke fires once the keystrokes stop: the debounce a search
192    /// field or a live preview needs, with no state in the model.
193    pub fn after_latest<F>(key: impl Into<Key>, delay: Duration, f: F) -> Self
194    where
195        F: FnOnce(Instant) -> Message + Send + 'static,
196    {
197        Self {
198            kind: EffectKind::After {
199                key: Some(key.into()),
200                delay,
201                fire: Box::new(f),
202            },
203        }
204    }
205
206    /// Several effects, started concurrently.
207    ///
208    /// Their completions may arrive in any order.
209    pub fn batch(effects: impl IntoIterator<Item = Self>) -> Self {
210        Self {
211            kind: EffectKind::Batch(effects.into_iter().collect()),
212        }
213    }
214
215    /// The same effect with its message passed through `f`.
216    ///
217    /// This is what lets a program hold another program: a parent whose message
218    /// wraps a child's calls the child's `update`, receives the child's effect,
219    /// and returns it mapped, without the child knowing the parent's message
220    /// type.
221    pub fn map<To>(self, f: impl Fn(Message) -> To + Send + Sync + 'static) -> Effect<To>
222    where
223        Message: Send + 'static,
224        To: Send + 'static,
225    {
226        self.map_with(Arc::new(f))
227    }
228
229    fn map_with<To>(self, f: Mapper<Message, To>) -> Effect<To>
230    where
231        Message: Send + 'static,
232        To: Send + 'static,
233    {
234        let kind = match self.kind {
235            EffectKind::None => EffectKind::None,
236            EffectKind::Shutdown => EffectKind::Shutdown,
237            EffectKind::Perform { key, work } => EffectKind::Perform {
238                key,
239                work: Box::new(move || f(work())),
240            },
241            EffectKind::Future { key, future } => EffectKind::Future {
242                key,
243                future: Box::pin(async move { f(future.await) }),
244            },
245            EffectKind::After { key, delay, fire } => EffectKind::After {
246                key,
247                delay,
248                fire: Box::new(move |at| f(fire(at))),
249            },
250            EffectKind::Batch(effects) => EffectKind::Batch(
251                effects
252                    .into_iter()
253                    .map(|effect| effect.map_with(Arc::clone(&f)))
254                    .collect(),
255            ),
256        };
257        Effect { kind }
258    }
259
260    /// The shape of this effect, for the runtime that interprets it.
261    pub(crate) fn into_kind(self) -> EffectKind<Message> {
262        self.kind
263    }
264
265    /// Whether this effect tree asks the runtime to stop before starting work.
266    pub(crate) fn requests_shutdown(&self) -> bool {
267        match &self.kind {
268            EffectKind::Shutdown => true,
269            EffectKind::Batch(effects) => effects.iter().any(Self::requests_shutdown),
270            EffectKind::None
271            | EffectKind::Perform { .. }
272            | EffectKind::Future { .. }
273            | EffectKind::After { .. } => false,
274        }
275    }
276}
277
278impl<Message> Default for Effect<Message> {
279    fn default() -> Self {
280        Self::none()
281    }
282}
283
284impl<Message> fmt::Debug for Effect<Message> {
285    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
286        match &self.kind {
287            EffectKind::None => formatter.write_str("Effect::none"),
288            EffectKind::Shutdown => formatter.write_str("Effect::shutdown"),
289            EffectKind::Perform { key, .. } => formatter
290                .debug_struct("Effect::perform")
291                .field("key", key)
292                .finish(),
293            EffectKind::Future { key, .. } => formatter
294                .debug_struct("Effect::future")
295                .field("key", key)
296                .finish(),
297            EffectKind::After { key, delay, .. } => formatter
298                .debug_struct("Effect::after")
299                .field("key", key)
300                .field("delay", delay)
301                .finish(),
302            EffectKind::Batch(effects) => formatter.debug_list().entries(effects.iter()).finish(),
303        }
304    }
305}
306
307#[cfg(test)]
308#[path = "effect_tests.rs"]
309mod tests;