crux_core/capabilities/
compose.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//! A capability which can spawn tasks which orchestrate across other capabilities. This
//! is useful for orchestrating a number of different effects into a single transaction.

use crate::capability::{CapabilityContext, Never};
use crate::Capability;
use futures::Future;

/// Compose capability can be used to orchestrate effects into a single transaction.
///
/// Example include:
/// * Running a number of HTTP requests in parallel and waiting for all to finish
/// * Chaining effects together, where the output of one is the input of the next and the intermediate
///   results are not useful to the app
/// * Implementing request timeouts by selecting across a HTTP effect and a time effect
/// * Any arbitrary graph of effects which depend on each other (or not).
///
/// The compose capability doesn't have any operations it emits to the shell, and type generation fails
/// on its operation type ([`Never`](crate::capability::Never))). This is difficult for crux to detect
/// at the moment. To avoid this problem until a better fix is found, use `#[effect(skip)]` to skip the
/// generation of an effect variant for the compose capability. For example
///
/// ```rust
/// # use crux_core::macros::Effect;
/// # use crux_core::{compose::Compose, render::Render};
/// # enum Event { Nothing }
/// #[derive(Effect)]
/// pub struct Capabilities {
///     pub render: Render<Event>,
///     #[effect(skip)]
///     pub compose: Compose<Event>,
/// }
/// ```
///
/// Note that testing composed effects is more difficult, because it is not possible to enter the effect
/// transaction "in the middle" - only from the beginning - or to ignore some of the effects with out
/// stalling the entire downstream dependency chain.
pub struct Compose<Ev> {
    context: CapabilityContext<Never, Ev>,
}

/// A restricted context given to the closure passed to [`Compose::spawn`]. This context can only
/// update the app, not request from the shell or spawn further tasks.
pub struct ComposeContext<Ev> {
    context: CapabilityContext<Never, Ev>,
}

impl<Ev> Clone for ComposeContext<Ev> {
    fn clone(&self) -> Self {
        Self {
            context: self.context.clone(),
        }
    }
}

impl<Ev> ComposeContext<Ev> {
    /// Update the app with an event. This forwards to [`CapabilityContext::update_app`].
    pub fn update_app(&self, event: Ev)
    where
        Ev: 'static,
    {
        self.context.update_app(event);
    }
}

impl<Ev> Compose<Ev> {
    pub fn new(context: CapabilityContext<Never, Ev>) -> Self {
        Self { context }
    }

    /// Spawn a task which orchestrates across other capabilities.
    ///
    /// The argument is a closure which receives a [`ComposeContext`] which can be used to send
    /// events to the app.
    ///
    /// For example:
    /// ```
    /// # use crux_core::macros::Effect;
    /// # use serde::Serialize;
    /// # #[derive(Default, Clone)]
    /// # pub struct App;
    /// # #[derive(Debug, PartialEq)]
    /// # pub enum Event {
    /// #     Trigger,
    /// #     Finished(usize, usize),
    /// # }
    /// # #[derive(Default, Serialize, Debug, PartialEq)]
    /// # pub struct Model {
    /// #     pub total: usize,
    /// # }
    /// # #[derive(Effect)]
    /// # pub struct Capabilities {
    /// #     one: doctest_support::compose::capabilities::capability_one::CapabilityOne<Event>,
    /// #     two: doctest_support::compose::capabilities::capability_two::CapabilityTwo<Event>,
    /// #     compose: crux_core::compose::Compose<Event>,
    /// # }
    /// # impl crux_core::App for App {
    /// #    type Event = Event;
    /// #    type Model = Model;
    /// #    type ViewModel = Model;
    /// #    type Capabilities = Capabilities;
    /// #
    ///     fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
    ///         match event {
    ///             Event::Trigger => caps.compose.spawn(|context| {
    ///                 let one = caps.one.clone();
    ///                 let two = caps.two.clone();
    ///
    ///                 async move {
    ///                     let (result_one, result_two) =
    ///                         futures::future::join(
    ///                             one.one_async(10),
    ///                             two.two_async(20)
    ///                         ).await;
    ///
    ///                     context.update_app(Event::Finished(result_one, result_two))
    ///                 }
    ///             }),
    ///             Event::Finished(one, two) => {
    ///                 model.total = one + two;
    ///             }
    ///         }
    ///     }
    /// #
    /// #    fn view(&self, _model: &Self::Model) -> Self::ViewModel {
    /// #        todo!()
    /// #    }
    /// # }
    /// ```
    pub fn spawn<F, Fut>(&self, effects_task: F)
    where
        F: FnOnce(ComposeContext<Ev>) -> Fut,
        Fut: Future<Output = ()> + 'static + Send,
        Ev: 'static,
    {
        let context = self.context.clone();
        self.context.spawn(effects_task(ComposeContext { context }));
    }
}

impl<E> Clone for Compose<E> {
    fn clone(&self) -> Self {
        Self {
            context: self.context.clone(),
        }
    }
}

impl<Ev> Capability<Ev> for Compose<Ev> {
    type Operation = Never;
    type MappedSelf<MappedEv> = Compose<MappedEv>;

    fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
    where
        F: Fn(NewEv) -> Ev + Send + Sync + 'static,
        Ev: 'static,
        NewEv: 'static,
    {
        Compose::new(self.context.map_event(f))
    }

    #[cfg(feature = "typegen")]
    fn register_types(_generator: &mut crate::typegen::TypeGen) -> crate::typegen::Result {
        panic!(
            r#"
            The Compose Capability should not be registered for type generation.
            Instead, use #[effect(skip)] to skip the generation of an effect variant for the Compose Capability.
            "#
        )
    }
}