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}