crux_core

Module command

Source
Expand description

Command represents one or more side-effects, resulting in interactions with the shell. Core creates Commands and returns them from the update function in response to events. Commands can be created directly, but more often they will be created and returned by capability APIs.

A Command can execute side-effects in parallel, in sequence or a combination of both. To allow this orchestration they provide both a simple synchronous API and access to an asynchronous API.

Command surfaces the effect requests and events sent in response with the Command::effects and Command::events methods. These can be used when testing the side effects requested by an update call.

Internally, Command resembles FuturesUnordered: it manages and polls a number of futures and provides a context which they can use to submit effects to the shell and events back to the application.

Command implements [Stream], making it useful in an async context, enabling, for example, wrapping Commands in one another.

§Examples

Commands are typically created by a capability and returned from the update function. Capabilities normally return a builder, which can be used in both sync and async context. The basic sync use is to bind the command to an Event which will be sent with the result of the command:

fn update(
    &self,
    event: Self::Event,
    model: &mut Self::Model,
    _caps: &Self::Capabilities)
-> Command<Effect, Event> {
    match event {
        //...
        Event::Increment => {
            let base = Url::parse(API_URL).unwrap();
            let url = base.join("/inc").unwrap();

            Http::post(url)            // creates an HTTP RequestBuilder
                .expect_json()
                .build()               // creates a Command RequestBuilder
                .then_send(Event::Set) // creates a Command
        }
        Event::Set(Ok(mut response)) => {
             let count = response.take_body().unwrap();
             model.count = count;
             render::render()
        }
        Event::Set(Err(_)) => todo!()
    }
}

Commands can be chained, allowing the outputs of the first effect to be used in constructing the second effect. For example, the following code creates a new post, then fetches the full created post based on a url read from the response to the creation request:

let cmd: Command<Effect, Event> =
    Http::post(API_URL)
        .body(serde_json::json!({"title":"New Post", "body":"Hello!"}))
        .expect_json::<Post>()
        .build()
        .then_request(|result| {
            let post = result.unwrap();
            let url = &post.body().unwrap().url;

            Http::get(url).expect_json().build()
        })
        .then_send(Event::GotPost);

// Run the http request concurrently with notifying the shell to render
Command::all([cmd, render()])

The same can be done with the async API, if you need more complex orchestration that is more naturally expressed in async rust

let cmd: Command<Effect, Event> = Command::new(|ctx| async move {
    let first = Http::post(API_URL)
        .body(serde_json::json!({"title":"New Post", "body":"Hello!"}))
        .expect_json::<Post>()
        .build()
        .into_future(ctx.clone())
        .await;

    let post = first.unwrap();
    let url = &post.body().unwrap().url;

    let second = Http::get(url).expect_json().build().into_future(ctx.clone()).await;

    ctx.send_event(Event::GotPost(second));
});

In the async context, you can spawn additional concurrent tasks, which can, for example, communicate with each other via channels, to enable more complex orchestrations, stateful connection handling and other advanced uses.

let mut cmd: Command<Effect, Event> = Command::new(|ctx| async move {
    let (tx, rx) = async_channel::unbounded();

    ctx.spawn(|ctx| async move {
        for i in 0..10u8 {
            let output = ctx.request_from_shell(AnOperation::One(i)).await;
            tx.send(output).await.unwrap();
        }
    });

    ctx.spawn(|ctx| async move {
        while let Ok(value) = rx.recv().await {
            ctx.send_event(Event::Completed(value));
        }
        ctx.send_event(Event::Aborted);
    });
});

Commands can be cancelled, by calling Command::abort_handle and then calling abort on the returned handle.

let mut cmd: Command<Effect, Event> = Command::all([
    Command::request_from_shell(AnOperation::One(1)).then_send(Event::Completed),
    Command::request_from_shell(AnOperation::Two(1)).then_send(Event::Completed),
]);

let handle = cmd.abort_handle();

// Command is still running
assert!(!cmd.was_aborted());

handle.abort();

// Command is now finished
assert!(cmd.is_done());
// And was aborted
assert!(cmd.was_aborted());

You can test that Commands yield the expected effects and events. Commands can be tested in isolation by creating them explicitly in a test, and then checking the effects and events they generated. Or you can call your app’s update function in a test, and perform the same checks on the returned Command.

const API_URL: &str = "https://example.com/api/posts";

// Create a command to post a new Post to API_URL
// and then dispatch an event with the result
let mut cmd = Http::post(API_URL)
    .body(serde_json::json!({"title":"New Post", "body":"Hello!"}))
    .expect_json()
    .build()
    .then_send(Event::GotPost);

// Check the effect is an HTTP request ...
let effect = cmd.effects().next().unwrap();
let Effect::Http(mut request) = effect else {
    panic!("Expected a HTTP effect")
};

// ... and the request is a POST to API_URL
assert_eq!(
    &request.operation,
    &HttpRequest::post(API_URL)
        .header("content-type", "application/json")
        .body(r#"{"body":"Hello!","title":"New Post"}"#)
        .build()
);

// Resolve the request with a successful response
let body = Post {
    url: API_URL.to_string(),
    title: "New Post".to_string(),
    body: "Hello!".to_string(),
};
request
    .resolve(HttpResult::Ok(HttpResponse::ok().json(&body).build()))
    .expect("Resolve should succeed");

// Check the event is a GotPost event with the successful response
let actual = cmd.events().next().unwrap();
let expected = Event::GotPost(Ok(ResponseBuilder::ok().body(body).build()));
assert_eq!(actual, expected);

assert!(cmd.is_done());

Structs§

Enums§

  • An item emitted from a Command when used as a Stream.