1#![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
46fn install_panic_hook() {
53 console_error_panic_hook::set_once();
54}
55
56use 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
66fn 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
84fn 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
92fn 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
100fn 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
108pub mod store_json {
114 use super::*;
115
116 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 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 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 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 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 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 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 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 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
226struct 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 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 fn drain(&mut self) -> Result<JsValue, JsValue> {
260 self.fold();
261 store_json::latest_to_js(self.latest.drain())
262 }
263}
264
265#[wasm_bindgen(js_name = AroraRuntime)]
292pub struct AroraWeb {
293 device: Rc<RefCell<Option<Arora>>>,
297 stop: RefCell<Option<futures::channel::oneshot::Sender<()>>>,
300 caller: LocalCaller,
301 store: Box<dyn DataStore>,
302 changes: Rc<RefCell<Undrained>>,
305 behavior_error: tokio::sync::watch::Receiver<Option<String>>,
308 behavior_error_changes: Rc<RefCell<Option<tokio::sync::watch::Receiver<Option<String>>>>>,
312}
313
314impl From<Arora> for AroraWeb {
315 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 #[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 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 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 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 slot.borrow_mut().replace(device);
410 match outcome {
411 Some(result) => {
413 result.map_err(|e| JsValue::from_str(&format!("run failed: {e}")))?;
414 Ok(JsValue::UNDEFINED)
415 }
416 None => Ok(JsValue::UNDEFINED),
418 }
419 })
420 }
421
422 pub fn stop(&self) {
426 if let Some(stop) = self.stop.borrow_mut().take() {
427 let _ = stop.send(());
428 }
429 }
430
431 #[wasm_bindgen(getter)]
433 pub fn running(&self) -> bool {
434 self.device.borrow().is_none()
435 }
436
437 #[wasm_bindgen(getter, js_name = behaviorError)]
442 pub fn behavior_error(&self) -> Option<String> {
443 self.behavior_error.borrow().clone()
444 }
445
446 #[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 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 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 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 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 #[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 #[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 #[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 #[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 #[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 pub fn snapshot(&self) -> Result<JsValue, JsValue> {
646 store_json::snapshot(&*self.store)
647 }
648
649 #[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 pub fn caller(&self) -> LocalCaller {
666 self.caller.clone()
667 }
668}
669
670#[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 #[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 #[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 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#[wasm_bindgen]
727pub struct CompiledModule {
728 inner: arora::CompiledModule,
729}
730
731#[wasm_bindgen]
732impl CompiledModule {
733 #[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#[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 #[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 #[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 #[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 #[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 #[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 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
858fn 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
874const TICK_ID_STRUCT_BYTES: [u8; 16] = [
884 0x6f, 0x49, 0xe6, 0x50, 0x84, 0xca, 0x48, 0x99, 0xa9, 0xbd, 0x1f, 0x3b, 0xf1, 0x7f, 0xab, 0x51,
885];
886const 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];
890const STATUS_SUCCESS_BYTES: [u8; 16] = [
892 0x76, 0x6e, 0x9e, 0x9a, 0x44, 0x6d, 0x4e, 0x46, 0x83, 0xe6, 0x14, 0xb7, 0xca, 0x10, 0x11, 0x69,
893];
894const STATUS_FAILURE_BYTES: [u8; 16] = [
896 0x24, 0x68, 0xf4, 0x6c, 0xbb, 0x60, 0x42, 0x5c, 0x9a, 0x4d, 0x9a, 0xd3, 0x26, 0xcc, 0xc7, 0xe2,
897];
898const STATUS_ENUM_BYTES: [u8; 16] = [
900 0x32, 0x5a, 0x57, 0x67, 0xe3, 0x44, 0x45, 0x32, 0x86, 0x0e, 0x07, 0x49, 0xbc, 0xf2, 0xe4, 0x28,
901];
902const 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#[derive(serde::Deserialize, Clone, Debug)]
922#[serde(rename_all = "snake_case")]
923enum BtExpression {
924 VariableId(Uuid),
925 Value(Value),
926}
927
928#[derive(serde::Deserialize, Debug)]
930struct BtNode {
931 id: Uuid,
932 function: Uuid,
933 #[serde(default)]
934 children: Option<Vec<Uuid>>,
935 #[serde(default)]
939 arguments: HashMap<Uuid, BtExpression>,
940}
941
942struct FnMeta {
944 module_id: Uuid,
945 children_param_id: Option<Uuid>,
948}
949
950struct 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 (¶m_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 let mutable_param_vars: HashMap<Uuid, Uuid> = self
1012 .arguments
1013 .iter()
1014 .filter_map(|(¶m_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 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
1069fn 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#[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 #[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 #[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 #[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 #[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 #[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 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 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#[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 #[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 #[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#[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 const KEYS: usize = 200;
1430
1431 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 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 fn channel_is_empty(web: &AroraWeb) -> bool {
1453 web.changes.borrow().subscription.try_recv().is_none()
1454 }
1455
1456 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 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}