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}