Skip to main content

arora_web/
lib.rs

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