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§
- Context enabling tasks to communicate with the parent Command, specifically submit effects, events and spawn further tasks
- A builder of one-off request command
- A builder of stream command
Enums§
- An item emitted from a Command when used as a Stream.