Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async capability combinators #179

Merged
merged 15 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ members = [
"crux_kv",
"crux_macros",
"crux_platform",
"crux_time"
"crux_time",
"doctest_support"
]
resolver = "1"

Expand Down
1 change: 1 addition & 0 deletions crux_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ async-channel = "2.1"
crux_macros = { version = "0.3", path = "../crux_macros" }
crux_http = { version = "0.4", path = "../crux_http" }
crux_time = { version = "0.1", path = "../crux_time" }
doctest_support = { path = "../doctest_support" }
serde = { version = "1.0.195", features = ["derive"] }
static_assertions = "1.1"
rand = "0.8"
Expand Down
134 changes: 134 additions & 0 deletions crux_core/src/capabilities/compose.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//! 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).
///
/// 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> 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_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, Clone)]
/// # 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 caps = caps.clone();
///
/// async move {
/// let (result_one, result_two) =
/// futures::future::join(
/// caps.one.one_async(10),
/// caps.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 + Copy + 'static,
Ev: 'static,
NewEv: 'static,
{
Compose::new(self.context.map_event(f))
}
}
1 change: 1 addition & 0 deletions crux_core/src/capabilities/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod compose;
pub mod render;
67 changes: 48 additions & 19 deletions crux_core/src/capability/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,35 @@ pub trait Operation: serde::Serialize + PartialEq + Send + 'static {
type Output: serde::de::DeserializeOwned + Send + 'static;
}

/// A type that can be used as a capability operation, but which will never be sent to the shell.
/// This type is useful for capabilities that don't request effects.
/// For example, you can use this type as the Operation for a
/// capability that just composes other capabilities.
///
/// e.g.
/// ```rust
/// # use crux_core::capability::{CapabilityContext, Never};
/// # use crux_macros::Capability;
/// #[derive(Capability)]
/// pub struct Compose<E> {
/// context: CapabilityContext<Never, E>,
/// }
/// # impl<E> Compose<E> {
/// # pub fn new(context: CapabilityContext<Never, E>) -> Self {
/// # Self { context }
/// # }
/// # }
///
/// ```

#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum Never {}

/// Implement `Operation` for `Never` to allow using it as a capability operation.
impl Operation for Never {
type Output = ();
}

/// Implement the `Capability` trait for your capability. This will allow
/// mapping events when composing apps from submodules.
///
Expand All @@ -226,19 +255,19 @@ pub trait Operation: serde::Serialize + PartialEq + Send + 'static {
/// Example:
///
/// ```rust
///# use crux_core::{Capability, capability::{CapabilityContext, Operation}};
///# pub struct Http<Ev> {
///# context: CapabilityContext<HttpOperation, Ev>,
///# }
///# #[derive(serde::Serialize, PartialEq, Eq)] pub struct HttpOperation;
///# impl Operation for HttpOperation {
///# type Output = ();
///# }
///# impl<Ev> Http<Ev> where Ev: 'static, {
///# pub fn new(context: CapabilityContext<HttpOperation, Ev>) -> Self {
///# Self { context }
///# }
///# }
/// # use crux_core::{Capability, capability::{CapabilityContext, Operation}};
/// # pub struct Http<Ev> {
/// # context: CapabilityContext<HttpOperation, Ev>,
/// # }
/// # #[derive(serde::Serialize, PartialEq, Eq)] pub struct HttpOperation;
/// # impl Operation for HttpOperation {
/// # type Output = ();
/// # }
/// # impl<Ev> Http<Ev> where Ev: 'static, {
/// # pub fn new(context: CapabilityContext<HttpOperation, Ev>) -> Self {
/// # Self { context }
/// # }
/// # }
/// impl<Ev> Capability<Ev> for Http<Ev> {
/// type Operation = HttpOperation;
/// type MappedSelf<MappedEv> = Http<MappedEv>;
Expand Down Expand Up @@ -299,10 +328,10 @@ pub trait Capability<Ev> {
/// # type ViewModel = ();
/// # type Capabilities = Capabilities;
/// # fn update(&self, _event: Self::Event, _model: &mut Self::Model, _caps: &Self::Capabilities) {
/// # todo!()
/// # unimplemented!()
/// # }
/// # fn view(&self, _model: &Self::Model) -> Self::ViewModel {
/// # todo!()
/// # unimplemented!()
/// # }
/// # }
/// # impl crux_core::Effect for Effect {
Expand Down Expand Up @@ -515,10 +544,10 @@ where
/// # _model: &mut Self::Model,
/// # _caps: &Self::Capabilities,
/// # ) {
/// # todo!()
/// # unimplemented!()
/// # }
/// # fn view(&self, _model: &Self::Model) -> Self::ViewModel {
/// # todo!()
/// # unimplemented!()
/// # }
/// # }
///impl From<&Capabilities> for child::Capabilities {
Expand Down Expand Up @@ -549,10 +578,10 @@ where
/// # _model: &mut Self::Model,
/// # _caps: &Self::Capabilities,
/// # ) {
/// # todo!()
/// # unimplemented!()
/// # }
/// # fn view(&self, _model: &Self::Model) -> Self::ViewModel {
/// # todo!()
/// # unimplemented!()
/// # }
/// # }
/// # }
Expand Down
Loading
Loading