crux_core/capabilities/
compose.rs

1//! A capability which can spawn tasks which orchestrate across other capabilities. This
2//! is useful for orchestrating a number of different effects into a single transaction.
3
4use crate::capability::{CapabilityContext, Never};
5use crate::Capability;
6use futures::Future;
7
8/// Compose capability can be used to orchestrate effects into a single transaction.
9///
10/// Example include:
11/// * Running a number of HTTP requests in parallel and waiting for all to finish
12/// * Chaining effects together, where the output of one is the input of the next and the intermediate
13///   results are not useful to the app
14/// * Implementing request timeouts by selecting across a HTTP effect and a time effect
15/// * Any arbitrary graph of effects which depend on each other (or not).
16///
17/// The compose capability doesn't have any operations it emits to the shell, and type generation fails
18/// on its operation type ([`Never`]). This is difficult for crux to detect
19/// at the moment. To avoid this problem until a better fix is found, use `#[effect(skip)]` to skip the
20/// generation of an effect variant for the compose capability. For example
21///
22/// ```rust
23/// # use crux_core::macros::Effect;
24/// # use crux_core::{compose::Compose, render::Render};
25/// # enum Event { Nothing }
26/// #[derive(Effect)]
27/// pub struct Capabilities {
28///     pub render: Render<Event>,
29///     #[effect(skip)]
30///     pub compose: Compose<Event>,
31/// }
32/// ```
33///
34/// Note that testing composed effects is more difficult, because it is not possible to enter the effect
35/// transaction "in the middle" - only from the beginning - or to ignore some of the effects with out
36/// stalling the entire downstream dependency chain.
37pub struct Compose<Ev> {
38    context: CapabilityContext<Never, Ev>,
39}
40
41/// A restricted context given to the closure passed to [`Compose::spawn`]. This context can only
42/// update the app, not request from the shell or spawn further tasks.
43pub struct ComposeContext<Ev> {
44    context: CapabilityContext<Never, Ev>,
45}
46
47impl<Ev> Clone for ComposeContext<Ev> {
48    fn clone(&self) -> Self {
49        Self {
50            context: self.context.clone(),
51        }
52    }
53}
54
55impl<Ev> ComposeContext<Ev> {
56    /// Update the app with an event. This forwards to [`CapabilityContext::update_app`].
57    pub fn update_app(&self, event: Ev)
58    where
59        Ev: 'static,
60    {
61        self.context.update_app(event);
62    }
63}
64
65impl<Ev> Compose<Ev> {
66    pub fn new(context: CapabilityContext<Never, Ev>) -> Self {
67        Self { context }
68    }
69
70    /// Spawn a task which orchestrates across other capabilities.
71    ///
72    /// The argument is a closure which receives a [`ComposeContext`] which can be used to send
73    /// events to the app.
74    ///
75    /// For example:
76    /// ```
77    /// # use crux_core::Command;
78    /// # use crux_core::macros::Effect;
79    /// # use serde::Serialize;
80    /// # #[derive(Default, Clone)]
81    /// # pub struct App;
82    /// # #[derive(Debug, PartialEq)]
83    /// # pub enum Event {
84    /// #     Trigger,
85    /// #     Finished(usize, usize),
86    /// # }
87    /// # #[derive(Default, Serialize, Debug, PartialEq)]
88    /// # pub struct Model {
89    /// #     pub total: usize,
90    /// # }
91    /// # #[derive(Effect)]
92    /// # pub struct Capabilities {
93    /// #     one: doctest_support::compose::capabilities::capability_one::CapabilityOne<Event>,
94    /// #     two: doctest_support::compose::capabilities::capability_two::CapabilityTwo<Event>,
95    /// #     compose: crux_core::compose::Compose<Event>,
96    /// # }
97    /// # impl crux_core::App for App {
98    /// #    type Event = Event;
99    /// #    type Model = Model;
100    /// #    type ViewModel = Model;
101    /// #    type Capabilities = Capabilities;
102    /// #    type Effect = Effect;
103    /// #
104    ///     fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) -> Command<Effect, Event> {
105    ///         match event {
106    ///             Event::Trigger => caps.compose.spawn(|context| {
107    ///                 let one = caps.one.clone();
108    ///                 let two = caps.two.clone();
109    ///
110    ///                 async move {
111    ///                     let (result_one, result_two) =
112    ///                         futures::future::join(
113    ///                             one.one_async(10),
114    ///                             two.two_async(20)
115    ///                         ).await;
116    ///
117    ///                     context.update_app(Event::Finished(result_one, result_two))
118    ///                 }
119    ///             }),
120    ///             Event::Finished(one, two) => {
121    ///                 model.total = one + two;
122    ///             }
123    ///         }
124    ///         Command::done()
125    ///     }
126    /// #
127    /// #    fn view(&self, _model: &Self::Model) -> Self::ViewModel {
128    /// #        todo!()
129    /// #    }
130    /// # }
131    /// ```
132    pub fn spawn<F, Fut>(&self, effects_task: F)
133    where
134        F: FnOnce(ComposeContext<Ev>) -> Fut,
135        Fut: Future<Output = ()> + 'static + Send,
136        Ev: 'static,
137    {
138        let context = self.context.clone();
139        self.context.spawn(effects_task(ComposeContext { context }));
140    }
141}
142
143impl<E> Clone for Compose<E> {
144    fn clone(&self) -> Self {
145        Self {
146            context: self.context.clone(),
147        }
148    }
149}
150
151impl<Ev> Capability<Ev> for Compose<Ev> {
152    type Operation = Never;
153    type MappedSelf<MappedEv> = Compose<MappedEv>;
154
155    fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
156    where
157        F: Fn(NewEv) -> Ev + Send + Sync + 'static,
158        Ev: 'static,
159        NewEv: 'static,
160    {
161        Compose::new(self.context.map_event(f))
162    }
163}