Commands
In this chapter, we'll have a look at commands, which are a simple yet extremely powerful mechanism to offload both CPU-bound and I/O-bound tasks to a separate runtime.
Commands are background tasks that can be spawned using a ComponentSender
or FactorySender
.
They run until they return their result as a CommandOutput
message that will be processed by the component.
First, we define our message type so we can use it for the associated CommandOutput
type in our component.
#[derive(Debug)]
enum CommandMsg {
Data(RemoteData),
}
impl Component for CommandModel {
type CommandOutput = CommandMsg;
Note: This only works with the
Component
trait. The simplifiedSimpleComponent
trait doesn't support commands.
In our update function, we start a new command using the oneshot_command()
method.
This method allows us to spawn a future that will yield exactly one CommandOutput
message at completion.
From the command, we call an asynchronous function that will handle the web request for us.
Once the future completes, the command returns a CommandMsg
.
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>, _: &Self::Root) {
match msg {
CommandModelMsg::FetchData => {
sender.oneshot_command(async {
// Run async background task
CommandMsg::Data(fetch_data().await)
});
}
}
}
Now, we can process the CommandMsg
similar to regular app updates.
The method we use is called update_cmd()
and is very similar to the regular update()
function.
Only the message type is CommandOutput
instead of Input
.
From here, we can simply assign the result of the web request to our model.
fn update_cmd(
&mut self,
message: Self::CommandOutput,
_sender: ComponentSender<Self>,
_: &Self::Root,
) {
match message {
CommandMsg::Data(data) => self.remote_data = data,
}
}
That's it! It's really as simple as starting a task and processing a message on completion.
With the
command()
method, you are even more flexible because you can send multiple messages.
Synchronous tasks
You can use commands for synchronous operations, too.
Compared to the asynchronous methods, we need to add the spawn_
prefix to the method name to get the synchronous version.
Then, you can just pass a closure or a function pointer as task.
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>, _: &Self::Root) {
match msg {
CommandModelMsg::FetchData => {
sender.spawn_oneshot_command(|| {
// Run CPU-bound background task
CommandMsg::Data(compute_result())
});
}
}
}
The rest is identical to the asynchronous version.
fn update_cmd(
&mut self,
message: Self::CommandOutput,
_sender: ComponentSender<Self>,
_: &Self::Root,
) {
match message {
CommandMsg::Data(data) => self.remote_data = data,
}
}
Configuration
Commands run on a tokio runtime.
If you spawn a lot of commands in your application or want to fine-tune the runtime, you can set two static variables at the start of your main function to override the default value.
For example, Relm4 only uses one thread for asynchronous background tasks, which might not be enough.
Setting RELM_THREADS
to 4 will increase the thread count by 3 additional threads.
Note: Setting the static variables must be done early. As soon as the runtime is initialized (which happens when it's accessed for the first time), the values cannot be changed anymore.