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    #[must_use]
67    pub fn new(context: CapabilityContext<Never, Ev>) -> Self {
68        Self { context }
69    }
70
71    /// Spawn a task which orchestrates across other capabilities.
72    ///
73    /// The argument is a closure which receives a [`ComposeContext`] which can be used to send
74    /// events to the app.
75    ///
76    /// For example:
77    /// ```
78    /// # use crux_core::Command;
79    /// # use crux_core::macros::Effect;
80    /// # use serde::Serialize;
81    /// # #[derive(Default, Clone)]
82    /// # pub struct App;
83    /// # #[derive(Debug, PartialEq)]
84    /// # pub enum Event {
85    /// #     Trigger,
86    /// #     Finished(usize, usize),
87    /// # }
88    /// # #[derive(Default, Serialize, Debug, PartialEq)]
89    /// # pub struct Model {
90    /// #     pub total: usize,
91    /// # }
92    /// # #[derive(Effect)]
93    /// # pub struct Capabilities {
94    /// #     one: doctest_support::compose::capabilities::capability_one::CapabilityOne<Event>,
95    /// #     two: doctest_support::compose::capabilities::capability_two::CapabilityTwo<Event>,
96    /// #     compose: crux_core::compose::Compose<Event>,
97    /// # }
98    /// # impl crux_core::App for App {
99    /// #    type Event = Event;
100    /// #    type Model = Model;
101    /// #    type ViewModel = Model;
102    /// #    type Capabilities = Capabilities;
103    /// #    type Effect = Effect;
104    /// #
105    ///     fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) -> Command<Effect, Event> {
106    ///         match event {
107    ///             Event::Trigger => caps.compose.spawn(|context| {
108    ///                 let one = caps.one.clone();
109    ///                 let two = caps.two.clone();
110    ///
111    ///                 async move {
112    ///                     let (result_one, result_two) =
113    ///                         futures::future::join(
114    ///                             one.one_async(10),
115    ///                             two.two_async(20)
116    ///                         ).await;
117    ///
118    ///                     context.update_app(Event::Finished(result_one, result_two))
119    ///                 }
120    ///             }),
121    ///             Event::Finished(one, two) => {
122    ///                 model.total = one + two;
123    ///             }
124    ///         }
125    ///         Command::done()
126    ///     }
127    /// #
128    /// #    fn view(&self, _model: &Self::Model) -> Self::ViewModel {
129    /// #        todo!()
130    /// #    }
131    /// # }
132    /// ```
133    pub fn spawn<F, Fut>(&self, effects_task: F)
134    where
135        F: FnOnce(ComposeContext<Ev>) -> Fut,
136        Fut: Future<Output = ()> + 'static + Send,
137        Ev: 'static,
138    {
139        let context = self.context.clone();
140        self.context.spawn(effects_task(ComposeContext { context }));
141    }
142}
143
144impl<E> Clone for Compose<E> {
145    fn clone(&self) -> Self {
146        Self {
147            context: self.context.clone(),
148        }
149    }
150}
151
152impl<Ev> Capability<Ev> for Compose<Ev> {
153    type Operation = Never;
154    type MappedSelf<MappedEv> = Compose<MappedEv>;
155
156    fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
157    where
158        F: Fn(NewEv) -> Ev + Send + Sync + 'static,
159        Ev: 'static,
160        NewEv: 'static,
161    {
162        Compose::new(self.context.map_event(f))
163    }
164}