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