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, Invoked, LocalCaller, TaskId};
61use arora_bridge::client::{self, KeyInfo};
62use arora_types::data::{DataStore, Key, StateChange, Subscription};
63use std::future::Future;
64use std::time::Duration;
65
66/// Hand `future` to JavaScript as a Promise, having polled it once already.
67///
68/// Every device operation goes on the device's inbound queue at its future's
69/// first poll; a `future_to_promise` alone would first poll it a microtask
70/// later. Polling once here means the operation is queued before the method
71/// returns, so a page that calls and then `step()`s in the same turn has it
72/// applied by that step. A future that is already done settles the promise
73/// at once.
74fn promise(future: impl Future<Output = Result<JsValue, JsValue>> + 'static) -> js_sys::Promise {
75    let mut future = Box::pin(future);
76    let mut cx = std::task::Context::from_waker(futures::task::noop_waker_ref());
77    match future.as_mut().poll(&mut cx) {
78        std::task::Poll::Ready(Ok(value)) => js_sys::Promise::resolve(&value),
79        std::task::Poll::Ready(Err(error)) => js_sys::Promise::reject(&error),
80        std::task::Poll::Pending => wasm_bindgen_futures::future_to_promise(future),
81    }
82}
83
84/// Serialize to a plain JS value — objects as objects, not `Map`s — the form
85/// every structured answer of this surface takes.
86fn to_js(value: &impl serde::Serialize) -> Result<JsValue, JsValue> {
87    value
88        .serialize(&serde_wasm_bindgen::Serializer::json_compatible())
89        .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
90}
91
92/// Serialize an Arora [`Value`] to the plain JS form of its JSON, e.g.
93/// `{"f32": 0.75}`.
94fn value_to_js(value: &Value) -> Result<JsValue, JsValue> {
95    let json = serde_json::to_value(value)
96        .map_err(|e| JsValue::from_str(&format!("serialize value failed: {e}")))?;
97    to_js(&json)
98}
99
100/// A run handle as a client reads it — the plain object `invoke` of a
101/// task-shaped method and `spawn` answer with: `arora_bridge::client::Run`.
102fn run_to_js(handle: &arora::TaskHandle) -> Result<JsValue, JsValue> {
103    let spawned = arora_behavior::interpreter_module::encode_spawn_result(handle);
104    let run = client::run_of(&spawned).map_err(|e| JsValue::from_str(&e))?;
105    to_js(&run)
106}
107
108/// Value↔JSON accessors over a device's [`DataStore`] and change
109/// [`Subscription`] — the store surface every browser device shares. Values
110/// cross the JS boundary as JSON in the Arora [`Value`] vocabulary, e.g.
111/// `{"f32": 0.75}`. [`AroraWeb`] exposes them as methods; a downstream device
112/// wrapping its own store reaches them here directly.
113pub mod store_json {
114    use super::*;
115
116    /// Write one key into the store. `value_json` is an Arora [`Value`] as
117    /// JSON, e.g. `{"f32": 0.75}`.
118    pub fn set_value(store: &dyn DataStore, path: &str, value_json: &str) -> Result<(), JsValue> {
119        let value: Value = serde_json::from_str(value_json)
120            .map_err(|e| JsValue::from_str(&format!("invalid value json for {path}: {e}")))?;
121        store
122            .write(StateChange::set(path, value))
123            .map_err(|e| JsValue::from_str(&format!("write {path} failed: {e}")))
124    }
125
126    /// Write several keys at once, as one store change. `values_json` is a JSON
127    /// object mapping each key path to an Arora [`Value`], e.g.
128    /// `{"sensor/x": {"f32": 0.75}}`.
129    pub fn write_values(store: &dyn DataStore, values_json: &str) -> Result<(), JsValue> {
130        let map: serde_json::Map<String, serde_json::Value> = serde_json::from_str(values_json)
131            .map_err(|e| JsValue::from_str(&format!("invalid values json: {e}")))?;
132        let mut change = StateChange::new();
133        for (path, raw) in map {
134            let value: Value = serde_json::from_value(raw)
135                .map_err(|e| JsValue::from_str(&format!("invalid value for {path}: {e}")))?;
136            change.set.insert(Key::new(path), Some(value));
137        }
138        store
139            .write(change)
140            .map_err(|e| JsValue::from_str(&format!("write failed: {e}")))
141    }
142
143    /// Read keys from the store. `paths` is a JS `string[]`; the result is a JS
144    /// object mapping each path to its Arora [`Value`] (or `null` if absent).
145    pub fn read_values(store: &dyn DataStore, paths: JsValue) -> Result<JsValue, JsValue> {
146        let paths: Vec<String> = serde_wasm_bindgen::from_value(paths)
147            .map_err(|e| JsValue::from_str(&format!("paths must be a string[]: {e}")))?;
148        let keys: Vec<Key> = paths.iter().map(Key::new).collect();
149        let values = store.read(&keys);
150        let mut out = serde_json::Map::with_capacity(paths.len());
151        for (path, value) in paths.into_iter().zip(values) {
152            out.insert(path, value_to_json(value)?);
153        }
154        to_js_object(out)
155    }
156
157    /// A snapshot of every key currently in the store, as a JS object mapping
158    /// path → Arora [`Value`].
159    pub fn snapshot(store: &dyn DataStore) -> Result<JsValue, JsValue> {
160        let state = store.snapshot();
161        let mut out = serde_json::Map::with_capacity(state.storage.len());
162        for (key, value) in state.storage {
163            out.insert(key.path, value_to_json(value)?);
164        }
165        to_js_object(out)
166    }
167
168    /// Drain the changes the subscription holds, as a JS object mapping each
169    /// changed path to its latest Arora [`Value`] (or `null` for a cleared
170    /// key). Poll-based counterpart to a push subscription:
171    /// [`DataStore::subscribe`] delivers over a std channel JavaScript cannot
172    /// await, so the channel holds every change until it is drained here. A
173    /// subscription opens on the store's whole current state, so the first
174    /// drain returns the full picture.
175    ///
176    /// [`AroraWeb`] does not leave its subscription's channel to grow between
177    /// drains: it folds it into the latest value per key on every step.
178    pub fn drain_changes(changes: &Subscription) -> Result<JsValue, JsValue> {
179        let mut latest = HashMap::new();
180        while let Some(change) = changes.try_recv() {
181            fold_change(&mut latest, change);
182        }
183        latest_to_js(latest.drain())
184    }
185
186    /// Fold `change` into `latest`, the latest value per key: a key `change`
187    /// sets holds its new value, a key it unsets holds `None`.
188    pub(crate) fn fold_change(latest: &mut HashMap<Key, Option<Value>>, change: StateChange) {
189        latest.extend(change.set);
190        latest.extend(change.unset.into_iter().map(|key| (key, None)));
191    }
192
193    /// The latest value per key as a JS object mapping path → Arora [`Value`]
194    /// (`null` for a cleared key).
195    pub(crate) fn latest_to_js(
196        latest: impl Iterator<Item = (Key, Option<Value>)>,
197    ) -> Result<JsValue, JsValue> {
198        let mut out = serde_json::Map::new();
199        for (key, value) in latest {
200            out.insert(key.path, value_to_json(value)?);
201        }
202        to_js_object(out)
203    }
204
205    /// Serialize a path-keyed JSON map to a **plain JS object**.
206    /// serde-wasm-bindgen's default representation for maps is a JS `Map`, but
207    /// these accessors promise plain objects (`{"sensor/x": {"f32": 0.5}}`),
208    /// so serialize JSON-compatibly.
209    fn to_js_object(out: serde_json::Map<String, serde_json::Value>) -> Result<JsValue, JsValue> {
210        use serde::Serialize;
211        serde_json::Value::Object(out)
212            .serialize(&serde_wasm_bindgen::Serializer::json_compatible())
213            .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
214    }
215
216    /// Serialize an optional Arora value to JSON (`null` when absent).
217    fn value_to_json(value: Option<Value>) -> Result<serde_json::Value, JsValue> {
218        match value {
219            Some(v) => serde_json::to_value(v)
220                .map_err(|e| JsValue::from_str(&format!("serialize value failed: {e}"))),
221            None => Ok(serde_json::Value::Null),
222        }
223    }
224}
225
226/// The store changes a page has not drained yet, as the latest value per key.
227///
228/// The subscription's channel takes a copy of every change the store applies;
229/// a device changing a few hundred keys per step at 100 steps a second fills
230/// it with tens of thousands of entries a second, all held until a drain.
231/// Folded on every step and on every write the page makes, the channel holds
232/// no more than one step's changes, and `latest` one entry per key changed
233/// since the last drain: what a page has not drained grows with the keys that
234/// change, not with the steps it goes without draining. A drain hands over the
235/// net change since the previous one — a key set twice reads its second value,
236/// a key set then unset reads cleared — which is what a page shows.
237struct Undrained {
238    subscription: Subscription,
239    latest: HashMap<Key, Option<Value>>,
240}
241
242impl Undrained {
243    fn new(subscription: Subscription) -> Self {
244        Self {
245            subscription,
246            latest: HashMap::new(),
247        }
248    }
249
250    /// Move what the subscription's channel holds into `latest`.
251    fn fold(&mut self) {
252        while let Some(change) = self.subscription.try_recv() {
253            store_json::fold_change(&mut self.latest, change);
254        }
255    }
256
257    /// Everything not drained yet, as a path → Value JS object; `latest`
258    /// keeps its capacity for the changes that follow.
259    fn drain(&mut self) -> Result<JsValue, JsValue> {
260        self.fold();
261        store_json::latest_to_js(self.latest.drain())
262    }
263}
264
265/// The browser Arora device, exported to JavaScript as `AroraRuntime`.
266///
267/// The JS constructor builds the self-contained demo device — the runtime's
268/// in-process defaults: fake hardware, no bridge, a private store, an empty
269/// behavior — and [`AroraWebBuilder`] (JS `AroraRuntimeBuilder`) additionally
270/// loads guest modules. A downstream device composes its own [`Arora`] with
271/// [`arora::AroraBuilder`] and wraps it with `AroraWeb::from`, reusing this
272/// whole JS surface.
273///
274/// Drive it by calling [`step`](Self::step) from your own clock (e.g.
275/// `requestAnimationFrame`) or by awaiting [`run`](Self::run), which hands the
276/// device to [`Arora::run`] until [`stop`](AroraWeb::stop) reclaims it.
277/// Either way the rest of the surface stays live: `setValue`/`readValues`/
278/// `snapshot` work on a sibling handle of the store, `drainChanges` on its
279/// subscription — folded into the latest value per key on every step, so what
280/// a page has not drained is one entry per changed key however many steps it
281/// goes without draining — and the client operations — `call`, `invoke`,
282/// `spawn`, `halt`, `listKeys`, `describeMethods` — on the device's in-process
283/// [`LocalCaller`], the same operations a remote client has over a bridge.
284/// None of them touch the stepping device: each is queued before the method
285/// returns and applied by the next step, so the device must be stepping (a
286/// `run()` loop, or your own `step` calls) for its promise to settle.
287///
288/// Every method is an ordinary Rust method too, for a Rust crate that wraps
289/// this device in its own `wasm_bindgen` type; [`caller`](Self::caller) hands
290/// such a crate the typed operations.
291#[wasm_bindgen(js_name = AroraRuntime)]
292pub struct AroraWeb {
293    // The device parks here between manual steps; `run` takes it out and
294    // `stop` puts it back. Shared with the run future, which returns the
295    // device on a stop.
296    device: Rc<RefCell<Option<Arora>>>,
297    // Ends the current `run` loop at its next step boundary; `None` while no
298    // loop runs (the sender is consumed by `stop`).
299    stop: RefCell<Option<futures::channel::oneshot::Sender<()>>>,
300    caller: LocalCaller,
301    store: Box<dyn DataStore>,
302    // Shared with the run future, which folds the subscription after every
303    // step it takes.
304    changes: Rc<RefCell<Undrained>>,
305    // The device's standing-error watch, taken before `run` can own the
306    // device, so the reading stays available while it self-paces.
307    behavior_error: tokio::sync::watch::Receiver<Option<String>>,
308    // The awaitable end of the same watch: taken out for the duration of one
309    // `behaviorErrorChanged` await, so sequential awaits share one seen
310    // cursor and no change is missed between them.
311    behavior_error_changes: Rc<RefCell<Option<tokio::sync::watch::Receiver<Option<String>>>>>,
312}
313
314impl From<Arora> for AroraWeb {
315    /// Wrap a composed device: keep its in-process [`LocalCaller`], a sibling
316    /// handle of its store, and a subscription (opening on the whole current
317    /// state), then expose it all to JavaScript.
318    fn from(arora: Arora) -> Self {
319        install_panic_hook();
320        let caller = arora.caller();
321        let store = arora.store().clone_box();
322        let changes = Rc::new(RefCell::new(Undrained::new(store.subscribe())));
323        let behavior_error = arora.behavior_error();
324        let behavior_error_changes = Rc::new(RefCell::new(Some(arora.behavior_error())));
325        Self {
326            device: Rc::new(RefCell::new(Some(arora))),
327            stop: RefCell::new(None),
328            caller,
329            store,
330            changes,
331            behavior_error,
332            behavior_error_changes,
333        }
334    }
335}
336
337#[wasm_bindgen(js_class = "AroraRuntime")]
338impl AroraWeb {
339    /// Build the demo device on the runtime's in-process defaults: fake HAL,
340    /// no bridge, a private store, an empty (idle) behavior interpreter.
341    #[wasm_bindgen(constructor)]
342    pub fn new() -> Result<AroraWeb, JsValue> {
343        Ok(AroraWeb::from(Arora::builder().build().map_err(|e| {
344            JsValue::from_str(&format!("arora build failed: {e:?}"))
345        })?))
346    }
347
348    /// Advance the device one step. `dt_ms` is the milliseconds elapsed since
349    /// the previous step — a plain JS number, exactly what a
350    /// `requestAnimationFrame` timestamp delta gives. This is the only place
351    /// the rAF millisecond unit is converted to the core `Duration` dt.
352    /// Unavailable once [`run`](Self::run) has taken the device.
353    pub fn step(&self, dt_ms: f64) -> Result<(), JsValue> {
354        let mut slot = self
355            .device
356            .try_borrow_mut()
357            .map_err(|_| JsValue::from_str("the device is mid-step; call between steps"))?;
358        let device = slot
359            .as_mut()
360            .ok_or_else(|| JsValue::from_str("run() drives the device; step() is unavailable"))?;
361        let stepped = device.step(Duration::from_secs_f64(dt_ms / 1_000.0));
362        self.changes.borrow_mut().fold();
363        stepped.map_err(|e| JsValue::from_str(&format!("step failed: {e}")))
364    }
365
366    /// Hand the device to [`Arora::run`]: a self-paced loop at `period_ms`
367    /// (default: the runtime's ~100 Hz) that owns the device until stepping
368    /// fails — the promise rejects — or [`stop`](Self::stop) reclaims it — the
369    /// promise resolves and `step()` works again. While it runs the rest of
370    /// the surface keeps working: it never touches the stepping device.
371    ///
372    /// `run` awaits between steps, so it shares its thread; see
373    /// [`Arora::run`] for where a cadence survives in a browser (main thread
374    /// vs dedicated worker).
375    pub fn run(&self, period_ms: Option<f64>) -> js_sys::Promise {
376        let Some(mut device) = self.device.borrow_mut().take() else {
377            return js_sys::Promise::reject(&JsValue::from_str("the device is already running"));
378        };
379        let slot = self.device.clone();
380        let changes = self.changes.clone();
381        let (stop_tx, stop_rx) = futures::channel::oneshot::channel::<()>();
382        *self.stop.borrow_mut() = Some(stop_tx);
383        wasm_bindgen_futures::future_to_promise(async move {
384            let period = period_ms
385                .map(|ms| Duration::from_secs_f64(ms / 1_000.0))
386                .unwrap_or(Arora::DEFAULT_STEP_PERIOD);
387            let outcome = {
388                use futures::FutureExt;
389                let run = device.run(period);
390                futures::pin_mut!(run);
391                // Each step runs inside one poll of the loop: folding after
392                // every poll folds every step's changes.
393                let run = futures::future::poll_fn(|cx| {
394                    let polled = run.as_mut().poll(cx);
395                    changes.borrow_mut().fold();
396                    polled
397                })
398                .fuse();
399                let mut stop_rx = stop_rx.fuse();
400                futures::pin_mut!(run);
401                futures::select_biased! {
402                    result = run => Some(result),
403                    _ = stop_rx => None,
404                }
405            };
406            // Whatever ended the loop, the device parks back in its slot: a
407            // failed run leaves a device the embedder can still step, probe
408            // ([`Arora::behavior_error`]), or run again.
409            slot.borrow_mut().replace(device);
410            match outcome {
411                // `Arora::run` only returns by failing; surface that.
412                Some(result) => {
413                    result.map_err(|e| JsValue::from_str(&format!("run failed: {e}")))?;
414                    Ok(JsValue::UNDEFINED)
415                }
416                // Stopped: the loop ended between steps.
417                None => Ok(JsValue::UNDEFINED),
418            }
419        })
420    }
421
422    /// Reclaim the device from [`run`](Self::run): the loop ends at its next
423    /// step boundary, the run promise resolves, and `step()` (or another
424    /// `run()`) works again. A no-op while nothing runs.
425    pub fn stop(&self) {
426        if let Some(stop) = self.stop.borrow_mut().take() {
427            let _ = stop.send(());
428        }
429    }
430
431    /// Whether [`run`](Self::run) currently owns the device.
432    #[wasm_bindgen(getter)]
433    pub fn running(&self) -> bool {
434        self.device.borrow().is_none()
435    }
436
437    /// The behavior's standing error — the message of its latest failed
438    /// tick, `undefined` while the behavior is healthy or none is installed.
439    /// A failing tick does not stop the device; the reading stays available
440    /// while `run()` owns it.
441    #[wasm_bindgen(getter, js_name = behaviorError)]
442    pub fn behavior_error(&self) -> Option<String> {
443        self.behavior_error.borrow().clone()
444    }
445
446    /// Resolves on the next change of the behavior's standing error, with
447    /// the new reading: a message when a distinct failure appears,
448    /// `undefined` when a tick recovers. Sequential awaits share one cursor,
449    /// so no change is missed between them; one await may be pending at a
450    /// time. Rejects when the device is gone.
451    #[wasm_bindgen(js_name = behaviorErrorChanged)]
452    pub fn behavior_error_changed(&self) -> js_sys::Promise {
453        let Some(mut errors) = self.behavior_error_changes.borrow_mut().take() else {
454            return js_sys::Promise::reject(&JsValue::from_str(
455                "a behaviorErrorChanged await is already pending",
456            ));
457        };
458        let slot = self.behavior_error_changes.clone();
459        wasm_bindgen_futures::future_to_promise(async move {
460            let outcome = errors.changed().await;
461            let value = errors.borrow_and_update().clone();
462            slot.borrow_mut().replace(errors);
463            outcome.map_err(|_| JsValue::from_str("the device is gone"))?;
464            Ok(match value {
465                Some(message) => JsValue::from_str(&message),
466                None => JsValue::UNDEFINED,
467            })
468        })
469    }
470
471    /// Dispatch a [`Call`] into the device through its in-process caller. The
472    /// call is queued before this returns, and the promise resolves after the
473    /// step that applies it — the device must be stepping (a `run()` loop, or
474    /// your own `step` calls) for it to land. `call_json` is an
475    /// `arora_types::call::Call` as JSON; the result is the `CallResult` as a
476    /// JSON string.
477    pub fn call(&self, call_json: &str) -> js_sys::Promise {
478        let parsed: Result<Call, _> = serde_json::from_str(call_json)
479            .map_err(|e| JsValue::from_str(&format!("invalid call json: {e}")));
480        let caller = self.caller.clone();
481        promise(async move {
482            let result = caller
483                .call(parsed?)
484                .await
485                .map_err(|e| JsValue::from_str(&format!("call failed: {e}")))?;
486            let json = serde_json::to_string(&result)
487                .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))?;
488            Ok(JsValue::from_str(&json))
489        })
490    }
491
492    /// Call the device's method named `method` with arguments by parameter
493    /// name, as a remote client invokes it over a bridge.
494    ///
495    /// `args_json` is a JSON object mapping each parameter name to an Arora
496    /// [`Value`] in the JSON form `call` takes, e.g. `{"x": {"f64": 2.5}}`. The
497    /// promise resolves to the return value, as a plain JS value of that JSON
498    /// form. A task-shaped method — one returning the behavior `Status` — is
499    /// spawned as a run instead, and resolves at once to the run's handle, the
500    /// plain object [`spawn`](Self::spawn) answers with.
501    ///
502    /// Names are the bare names modules export, so two modules may share one:
503    /// `module_id` (a uuid string) names the exporting module to choose. A name
504    /// more than one module exports, without `module_id`, rejects naming those
505    /// modules. An argument the method does not declare rejects too.
506    ///
507    /// Queued before this returns, like every operation; it reads the method's
508    /// signature on the next step and applies its call on the step after.
509    pub fn invoke(
510        &self,
511        method: &str,
512        args_json: &str,
513        module_id: Option<String>,
514    ) -> js_sys::Promise {
515        let args: Result<HashMap<String, Value>, _> = serde_json::from_str(args_json)
516            .map_err(|e| JsValue::from_str(&format!("invalid args json: {e}")));
517        let module = module_id
518            .map(|id| {
519                Uuid::parse_str(&id)
520                    .map_err(|e| JsValue::from_str(&format!("'{id}' is not a module id: {e}")))
521            })
522            .transpose();
523        let caller = self.caller.clone();
524        let method = method.to_string();
525        promise(async move {
526            let invoked = caller
527                .invoke(&method, args?, module?)
528                .await
529                .map_err(|e| JsValue::from_str(&format!("invoke {method} failed: {e}")))?;
530            match invoked {
531                Invoked::Returned(value) => value_to_js(&value),
532                Invoked::Started(handle) => run_to_js(&handle),
533            }
534        })
535    }
536
537    /// Start `call_json` (an `arora_types::call::Call` as JSON) as a task run,
538    /// concurrently with every other run. The promise resolves to the run's
539    /// handle, a plain object `{run, status, feedback, result, update}`: `run`
540    /// the run id (a uuid string, which [`halt`](Self::halt) takes), `status`
541    /// the path of the key that reports how the run goes and ends, and
542    /// `feedback`, `result` and `update` the paths of the keys carrying its
543    /// progress, its result, and what steers it. Queued before this returns;
544    /// applied by the next step.
545    pub fn spawn(&self, call_json: &str) -> js_sys::Promise {
546        let parsed: Result<Call, _> = serde_json::from_str(call_json)
547            .map_err(|e| JsValue::from_str(&format!("invalid call json: {e}")));
548        let caller = self.caller.clone();
549        promise(async move {
550            let handle = caller
551                .spawn(parsed?)
552                .await
553                .map_err(|e| JsValue::from_str(&format!("spawn failed: {e}")))?;
554            run_to_js(&handle)
555        })
556    }
557
558    /// Stop the run `run_id` names (the `run` of its handle, a uuid string):
559    /// it ends `Failure` on the step after the halt is applied. Idempotent —
560    /// halting a finished or unknown run resolves all the same. Queued before
561    /// this returns; applied by the next step.
562    pub fn halt(&self, run_id: &str) -> js_sys::Promise {
563        let run = Uuid::parse_str(run_id)
564            .map_err(|e| JsValue::from_str(&format!("'{run_id}' is not a run id: {e}")));
565        let caller = self.caller.clone();
566        promise(async move {
567            caller
568                .halt(TaskId(run?))
569                .await
570                .map_err(|e| JsValue::from_str(&format!("halt failed: {e}")))?;
571            Ok(JsValue::UNDEFINED)
572        })
573    }
574
575    /// The device's keys whose path starts with `prefix` (all of them when
576    /// absent), sorted by path. The promise resolves to an array of
577    /// `{path, __meta}` objects: each key with what its store says it is — the
578    /// shape it holds, its range and unit, where it rests, whether a client may
579    /// write it — as every Arora bridge lists keys. Queued before this returns;
580    /// applied by the next step.
581    #[wasm_bindgen(js_name = listKeys)]
582    pub fn list_keys(&self, prefix: Option<String>) -> js_sys::Promise {
583        let listed = self.caller.list_keys(prefix);
584        promise(async move {
585            let keys: Vec<KeyInfo> = listed
586                .await
587                .map_err(|e| JsValue::from_str(&format!("listKeys failed: {e}")))?
588                .into_iter()
589                .map(|(path, meta)| KeyInfo { path, meta })
590                .collect();
591            to_js(&keys)
592        })
593    }
594
595    /// The device's callable methods whose name starts with `prefix` (all of
596    /// them when absent), sorted by name. The promise resolves to an array of
597    /// method descriptions, as a bridge lists methods — `path` (the name),
598    /// `params` (each `name`, `param_type`, `required`, in declaration order),
599    /// `return_type`, `task` (whether invoking it starts a run) — plus `module`,
600    /// the exporting module's id, which [`invoke`](Self::invoke) takes when two
601    /// modules export one name. Queued before this returns; applied by the next
602    /// step.
603    #[wasm_bindgen(js_name = describeMethods)]
604    pub fn describe_methods(&self, prefix: Option<String>) -> js_sys::Promise {
605        let described = self.caller.describe_methods(prefix);
606        promise(async move {
607            let signatures = described
608                .await
609                .map_err(|e| JsValue::from_str(&format!("describeMethods failed: {e}")))?;
610            let mut methods = Vec::with_capacity(signatures.len());
611            for signature in &signatures {
612                let mut method = serde_json::to_value(client::method_info(signature))
613                    .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))?;
614                method["module"] = serde_json::Value::String(signature.module_id.to_string());
615                methods.push(method);
616            }
617            to_js(&methods)
618        })
619    }
620
621    /// Write one key into the store (Arora [`Value`] as JSON, e.g. `{"f32": 1}`).
622    #[wasm_bindgen(js_name = setValue)]
623    pub fn set_value(&self, path: &str, value_json: &str) -> Result<(), JsValue> {
624        let written = store_json::set_value(&*self.store, path, value_json);
625        self.changes.borrow_mut().fold();
626        written
627    }
628
629    /// Write several keys at once, as one store change (a JSON object mapping
630    /// each key path to an Arora [`Value`]).
631    #[wasm_bindgen(js_name = writeValues)]
632    pub fn write_values(&self, values_json: &str) -> Result<(), JsValue> {
633        let written = store_json::write_values(&*self.store, values_json);
634        self.changes.borrow_mut().fold();
635        written
636    }
637
638    /// Read keys from the store; `paths` is a `string[]`, result maps path→Value.
639    #[wasm_bindgen(js_name = readValues)]
640    pub fn read_values(&self, paths: JsValue) -> Result<JsValue, JsValue> {
641        store_json::read_values(&*self.store, paths)
642    }
643
644    /// A snapshot of every key in the store as a path→Value object.
645    pub fn snapshot(&self) -> Result<JsValue, JsValue> {
646        store_json::snapshot(&*self.store)
647    }
648
649    /// Drain the keys that changed since the last call, as a path→Value object
650    /// holding each key's latest value (`null` for a cleared key): the net
651    /// change since the previous drain. The first drain returns the store's
652    /// whole current state — the subscription opens on it. What is not
653    /// drained is held as one value per key, however long the page goes
654    /// without draining.
655    #[wasm_bindgen(js_name = drainChanges)]
656    pub fn drain_changes(&self) -> Result<JsValue, JsValue> {
657        self.changes.borrow_mut().drain()
658    }
659}
660
661impl AroraWeb {
662    /// The device's in-process caller: the typed operations behind `call`,
663    /// `invoke`, `spawn`, `halt`, `listKeys` and `describeMethods`, for a Rust
664    /// crate that wraps this device and wants them unserialized.
665    pub fn caller(&self) -> LocalCaller {
666        self.caller.clone()
667    }
668}
669
670/// Assemble a browser device from what the JS world can supply — guest wasm
671/// modules over the demo defaults; exported to JavaScript as
672/// `AroraRuntimeBuilder`. A device that needs a real HAL, bridge, store, or
673/// behavior interpreter is composed in Rust with [`arora::AroraBuilder`] and
674/// wrapped with `AroraWeb::from` instead: those seams are trait objects that
675/// cannot cross the JS boundary.
676#[wasm_bindgen(js_name = AroraRuntimeBuilder)]
677#[derive(Default)]
678pub struct AroraWebBuilder {
679    inner: AroraBuilder,
680}
681
682#[wasm_bindgen(js_class = "AroraRuntimeBuilder")]
683impl AroraWebBuilder {
684    #[wasm_bindgen(constructor)]
685    pub fn new() -> AroraWebBuilder {
686        AroraWebBuilder::default()
687    }
688
689    /// Load a guest module into the device's engine — in the browser, a
690    /// `.wasm` run by the native `WebAssembly` host. `header_json` is the
691    /// module's low-level header as JSON; `executable` its wasm bytes.
692    /// Repeatable — each call loads one module.
693    #[wasm_bindgen(js_name = withModule)]
694    pub fn with_module(&mut self, header_json: &str, executable: &[u8]) -> Result<(), JsValue> {
695        let header: Header = serde_json::from_str(header_json)
696            .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
697        let inner = std::mem::take(&mut self.inner);
698        self.inner = inner.with_module(header, executable.to_vec());
699        Ok(())
700    }
701
702    /// Load a guest module compiled ahead into the device's engine, as
703    /// `withModule` loads one from its bytes. The device instantiates the
704    /// compiled `WebAssembly.Module` instead of compiling the bytes, so devices
705    /// built from one `CompiledModule` compile it once between them.
706    /// Repeatable — each call loads one module.
707    #[wasm_bindgen(js_name = withCompiledModule)]
708    pub fn with_compiled_module(&mut self, module: &CompiledModule) {
709        let inner = std::mem::take(&mut self.inner);
710        self.inner = inner.with_compiled_module(&module.inner);
711    }
712
713    /// Build the device. The builder resets to its defaults, ready to
714    /// assemble another.
715    pub fn build(&mut self) -> Result<AroraWeb, JsValue> {
716        let arora = std::mem::take(&mut self.inner)
717            .build()
718            .map_err(|e| JsValue::from_str(&format!("arora build failed: {e:?}")))?;
719        Ok(AroraWeb::from(arora))
720    }
721}
722
723/// A guest module compiled once, to load into any number of devices with
724/// `AroraRuntimeBuilder.withCompiledModule`: each device instantiates it —
725/// its own memory and state — without compiling it again.
726#[wasm_bindgen]
727pub struct CompiledModule {
728    inner: arora::CompiledModule,
729}
730
731#[wasm_bindgen]
732impl CompiledModule {
733    /// Compile a guest module. `header_json` is the module's low-level header
734    /// as JSON; `executable` its wasm bytes. Compiles synchronously
735    /// (`new WebAssembly.Module`), which Chrome rejects above 8 MB on the main
736    /// thread. Fails on an invalid header or an executable that does not
737    /// compile.
738    #[wasm_bindgen(constructor)]
739    pub fn new(header_json: &str, executable: &[u8]) -> Result<CompiledModule, JsValue> {
740        install_panic_hook();
741        let header: Header = serde_json::from_str(header_json)
742            .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
743        let inner = arora::CompiledModule::new(header, executable)
744            .map_err(|e| JsValue::from_str(&format!("module does not compile: {e}")))?;
745        Ok(CompiledModule { inner })
746    }
747}
748
749/// JS-callable handle to a configured Arora engine.
750#[wasm_bindgen]
751pub struct Engine {
752    inner: std::pin::Pin<Box<arora_engine::engine::Engine>>,
753    loader: SharedLoaderRc,
754    function_module: HashMap<Uuid, Uuid>,
755    module_headers: HashMap<Uuid, String>,
756}
757
758#[wasm_bindgen]
759impl Engine {
760    #[wasm_bindgen(constructor)]
761    pub fn new() -> Engine {
762        install_panic_hook();
763        let executor = BrowserExecutor::new();
764        let loader = executor.shared();
765        let inner = EngineBuilder::new().add_executor(executor).build();
766        Engine {
767            inner,
768            loader,
769            function_module: HashMap::new(),
770            module_headers: HashMap::new(),
771        }
772    }
773
774    /// Load a module given its header (as JSON) and executable bytes.
775    /// Returns the module's UUID as a string.
776    ///
777    /// Compiles and instantiates synchronously: Chrome rejects both above
778    /// 8 MB on the main thread — use `prepareModule` + `loadPreparedModule`
779    /// for large executables.
780    #[wasm_bindgen(js_name = loadModule)]
781    pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
782        self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
783    }
784
785    /// Asynchronously compile and instantiate a module's executable
786    /// (via `WebAssembly.instantiate`, no main-thread size limit).
787    /// Follow up with `loadPreparedModule` to complete the load.
788    #[wasm_bindgen(js_name = prepareModule)]
789    pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
790        prepare_module_impl(self.loader.clone(), header_json, executable)
791    }
792
793    /// Load a module whose executable was staged by `prepareModule`.
794    /// Returns the module's UUID as a string.
795    #[wasm_bindgen(js_name = loadPreparedModule)]
796    pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
797        self.load_module_inner(header_json, Box::new([]))
798    }
799
800    fn load_module_inner(
801        &mut self,
802        header_json: &str,
803        executable: Box<[u8]>,
804    ) -> Result<String, JsValue> {
805        let header: Header = serde_json::from_str(header_json)
806            .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
807        let header_json_str = header_json.to_string();
808        let loaded = load_module_from_parts(&mut self.inner, header, executable)
809            .map_err(|e| JsValue::from_str(&format!("load_module failed: {e}")))?;
810        for fn_id in &loaded.function_ids {
811            self.function_module.insert(*fn_id, loaded.id);
812        }
813        self.module_headers.insert(loaded.id, header_json_str);
814        Ok(loaded.id.to_string())
815    }
816
817    /// Returns a JSON array of all loaded module headers.
818    #[wasm_bindgen(js_name = listModules)]
819    pub fn list_modules(&self) -> String {
820        let headers: Vec<serde_json::Value> = self
821            .module_headers
822            .values()
823            .filter_map(|s| serde_json::from_str(s).ok())
824            .collect();
825        serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
826    }
827
828    /// Call a function. `call_json` is a JSON document matching
829    /// `arora_engine::call::Call`. Returns the result as a JSON string.
830    #[wasm_bindgen]
831    pub fn call(&mut self, call_json: &str) -> Result<String, JsValue> {
832        let mut call: Call = serde_json::from_str(call_json)
833            .map_err(|e| JsValue::from_str(&format!("invalid call json: {e}")))?;
834        if call.module_id.is_none() {
835            // Resolve locally: the module the function was loaded with.
836            call.module_id = Some(
837                *self
838                    .function_module
839                    .get(&call.id)
840                    .ok_or_else(|| JsValue::from_str("no module known for function"))?,
841            );
842        }
843        let result = self
844            .inner
845            .arora_call(call)
846            .map_err(|e| JsValue::from_str(&format!("call failed: {e}")))?;
847        serde_json::to_string(&result)
848            .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
849    }
850}
851
852impl Default for Engine {
853    fn default() -> Self {
854        Self::new()
855    }
856}
857
858/// Shared implementation of `prepareModule`: parses the header for the
859/// module id, then stages an asynchronously-instantiated module in the
860/// loader. Returns a `Promise<void>`.
861fn prepare_module_impl(
862    loader: SharedLoaderRc,
863    header_json: &str,
864    executable: Vec<u8>,
865) -> js_sys::Promise {
866    let header: Result<Header, _> = serde_json::from_str(header_json);
867    wasm_bindgen_futures::future_to_promise(async move {
868        let header = header.map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
869        loader.prepare(header.id, executable).await?;
870        Ok(JsValue::UNDEFINED)
871    })
872}
873
874// =============================================================================
875// Behavior-tree runner
876//
877// A self-contained BT runtime built directly on arora's engine primitives —
878// no dependency on the arora-behavior-tree crate.
879// =============================================================================
880
881// UUID constants from arora-behavior-tree-types generated code.
882// TickId struct id: 6f49e650-84ca-4899-a9bd-1f3bf17fab51
883const TICK_ID_STRUCT_BYTES: [u8; 16] = [
884    0x6f, 0x49, 0xe6, 0x50, 0x84, 0xca, 0x48, 0x99, 0xa9, 0xbd, 0x1f, 0x3b, 0xf1, 0x7f, 0xab, 0x51,
885];
886// TickId::callable_id field: 237992d2-17d1-459f-bca1-7185fa6a69d7
887const TICK_ID_CALLABLE_FIELD_BYTES: [u8; 16] = [
888    0x23, 0x79, 0x92, 0xd2, 0x17, 0xd1, 0x45, 0x9f, 0xbc, 0xa1, 0x71, 0x85, 0xfa, 0x6a, 0x69, 0xd7,
889];
890// Status::Success variant: 766e9e9a-446d-4e46-83e6-14b7ca101169
891const STATUS_SUCCESS_BYTES: [u8; 16] = [
892    0x76, 0x6e, 0x9e, 0x9a, 0x44, 0x6d, 0x4e, 0x46, 0x83, 0xe6, 0x14, 0xb7, 0xca, 0x10, 0x11, 0x69,
893];
894// Status::Failure variant: 2468f46c-bb60-425c-9a4d-9ad326ccc7e2
895const STATUS_FAILURE_BYTES: [u8; 16] = [
896    0x24, 0x68, 0xf4, 0x6c, 0xbb, 0x60, 0x42, 0x5c, 0x9a, 0x4d, 0x9a, 0xd3, 0x26, 0xcc, 0xc7, 0xe2,
897];
898// Status enum type: 325a5767-e344-4532-860e-0749bcf2e428
899const STATUS_ENUM_BYTES: [u8; 16] = [
900    0x32, 0x5a, 0x57, 0x67, 0xe3, 0x44, 0x45, 0x32, 0x86, 0x0e, 0x07, 0x49, 0xbc, 0xf2, 0xe4, 0x28,
901];
902// _ret special out-parameter: 5f726574-0000-4000-8000-000000000000 ("_ret" in ASCII)
903const RET_PARAM_BYTES: [u8; 16] = [
904    0x5f, 0x72, 0x65, 0x74, 0x00, 0x00, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
905];
906
907fn value_to_status(v: &Value) -> &'static str {
908    if let Value::Enumeration(e) = v {
909        if *e.variant_id.as_bytes() == STATUS_SUCCESS_BYTES {
910            return "success";
911        }
912        if *e.variant_id.as_bytes() == STATUS_FAILURE_BYTES {
913            return "failure";
914        }
915    }
916    "running"
917}
918
919/// A node argument expression: either a literal value or a reference to a
920/// named variable (identified by UUID).
921#[derive(serde::Deserialize, Clone, Debug)]
922#[serde(rename_all = "snake_case")]
923enum BtExpression {
924    VariableId(Uuid),
925    Value(Value),
926}
927
928/// A single BT node – mirrors the arora-behavior-tree YAML schema.
929#[derive(serde::Deserialize, Debug)]
930struct BtNode {
931    id: Uuid,
932    function: Uuid,
933    #[serde(default)]
934    children: Option<Vec<Uuid>>,
935    /// Parameter arguments: maps parameter UUID to a literal value or variable.
936    /// Use the special key `5f726574-0000-4000-8000-000000000000` (_ret) to
937    /// capture the return value into a variable; the node then always succeeds.
938    #[serde(default)]
939    arguments: HashMap<Uuid, BtExpression>,
940}
941
942/// Metadata extracted from a module header for one exported function.
943struct FnMeta {
944    module_id: Uuid,
945    /// Parameter ID of the `children: TickId[]` argument, present only for
946    /// control nodes (seq, fallback, parallel …).
947    children_param_id: Option<Uuid>,
948}
949
950/// An arora Callable that wraps one BT node.
951struct NodeCallable {
952    node_id: Uuid,
953    fn_id: Uuid,
954    module_id: Uuid,
955    children_param_id: Option<Uuid>,
956    children_callable_ids: Vec<u64>,
957    arguments: HashMap<Uuid, BtExpression>,
958    variables: Rc<RefCell<HashMap<Uuid, Value>>>,
959    trace: Rc<RefCell<Vec<(Uuid, &'static str)>>>,
960}
961
962impl Callable for NodeCallable {
963    fn call(&self, caller: &mut dyn CallBridge) -> Result<Value, arora_engine::call::CallError> {
964        let tick_id_type = Uuid::from_bytes(TICK_ID_STRUCT_BYTES);
965        let callable_field = Uuid::from_bytes(TICK_ID_CALLABLE_FIELD_BYTES);
966        let ret_param_id = Uuid::from_bytes(RET_PARAM_BYTES);
967
968        let mut args = Vec::new();
969
970        if let Some(children_param_id) = self.children_param_id {
971            let elements: Vec<StructureWithoutId> = self
972                .children_callable_ids
973                .iter()
974                .map(|&id| StructureWithoutId {
975                    fields: vec![StructureField {
976                        id: callable_field,
977                        value: Box::new(Value::U64(id)),
978                    }],
979                })
980                .collect();
981            args.push(StructureField {
982                id: children_param_id,
983                value: Box::new(Value::ArrayStructure {
984                    id: tick_id_type,
985                    elements,
986                }),
987            });
988        }
989
990        for (&param_id, expr) in &self.arguments {
991            if param_id == ret_param_id {
992                continue;
993            }
994            let value = match expr {
995                BtExpression::Value(v) => v.clone(),
996                BtExpression::VariableId(var_id) => self
997                    .variables
998                    .borrow()
999                    .get(var_id)
1000                    .cloned()
1001                    .unwrap_or(Value::Unit),
1002            };
1003            args.push(StructureField {
1004                id: param_id,
1005                value: Box::new(value),
1006            });
1007        }
1008
1009        // Build a map of param_id -> variable_id for mutable arguments so we can
1010        // write mutated values back to the variable store after the call.
1011        let mutable_param_vars: HashMap<Uuid, Uuid> = self
1012            .arguments
1013            .iter()
1014            .filter_map(|(&param_id, expr)| {
1015                if param_id == ret_param_id {
1016                    return None;
1017                }
1018                if let BtExpression::VariableId(var_id) = expr {
1019                    Some((param_id, *var_id))
1020                } else {
1021                    None
1022                }
1023            })
1024            .collect();
1025
1026        let result = caller.arora_call(Call {
1027            module_id: Some(self.module_id),
1028            id: self.fn_id,
1029            args,
1030        })?;
1031
1032        // Write back mutated parameter values to bound variables.
1033        for mutated in &result.mutated {
1034            if let Some(&var_id) = mutable_param_vars.get(&mutated.id) {
1035                self.variables
1036                    .borrow_mut()
1037                    .insert(var_id, *mutated.value.clone());
1038            }
1039        }
1040
1041        let has_ret = self.arguments.contains_key(&ret_param_id);
1042        if has_ret {
1043            if let Some(BtExpression::VariableId(var_id)) = self.arguments.get(&ret_param_id) {
1044                self.variables
1045                    .borrow_mut()
1046                    .insert(*var_id, result.ret.clone());
1047            }
1048        }
1049
1050        let s = if has_ret {
1051            "success"
1052        } else {
1053            value_to_status(&result.ret)
1054        };
1055        self.trace.borrow_mut().push((self.node_id, s));
1056
1057        if has_ret {
1058            Ok(Value::Enumeration(Enumeration {
1059                id: Uuid::from_bytes(STATUS_ENUM_BYTES),
1060                variant_id: Uuid::from_bytes(STATUS_SUCCESS_BYTES),
1061                value: Box::new(Value::Unit),
1062            }))
1063        } else {
1064            Ok(result.ret)
1065        }
1066    }
1067}
1068
1069/// Recursively registers callables for `node_id` and all descendants.
1070/// Returns the callable id of the registered root callable.
1071fn register_node(
1072    engine: &mut dyn CallBridge,
1073    node_id: Uuid,
1074    node_index: &HashMap<Uuid, BtNode>,
1075    fn_meta: &HashMap<Uuid, FnMeta>,
1076    trace: &Rc<RefCell<Vec<(Uuid, &'static str)>>>,
1077    variables: &Rc<RefCell<HashMap<Uuid, Value>>>,
1078) -> Result<u64, String> {
1079    let node = node_index
1080        .get(&node_id)
1081        .ok_or_else(|| format!("node {node_id} not found in tree"))?;
1082    let meta = fn_meta
1083        .get(&node.function)
1084        .ok_or_else(|| format!("function {} not registered in fn_meta", node.function))?;
1085
1086    let children_callable_ids = match &node.children {
1087        None => vec![],
1088        Some(ids) => ids
1089            .iter()
1090            .map(|&child_id| register_node(engine, child_id, node_index, fn_meta, trace, variables))
1091            .collect::<Result<Vec<_>, _>>()?,
1092    };
1093
1094    let callable: Rc<dyn Callable> = Rc::new(NodeCallable {
1095        node_id,
1096        fn_id: node.function,
1097        module_id: meta.module_id,
1098        children_param_id: meta.children_param_id,
1099        children_callable_ids,
1100        arguments: node.arguments.clone(),
1101        variables: variables.clone(),
1102        trace: trace.clone(),
1103    });
1104    let id = engine.arora_register_callable(callable);
1105    Ok(id.id)
1106}
1107
1108/// JS-callable handle for loading modules and executing behavior trees.
1109///
1110/// Usage:
1111/// 1. `new BehaviorTreeRunner()`
1112/// 2. `runner.loadModule(headerJson, wasmBytes)` – can be called for multiple modules
1113/// 3. `runner.run(nodesJson)` – returns `{status, trace}`
1114/// 4. Or `runner.setVariable(varId, valueJson)` + `runner.tick(nodesJson)` for
1115///    stateful tick-by-tick execution with variable bindings.
1116#[wasm_bindgen]
1117pub struct BehaviorTreeRunner {
1118    inner: Pin<Box<arora_engine::engine::Engine>>,
1119    loader: SharedLoaderRc,
1120    fn_meta: HashMap<Uuid, FnMeta>,
1121    variables: Rc<RefCell<HashMap<Uuid, Value>>>,
1122    module_headers: HashMap<Uuid, String>,
1123}
1124
1125#[wasm_bindgen]
1126impl BehaviorTreeRunner {
1127    #[wasm_bindgen(constructor)]
1128    pub fn new() -> BehaviorTreeRunner {
1129        install_panic_hook();
1130        let executor = BrowserExecutor::new();
1131        let loader = executor.shared();
1132        let inner = EngineBuilder::new().add_executor(executor).build();
1133        BehaviorTreeRunner {
1134            inner,
1135            loader,
1136            fn_meta: HashMap::new(),
1137            variables: Rc::new(RefCell::new(HashMap::new())),
1138            module_headers: HashMap::new(),
1139        }
1140    }
1141
1142    /// Load a WASM module. `header_json` must be the module's YAML header
1143    /// converted to JSON (the JS side can use js-yaml for that).
1144    /// Returns the module UUID string.
1145    ///
1146    /// Compiles and instantiates synchronously: Chrome rejects both above
1147    /// 8 MB on the main thread — use `prepareModule` + `loadPreparedModule`
1148    /// for large executables.
1149    #[wasm_bindgen(js_name = loadModule)]
1150    pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
1151        self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
1152    }
1153
1154    /// Asynchronously compile and instantiate a module's executable
1155    /// (via `WebAssembly.instantiate`, no main-thread size limit).
1156    /// Follow up with `loadPreparedModule` to complete the load.
1157    #[wasm_bindgen(js_name = prepareModule)]
1158    pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
1159        prepare_module_impl(self.loader.clone(), header_json, executable)
1160    }
1161
1162    /// Load a module whose executable was staged by `prepareModule`.
1163    /// Returns the module UUID string.
1164    #[wasm_bindgen(js_name = loadPreparedModule)]
1165    pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
1166        self.load_module_inner(header_json, Box::new([]))
1167    }
1168
1169    fn load_module_inner(
1170        &mut self,
1171        header_json: &str,
1172        executable: Box<[u8]>,
1173    ) -> Result<String, JsValue> {
1174        let header: Header = serde_json::from_str(header_json)
1175            .map_err(|e| JsValue::from_str(&format!("invalid header: {e}")))?;
1176        let module_id = header.id;
1177        let header_json_str = header_json.to_string();
1178
1179        for export in &header.exports {
1180            let arora_types::module::low::ExportSymbol::Function(f) = export;
1181            let children_param_id = f.parameters.first().and_then(|p| {
1182                if let arora_types::module::low::TypeRef::Array { id } = &p.ty {
1183                    if id == &Uuid::from_bytes(TICK_ID_STRUCT_BYTES) {
1184                        Some(p.id)
1185                    } else {
1186                        None
1187                    }
1188                } else {
1189                    None
1190                }
1191            });
1192
1193            self.fn_meta.insert(
1194                f.id,
1195                FnMeta {
1196                    module_id,
1197                    children_param_id,
1198                },
1199            );
1200        }
1201
1202        let result = load_module_from_parts(&mut self.inner, header, executable)
1203            .map_err(|e| JsValue::from_str(&format!("load failed: {e}")))?;
1204        self.module_headers.insert(result.id, header_json_str);
1205        Ok(result.id.to_string())
1206    }
1207
1208    /// Returns a JSON array of all loaded module headers.
1209    #[wasm_bindgen(js_name = listModules)]
1210    pub fn list_modules(&self) -> String {
1211        let headers: Vec<serde_json::Value> = self
1212            .module_headers
1213            .values()
1214            .filter_map(|s| serde_json::from_str(s).ok())
1215            .collect();
1216        serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
1217    }
1218
1219    /// Initialize or update a variable. `var_id` is a UUID string; `value_json`
1220    /// is the serialized `Value` (e.g. `{"f32": 0.0}`).
1221    #[wasm_bindgen(js_name = setVariable)]
1222    pub fn set_variable(&mut self, var_id: &str, value_json: &str) -> Result<(), JsValue> {
1223        let var_id: Uuid = var_id
1224            .parse()
1225            .map_err(|_| JsValue::from_str("bad var_id: not a valid UUID"))?;
1226        let value: Value = serde_json::from_str(value_json)
1227            .map_err(|e| JsValue::from_str(&format!("bad value JSON: {e}")))?;
1228        self.variables.borrow_mut().insert(var_id, value);
1229        Ok(())
1230    }
1231
1232    /// Run one tick of the behavior tree. Variables persist across calls.
1233    ///
1234    /// `nodes_json` is a JSON array where each element is:
1235    ///   `{ id, function, children?, arguments?, return_binding? }`
1236    ///
1237    /// Returns: `{ "status": "...", "trace": [...], "variables": { varId: value } }`
1238    pub fn tick(&mut self, nodes_json: &str) -> Result<String, JsValue> {
1239        let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
1240            .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
1241        if nodes.is_empty() {
1242            return Err(JsValue::from_str("tree has no nodes"));
1243        }
1244
1245        let root_id = nodes[0].id;
1246        let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
1247        let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
1248
1249        let fn_meta = &self.fn_meta;
1250        let root_callable_id = register_node(
1251            &mut *self.inner,
1252            root_id,
1253            &node_index,
1254            fn_meta,
1255            &trace,
1256            &self.variables,
1257        )
1258        .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
1259
1260        let callable_id = CallableId {
1261            id: root_callable_id,
1262        };
1263        let result = Callable::call(&callable_id, &mut *self.inner)
1264            .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
1265        let status = value_to_status(&result);
1266
1267        let trace_json: Vec<serde_json::Value> = trace
1268            .borrow()
1269            .iter()
1270            .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
1271            .collect();
1272
1273        let vars_json: serde_json::Map<String, serde_json::Value> = self
1274            .variables
1275            .borrow()
1276            .iter()
1277            .filter_map(|(id, v)| serde_json::to_value(v).ok().map(|jv| (id.to_string(), jv)))
1278            .collect();
1279
1280        Ok(serde_json::json!({
1281          "status": status,
1282          "trace": trace_json,
1283          "variables": vars_json,
1284        })
1285        .to_string())
1286    }
1287
1288    /// Run a behavior tree to completion (ticks until not Running).
1289    ///
1290    /// `nodes_json` is a JSON array where each element is:
1291    ///   `{ id: "<uuid>", function: "<uuid>", children?: ["<uuid>", ...] }`
1292    /// The first element is the root node.
1293    ///
1294    /// Returns a JSON string:
1295    ///   `{ "status": "success"|"failure"|"running",
1296    ///      "trace": [{"nodeId": "<uuid>", "status": "..."}] }`
1297    pub fn run(&mut self, nodes_json: &str) -> Result<String, JsValue> {
1298        let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
1299            .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
1300        if nodes.is_empty() {
1301            return Err(JsValue::from_str("tree has no nodes"));
1302        }
1303
1304        let root_id = nodes[0].id;
1305        let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
1306        let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
1307
1308        let fn_meta = &self.fn_meta;
1309        let root_callable_id = register_node(
1310            &mut *self.inner,
1311            root_id,
1312            &node_index,
1313            fn_meta,
1314            &trace,
1315            &self.variables,
1316        )
1317        .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
1318
1319        let callable_id = CallableId {
1320            id: root_callable_id,
1321        };
1322
1323        let mut last_status = "running";
1324        for _ in 0..10_000 {
1325            let result = Callable::call(&callable_id, &mut *self.inner)
1326                .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
1327            last_status = value_to_status(&result);
1328            if last_status != "running" {
1329                break;
1330            }
1331        }
1332
1333        let trace_json: Vec<serde_json::Value> = trace
1334            .borrow()
1335            .iter()
1336            .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
1337            .collect();
1338
1339        let out = serde_json::json!({
1340          "status": last_status,
1341          "trace": trace_json,
1342        });
1343        Ok(out.to_string())
1344    }
1345}
1346
1347impl Default for BehaviorTreeRunner {
1348    fn default() -> Self {
1349        Self::new()
1350    }
1351}
1352
1353// =============================================================================
1354// Type registry
1355//
1356// A minimal wasm-compatible registry for resolving type UUIDs to names.
1357// Loads from the records.json files emitted by arora-module-rust::generate_records.
1358// =============================================================================
1359
1360/// JS-callable type registry that resolves type UUIDs to human-readable names.
1361#[wasm_bindgen]
1362pub struct Registry {
1363    entries: HashMap<Uuid, (String, Option<Uuid>)>,
1364}
1365
1366#[derive(serde::Deserialize)]
1367struct RecordEntry {
1368    id: Uuid,
1369    name: String,
1370    parent: Option<Uuid>,
1371}
1372
1373#[wasm_bindgen]
1374impl Registry {
1375    #[wasm_bindgen(constructor)]
1376    pub fn new() -> Registry {
1377        Registry {
1378            entries: HashMap::new(),
1379        }
1380    }
1381
1382    /// Load type records from a JSON array: `[{id, name, parent?}, ...]`.
1383    #[wasm_bindgen(js_name = loadRecordsJson)]
1384    pub fn load_records_json(&mut self, json: &str) -> Result<(), JsValue> {
1385        let records: Vec<RecordEntry> = serde_json::from_str(json)
1386            .map_err(|e| JsValue::from_str(&format!("invalid records JSON: {e}")))?;
1387        for r in records {
1388            self.entries.insert(r.id, (r.name, r.parent));
1389        }
1390        Ok(())
1391    }
1392
1393    /// Resolve a UUID string to a dot-separated name path (e.g. `"behavior_tree.Status"`).
1394    /// Returns `None` if the UUID is not known.
1395    #[wasm_bindgen(js_name = resolveId)]
1396    pub fn resolve_id(&self, id: &str) -> Option<String> {
1397        let uuid: Uuid = id.parse().ok()?;
1398        self.compute_path(&uuid)
1399    }
1400
1401    fn compute_path(&self, id: &Uuid) -> Option<String> {
1402        let (name, parent) = self.entries.get(id)?;
1403        match parent {
1404            None => Some(name.clone()),
1405            Some(parent_id) if !self.entries.contains_key(parent_id) => Some(name.clone()),
1406            Some(parent_id) => Some(format!("{}.{}", self.compute_path(parent_id)?, name)),
1407        }
1408    }
1409}
1410
1411impl Default for Registry {
1412    fn default() -> Self {
1413        Self::new()
1414    }
1415}
1416
1417/// What a page has not drained stays one entry per changed key, whichever
1418/// way the device steps. Run in a browser with `wasm-pack test --headless
1419/// --firefox crates/arora-web --lib`.
1420#[cfg(test)]
1421mod undrained_tests {
1422    use super::*;
1423    use wasm_bindgen_futures::JsFuture;
1424    use wasm_bindgen_test::*;
1425
1426    wasm_bindgen_test_configure!(run_in_browser);
1427
1428    /// The keys a busy device changes on each of its steps.
1429    const KEYS: usize = 200;
1430
1431    /// Write every one of the [`KEYS`] to `value`, as one store change.
1432    fn frame(store: &dyn DataStore, value: f32) {
1433        let mut change = StateChange::new();
1434        for key in 0..KEYS {
1435            change
1436                .set
1437                .insert(Key::new(format!("face/{key}")), Some(Value::F32(value)));
1438        }
1439        store.write(change).expect("write a frame");
1440    }
1441
1442    /// A demo device and a sibling handle of its store, through which a test
1443    /// writes as the device's own writers do.
1444    fn device() -> (AroraWeb, Box<dyn DataStore>) {
1445        let arora = Arora::builder().build().expect("build the device");
1446        let store = arora.store().clone_box();
1447        (AroraWeb::from(arora), store)
1448    }
1449
1450    /// Whether the subscription's channel is empty: every change it received
1451    /// has been folded.
1452    fn channel_is_empty(web: &AroraWeb) -> bool {
1453        web.changes.borrow().subscription.try_recv().is_none()
1454    }
1455
1456    /// The value the backlog holds for `face/{key}`.
1457    fn held(web: &AroraWeb, key: usize) -> Option<Value> {
1458        web.changes
1459            .borrow()
1460            .latest
1461            .get(&Key::new(format!("face/{key}")))
1462            .cloned()
1463            .flatten()
1464    }
1465
1466    async fn sleep(ms: i32) {
1467        let timer = js_sys::Promise::new(&mut |resolve, _| {
1468            let set_timeout: js_sys::Function =
1469                js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("setTimeout"))
1470                    .expect("a global setTimeout")
1471                    .into();
1472            set_timeout
1473                .call2(&JsValue::NULL, &resolve, &JsValue::from(ms))
1474                .expect("setTimeout");
1475        });
1476        JsFuture::from(timer).await.expect("the timer fires");
1477    }
1478
1479    #[wasm_bindgen_test]
1480    fn stepping_without_a_drain_holds_one_value_per_key() {
1481        let (web, store) = device();
1482        for step in 0..1_000 {
1483            frame(&*store, step as f32);
1484            web.step(10.0).expect("step");
1485            assert!(channel_is_empty(&web), "step {step} left changes unfolded");
1486        }
1487        let keys = store.snapshot().storage.len();
1488        assert!(keys >= KEYS, "the store holds the frame's keys");
1489        assert!(
1490            web.changes.borrow().latest.len() <= keys,
1491            "the backlog holds no more than one entry per key"
1492        );
1493        assert_eq!(held(&web, 7), Some(Value::F32(999.0)));
1494    }
1495
1496    #[wasm_bindgen_test]
1497    async fn a_running_device_folds_every_step() {
1498        let (web, store) = device();
1499        let running = web.run(Some(1.0));
1500        for step in 0..50 {
1501            frame(&*store, step as f32);
1502            // Several periods: the loop steps, and folds, before this resumes.
1503            sleep(5).await;
1504            assert!(channel_is_empty(&web), "step {step} left changes unfolded");
1505            assert_eq!(held(&web, 0), Some(Value::F32(step as f32)));
1506        }
1507        web.stop();
1508        JsFuture::from(running).await.expect("the run stops");
1509    }
1510
1511    #[wasm_bindgen_test]
1512    fn a_page_write_is_folded_at_once() {
1513        let (web, _store) = device();
1514        web.drain_changes().expect("the first drain");
1515        for value in 0..100 {
1516            web.set_value("face/0", &format!(r#"{{"f32": {value}}}"#))
1517                .expect("set a key");
1518            assert!(channel_is_empty(&web));
1519        }
1520        assert_eq!(held(&web, 0), Some(Value::F32(99.0)));
1521    }
1522}