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 simplified SimpleComponent 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.