Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number etrans An SSH and SFTP client for macOS Elyra VM Virtual machines for macOS, Linux and Windows on your Mac Litr A small, native web browser for macOS Notr A notebook for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Refr Local-first PDF workspace for macOS Elyra Workspace A desktop workspace for coding agents Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra
Events — the EventBus

Events — the EventBus

The EventBus is Elyra's Broadcasting: Rust pushes events to the frontend, batched per flush. Rust owns the state; the frontend subscribes to changes instead of polling.

Emitting (Rust)

The bus is created by App and bound in the container, so any command, provider, or task can resolve it:

#[command]
async fn tick(ctx: Ctx) {
    ctx.get::<EventBus>().emit("tick", &42u32).ok();
}

emit<T: Serialize>(channel, &value) is non-blocking and callable from any thread. To emit from main or a background thread, grab a clone before running:

let app = App::new().commands(commands![..]);
let bus = app.events();                       // a clone of the bus
std::thread::spawn(move || { bus.emit("tick", &1u32).ok(); });
app.run()?;

Subscribing (frontend)

channel(name) returns a Svelte-readable store, multiplexed over a single connection:

<script>
  import { channel } from "@elyra/runtime";
  const ticks = channel("tick");   // usable as $ticks
</script>
<p>{$ticks}</p>

Or subscribe manually:

const unsubscribe = channel<number>("tick").subscribe((n) => console.log(n));

Transport & batching

Events travel over a long-poll of elyra://localhost/__events: the shell holds the request open until events are ready, responds with a MessagePack batch ([[channel, value], ...]), and the frontend immediately reconnects. Binary, no base64, one connection for all channels. See wire format.

Emits accumulate and flush together, so N state changes cost one IPC round, not N. By default there's no artificial delay — the natural response→reconnect gap coalesces bursts. For sustained, time-spaced streams you can force frame-level coalescing:

App::new().batch_window(std::time::Duration::from_millis(8));

After ~20s idle the poll returns an empty keep-alive batch and the connection refreshes.

Fan-out: one queue per window

Every webview identifies itself with a random client id (x-elyra-client-id, added by @elyra/runtime) and gets its own queue. An emit is fanned out to all connected windows, so a multi-window app can't lose events — previously one shared queue meant whichever window polled first took the batch and the others never saw it.

Events emitted before any window has connected are held and delivered to the first poll (nothing emitted during startup is lost). A window that opens later does not get a replay of what it missed — push current state from a command instead when a new window needs to catch up.

Domain events (Dispatcher)

The EventBus pushes values out to the webview. For events inside the Rust side — one part announces that something happened, others react without the announcer knowing who they are — use the Dispatcher: Laravel's Event::dispatch and listeners.

#[derive(Clone)]
struct OrderShipped { order_id: i64 }

App::new().listen(|e: OrderShipped, ctx: Ctx| async move {
    ctx.get::<dyn Mailer>().send_shipped(e.order_id).await
});

#[command]
async fn ship(ctx: Ctx, order_id: i64) -> Result<()> {
    // … mark it shipped …
    ctx.dispatch(OrderShipped { order_id }).await
}

Listeners can also be registered from a provider — Laravel's EventServiceProvider — in register or boot:

impl Provider for AuditProvider {
    fn boot(&self, ctx: &Ctx) {
        ctx.get::<Dispatcher>().listen(|e: OrderShipped, ctx: Ctx| async move {
            ctx.get::<AuditLog>().record("shipped", e.order_id)
        });
    }
}
  • An event is any Clone + Send + Sync + 'static type; each listener gets its own copy.
  • Listeners run in registration order, one at a time: App::listen calls first, then providers in the order they were added.
  • The first error stops the chain and is returned from dispatch, so the command that dispatched fails with the listener's message. An event nobody listens for is not an error.
  • ctx.dispatch_background(event) returns immediately and runs the chain on its own task; a listener error is logged under elyra::events instead.
  • A listener may dispatch further events.

Broadcasting to the frontend

App::broadcast forwards every dispatched event to the EventBus and declares its channel type for codegen — Laravel's ShouldBroadcast, typed end to end:

#[derive(Clone, serde::Serialize, specta::Type)]
struct OrderShipped { order_id: i64 }

App::new().broadcast::<OrderShipped>("orders:shipped");
<script>
  import { channel } from "./bindings";            // the generated, typed helper
  const shipped = channel("orders:shipped");        // typed as OrderShipped
</script>
{#if $shipped}<p>Order {$shipped.order_id} shipped</p>{/if}

The Rust side dispatches a domain event and never mentions the webview; the frontend gets a typed channel.

Related