# 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:
```rust
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](src/lib.rs)):
```ts
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:
```json
{
"id": "<uuid>",
"function": "<fn-uuid>",
"children": ["<child-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
```bash
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)
```bash
# 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
```bash
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.