Module crux_core::capability

source ·
Expand description

Capabilities provide a user-friendly API to request side-effects from the shell.

Typically, capabilities provide I/O and host API access. Capabilities are external to the core Crux library. Some are part of the Crux core distribution, others are expected to be built by the community. Apps can also build single-use capabilities where necessary.

§Example use

A typical use of a capability would look like the following:

fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
    match event {
        //...
        Event::Increment => {
            model.count += 1;
            caps.render.render(); // Render capability

            let base = Url::parse(API_URL).unwrap();
            let url = base.join("/inc").unwrap();
            caps.http.post(url).expect_json().send(Event::Set); // HTTP client capability
        }
        Event::Set(_) => todo!(),
    }
}

Capabilities don’t perform side-effects themselves, they request them from the Shell. As a consequence the capability calls within the update function only queue up the requests. The side-effects themselves are performed concurrently and don’t block the update function.

In order to use a capability, the app needs to include it in its Capabilities associated type and WithContext trait implementation (which can be provided by the crux_core::macros::Effect macro). For example:

mod root {

// An app module which can be reused in different apps
mod my_app {
    use crux_core::{capability::CapabilityContext, App, render::Render};
    use crux_core::macros::Effect;
    use serde::{Serialize, Deserialize};

    #[derive(Default)]
    pub struct MyApp;
    #[derive(Serialize, Deserialize)]
    pub struct Event;

    // The `Effect` derive macro generates an `Effect` type that is used by the
    // Shell to dispatch side-effect requests to the right capability implementation
    // (and, in some languages, checking that all necessary capabilities are implemented)
    #[derive(Effect)]
    pub struct Capabilities {
        pub render: Render<Event>
    }

    impl App for MyApp {
        type Model = ();
        type Event = Event;
        type ViewModel = ();
        type Capabilities = Capabilities;

        fn update(&self, event: Event, model: &mut (), caps: &Capabilities) {
            caps.render.render();
        }

        fn view(&self, model: &()) {
            ()
        }
    }
}
}

§Implementing a capability

Capabilities provide an interface to request side-effects. The interface has asynchronous semantics with a form of callback. A typical capability call can look like this:

caps.ducks.get_in_a_row(10, Event::RowOfDucks)

The call above translates into “Get 10 ducks in a row and return them to me using the RowOfDucks event”. The capability’s job is to translate this request into a serializable message and instruct the Shell to do the duck herding and when it receives the ducks back, wrap them in the requested event and return it to the app.

We will refer to get_in_row in the above call as an operation, the 10 is an input, and the Event::RowOfDucks is an event constructor - a function, which eventually receives the row of ducks and returns a variant of the Event enum. Conveniently, enum tuple variants can be used as functions, and so that will be the typical use.

This is what the capability implementation could look like:

use crux_core::{
    capability::{CapabilityContext, Operation},
};
use crux_core::macros::Capability;
use serde::{Serialize, Deserialize};

// A duck
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
struct Duck;

// Operations that can be requested from the Shell
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
enum DuckOperation {
    GetInARow(usize)
}

// Respective outputs for those operations
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
enum DuckOutput {
    GetInRow(Vec<Duck>)
}

// Link the input and output type
impl Operation for DuckOperation {
    type Output = DuckOutput;
}

// The capability. Context will provide the interface to the rest of the system.
#[derive(Capability)]
struct Ducks<Event> {
    context: CapabilityContext<DuckOperation, Event>
};

impl<Event> Ducks<Event> {
    pub fn new(context: CapabilityContext<DuckOperation, Event>) -> Self {
        Self { context }
    }

    pub fn get_in_a_row<F>(&self, number_of_ducks: usize, event: F)
    where
        Event: 'static,
        F: FnOnce(Vec<Duck>) -> Event + Send + 'static,
    {
        let ctx = self.context.clone();
        // Start a shell interaction
        self.context.spawn(async move {
            // Instruct Shell to get ducks in a row and await the ducks
            let ducks = ctx.request_from_shell(DuckOperation::GetInARow(number_of_ducks)).await;

            // Unwrap the ducks and wrap them in the requested event
            // This will always succeed, as long as the Shell implementation is correct
            // and doesn't send the wrong output type back
            if let DuckOutput::GetInRow(ducks) = ducks {
                // Queue an app update with the ducks event
                ctx.update_app(event(ducks));
            }
        })
   }
}

The self.context.spawn API allows a multi-step transaction with the Shell to be performed by a capability without involving the app, until the exchange has completed. During the exchange, one or more events can be emitted (allowing a subscription or streaming like capability to be built).

For Shell requests that have no output, you can use CapabilityContext::notify_shell.

DuckOperation and DuckOutput show how the set of operations can be extended. In simple capabilities, with a single operation, these can be structs, or simpler types. For example, the HTTP capability works directly with HttpRequest and HttpResponse.

Structs§

  • An interface for capabilities to interact with the app and the shell.
  • Initial version of capability Context which has not yet been specialized to a chosen capability

Enums§

  • 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.

Traits§

  • Implement the Capability trait for your capability. This will allow mapping events when composing apps from submodules.
  • Operation trait links together input and output of a side-effect.
  • Allows Crux to construct app’s set of required capabilities, providing context they can then use to request effects and dispatch events.