arora-web 0.1.0

Run an Arora device in the browser: an injectable BrowserRuntime primitive (wasm-bindgen) over any HAL, bridge, and data store.
Documentation

arora-web

Run an Arora device inside a browser. Compiles arora (with --no-default-features, so no wasmtime, no libloading) to wasm32-unknown-unknown and exposes two things:

  • BrowserRuntime — the reusable Rust primitive that runs a full Arora runtime in the browser over an injected HAL, bridge, and data store (all trait objects), with a synchronous step() and Value↔JSON store accessors. Each device ships a thin #[wasm_bindgen] cdylib that constructs its concrete parts and forwards to a BrowserRuntime — the wasm-bindgen boundary can't carry Arc<dyn Trait>, so the generic core stays Rust-side.
  • Engine / BehaviorTreeRunner — a lower-level JS surface for loading guest modules and running behavior trees directly on the engine.

BrowserRuntime (the primitive)

BrowserRuntime is a plain Rust type, not a JS class:

let store: Arc<dyn DataStore> = Arc::new(MyStore::new());
let mut rt = BrowserRuntime::start(
    Arc::new(MyHal::new()),      // Arc<dyn Hal>
    Arc::new(MyBridge::new()),   // Arc<dyn Bridge>
    store,                       // Arc<dyn DataStore>
).await?;
rt.queue_behavior(Box::new(my_behavior)); // or rt.queue_groot_xml(xml)?
// drive from requestAnimationFrame / a Web Worker loop:
rt.step()?;                      // -> bool (false once unregistered)

Its methods (step, set_value, write_values, read_values, snapshot, drain_changes) all take/return JsValue or JSON strings, so a device's cdylib forwards them as one-liners. Values cross the JS boundary as JSON in the Arora Value vocabulary, e.g. {"f32": 0.75}. drain_changes is the poll-based counterpart to a store subscription (JavaScript can't await the std channel DataStore::subscribe delivers on), so call it right after step.

AroraRuntime is the bundled demo device built on BrowserRuntime: the in-process fake HAL and bridge over a plain SimpleDataStore, driving native behavior-tree nodes. Downstream devices (e.g. Vizij) ship their own wrapper the same way.

Engine / BehaviorTreeRunner JS surface (see src/lib.rs):

class Engine {
  constructor();
  loadModule(headerJson: string, executable: Uint8Array): string; // returns module id; sync compile (< 8 MB in Chrome)
  prepareModule(headerJson: string, executable: Uint8Array): Promise<void>; // async compile + instantiate, any size
  loadPreparedModule(headerJson: string): string;                   // completes a prepareModule load; returns module id
  call(callJson: string): string;                                   // returns result JSON
  listModules(): string;                                            // returns JSON array of loaded module headers
}

class BehaviorTreeRunner {
  constructor();
  loadModule(headerJson: string, executable: Uint8Array): string;
  prepareModule(headerJson: string, executable: Uint8Array): Promise<void>;
  loadPreparedModule(headerJson: string): string;
  listModules(): string;                                            // returns JSON array of loaded module headers
  setVariable(varId: string, valueJson: string): void;
  tick(nodesJson: string): string;  // returns {status, trace, variables}
  run(nodesJson: string): string;   // runs until not Running
}

loadModule compiles and instantiates synchronously, which Chrome rejects above 8 MB on the main thread. The prepareModule + loadPreparedModule pair routes through the async WebAssembly.instantiate and works for any size in every browser.

callJson matches arora_engine::call::Call: {"id":"<function-uuid>","args":[...]}. If the call doesn't carry a module_id, arora-web looks one up from the function ID it was loaded with.

Each node in nodesJson is:

{
  "id": "<uuid>",
  "function": "<fn-uuid>",
  "children": ["<child-uuid>"],
  "arguments": { "<param-uuid>": {"value": {...}} | {"variable_id": "<uuid>"} },
  "return_binding": "<var-uuid>"
}

When return_binding is set the function's raw return value is stored in that variable and the node always reports success to its parent.

Build

wasm-pack build crates/arora-web --target web --dev

Output lands under crates/arora-web/pkg/. To consume from a bundler swap --target web for --target bundler.

Integration test (headless browser)

# First, force a wasm32-wasip1 build of the test guest module:
cargo test -p arora-integration-tests

# Then run the wasm-bindgen-test in a headless browser:
GECKODRIVER=$(which geckodriver) wasm-pack test --headless --firefox crates/arora-web
# (or --chrome — see notes below)

The test loads test-rust-wasm.wasm through Engine.loadModule and calls ping, asserting the round-trip works.

Browser pick: wasm-pack downloads a pinned chromedriver; if it doesn't match the locally installed Chrome it 404s. Firefox / geckodriver is more forgiving; CI uses --firefox.

Apple Silicon: the geckodriver wasm-pack auto-downloads is x86_64 and SIGABRTs under Rosetta with a "rosetta error: Attachment of code signature supplement failed" message. Install a native arm64 driver (brew install geckodriver) and point at it via the GECKODRIVER env var (as shown above). Same idea for chromedriver via the CHROMEDRIVER env var.

Demo pages

crates/arora-web/www/serve.sh
# open http://localhost:8080          – Engine call demo (ping, succeed, cos)
# open http://localhost:8080/demo.html – BehaviorTreeRunner tick demo

serve.sh runs wasm-pack build, stages guest modules under www/modules/<name>/, then starts python3 -m http.server.

demo.html shows a live behavior tree: each tick increments x by 0.1 via add(), then computes cos(x). Variables persist across ticks. The SVG tree panel highlights node status; the side panel displays variable values and a tick log.

Why a separate crate

arora is dual-target now (wasmtime-host and native-host features default-on for native; gated out on wasm32, where the browser executor takes over). arora-web exists purely to attach the wasm-bindgen surface and keep host builds free of wasm-bindgen deps.