Skip to main content

arora_web/
lib.rs

1//! Run an Arora device in the browser.
2//!
3//! The centerpiece is [`BrowserRuntime`], the reusable primitive that wires a
4//! full [`arora::Arora`] over an injected HAL, bridge, and data store and
5//! exposes the JS-facing surface every browser device needs — a synchronous
6//! `step()` plus Value↔JSON accessors on the store. [`AroraRuntime`] is the
7//! bundled demo device built on it (in-process fakes + `SimpleDataStore`); each
8//! downstream device ships its own thin `#[wasm_bindgen]` wrapper the same way.
9//!
10//! It also carries a lower-level surface: [`Engine`] and [`BehaviorTreeRunner`]
11//! load guest modules (header JSON + executable bytes) and run behavior trees
12//! directly on the engine, hosting modules via the browser's native
13//! `WebAssembly` runtime — see `arora_engine::executor::browser`.
14//!
15//! This crate only carries non-trivial content when built for `wasm32-*`
16//! targets. On the host it is an empty shim (deps are gated to `wasm32`) so it
17//! can sit in the workspace and be verified by `cargo package` on publish
18//! without pulling wasm-only deps into a host link.
19
20#![cfg(target_arch = "wasm32")]
21
22use std::cell::RefCell;
23use std::collections::HashMap;
24use std::pin::Pin;
25use std::rc::Rc;
26
27use arora_engine::{
28    call::{CallBridge, Callable, CallableId},
29    engine::EngineBuilder,
30    executor::browser::{BrowserExecutor, SharedLoaderRc},
31    load::load_module_from_parts,
32};
33use arora_types::module::low::Header;
34use arora_types::{
35    call::Call,
36    value::{Enumeration, StructureField, StructureWithoutId, Value},
37};
38use uuid::Uuid;
39use wasm_bindgen::prelude::*;
40
41/// Route Rust panics to the browser console. Called from every entry point
42/// (`BrowserRuntime::start`, `Engine::new`, `BehaviorTreeRunner::new`) rather
43/// than from a `#[wasm_bindgen(start)]`: a reusable library must not claim the
44/// module `start`, or every downstream cdylib that also defines one collides on
45/// the `_start` symbol at link time. `set_once` makes repeat calls free.
46fn install_panic_hook() {
47    console_error_panic_hook::set_once();
48}
49
50// =============================================================================
51// Browser runtime primitive
52//
53// `BrowserRuntime` is the reusable core for running any Arora device in the
54// browser. It wires an `arora::Arora` over an injected HAL, bridge, and data
55// store (all trait objects, so the caller picks the backends) and exposes the
56// JS-facing surface every browser device needs: a synchronous `step()` plus
57// Value↔JSON accessors on the injected store. Nothing runs between steps — the
58// bridge and HAL own any async internally behind their stream/push seams, and
59// `step`'s sweep drains them, so the whole thing is a plain synchronous object
60// driven by `step()`.
61//
62// It is a plain Rust type, not a `#[wasm_bindgen]` export — the wasm-bindgen
63// boundary cannot carry `dyn Trait` objects. Each device ships a thin
64// `#[wasm_bindgen]` cdylib that constructs its concrete HAL/bridge/store and
65// behaviors, then forwards to a `BrowserRuntime` held inside. `AroraRuntime`
66// below is one such wrapper (the in-process fakes); Vizij ships another.
67// =============================================================================
68
69use arora::{Arora, AroraBuilder, BehaviorTreeInterpreter, ModuleFunction, StepOutcome};
70use arora_behavior::BehaviorInterpreter;
71use arora_bridge::{Bridge, FakeBridge};
72use arora_hal::{FakeHal, Hal};
73use arora_simple_data_store::SimpleDataStore;
74use arora_types::data::{DataStore, Key, StateChange, Subscription};
75use std::time::Duration;
76
77/// The reusable core of a browser-hosted Arora device.
78///
79/// Assemble it with [`BrowserRuntime::start`] over your chosen HAL, bridge,
80/// [`DataStore`], and behavior interpreter; then drive it a tick at a time with
81/// [`step`](Self::step) (e.g. from `requestAnimationFrame` or a Web Worker
82/// loop). Read and write the injected store across the JS boundary with the
83/// Value↔JSON accessors — values cross as JSON in the Arora [`Value`] vocabulary,
84/// e.g. `{"f32": 0.75}`.
85pub struct BrowserRuntime {
86    arora: Arora,
87    changes: Subscription,
88}
89
90impl BrowserRuntime {
91    /// Assemble an [`Arora`] (engine + native behavior-tree nodes) over the given
92    /// `hal`, `bridge`, `store`, and behavior interpreter with the
93    /// [builder](Arora::builder), each owned by the device. There is nothing to
94    /// spawn — the bridge and HAL own any async internally, behind their
95    /// stream/push seams.
96    ///
97    /// The interpreter is an executor injected once here, not swapped afterwards:
98    /// the caller constructs it (an empty [`BehaviorTreeInterpreter`], or its own)
99    /// and loads a behavior into it as a separate step before handing it in. Then
100    /// drive with [`step`](Self::step).
101    ///
102    /// `async` only so the JS surface can `await` construction uniformly; the
103    /// build itself is synchronous.
104    pub async fn start(
105        hal: Box<dyn Hal>,
106        bridge: Box<dyn Bridge>,
107        store: Box<dyn DataStore>,
108        behavior_interpreter: Box<dyn BehaviorInterpreter>,
109    ) -> Result<BrowserRuntime, JsValue> {
110        Self::builder()
111            .with_hal(hal)
112            .with_bridge(bridge)
113            .with_data_store(store)
114            .with_behavior_interpreter(behavior_interpreter)
115            .build()
116    }
117
118    /// Start assembling a [`BrowserRuntime`] seam by seam, mirroring
119    /// [`arora::Arora::builder`] — the same `with_*` setters, so a browser
120    /// device is configured exactly like a native one, plus the store-change
121    /// subscription wired at [`build`](BrowserRuntimeBuilder::build). Use this
122    /// over [`start`](Self::start) when the device needs modules, several
123    /// bridges, or a non-default store.
124    pub fn builder() -> BrowserRuntimeBuilder {
125        BrowserRuntimeBuilder::default()
126    }
127
128    /// The device's store, for direct access beyond the JSON accessors.
129    pub fn store(&self) -> &dyn DataStore {
130        self.arora.store()
131    }
132
133    /// Advance the device one tick. `dt` is the wall time elapsed since the
134    /// previous step. A web driver measures it from `requestAnimationFrame`
135    /// timestamps (milliseconds) and converts to a [`Duration`] at that boundary
136    /// — see [`AroraRuntime::step`]. The device publishes it (and the accumulated
137    /// time) under the golden keys before ticking. Returns `true` while live,
138    /// `false` once the device has been unregistered (stop stepping then).
139    pub fn step(&mut self, dt: Duration) -> Result<bool, JsValue> {
140        match self.arora.step(dt) {
141            Ok(StepOutcome::Live) => Ok(true),
142            Ok(StepOutcome::Unregistered) => Ok(false),
143            Err(e) => Err(JsValue::from_str(&format!("step failed: {e}"))),
144        }
145    }
146
147    /// Write one key into the store. `value_json` is an Arora [`Value`] as JSON,
148    /// e.g. `{"f32": 0.75}`.
149    pub fn set_value(&self, path: &str, value_json: &str) -> Result<(), JsValue> {
150        let value: Value = serde_json::from_str(value_json)
151            .map_err(|e| JsValue::from_str(&format!("invalid value json for {path}: {e}")))?;
152        self.store()
153            .write(StateChange::set(path, value))
154            .map_err(|e| JsValue::from_str(&format!("write {path} failed: {e}")))
155    }
156
157    /// Write several keys at once, as one store change. `values_json` is a JSON
158    /// object mapping each key path to an Arora [`Value`], e.g.
159    /// `{"sensor/x": {"f32": 0.75}}`.
160    pub fn write_values(&self, values_json: &str) -> Result<(), JsValue> {
161        let map: serde_json::Map<String, serde_json::Value> = serde_json::from_str(values_json)
162            .map_err(|e| JsValue::from_str(&format!("invalid values json: {e}")))?;
163        let mut change = StateChange::new();
164        for (path, raw) in map {
165            let value: Value = serde_json::from_value(raw)
166                .map_err(|e| JsValue::from_str(&format!("invalid value for {path}: {e}")))?;
167            change.set.insert(Key::new(path), Some(value));
168        }
169        self.store()
170            .write(change)
171            .map_err(|e| JsValue::from_str(&format!("write failed: {e}")))
172    }
173
174    /// Read keys from the store. `paths` is a JS `string[]`; the result is a JS
175    /// object mapping each path to its Arora [`Value`] (or `null` if absent).
176    pub fn read_values(&self, paths: JsValue) -> Result<JsValue, JsValue> {
177        let paths: Vec<String> = serde_wasm_bindgen::from_value(paths)
178            .map_err(|e| JsValue::from_str(&format!("paths must be a string[]: {e}")))?;
179        let keys: Vec<Key> = paths.iter().map(Key::new).collect();
180        let values = self.store().read(&keys);
181        let mut out = serde_json::Map::with_capacity(paths.len());
182        for (path, value) in paths.into_iter().zip(values) {
183            out.insert(path, value_to_json(value)?);
184        }
185        to_js_object(out)
186    }
187
188    /// A snapshot of every key currently in the store, as a JS object mapping
189    /// path → Arora [`Value`].
190    pub fn snapshot(&self) -> Result<JsValue, JsValue> {
191        let state = self.store().snapshot();
192        let mut out = serde_json::Map::with_capacity(state.storage.len());
193        for (key, value) in state.storage {
194            out.insert(key.path, value_to_json(value)?);
195        }
196        to_js_object(out)
197    }
198
199    /// Drain the keys that changed in the store since the last call, as a JS
200    /// object mapping path → new Arora [`Value`] (or `null` for a cleared key).
201    /// Poll-based counterpart to a push subscription: [`DataStore::subscribe`]
202    /// delivers over a std channel JavaScript cannot await, so changes accumulate
203    /// and are handed over on demand — call it right after [`step`](Self::step).
204    pub fn drain_changes(&self) -> Result<JsValue, JsValue> {
205        let mut out = serde_json::Map::new();
206        while let Some(change) = self.changes.try_recv() {
207            for (key, value) in change.set {
208                out.insert(key.path, value_to_json(value)?);
209            }
210            for key in change.unset {
211                out.insert(key.path, serde_json::Value::Null);
212            }
213        }
214        to_js_object(out)
215    }
216}
217
218/// Assembles a [`BrowserRuntime`], mirroring [`arora::AroraBuilder`] so a browser
219/// device is configured exactly like a native one: a HAL, zero or more bridges, a
220/// data store, the behavior interpreter, and the modules whose functions behaviors
221/// may call — each defaulted. Every setter forwards to the underlying
222/// [`AroraBuilder`]; [`build`](Self::build) assembles the [`Arora`] and subscribes
223/// to its store's change feed.
224#[derive(Default)]
225pub struct BrowserRuntimeBuilder {
226    inner: AroraBuilder,
227}
228
229impl BrowserRuntimeBuilder {
230    /// See [`AroraBuilder::with_hal`].
231    pub fn with_hal(mut self, hal: Box<dyn Hal>) -> Self {
232        self.inner = self.inner.with_hal(hal);
233        self
234    }
235
236    /// See [`AroraBuilder::with_bridge`]. Repeatable — reads fan in and writes
237    /// fan out across every bridge.
238    pub fn with_bridge(mut self, bridge: Box<dyn Bridge>) -> Self {
239        self.inner = self.inner.with_bridge(bridge);
240        self
241    }
242
243    /// See [`AroraBuilder::with_data_store`].
244    pub fn with_data_store(mut self, store: Box<dyn DataStore>) -> Self {
245        self.inner = self.inner.with_data_store(store);
246        self
247    }
248
249    /// See [`AroraBuilder::with_behavior_interpreter`].
250    pub fn with_behavior_interpreter(mut self, interpreter: Box<dyn BehaviorInterpreter>) -> Self {
251        self.inner = self.inner.with_behavior_interpreter(interpreter);
252        self
253    }
254
255    /// See [`AroraBuilder::with_module`]. Loads a guest module into the device's
256    /// engine — in the browser, a `.wasm` run by the native `WebAssembly` host.
257    pub fn with_module(mut self, header: Header, executable: impl Into<Box<[u8]>>) -> Self {
258        self.inner = self.inner.with_module(header, executable);
259        self
260    }
261
262    /// See [`AroraBuilder::with_host_module`].
263    pub fn with_host_module(mut self, functions: impl IntoIterator<Item = ModuleFunction>) -> Self {
264        self.inner = self.inner.with_host_module(functions);
265        self
266    }
267
268    /// Assemble the [`Arora`] and subscribe to its store's change feed (for
269    /// [`drain_changes`](BrowserRuntime::drain_changes)). Fails only if the
270    /// engine host cannot be created or a module fails to load.
271    pub fn build(self) -> Result<BrowserRuntime, JsValue> {
272        install_panic_hook();
273        let arora = self
274            .inner
275            .build()
276            .map_err(|e| JsValue::from_str(&format!("arora build failed: {e:?}")))?;
277        let changes = arora.store().subscribe();
278        Ok(BrowserRuntime { arora, changes })
279    }
280}
281
282/// Serialize a path-keyed JSON map to a **plain JS object**. serde-wasm-bindgen's
283/// default representation for maps is a JS `Map`, but these accessors promise
284/// plain objects (`{"sensor/x": {"f32": 0.5}}`), so serialize JSON-compatibly.
285fn to_js_object(out: serde_json::Map<String, serde_json::Value>) -> Result<JsValue, JsValue> {
286    use serde::Serialize;
287    serde_json::Value::Object(out)
288        .serialize(&serde_wasm_bindgen::Serializer::json_compatible())
289        .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
290}
291
292/// Serialize an optional Arora value to JSON (`null` when absent).
293fn value_to_json(value: Option<Value>) -> Result<serde_json::Value, JsValue> {
294    match value {
295        Some(v) => serde_json::to_value(v)
296            .map_err(|e| JsValue::from_str(&format!("serialize value failed: {e}"))),
297        None => Ok(serde_json::Value::Null),
298    }
299}
300
301// =============================================================================
302// Opinionated Arora runtime (demo)
303//
304// A `#[wasm_bindgen]` device built on `BrowserRuntime` with the in-process fake
305// HAL and bridge over a plain `SimpleDataStore`. The basic behavior-tree control
306// nodes are wired natively into the engine, so no node module needs to be
307// fetched or loaded. Drive it by calling `step()`.
308// =============================================================================
309
310/// JS-callable handle to a running opinionated Arora runtime.
311#[wasm_bindgen]
312pub struct AroraRuntime {
313    inner: BrowserRuntime,
314}
315
316#[wasm_bindgen]
317impl AroraRuntime {
318    /// Start the runtime with an in-process fake HAL and bridge over a plain
319    /// `SimpleDataStore`, with an empty (idle) behavior-tree interpreter. Purely
320    /// synchronous — nothing runs between steps; drive the device by calling `step()`.
321    pub async fn start() -> Result<AroraRuntime, JsValue> {
322        // Construct the executor empty and ready — it registers no modules, so its
323        // function index is empty — and inject it at build. It idles (tick is a
324        // no-op) until a behavior is loaded into it.
325        let interpreter = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
326        let inner = BrowserRuntime::start(
327            Box::new(FakeHal::new()),
328            Box::new(FakeBridge::new()),
329            Box::new(SimpleDataStore::new()),
330            Box::new(interpreter),
331        )
332        .await?;
333        Ok(AroraRuntime { inner })
334    }
335
336    /// Advance the device one step. `dt_ms` is the milliseconds elapsed since the
337    /// previous step — a plain JS number, exactly what a `requestAnimationFrame`
338    /// timestamp delta gives. This is the only place the rAF millisecond unit is
339    /// converted to the core [`Duration`] `dt`. Returns `true` while live,
340    /// `false` once the device has been unregistered (stop calling then).
341    pub fn step(&mut self, dt_ms: f64) -> Result<bool, JsValue> {
342        self.inner.step(Duration::from_secs_f64(dt_ms / 1_000.0))
343    }
344
345    /// Write one key into the store (Arora [`Value`] as JSON, e.g. `{"f32": 1}`).
346    #[wasm_bindgen(js_name = setValue)]
347    pub fn set_value(&self, path: &str, value_json: &str) -> Result<(), JsValue> {
348        self.inner.set_value(path, value_json)
349    }
350
351    /// Read keys from the store; `paths` is a `string[]`, result maps path→Value.
352    #[wasm_bindgen(js_name = readValues)]
353    pub fn read_values(&self, paths: JsValue) -> Result<JsValue, JsValue> {
354        self.inner.read_values(paths)
355    }
356
357    /// A snapshot of every key in the store as a path→Value object.
358    pub fn snapshot(&self) -> Result<JsValue, JsValue> {
359        self.inner.snapshot()
360    }
361}
362
363/// JS-callable handle to a configured Arora engine.
364#[wasm_bindgen]
365pub struct Engine {
366    inner: std::pin::Pin<Box<arora_engine::engine::Engine>>,
367    loader: SharedLoaderRc,
368    function_module: HashMap<Uuid, Uuid>,
369    module_headers: HashMap<Uuid, String>,
370}
371
372#[wasm_bindgen]
373impl Engine {
374    #[wasm_bindgen(constructor)]
375    pub fn new() -> Engine {
376        install_panic_hook();
377        let executor = BrowserExecutor::new();
378        let loader = executor.shared();
379        let inner = EngineBuilder::new().add_executor(executor).build();
380        Engine {
381            inner,
382            loader,
383            function_module: HashMap::new(),
384            module_headers: HashMap::new(),
385        }
386    }
387
388    /// Load a module given its header (as JSON) and executable bytes.
389    /// Returns the module's UUID as a string.
390    ///
391    /// Compiles and instantiates synchronously: Chrome rejects both above
392    /// 8 MB on the main thread — use `prepareModule` + `loadPreparedModule`
393    /// for large executables.
394    #[wasm_bindgen(js_name = loadModule)]
395    pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
396        self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
397    }
398
399    /// Asynchronously compile and instantiate a module's executable
400    /// (via `WebAssembly.instantiate`, no main-thread size limit).
401    /// Follow up with `loadPreparedModule` to complete the load.
402    #[wasm_bindgen(js_name = prepareModule)]
403    pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
404        prepare_module_impl(self.loader.clone(), header_json, executable)
405    }
406
407    /// Load a module whose executable was staged by `prepareModule`.
408    /// Returns the module's UUID as a string.
409    #[wasm_bindgen(js_name = loadPreparedModule)]
410    pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
411        self.load_module_inner(header_json, Box::new([]))
412    }
413
414    fn load_module_inner(
415        &mut self,
416        header_json: &str,
417        executable: Box<[u8]>,
418    ) -> Result<String, JsValue> {
419        let header: Header = serde_json::from_str(header_json)
420            .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
421        let header_json_str = header_json.to_string();
422        let loaded = load_module_from_parts(&mut *self.inner, header, executable)
423            .map_err(|e| JsValue::from_str(&format!("load_module failed: {e}")))?;
424        for fn_id in &loaded.function_ids {
425            self.function_module.insert(*fn_id, loaded.id);
426        }
427        self.module_headers.insert(loaded.id, header_json_str);
428        Ok(loaded.id.to_string())
429    }
430
431    /// Returns a JSON array of all loaded module headers.
432    #[wasm_bindgen(js_name = listModules)]
433    pub fn list_modules(&self) -> String {
434        let headers: Vec<serde_json::Value> = self
435            .module_headers
436            .values()
437            .filter_map(|s| serde_json::from_str(s).ok())
438            .collect();
439        serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
440    }
441
442    /// Call a function. `call_json` is a JSON document matching
443    /// `arora_engine::call::Call`. Returns the result as a JSON string.
444    #[wasm_bindgen]
445    pub fn call(&mut self, call_json: &str) -> Result<String, JsValue> {
446        let call: Call = serde_json::from_str(call_json)
447            .map_err(|e| JsValue::from_str(&format!("invalid call json: {e}")))?;
448        let module_id = if let Some(m) = call.module_id {
449            m
450        } else {
451            *self
452                .function_module
453                .get(&call.id)
454                .ok_or_else(|| JsValue::from_str("no module known for function"))?
455        };
456        let result = self
457            .inner
458            .arora_call(&module_id, call)
459            .map_err(|e| JsValue::from_str(&format!("call failed: {e}")))?;
460        serde_json::to_string(&result)
461            .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
462    }
463}
464
465impl Default for Engine {
466    fn default() -> Self {
467        Self::new()
468    }
469}
470
471/// Shared implementation of `prepareModule`: parses the header for the
472/// module id, then stages an asynchronously-instantiated module in the
473/// loader. Returns a `Promise<void>`.
474fn prepare_module_impl(
475    loader: SharedLoaderRc,
476    header_json: &str,
477    executable: Vec<u8>,
478) -> js_sys::Promise {
479    let header: Result<Header, _> = serde_json::from_str(header_json);
480    wasm_bindgen_futures::future_to_promise(async move {
481        let header = header.map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
482        loader.prepare(header.id, executable).await?;
483        Ok(JsValue::UNDEFINED)
484    })
485}
486
487// =============================================================================
488// Behavior-tree runner
489//
490// A self-contained BT runtime built directly on arora's engine primitives —
491// no dependency on the arora-behavior-tree crate.
492// =============================================================================
493
494// UUID constants from arora-behavior-tree-types generated code.
495// TickId struct id: 6f49e650-84ca-4899-a9bd-1f3bf17fab51
496const TICK_ID_STRUCT_BYTES: [u8; 16] = [
497    0x6f, 0x49, 0xe6, 0x50, 0x84, 0xca, 0x48, 0x99, 0xa9, 0xbd, 0x1f, 0x3b, 0xf1, 0x7f, 0xab, 0x51,
498];
499// TickId::callable_id field: 237992d2-17d1-459f-bca1-7185fa6a69d7
500const TICK_ID_CALLABLE_FIELD_BYTES: [u8; 16] = [
501    0x23, 0x79, 0x92, 0xd2, 0x17, 0xd1, 0x45, 0x9f, 0xbc, 0xa1, 0x71, 0x85, 0xfa, 0x6a, 0x69, 0xd7,
502];
503// Status::Success variant: 766e9e9a-446d-4e46-83e6-14b7ca101169
504const STATUS_SUCCESS_BYTES: [u8; 16] = [
505    0x76, 0x6e, 0x9e, 0x9a, 0x44, 0x6d, 0x4e, 0x46, 0x83, 0xe6, 0x14, 0xb7, 0xca, 0x10, 0x11, 0x69,
506];
507// Status::Failure variant: 2468f46c-bb60-425c-9a4d-9ad326ccc7e2
508const STATUS_FAILURE_BYTES: [u8; 16] = [
509    0x24, 0x68, 0xf4, 0x6c, 0xbb, 0x60, 0x42, 0x5c, 0x9a, 0x4d, 0x9a, 0xd3, 0x26, 0xcc, 0xc7, 0xe2,
510];
511// Status enum type: 325a5767-e344-4532-860e-0749bcf2e428
512const STATUS_ENUM_BYTES: [u8; 16] = [
513    0x32, 0x5a, 0x57, 0x67, 0xe3, 0x44, 0x45, 0x32, 0x86, 0x0e, 0x07, 0x49, 0xbc, 0xf2, 0xe4, 0x28,
514];
515// _ret special out-parameter: 5f726574-0000-4000-8000-000000000000 ("_ret" in ASCII)
516const RET_PARAM_BYTES: [u8; 16] = [
517    0x5f, 0x72, 0x65, 0x74, 0x00, 0x00, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
518];
519
520fn value_to_status(v: &Value) -> &'static str {
521    if let Value::Enumeration(e) = v {
522        if *e.variant_id.as_bytes() == STATUS_SUCCESS_BYTES {
523            return "success";
524        }
525        if *e.variant_id.as_bytes() == STATUS_FAILURE_BYTES {
526            return "failure";
527        }
528    }
529    "running"
530}
531
532/// A node argument expression: either a literal value or a reference to a
533/// named variable (identified by UUID).
534#[derive(serde::Deserialize, Clone, Debug)]
535#[serde(rename_all = "snake_case")]
536enum BtExpression {
537    VariableId(Uuid),
538    Value(Value),
539}
540
541/// A single BT node – mirrors the arora-behavior-tree YAML schema.
542#[derive(serde::Deserialize, Debug)]
543struct BtNode {
544    id: Uuid,
545    function: Uuid,
546    #[serde(default)]
547    children: Option<Vec<Uuid>>,
548    /// Parameter arguments: maps parameter UUID to a literal value or variable.
549    /// Use the special key `5f726574-0000-4000-8000-000000000000` (_ret) to
550    /// capture the return value into a variable; the node then always succeeds.
551    #[serde(default)]
552    arguments: HashMap<Uuid, BtExpression>,
553}
554
555/// Metadata extracted from a module header for one exported function.
556struct FnMeta {
557    module_id: Uuid,
558    /// Parameter ID of the `children: TickId[]` argument, present only for
559    /// control nodes (seq, fallback, parallel …).
560    children_param_id: Option<Uuid>,
561}
562
563/// An arora Callable that wraps one BT node.
564struct NodeCallable {
565    node_id: Uuid,
566    fn_id: Uuid,
567    module_id: Uuid,
568    children_param_id: Option<Uuid>,
569    children_callable_ids: Vec<u64>,
570    arguments: HashMap<Uuid, BtExpression>,
571    variables: Rc<RefCell<HashMap<Uuid, Value>>>,
572    trace: Rc<RefCell<Vec<(Uuid, &'static str)>>>,
573}
574
575impl Callable for NodeCallable {
576    fn call(&self, caller: &mut dyn CallBridge) -> Result<Value, arora_engine::call::CallError> {
577        let tick_id_type = Uuid::from_bytes(TICK_ID_STRUCT_BYTES);
578        let callable_field = Uuid::from_bytes(TICK_ID_CALLABLE_FIELD_BYTES);
579        let ret_param_id = Uuid::from_bytes(RET_PARAM_BYTES);
580
581        let mut args = Vec::new();
582
583        if let Some(children_param_id) = self.children_param_id {
584            let elements: Vec<StructureWithoutId> = self
585                .children_callable_ids
586                .iter()
587                .map(|&id| StructureWithoutId {
588                    fields: vec![StructureField {
589                        id: callable_field,
590                        value: Box::new(Value::U64(id)),
591                    }],
592                })
593                .collect();
594            args.push(StructureField {
595                id: children_param_id,
596                value: Box::new(Value::ArrayStructure {
597                    id: tick_id_type,
598                    elements,
599                }),
600            });
601        }
602
603        for (&param_id, expr) in &self.arguments {
604            if param_id == ret_param_id {
605                continue;
606            }
607            let value = match expr {
608                BtExpression::Value(v) => v.clone(),
609                BtExpression::VariableId(var_id) => self
610                    .variables
611                    .borrow()
612                    .get(var_id)
613                    .cloned()
614                    .unwrap_or(Value::Unit),
615            };
616            args.push(StructureField {
617                id: param_id,
618                value: Box::new(value),
619            });
620        }
621
622        // Build a map of param_id -> variable_id for mutable arguments so we can
623        // write mutated values back to the variable store after the call.
624        let mutable_param_vars: HashMap<Uuid, Uuid> = self
625            .arguments
626            .iter()
627            .filter_map(|(&param_id, expr)| {
628                if param_id == ret_param_id {
629                    return None;
630                }
631                if let BtExpression::VariableId(var_id) = expr {
632                    Some((param_id, *var_id))
633                } else {
634                    None
635                }
636            })
637            .collect();
638
639        let result = caller.arora_call(
640            &self.module_id,
641            Call {
642                module_id: None,
643                id: self.fn_id,
644                args,
645            },
646        )?;
647
648        // Write back mutated parameter values to bound variables.
649        for mutated in &result.mutated {
650            if let Some(&var_id) = mutable_param_vars.get(&mutated.id) {
651                self.variables
652                    .borrow_mut()
653                    .insert(var_id, *mutated.value.clone());
654            }
655        }
656
657        let has_ret = self.arguments.contains_key(&ret_param_id);
658        if has_ret {
659            if let Some(BtExpression::VariableId(var_id)) = self.arguments.get(&ret_param_id) {
660                self.variables
661                    .borrow_mut()
662                    .insert(*var_id, result.ret.clone());
663            }
664        }
665
666        let s = if has_ret {
667            "success"
668        } else {
669            value_to_status(&result.ret)
670        };
671        self.trace.borrow_mut().push((self.node_id, s));
672
673        if has_ret {
674            Ok(Value::Enumeration(Enumeration {
675                id: Uuid::from_bytes(STATUS_ENUM_BYTES),
676                variant_id: Uuid::from_bytes(STATUS_SUCCESS_BYTES),
677                value: Box::new(Value::Unit),
678            }))
679        } else {
680            Ok(result.ret)
681        }
682    }
683}
684
685/// Recursively registers callables for `node_id` and all descendants.
686/// Returns the callable id of the registered root callable.
687fn register_node(
688    engine: &mut dyn CallBridge,
689    node_id: Uuid,
690    node_index: &HashMap<Uuid, BtNode>,
691    fn_meta: &HashMap<Uuid, FnMeta>,
692    trace: &Rc<RefCell<Vec<(Uuid, &'static str)>>>,
693    variables: &Rc<RefCell<HashMap<Uuid, Value>>>,
694) -> Result<u64, String> {
695    let node = node_index
696        .get(&node_id)
697        .ok_or_else(|| format!("node {node_id} not found in tree"))?;
698    let meta = fn_meta
699        .get(&node.function)
700        .ok_or_else(|| format!("function {} not registered in fn_meta", node.function))?;
701
702    let children_callable_ids = match &node.children {
703        None => vec![],
704        Some(ids) => ids
705            .iter()
706            .map(|&child_id| register_node(engine, child_id, node_index, fn_meta, trace, variables))
707            .collect::<Result<Vec<_>, _>>()?,
708    };
709
710    let callable: Rc<dyn Callable> = Rc::new(NodeCallable {
711        node_id,
712        fn_id: node.function,
713        module_id: meta.module_id,
714        children_param_id: meta.children_param_id,
715        children_callable_ids,
716        arguments: node.arguments.clone(),
717        variables: variables.clone(),
718        trace: trace.clone(),
719    });
720    let id = engine.arora_register_callable(callable);
721    Ok(id.id)
722}
723
724/// JS-callable handle for loading modules and executing behavior trees.
725///
726/// Usage:
727/// 1. `new BehaviorTreeRunner()`
728/// 2. `runner.loadModule(headerJson, wasmBytes)` – can be called for multiple modules
729/// 3. `runner.run(nodesJson)` – returns `{status, trace}`
730/// 4. Or `runner.setVariable(varId, valueJson)` + `runner.tick(nodesJson)` for
731///    stateful tick-by-tick execution with variable bindings.
732#[wasm_bindgen]
733pub struct BehaviorTreeRunner {
734    inner: Pin<Box<arora_engine::engine::Engine>>,
735    loader: SharedLoaderRc,
736    fn_meta: HashMap<Uuid, FnMeta>,
737    variables: Rc<RefCell<HashMap<Uuid, Value>>>,
738    module_headers: HashMap<Uuid, String>,
739}
740
741#[wasm_bindgen]
742impl BehaviorTreeRunner {
743    #[wasm_bindgen(constructor)]
744    pub fn new() -> BehaviorTreeRunner {
745        install_panic_hook();
746        let executor = BrowserExecutor::new();
747        let loader = executor.shared();
748        let inner = EngineBuilder::new().add_executor(executor).build();
749        BehaviorTreeRunner {
750            inner,
751            loader,
752            fn_meta: HashMap::new(),
753            variables: Rc::new(RefCell::new(HashMap::new())),
754            module_headers: HashMap::new(),
755        }
756    }
757
758    /// Load a WASM module. `header_json` must be the module's YAML header
759    /// converted to JSON (the JS side can use js-yaml for that).
760    /// Returns the module UUID string.
761    ///
762    /// Compiles and instantiates synchronously: Chrome rejects both above
763    /// 8 MB on the main thread — use `prepareModule` + `loadPreparedModule`
764    /// for large executables.
765    #[wasm_bindgen(js_name = loadModule)]
766    pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
767        self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
768    }
769
770    /// Asynchronously compile and instantiate a module's executable
771    /// (via `WebAssembly.instantiate`, no main-thread size limit).
772    /// Follow up with `loadPreparedModule` to complete the load.
773    #[wasm_bindgen(js_name = prepareModule)]
774    pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
775        prepare_module_impl(self.loader.clone(), header_json, executable)
776    }
777
778    /// Load a module whose executable was staged by `prepareModule`.
779    /// Returns the module UUID string.
780    #[wasm_bindgen(js_name = loadPreparedModule)]
781    pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
782        self.load_module_inner(header_json, Box::new([]))
783    }
784
785    fn load_module_inner(
786        &mut self,
787        header_json: &str,
788        executable: Box<[u8]>,
789    ) -> Result<String, JsValue> {
790        let header: Header = serde_json::from_str(header_json)
791            .map_err(|e| JsValue::from_str(&format!("invalid header: {e}")))?;
792        let module_id = header.id;
793        let header_json_str = header_json.to_string();
794
795        for export in &header.exports {
796            let arora_types::module::low::ExportSymbol::Function(f) = export;
797            let children_param_id = f.parameters.first().and_then(|p| {
798                if let arora_types::module::low::TypeRef::Array { id } = &p.ty {
799                    if id == &Uuid::from_bytes(TICK_ID_STRUCT_BYTES) {
800                        Some(p.id)
801                    } else {
802                        None
803                    }
804                } else {
805                    None
806                }
807            });
808
809            self.fn_meta.insert(
810                f.id,
811                FnMeta {
812                    module_id,
813                    children_param_id,
814                },
815            );
816        }
817
818        let result = load_module_from_parts(&mut *self.inner, header, executable)
819            .map_err(|e| JsValue::from_str(&format!("load failed: {e}")))?;
820        self.module_headers.insert(result.id, header_json_str);
821        Ok(result.id.to_string())
822    }
823
824    /// Returns a JSON array of all loaded module headers.
825    #[wasm_bindgen(js_name = listModules)]
826    pub fn list_modules(&self) -> String {
827        let headers: Vec<serde_json::Value> = self
828            .module_headers
829            .values()
830            .filter_map(|s| serde_json::from_str(s).ok())
831            .collect();
832        serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
833    }
834
835    /// Initialize or update a variable. `var_id` is a UUID string; `value_json`
836    /// is the serialized `Value` (e.g. `{"f32": 0.0}`).
837    #[wasm_bindgen(js_name = setVariable)]
838    pub fn set_variable(&mut self, var_id: &str, value_json: &str) -> Result<(), JsValue> {
839        let var_id: Uuid = var_id
840            .parse()
841            .map_err(|_| JsValue::from_str("bad var_id: not a valid UUID"))?;
842        let value: Value = serde_json::from_str(value_json)
843            .map_err(|e| JsValue::from_str(&format!("bad value JSON: {e}")))?;
844        self.variables.borrow_mut().insert(var_id, value);
845        Ok(())
846    }
847
848    /// Run one tick of the behavior tree. Variables persist across calls.
849    ///
850    /// `nodes_json` is a JSON array where each element is:
851    ///   `{ id, function, children?, arguments?, return_binding? }`
852    ///
853    /// Returns: `{ "status": "...", "trace": [...], "variables": { varId: value } }`
854    pub fn tick(&mut self, nodes_json: &str) -> Result<String, JsValue> {
855        let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
856            .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
857        if nodes.is_empty() {
858            return Err(JsValue::from_str("tree has no nodes"));
859        }
860
861        let root_id = nodes[0].id;
862        let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
863        let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
864
865        let fn_meta = &self.fn_meta;
866        let root_callable_id = register_node(
867            &mut *self.inner,
868            root_id,
869            &node_index,
870            fn_meta,
871            &trace,
872            &self.variables,
873        )
874        .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
875
876        let callable_id = CallableId {
877            id: root_callable_id,
878        };
879        let result = Callable::call(&callable_id, &mut *self.inner)
880            .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
881        let status = value_to_status(&result);
882
883        let trace_json: Vec<serde_json::Value> = trace
884            .borrow()
885            .iter()
886            .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
887            .collect();
888
889        let vars_json: serde_json::Map<String, serde_json::Value> = self
890            .variables
891            .borrow()
892            .iter()
893            .filter_map(|(id, v)| serde_json::to_value(v).ok().map(|jv| (id.to_string(), jv)))
894            .collect();
895
896        Ok(serde_json::json!({
897          "status": status,
898          "trace": trace_json,
899          "variables": vars_json,
900        })
901        .to_string())
902    }
903
904    /// Run a behavior tree to completion (ticks until not Running).
905    ///
906    /// `nodes_json` is a JSON array where each element is:
907    ///   `{ id: "<uuid>", function: "<uuid>", children?: ["<uuid>", ...] }`
908    /// The first element is the root node.
909    ///
910    /// Returns a JSON string:
911    ///   `{ "status": "success"|"failure"|"running",
912    ///      "trace": [{"nodeId": "<uuid>", "status": "..."}] }`
913    pub fn run(&mut self, nodes_json: &str) -> Result<String, JsValue> {
914        let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
915            .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
916        if nodes.is_empty() {
917            return Err(JsValue::from_str("tree has no nodes"));
918        }
919
920        let root_id = nodes[0].id;
921        let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
922        let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
923
924        let fn_meta = &self.fn_meta;
925        let root_callable_id = register_node(
926            &mut *self.inner,
927            root_id,
928            &node_index,
929            fn_meta,
930            &trace,
931            &self.variables,
932        )
933        .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
934
935        let callable_id = CallableId {
936            id: root_callable_id,
937        };
938
939        let mut last_status = "running";
940        for _ in 0..10_000 {
941            let result = Callable::call(&callable_id, &mut *self.inner)
942                .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
943            last_status = value_to_status(&result);
944            if last_status != "running" {
945                break;
946            }
947        }
948
949        let trace_json: Vec<serde_json::Value> = trace
950            .borrow()
951            .iter()
952            .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
953            .collect();
954
955        let out = serde_json::json!({
956          "status": last_status,
957          "trace": trace_json,
958        });
959        Ok(out.to_string())
960    }
961}
962
963impl Default for BehaviorTreeRunner {
964    fn default() -> Self {
965        Self::new()
966    }
967}
968
969// =============================================================================
970// Type registry
971//
972// A minimal wasm-compatible registry for resolving type UUIDs to names.
973// Loads from the records.json files emitted by arora-module-rust::generate_records.
974// =============================================================================
975
976/// JS-callable type registry that resolves type UUIDs to human-readable names.
977#[wasm_bindgen]
978pub struct Registry {
979    entries: HashMap<Uuid, (String, Option<Uuid>)>,
980}
981
982#[derive(serde::Deserialize)]
983struct RecordEntry {
984    id: Uuid,
985    name: String,
986    parent: Option<Uuid>,
987}
988
989#[wasm_bindgen]
990impl Registry {
991    #[wasm_bindgen(constructor)]
992    pub fn new() -> Registry {
993        Registry {
994            entries: HashMap::new(),
995        }
996    }
997
998    /// Load type records from a JSON array: `[{id, name, parent?}, ...]`.
999    #[wasm_bindgen(js_name = loadRecordsJson)]
1000    pub fn load_records_json(&mut self, json: &str) -> Result<(), JsValue> {
1001        let records: Vec<RecordEntry> = serde_json::from_str(json)
1002            .map_err(|e| JsValue::from_str(&format!("invalid records JSON: {e}")))?;
1003        for r in records {
1004            self.entries.insert(r.id, (r.name, r.parent));
1005        }
1006        Ok(())
1007    }
1008
1009    /// Resolve a UUID string to a dot-separated name path (e.g. `"behavior_tree.Status"`).
1010    /// Returns `None` if the UUID is not known.
1011    #[wasm_bindgen(js_name = resolveId)]
1012    pub fn resolve_id(&self, id: &str) -> Option<String> {
1013        let uuid: Uuid = id.parse().ok()?;
1014        self.compute_path(&uuid)
1015    }
1016
1017    fn compute_path(&self, id: &Uuid) -> Option<String> {
1018        let (name, parent) = self.entries.get(id)?;
1019        match parent {
1020            None => Some(name.clone()),
1021            Some(parent_id) if !self.entries.contains_key(parent_id) => Some(name.clone()),
1022            Some(parent_id) => Some(format!("{}.{}", self.compute_path(parent_id)?, name)),
1023        }
1024    }
1025}
1026
1027impl Default for Registry {
1028    fn default() -> Self {
1029        Self::new()
1030    }
1031}