Commands: Rust that the page can call
A command is the compiled equivalent of a Laravel controller action. Freddy writes one, registers it, generates its TypeScript, and calls it from a Svelte page as if it were a local function.
A command
Annotate an async fn with #[command] and register it
with commands![...]. This is the guide's own first example:
use elyra::{command, commands, App, Ctx};
#[command]
async fn greet(ctx: Ctx, name: String) -> String {
format!("Hello, {name}!")
}
App::new().commands(commands![greet, add]).run();
commands![...] is Freddy's routes/web.php. The macro
turns the function into a type of the same name, which is why the function name
is also what you pass to the list.
The first parameter is always the context, Ctx, passed through
untouched. It is how a command reaches the container, which chapter 4 is about;
name it _ctx when a command does not need it. Arguments must
implement serde::Deserialize and specta::Type, and the
return type serde::Serialize and specta::Type. Structs
go over as named maps, so they arrive as plain JavaScript objects and survive
fields being reordered between versions.
Calling it from Svelte
The low-level call is invoke from @elyra/runtime. The
one Freddy will actually use is the typed facade that rata codegen
writes:
import { api } from "./bindings";
const greeting = await api.greet("world");
rata codegen writes app/src/bindings.ts from the
app's commands: a TypeScript type for every Rust type they use, and an
api object whose methods call invoke. It is the
framework's Facade, generated, so a renamed field in Rust is a type error in
Svelte instead of a surprise at runtime. Run it after changing a command's
signature or a type it uses.
One detail from the codegen page that matters for
Freddy's note ids: 64-bit integers render as number, because on the
MessagePack wire they already arrive as JavaScript numbers.
When it fails
A command that can fail returns Result<T, E> where
E: Display. Ok becomes the value; Err
makes the frontend's promise reject with a CommandError. When a
command mixes kinds of failure, elyra::Result<T> is the
convenient shape: ? converts a validation error bag, which reaches
the frontend as a ValidationError with its fields intact, and a query
error, and elyra::Error::command(msg) covers the rest.
The frontend can tell failures apart, because every error carries a kind:
| Kind | Meaning | Frontend type |
|---|---|---|
command | the command's own Err | CommandError |
validation | a validation error bag | ValidationError |
panic | the handler panicked | CommandError |
cancelled | cancelled from the frontend | CommandError |
forbidden | missing token or capability | ForbiddenError |
| your own | Error::with_kind("archived", "…") | CommandError with that kind |
A panic does not hang the caller: every command runs on its own task, so a panic
becomes an error response with the panic message, and is logged. Chapter 11 adds
the kinds a Laravel backend's answers become, such as offline and
unauthenticated.
Long commands: cancel and progress
Freddy's export of all his notes to Markdown takes a while. The page can cancel
it with invokeCancellable; the Rust task is aborted at its next
.await, so a cancellable command should await now and then:
import { invokeCancellable } from "@elyra/runtime";
const job = invokeCancellable("build_report", opts);
onDestroy(() => job.cancel());
const report = await job.result;
There is no special progress channel. The command emits on the event bus, and
the page listens with channel(), which chapter 4 introduces.
Keeping the page away from a command
Remember the untrusted page. One grant opens all commands to it, so a command that should not be in that blanket is marked with an ability:
#[command(can = "posts.delete")]
async fn delete_post(ctx: Ctx, id: i64) -> elyra::Result<()> {
// ...
}
App::new()
.commands(commands![delete_post])
.allow_ability("posts.delete"); // or "posts.*"
Without the grant, a call from the page answers 403 and names what is missing. Calls from Rust, from providers, queue jobs and tests, are not affected. Freddy marks his "delete every note" command this way and grants it only once he is sure.
The whole story is on the commands page, with the wire format on its own page and the frontend side on the frontend runtime page.
What you learned
- A command is
#[command] async fnwithCtxfirst, registered withcommands![...]. rata codegenwritesbindings.ts: types and a typedapi.*.elyra::Resultand error kinds let the page tell a validation error from a panic, a cancellation or a refusal.invokeCancellablestops a long command at its next await.#[command(can = "...")]keeps a command away from the page until the app grants the ability.