Commands
A command is the compiled equivalent of a Laravel controller action. Annotate an
async fn with #[command] and register it with commands![...].
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();
How it works
#[command] turns the function into a zero-sized type of the same name that
implements the Command trait — so the function name doubles as the value you
pass to commands![...]. Arguments are decoded from a compact MessagePack array;
the return value is encoded as a named map (see wire format).
The Ctx
The first parameter is always the context and is passed through untouched. Use it to resolve services from the container:
#[command]
async fn greet(ctx: Ctx, name: String) -> String {
let cfg = ctx.get::<Config>(); // Arc<Config>, panics if unbound
format!("{} {name}", cfg.greeting)
}
Name it _ctx if unused.
Arguments and return types
- Arguments must be simple identifiers with types that implement
serde::Deserializeandspecta::Type(for codegen). - The return type must implement
serde::Serialize+specta::Type. - Structs are serialized as named maps → plain JS objects, resilient to field reordering across versions.
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
struct Point { x: i64, y: i64 }
#[command]
async fn shift(_ctx: Ctx, p: Point) -> Point { Point { x: p.x + 1, y: p.y + 1 } }
Zero-argument commands ignore the request body entirely.
Fallible commands (Result)
Return Result<T, E> where E: Display. Ok(v) is serialized as T; Err(e)
becomes an error response — the frontend promise rejects with a
CommandError. Codegen surfaces T:
#[command]
async fn checked_div(_ctx: Ctx, a: i64, b: i64) -> Result<i64, String> {
if b == 0 { Err("cannot divide by zero".into()) } else { Ok(a / b) }
}
try { await api.checked_div(1, 0); } catch (e) { /* CommandError */ }
Calling from the frontend
import { invoke } from "@elyra/runtime";
const greeting = await invoke<string>("greet", "world");
// or the typed facade after `rata codegen`:
import { api } from "./bindings";
const greeting = await api.greet("world");
Limitations (deliberate)
- The macro assumes the first parameter is the
Ctx. - No generics, no
Option<Ctx>, noselfreceivers. - Numeric codegen: 64-bit integers render as
number— see codegen.
Cancellation
A slow or long-running command can be cancelled from the frontend with
invokeCancellable — the Rust task is aborted at its next .await. Cancelling
rejects the result promise with a CommandError.
import { invokeCancellable } from "@elyra/runtime";
const job = invokeCancellable<Report>("build_report", opts);
onDestroy(() => job.cancel()); // stop it when the component unmounts
const report = await job.result;
The generated api.* uses the plain (non-cancellable) invoke; reach for
invokeCancellable when you specifically need to abort. Because abortion happens
at await points, make cancellable commands .await periodically (I/O, chunks)
for prompt cancellation.
Progress
There's no special progress channel — emit on the event bus, which is exactly what it's for:
#[command]
async fn build_report(ctx: Ctx) -> Report {
let bus = ctx.get::<EventBus>();
for (i, step) in steps.iter().enumerate() {
let pct = ((i as f64 / steps.len() as f64) * 100.0) as u8;
let _ = bus.emit("report:progress", &pct);
// … do the step …
}
report
}
import { channel, invokeCancellable } from "@elyra/runtime";
let pct = 0;
const off = channel<number>("report:progress").subscribe((p) => { if (p != null) pct = p; });
const job = invokeCancellable<Report>("build_report");
onDestroy(() => { job.cancel(); off(); });
const report = await job.result;
Errors on the wire
A failed command responds with x-elyra-status: error and an
x-elyra-error-kind telling the frontend what kind of failure it was:
| Kind | Meaning | Frontend type |
|---|---|---|
command |
the command's own Err (message verbatim) |
CommandError |
validation |
a ValidationErrors bag |
ValidationError (parsed errors) |
panic |
the handler panicked | CommandError (kind panic) |
cancelled |
aborted via invokeCancellable().cancel() |
CommandError |
forbidden |
missing IPC token or capability | ForbiddenError |
bad-request |
body too large / nested too deep | CommandError |
A panicking command answers with an error instead of hanging the caller: every
command runs on its own task, so a panic (including a missing container binding,
which panics by design) becomes a 500 with the panic message, and is logged at
error level.
Input limits
Request bodies are capped at 16 MiB (App::max_request_body) and MessagePack
nested deeper than 64 levels is rejected before it reaches serde — a ~10 KB body
of nested arrays would otherwise overflow the stack. See security.
Related
- Container & providers
- Middleware — wrap dispatch
- Codegen — the typed
api.* - Events — progress + push updates