arora-web 5.2.1

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:

```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>"],
  "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

```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.