Phase 8 — guest ergonomics
Goal: make writing a RUSM guest pleasant — in TypeScript or Rust — so a component is just exported functions, and calling another component reads like a local function call. The actor ABI from Phase 7 is powerful but raw; Phase 8 is the ergonomic layer on top, in both languages, over one shared wire.
Why this matters
Phase 7 proved a component can be a supervised, addressable process. But a guest still hand-rolled bindings and hand-parsed bytes. Phase 8 delivers the developer experience: services (exported functions), a concealed typed client (spawn + send + receive, hidden behind await svc.method(...)), streaming and callbacks, an in-guest Supervisor, and rusm dev watch + reload — the same story for Rust and TS, interoperable because they share one JSON wire.
What we built (TDD throughout)
- rusm-ts — a TypeScript guest is plain TS, Bun-bundled (
rusm build→bun build --format=cjs) and run on the shared rquickjs js-runner. A service justexports functions (RUSM runs the receive→dispatch→reply loop); a worker isexport default async function. TheProcessactor API is async (await Process.receive()— the host call suspends the instance's fiber, so "blocking" stays cheap). Web APIs (URL,TextEncoder,Headers,ReadableStream,console,crypto) are polyfilled, and a capability-gated streamingfetchrides the host'swasi:httpclient (refused for a sandboxed guest). (Both shipped with Phase 11.) - The concealed typed client —
spawn<Svc>("svc")returns a proxy whoseawait svc.method(args)is a real cross-process call. Generator handlers stream (for await (const x of svc.gen(...))); function arguments are callbacks that stay in the caller (their invocations travel back as messages);svc.cast.method(...)is fire-and-forget. Wire: JSON{op,args,from,ref}→{ref,ok|err}, with{op:"__cb"}callbacks. - rusm-rs — the Rust twin: ergonomic
Pid/send/receive(serde JSON, the same wire as TS)/spawn/registry/Streamover the actor world, via the wit-bindgen library/binary split (rusm-rs owns an imports-only world; a guest maps the actor interface to it andexport!s its ownrun, so the interface is imported exactly once). A#[rusm_rs::service]macro over amodof free functions (mirroring TS'sexport functions — noimpl, noself) generates aserve()dispatch loop and a typedClientwith call/cast/streaming/callbacks. A Rust client and a TS service interoperate. - Spawn-from-guest + monitor (actor ABI) —
spawninstantiates a registered component by name → a new pid;monitormakes a dead process arrive as a__downmessage (receivetranslates the runtimeDown— no watcher process, no polling). Both are capability-gated; theallow-spawncapability gates who may spawn, and a node-registered component runs under its own manifest-declared profile (a guest can't fabricate capabilities the operator never granted). - In-guest
Supervisor— in both rusm-rs and rusm-ts: spawn + monitor named children and restart per strategy —one_for_one/one_for_all/rest_for_one, withmax_restarts(overload protection). The OTP supervision tree, written from inside a guest. - The
rusm-tsnpm package — the TS guest API ships as an importable package (import { Process, spawn, supervise } from "rusm-ts"), added withbun add rusm-ts;rusm buildrunsbun installfor you. (Root-caused a subtle hang doing this: the runner now wraps each bundle in a CommonJS scope, so a bundle's top-levelvarcan never clobber the runtime globals — correct CJS isolation.) - Custom capability profiles —
rusm.tomlaccepts[capabilities.<name>]profiles, Cargo-style: eachinheritsa built-in base and overrides specific grants (allow-network/allow-spawn/allow-process-control/allow-stdio/max-memory-mb/env/preopen). A component selects one by name. rusm devwatch + reload —rusm devbuilds, runs, and watches./components; on a source edit it rebuilds and reloads the components. A dependency-free mtime poll (skipstarget/andnode_modules/).
Concepts introduced
- Components & the actor world — the actor ABI the guest crates wrap; composition is message passing.
- Permissions & sandboxing —
spawn,monitor, and custom[capabilities.<name>]profiles. - The full guest story (TS + Rust, service / client / supervisor) — see Getting started, the
rusm-tspackage, and therusm-rscrate.
Play with it
# A two-component TS app (calc service + commander) — build with Bun, run on RUSM:
cd examples/ts-app
rusm build # bun install (if needed) + bundle each components/<name>/index.ts
rusm run # → 2 + 3 = 5 / hi RUSM / counted: 1,2,3 / work done after 25/50/100
rusm dev # same, then watch & reload on edit// A Rust service — functions become a dispatch loop + a typed Client:
#[rusm_rs::service]
pub mod calc {
pub fn add(a: i64, b: i64) -> i64 { a + b }
pub fn count_to(n: i64) -> impl Iterator<Item = i64> { 1..=n } // streaming
pub fn work(progress: rusm_rs::Callback<i64>) -> String { /* … */ } // callback
}
// caller: let calc = calc::Client::spawn("calc")?; calc.add(2, 3)?;// A TS service — exported functions become a dispatch loop; the contract is derived:
export function add(a: number, b: number): number { return a + b; }
export function* countTo(n: number) { for (let i = 1; i <= n; i++) yield i; } // streaming
export function work(progress: (pct: number) => void): string { // callback
for (const pct of [25, 50, 100]) progress(pct);
return "done";
}
export type Calc = typeof import(".");
// caller: const calc = spawn<Calc>("calc"); await calc.add(2, 3);Verification
cargo test green — host-level spawn gating + per-component declared profiles, the JS service dispatch (sync + async handlers), a TS commander calling a service via the typed client (call + streaming + callback), the Rust #[service] macro driven end to end (call + streaming + callback), and a Supervisor (Rust and TS) restarting a killed child. The Bun-built ts-app example runs end to end; the component-storm spawn path holds ~440k spawns/sec (no regression from spawn-from-guest). The Wasm-free invariant still holds (no wasmtime under rusm-otp).
Reclassified to Phase 11: a native p3-typed stream<u8> WIT signature — a standards-surface refinement; the byte streams already work over a handle ABI.
Next
Phase 9: distributed clusters + live attach — QUIC + TLS, remote spawn, and a global registry, so processes spawn and message across nodes.