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.