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