frus_shell/command.rs
1//! [`Command`]: the **effects** `Application::update` returns.
2//!
3//! A command describes work to be done **outside** the `update` cycle — a file
4//! write, a load, a network `fetch`, a long computation — whose result comes back as
5//! a **message** fed into `update`. There are three shapes:
6//!
7//! - **synchronous** ([`Command::perform`] / [`Command::run`]): a closure that
8//! blocks. It gets a background thread natively, or a microtask on the Web.
9//! - **asynchronous** ([`Command::perform_async`] / [`Command::run_async`]): a
10//! **future** that can `await`. Natively it goes to the shell's executor, which
11//! has a reactor, so a future may wait on a timer or on I/O without holding a
12//! thread. On the **Web**, which is single-threaded, the browser drives it
13//! (`spawn_local`) and always did.
14//! - **delayed** ([`Command::after`]): a message on a real timer.
15//!
16//! The distinction that matters is the first against the second, and it is about
17//! **what the work does while it is not finished**. Something that blocks — a
18//! synchronous file read, a CPU-bound computation — must be a `perform`, because
19//! occupying a thread is exactly what a thread is for. Something that *waits* should
20//! be a `perform_async`, because waiting no longer costs a thread. Putting a blocking
21//! call inside `perform_async` is the one mistake worth naming here: it parks one of
22//! the executor's few workers and starves every other effect.
23//!
24//! Until milestone 303 the asynchronous side was a thread and a `block_on` with no
25//! reactor underneath it — `frus-shell`'s own `runtime` module records what that
26//! could and could not do.
27
28use std::future::Future;
29use std::pin::Pin;
30use std::time::Duration;
31
32use frus_widgets::{ScrollTo, SheetTo};
33
34/// A command taken apart, for the shell to run. A struct rather than a tuple: there
35/// are several kinds of effect, and a tuple at a call site says nothing about which is
36/// which.
37pub(crate) struct Parts<Msg> {
38 pub(crate) tasks: Vec<Task<Msg>>,
39 pub(crate) async_tasks: Vec<AsyncTask<Msg>>,
40 pub(crate) timers: Vec<(Duration, Msg)>,
41 pub(crate) focus: Vec<u64>,
42 pub(crate) scrolls: Vec<(u64, ScrollTo)>,
43 pub(crate) sheets: Vec<(u64, SheetTo)>,
44}
45
46/// A **synchronous** task: work that may produce a message.
47type Task<Msg> = Box<dyn FnOnce() -> Option<Msg> + Send + 'static>;
48
49/// An **asynchronous** task: a future that may produce a message.
50///
51/// **Natively** it crosses a thread (`block_on`), hence the `Send` bound. On the
52/// **Web**, which is single-threaded, the browser's futures (`JsFuture`/`fetch`) are
53/// **not** `Send` and do not need to be — hence the relaxed bound.
54#[cfg(not(web))]
55type AsyncTask<Msg> = Pin<Box<dyn Future<Output = Option<Msg>> + Send + 'static>>;
56#[cfg(web)]
57type AsyncTask<Msg> = Pin<Box<dyn Future<Output = Option<Msg>> + 'static>>;
58
59/// A batch of effects to run, possibly empty: background **tasks**, synchronous or
60/// asynchronous, and **focus** and **scroll** requests addressed by widget key.
61pub struct Command<Msg> {
62 tasks: Vec<Task<Msg>>,
63 async_tasks: Vec<AsyncTask<Msg>>,
64 /// Messages to deliver after a delay. Kept as data rather than as a future, so
65 /// that the one piece of platform knowledge — how this machine waits — stays in
66 /// the shell with all the other platform knowledge.
67 timers: Vec<(Duration, Msg)>,
68 /// The keys of widgets to focus — the key's hash, as in [`crate::Subscription`]
69 /// and the widgets' `keyed(...)`. The shell resolves them after the next build.
70 focus: Vec<u64>,
71 /// The scroll regions to move, by the same kind of key. Resolved against the frame
72 /// the request is returned into.
73 scrolls: Vec<(u64, ScrollTo)>,
74 /// The sheets to move, by the same kind of key and on the same terms.
75 sheets: Vec<(u64, SheetTo)>,
76}
77
78/// A key's hash — **identical** to the hash the widgets' `keyed(key, …)` uses, so that
79/// `Command::focus(k)` and `Command::scroll(k, …)` both target the `keyed(k, …)` widget.
80fn focus_key(key: impl std::hash::Hash) -> u64 {
81 use std::hash::Hasher;
82 let mut hasher = std::collections::hash_map::DefaultHasher::new();
83 key.hash(&mut hasher);
84 hasher.finish()
85}
86
87impl<Msg: Send + 'static> Command<Msg> {
88 /// No effect at all.
89 pub fn none() -> Self {
90 Self {
91 tasks: Vec::new(),
92 async_tasks: Vec::new(),
93 timers: Vec::new(),
94 focus: Vec::new(),
95 scrolls: Vec::new(),
96 sheets: Vec::new(),
97 }
98 }
99
100 /// Groups several commands into one.
101 pub fn batch(commands: impl IntoIterator<Item = Command<Msg>>) -> Self {
102 let mut tasks = Vec::new();
103 let mut async_tasks = Vec::new();
104 let mut timers = Vec::new();
105 let mut focus = Vec::new();
106 let mut scrolls = Vec::new();
107 let mut sheets = Vec::new();
108 for command in commands {
109 tasks.extend(command.tasks);
110 async_tasks.extend(command.async_tasks);
111 timers.extend(command.timers);
112 focus.extend(command.focus);
113 scrolls.extend(command.scrolls);
114 sheets.extend(command.sheets);
115 }
116 Self {
117 tasks,
118 async_tasks,
119 timers,
120 focus,
121 scrolls,
122 sheets,
123 }
124 }
125
126 /// Delivers `message` once, after `delay`.
127 ///
128 /// This waits without occupying anything: natively it is a task on the shell's
129 /// executor parked on the reactor's timer wheel, and on the Web a `setTimeout`.
130 /// A hundred pending timers are a hundred entries in a queue, not a hundred
131 /// threads.
132 ///
133 /// For a message that repeats, declare a [`Subscription::every`](crate::Subscription::every)
134 /// instead — a subscription is a function of the state and stops when the state
135 /// says so, where a chain of `after`s has to be stopped by hand.
136 pub fn after(delay: Duration, message: Msg) -> Self {
137 Self {
138 timers: vec![(delay, message)],
139 ..Self::none()
140 }
141 }
142
143 /// Runs a **synchronous** task in the background; its result becomes a message.
144 pub fn perform(task: impl FnOnce() -> Msg + Send + 'static) -> Self {
145 Self {
146 tasks: vec![Box::new(move || Some(task()))],
147 ..Self::none()
148 }
149 }
150
151 /// Runs a **synchronous** side effect; it may return a message (`None` for none).
152 pub fn run(task: impl FnOnce() -> Option<Msg> + Send + 'static) -> Self {
153 Self {
154 tasks: vec![Box::new(task)],
155 ..Self::none()
156 }
157 }
158
159 /// Runs an asynchronous **future**; its value becomes a message.
160 ///
161 /// On the **Web**, which is single-threaded, the browser drives the future
162 /// (`spawn_local`), so a real `fetch` can `await` without blocking. **Natively**
163 /// it runs on the shell's executor (the `runtime` module), which has a reactor: a
164 /// future may wait on a timer or on a socket and costs nothing while it does.
165 ///
166 /// Do not put a **blocking** call in here — that is what [`Command::perform`] is
167 /// for. The executor has four worker threads, and a blocking call holds one of
168 /// them for its whole duration.
169 #[cfg(not(web))]
170 pub fn perform_async<F>(future: F) -> Self
171 where
172 F: Future<Output = Msg> + Send + 'static,
173 {
174 Self {
175 async_tasks: vec![Box::pin(async move { Some(future.await) })],
176 ..Self::none()
177 }
178 }
179
180 /// Runs an asynchronous **future**; its value becomes a message. See the native
181 /// version for the full semantics; the `Send` bounds are relaxed on the Web.
182 #[cfg(web)]
183 pub fn perform_async<F>(future: F) -> Self
184 where
185 F: Future<Output = Msg> + 'static,
186 {
187 Self {
188 async_tasks: vec![Box::pin(async move { Some(future.await) })],
189 ..Self::none()
190 }
191 }
192
193 /// Runs an asynchronous **future** for its side effect; it may return a message
194 /// (`None` for none). The asynchronous counterpart of [`Command::run`].
195 #[cfg(not(web))]
196 pub fn run_async<F>(future: F) -> Self
197 where
198 F: Future<Output = Option<Msg>> + Send + 'static,
199 {
200 Self {
201 async_tasks: vec![Box::pin(future)],
202 ..Self::none()
203 }
204 }
205
206 /// Runs an asynchronous **future** for its side effect (`None` for no message).
207 /// The Web version, with the `Send` bound relaxed.
208 #[cfg(web)]
209 pub fn run_async<F>(future: F) -> Self
210 where
211 F: Future<Output = Option<Msg>> + 'static,
212 {
213 Self {
214 async_tasks: vec![Box::pin(future)],
215 ..Self::none()
216 }
217 }
218
219 /// Requests **focus** for the widget carrying `key` — the field wrapped in
220 /// `keyed(key, …)`. The shell resolves it after the view is next built; it is
221 /// typically returned when a form submission fails, to jump to the first invalid
222 /// field (`Form::first_invalid`).
223 pub fn focus(key: impl std::hash::Hash) -> Self {
224 Self {
225 focus: vec![focus_key(key)],
226 ..Self::none()
227 }
228 }
229
230 /// **Moves a scroll region**: the one named `key` goes where `to` says.
231 ///
232 /// A scroll is not a function of the state — the same list, the same offset, and yet
233 /// "go back to the top" happens once — so it is an effect and belongs here rather
234 /// than on a widget. The offset it changes *is* state, and it stays where it was: in
235 /// the runtime, beside every other offset, written by the frame this request is
236 /// resolved into.
237 ///
238 /// Named the way a focus request is named: the region is wrapped in `keyed(k, …)`
239 /// and addressed by `k`. The framework's own identities are hashes of a position in
240 /// a tree, which an application cannot know and should not have to.
241 ///
242 /// Resolved against the **frame that follows**, once and once only — the same rule
243 /// as [`Command::focus`]. A request naming a region that frame has not got is
244 /// dropped rather than kept waiting: an effect that outlived the state it was
245 /// written for would arrive at a list that had since become somebody else's.
246 ///
247 /// ```no_run
248 /// # use frus_shell::Command;
249 /// # use frus_widgets::ScrollTo;
250 /// # fn f() -> Command<()> {
251 /// Command::scroll("journal", ScrollTo::start())
252 /// # }
253 /// ```
254 pub fn scroll(key: impl std::hash::Hash, to: ScrollTo) -> Self {
255 Self {
256 scrolls: vec![(focus_key(key), to)],
257 ..Self::none()
258 }
259 }
260
261 /// **Moves a sheet**: the one named `key` goes where `to` says — the reference's
262 /// `DraggableScrollableController`, as an effect.
263 ///
264 /// For the reason a scroll is one: "raise the sheet" happens once, and the height it
265 /// leaves behind is state that stays where every other sheet height is, in the
266 /// runtime. Named the same way — the sheet, or anything around it, wrapped in
267 /// `keyed(k, …)` — and resolved on the same terms: against the frame that follows, a
268 /// sheet that has only just appeared included, and dropped if that frame has not got
269 /// it. A finger on the sheet refuses it.
270 ///
271 /// ```no_run
272 /// # use frus_shell::Command;
273 /// # use frus_widgets::{Curve, SheetTo};
274 /// # fn f() -> Command<()> {
275 /// Command::sheet("places", SheetTo::size(1.0).animate(0.3, Curve::ease()))
276 /// # }
277 /// ```
278 pub fn sheet(key: impl std::hash::Hash, to: SheetTo) -> Self {
279 Self {
280 sheets: vec![(focus_key(key), to)],
281 ..Self::none()
282 }
283 }
284
285 /// A focus request whose key is already a hash — what a component's effect carries.
286 pub(crate) fn focus_hashed(key: u64) -> Self {
287 Self {
288 focus: vec![key],
289 ..Self::none()
290 }
291 }
292
293 /// A scroll request whose key is already a hash.
294 pub(crate) fn scroll_hashed(key: u64, to: ScrollTo) -> Self {
295 Self {
296 scrolls: vec![(key, to)],
297 ..Self::none()
298 }
299 }
300
301 /// A sheet request whose key is already a hash.
302 pub(crate) fn sheet_hashed(key: u64, to: SheetTo) -> Self {
303 Self {
304 sheets: vec![(key, to)],
305 ..Self::none()
306 }
307 }
308
309 /// `true` when the command has no effect and no request of any kind.
310 pub fn is_empty(&self) -> bool {
311 self.tasks.is_empty()
312 && self.async_tasks.is_empty()
313 && self.timers.is_empty()
314 && self.focus.is_empty()
315 && self.scrolls.is_empty()
316 && self.sheets.is_empty()
317 }
318
319 /// Takes the command apart for the framework to run.
320 pub(crate) fn into_parts(self) -> Parts<Msg> {
321 Parts {
322 tasks: self.tasks,
323 async_tasks: self.async_tasks,
324 timers: self.timers,
325 focus: self.focus,
326 scrolls: self.scrolls,
327 sheets: self.sheets,
328 }
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn none_is_empty() {
338 assert!(Command::<u32>::none().is_empty());
339 }
340
341 #[test]
342 fn a_batch_keeps_its_sheet_requests() {
343 let sheet = Command::<u32>::sheet("places", SheetTo::size(1.0));
344 assert!(!sheet.is_empty());
345 let batch = Command::batch([
346 sheet,
347 Command::none(),
348 Command::sheet("other", SheetTo::initial()),
349 ]);
350 assert_eq!(batch.into_parts().sheets.len(), 2);
351 }
352
353 #[test]
354 fn perform_yields_a_message() {
355 let command = Command::perform(|| 42u32);
356 let tasks = command.into_parts().tasks;
357 assert_eq!(tasks.len(), 1);
358 let produced = (tasks.into_iter().next().unwrap())();
359 assert_eq!(produced, Some(42));
360 }
361
362 #[test]
363 fn run_may_produce_nothing() {
364 let command = Command::run(|| -> Option<u32> { None });
365 let tasks = command.into_parts().tasks;
366 assert_eq!((tasks.into_iter().next().unwrap())(), None);
367 }
368
369 #[test]
370 fn batch_flattens_and_drops_empties() {
371 let command = Command::batch([
372 Command::perform(|| 1u32),
373 Command::none(),
374 Command::run(|| Some(2u32)),
375 ]);
376 assert_eq!(command.into_parts().tasks.len(), 2);
377 }
378
379 #[test]
380 fn focus_carries_a_key_and_no_task() {
381 // A focus command carries no task, yet it is not "empty".
382 let f = Command::<u32>::focus("email");
383 assert!(!f.is_empty());
384 let parts = f.into_parts();
385 assert!(parts.tasks.is_empty());
386 assert!(parts.async_tasks.is_empty());
387 assert_eq!(parts.focus, vec![focus_key("email")]);
388 }
389
390 #[cfg(not(web))]
391 #[test]
392 fn perform_async_yields_a_message() {
393 // The future is driven to completion (`block_on`) and its value becomes a message.
394 let command = Command::perform_async(async { 7u32 });
395 let mut parts = command.into_parts();
396 assert!(parts.tasks.is_empty());
397 assert_eq!(parts.async_tasks.len(), 1);
398 let produced = pollster::block_on(parts.async_tasks.remove(0));
399 assert_eq!(produced, Some(7));
400 }
401
402 #[cfg(not(web))]
403 #[test]
404 fn run_async_may_produce_nothing() {
405 let command = Command::run_async(async { None::<u32> });
406 let mut parts = command.into_parts();
407 assert_eq!(pollster::block_on(parts.async_tasks.remove(0)), None);
408 }
409
410 #[cfg(not(web))]
411 #[test]
412 fn batch_combines_sync_and_async_tasks() {
413 let command = Command::batch([
414 Command::perform(|| 1u32),
415 Command::perform_async(async { 2u32 }),
416 ]);
417 let parts = command.into_parts();
418 assert_eq!(parts.tasks.len(), 1);
419 assert_eq!(parts.async_tasks.len(), 1);
420 }
421
422 #[test]
423 fn after_carries_its_delay_and_is_not_empty() {
424 let command = Command::after(Duration::from_millis(250), 9u32);
425 assert!(!command.is_empty(), "a pending timer is an effect");
426 let parts = command.into_parts();
427 assert!(parts.tasks.is_empty() && parts.async_tasks.is_empty());
428 assert_eq!(parts.timers, vec![(Duration::from_millis(250), 9u32)]);
429 }
430
431 #[test]
432 fn batch_carries_timers_through() {
433 let command = Command::batch([
434 Command::after(Duration::from_millis(10), 1u32),
435 Command::perform(|| 2u32),
436 Command::after(Duration::from_millis(20), 3u32),
437 ]);
438 let parts = command.into_parts();
439 assert_eq!(parts.tasks.len(), 1);
440 assert_eq!(parts.timers.len(), 2, "a batch must not swallow a timer");
441 }
442}