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