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 records: arora::LocalRegistry,
681}
682
683#[wasm_bindgen(js_class = "AroraRuntimeBuilder")]
684impl AroraWebBuilder {
685 #[wasm_bindgen(constructor)]
686 pub fn new() -> AroraWebBuilder {
687 AroraWebBuilder::default()
688 }
689
690 #[wasm_bindgen(js_name = withModule)]
695 pub fn with_module(&mut self, header_json: &str, executable: &[u8]) -> Result<(), JsValue> {
696 let header: Header = serde_json::from_str(header_json)
697 .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
698 let inner = std::mem::take(&mut self.inner);
699 self.inner = inner.with_module(header, executable.to_vec());
700 Ok(())
701 }
702
703 #[wasm_bindgen(js_name = withCompiledModule)]
709 pub fn with_compiled_module(&mut self, module: &CompiledModule) {
710 let inner = std::mem::take(&mut self.inner);
711 self.inner = inner.with_compiled_module(&module.inner);
712 }
713
714 #[wasm_bindgen(js_name = withRecords)]
730 pub fn with_records(
731 &mut self,
732 #[wasm_bindgen(unchecked_param_type = "Record<string, string>")] records: JsValue,
733 ) -> Result<(), JsValue> {
734 use arora_registry::local_yaml::{load_records, RecordKind};
735 use futures::FutureExt;
736
737 let files: std::collections::BTreeMap<String, String> =
738 serde_wasm_bindgen::from_value(records).map_err(|e| {
739 JsValue::from_str(&format!(
740 "records must map each record file's path to its YAML: {e}"
741 ))
742 })?;
743 let mut entries = Vec::with_capacity(files.len());
744 for (path, yaml) in &files {
745 let record = path.split_once('/').and_then(|(directory, file)| {
746 Some((
747 RecordKind::from_directory(directory)?,
748 file.strip_suffix(".yaml")?,
749 ))
750 });
751 let Some((kind, stem)) = record else {
752 return Err(JsValue::from_str(&format!(
753 "'{path}' is not a record file's path: <folder|enumeration|structure|module>/<id>[@<version>].yaml"
754 )));
755 };
756 entries.push((kind, stem, yaml.clone()));
757 }
758 load_records(entries, &mut self.records)
759 .now_or_never()
760 .ok_or_else(|| JsValue::from_str("the records did not load synchronously"))?
761 .map_err(|e| JsValue::from_str(&format!("the records do not load: {e}")))
762 }
763
764 pub fn build(&mut self) -> Result<AroraWeb, JsValue> {
767 let arora = std::mem::take(&mut self.inner)
768 .with_registry(std::mem::take(&mut self.records))
769 .build()
770 .map_err(|e| JsValue::from_str(&format!("arora build failed: {e:?}")))?;
771 Ok(AroraWeb::from(arora))
772 }
773}
774
775#[wasm_bindgen]
779pub struct CompiledModule {
780 inner: arora::CompiledModule,
781}
782
783#[wasm_bindgen]
784impl CompiledModule {
785 #[wasm_bindgen(constructor)]
791 pub fn new(header_json: &str, executable: &[u8]) -> Result<CompiledModule, JsValue> {
792 install_panic_hook();
793 let header: Header = serde_json::from_str(header_json)
794 .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
795 let inner = arora::CompiledModule::new(header, executable)
796 .map_err(|e| JsValue::from_str(&format!("module does not compile: {e}")))?;
797 Ok(CompiledModule { inner })
798 }
799}
800
801#[wasm_bindgen]
803pub struct Engine {
804 inner: std::pin::Pin<Box<arora_engine::engine::Engine>>,
805 loader: SharedLoaderRc,
806 function_module: HashMap<Uuid, Uuid>,
807 module_headers: HashMap<Uuid, String>,
808}
809
810#[wasm_bindgen]
811impl Engine {
812 #[wasm_bindgen(constructor)]
813 pub fn new() -> Engine {
814 install_panic_hook();
815 let executor = BrowserExecutor::new();
816 let loader = executor.shared();
817 let inner = EngineBuilder::new().add_executor(executor).build();
818 Engine {
819 inner,
820 loader,
821 function_module: HashMap::new(),
822 module_headers: HashMap::new(),
823 }
824 }
825
826 #[wasm_bindgen(js_name = loadModule)]
833 pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
834 self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
835 }
836
837 #[wasm_bindgen(js_name = prepareModule)]
841 pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
842 prepare_module_impl(self.loader.clone(), header_json, executable)
843 }
844
845 #[wasm_bindgen(js_name = loadPreparedModule)]
848 pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
849 self.load_module_inner(header_json, Box::new([]))
850 }
851
852 fn load_module_inner(
853 &mut self,
854 header_json: &str,
855 executable: Box<[u8]>,
856 ) -> Result<String, JsValue> {
857 let header: Header = serde_json::from_str(header_json)
858 .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
859 let header_json_str = header_json.to_string();
860 let loaded = load_module_from_parts(&mut self.inner, header, executable)
861 .map_err(|e| JsValue::from_str(&format!("load_module failed: {e}")))?;
862 for fn_id in &loaded.function_ids {
863 self.function_module.insert(*fn_id, loaded.id);
864 }
865 self.module_headers.insert(loaded.id, header_json_str);
866 Ok(loaded.id.to_string())
867 }
868
869 #[wasm_bindgen(js_name = listModules)]
871 pub fn list_modules(&self) -> String {
872 let headers: Vec<serde_json::Value> = self
873 .module_headers
874 .values()
875 .filter_map(|s| serde_json::from_str(s).ok())
876 .collect();
877 serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
878 }
879
880 #[wasm_bindgen]
883 pub fn call(&mut self, call_json: &str) -> Result<String, JsValue> {
884 let mut call: Call = serde_json::from_str(call_json)
885 .map_err(|e| JsValue::from_str(&format!("invalid call json: {e}")))?;
886 if call.module_id.is_none() {
887 call.module_id = Some(
889 *self
890 .function_module
891 .get(&call.id)
892 .ok_or_else(|| JsValue::from_str("no module known for function"))?,
893 );
894 }
895 let result = self
896 .inner
897 .arora_call(call)
898 .map_err(|e| JsValue::from_str(&format!("call failed: {e}")))?;
899 serde_json::to_string(&result)
900 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
901 }
902}
903
904impl Default for Engine {
905 fn default() -> Self {
906 Self::new()
907 }
908}
909
910fn prepare_module_impl(
914 loader: SharedLoaderRc,
915 header_json: &str,
916 executable: Vec<u8>,
917) -> js_sys::Promise {
918 let header: Result<Header, _> = serde_json::from_str(header_json);
919 wasm_bindgen_futures::future_to_promise(async move {
920 let header = header.map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
921 loader.prepare(header.id, executable).await?;
922 Ok(JsValue::UNDEFINED)
923 })
924}
925
926const TICK_ID_STRUCT_BYTES: [u8; 16] = [
936 0x6f, 0x49, 0xe6, 0x50, 0x84, 0xca, 0x48, 0x99, 0xa9, 0xbd, 0x1f, 0x3b, 0xf1, 0x7f, 0xab, 0x51,
937];
938const TICK_ID_CALLABLE_FIELD_BYTES: [u8; 16] = [
940 0x23, 0x79, 0x92, 0xd2, 0x17, 0xd1, 0x45, 0x9f, 0xbc, 0xa1, 0x71, 0x85, 0xfa, 0x6a, 0x69, 0xd7,
941];
942const STATUS_SUCCESS_BYTES: [u8; 16] = [
944 0x76, 0x6e, 0x9e, 0x9a, 0x44, 0x6d, 0x4e, 0x46, 0x83, 0xe6, 0x14, 0xb7, 0xca, 0x10, 0x11, 0x69,
945];
946const STATUS_FAILURE_BYTES: [u8; 16] = [
948 0x24, 0x68, 0xf4, 0x6c, 0xbb, 0x60, 0x42, 0x5c, 0x9a, 0x4d, 0x9a, 0xd3, 0x26, 0xcc, 0xc7, 0xe2,
949];
950const STATUS_ENUM_BYTES: [u8; 16] = [
952 0x32, 0x5a, 0x57, 0x67, 0xe3, 0x44, 0x45, 0x32, 0x86, 0x0e, 0x07, 0x49, 0xbc, 0xf2, 0xe4, 0x28,
953];
954const RET_PARAM_BYTES: [u8; 16] = [
956 0x5f, 0x72, 0x65, 0x74, 0x00, 0x00, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
957];
958
959fn value_to_status(v: &Value) -> &'static str {
960 if let Value::Enumeration(e) = v {
961 if *e.variant_id.as_bytes() == STATUS_SUCCESS_BYTES {
962 return "success";
963 }
964 if *e.variant_id.as_bytes() == STATUS_FAILURE_BYTES {
965 return "failure";
966 }
967 }
968 "running"
969}
970
971#[derive(serde::Deserialize, Clone, Debug)]
974#[serde(rename_all = "snake_case")]
975enum BtExpression {
976 VariableId(Uuid),
977 Value(Value),
978}
979
980#[derive(serde::Deserialize, Debug)]
982struct BtNode {
983 id: Uuid,
984 function: Uuid,
985 #[serde(default)]
986 children: Option<Vec<Uuid>>,
987 #[serde(default)]
991 arguments: HashMap<Uuid, BtExpression>,
992}
993
994struct FnMeta {
996 module_id: Uuid,
997 children_param_id: Option<Uuid>,
1000}
1001
1002struct NodeCallable {
1004 node_id: Uuid,
1005 fn_id: Uuid,
1006 module_id: Uuid,
1007 children_param_id: Option<Uuid>,
1008 children_callable_ids: Vec<u64>,
1009 arguments: HashMap<Uuid, BtExpression>,
1010 variables: Rc<RefCell<HashMap<Uuid, Value>>>,
1011 trace: Rc<RefCell<Vec<(Uuid, &'static str)>>>,
1012}
1013
1014impl Callable for NodeCallable {
1015 fn call(&self, caller: &mut dyn CallBridge) -> Result<Value, arora_engine::call::CallError> {
1016 let tick_id_type = Uuid::from_bytes(TICK_ID_STRUCT_BYTES);
1017 let callable_field = Uuid::from_bytes(TICK_ID_CALLABLE_FIELD_BYTES);
1018 let ret_param_id = Uuid::from_bytes(RET_PARAM_BYTES);
1019
1020 let mut args = Vec::new();
1021
1022 if let Some(children_param_id) = self.children_param_id {
1023 let elements: Vec<StructureWithoutId> = self
1024 .children_callable_ids
1025 .iter()
1026 .map(|&id| StructureWithoutId {
1027 fields: vec![StructureField {
1028 id: callable_field,
1029 value: Box::new(Value::U64(id)),
1030 }],
1031 })
1032 .collect();
1033 args.push(StructureField {
1034 id: children_param_id,
1035 value: Box::new(Value::ArrayStructure {
1036 id: tick_id_type,
1037 elements,
1038 }),
1039 });
1040 }
1041
1042 for (¶m_id, expr) in &self.arguments {
1043 if param_id == ret_param_id {
1044 continue;
1045 }
1046 let value = match expr {
1047 BtExpression::Value(v) => v.clone(),
1048 BtExpression::VariableId(var_id) => self
1049 .variables
1050 .borrow()
1051 .get(var_id)
1052 .cloned()
1053 .unwrap_or(Value::Unit),
1054 };
1055 args.push(StructureField {
1056 id: param_id,
1057 value: Box::new(value),
1058 });
1059 }
1060
1061 let mutable_param_vars: HashMap<Uuid, Uuid> = self
1064 .arguments
1065 .iter()
1066 .filter_map(|(¶m_id, expr)| {
1067 if param_id == ret_param_id {
1068 return None;
1069 }
1070 if let BtExpression::VariableId(var_id) = expr {
1071 Some((param_id, *var_id))
1072 } else {
1073 None
1074 }
1075 })
1076 .collect();
1077
1078 let result = caller.arora_call(Call {
1079 module_id: Some(self.module_id),
1080 id: self.fn_id,
1081 args,
1082 })?;
1083
1084 for mutated in &result.mutated {
1086 if let Some(&var_id) = mutable_param_vars.get(&mutated.id) {
1087 self.variables
1088 .borrow_mut()
1089 .insert(var_id, *mutated.value.clone());
1090 }
1091 }
1092
1093 let has_ret = self.arguments.contains_key(&ret_param_id);
1094 if has_ret {
1095 if let Some(BtExpression::VariableId(var_id)) = self.arguments.get(&ret_param_id) {
1096 self.variables
1097 .borrow_mut()
1098 .insert(*var_id, result.ret.clone());
1099 }
1100 }
1101
1102 let s = if has_ret {
1103 "success"
1104 } else {
1105 value_to_status(&result.ret)
1106 };
1107 self.trace.borrow_mut().push((self.node_id, s));
1108
1109 if has_ret {
1110 Ok(Value::Enumeration(Enumeration {
1111 id: Uuid::from_bytes(STATUS_ENUM_BYTES),
1112 variant_id: Uuid::from_bytes(STATUS_SUCCESS_BYTES),
1113 value: Box::new(Value::Unit),
1114 }))
1115 } else {
1116 Ok(result.ret)
1117 }
1118 }
1119}
1120
1121fn register_node(
1124 engine: &mut dyn CallBridge,
1125 node_id: Uuid,
1126 node_index: &HashMap<Uuid, BtNode>,
1127 fn_meta: &HashMap<Uuid, FnMeta>,
1128 trace: &Rc<RefCell<Vec<(Uuid, &'static str)>>>,
1129 variables: &Rc<RefCell<HashMap<Uuid, Value>>>,
1130) -> Result<u64, String> {
1131 let node = node_index
1132 .get(&node_id)
1133 .ok_or_else(|| format!("node {node_id} not found in tree"))?;
1134 let meta = fn_meta
1135 .get(&node.function)
1136 .ok_or_else(|| format!("function {} not registered in fn_meta", node.function))?;
1137
1138 let children_callable_ids = match &node.children {
1139 None => vec![],
1140 Some(ids) => ids
1141 .iter()
1142 .map(|&child_id| register_node(engine, child_id, node_index, fn_meta, trace, variables))
1143 .collect::<Result<Vec<_>, _>>()?,
1144 };
1145
1146 let callable: Rc<dyn Callable> = Rc::new(NodeCallable {
1147 node_id,
1148 fn_id: node.function,
1149 module_id: meta.module_id,
1150 children_param_id: meta.children_param_id,
1151 children_callable_ids,
1152 arguments: node.arguments.clone(),
1153 variables: variables.clone(),
1154 trace: trace.clone(),
1155 });
1156 let id = engine.arora_register_callable(callable);
1157 Ok(id.id)
1158}
1159
1160#[wasm_bindgen]
1169pub struct BehaviorTreeRunner {
1170 inner: Pin<Box<arora_engine::engine::Engine>>,
1171 loader: SharedLoaderRc,
1172 fn_meta: HashMap<Uuid, FnMeta>,
1173 variables: Rc<RefCell<HashMap<Uuid, Value>>>,
1174 module_headers: HashMap<Uuid, String>,
1175}
1176
1177#[wasm_bindgen]
1178impl BehaviorTreeRunner {
1179 #[wasm_bindgen(constructor)]
1180 pub fn new() -> BehaviorTreeRunner {
1181 install_panic_hook();
1182 let executor = BrowserExecutor::new();
1183 let loader = executor.shared();
1184 let inner = EngineBuilder::new().add_executor(executor).build();
1185 BehaviorTreeRunner {
1186 inner,
1187 loader,
1188 fn_meta: HashMap::new(),
1189 variables: Rc::new(RefCell::new(HashMap::new())),
1190 module_headers: HashMap::new(),
1191 }
1192 }
1193
1194 #[wasm_bindgen(js_name = loadModule)]
1202 pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
1203 self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
1204 }
1205
1206 #[wasm_bindgen(js_name = prepareModule)]
1210 pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
1211 prepare_module_impl(self.loader.clone(), header_json, executable)
1212 }
1213
1214 #[wasm_bindgen(js_name = loadPreparedModule)]
1217 pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
1218 self.load_module_inner(header_json, Box::new([]))
1219 }
1220
1221 fn load_module_inner(
1222 &mut self,
1223 header_json: &str,
1224 executable: Box<[u8]>,
1225 ) -> Result<String, JsValue> {
1226 let header: Header = serde_json::from_str(header_json)
1227 .map_err(|e| JsValue::from_str(&format!("invalid header: {e}")))?;
1228 let module_id = header.id;
1229 let header_json_str = header_json.to_string();
1230
1231 for export in &header.exports {
1232 let arora_types::module::low::ExportSymbol::Function(f) = export;
1233 let children_param_id = f.parameters.first().and_then(|p| {
1234 if let arora_types::module::low::TypeRef::Array { id } = &p.ty {
1235 if id == &Uuid::from_bytes(TICK_ID_STRUCT_BYTES) {
1236 Some(p.id)
1237 } else {
1238 None
1239 }
1240 } else {
1241 None
1242 }
1243 });
1244
1245 self.fn_meta.insert(
1246 f.id,
1247 FnMeta {
1248 module_id,
1249 children_param_id,
1250 },
1251 );
1252 }
1253
1254 let result = load_module_from_parts(&mut self.inner, header, executable)
1255 .map_err(|e| JsValue::from_str(&format!("load failed: {e}")))?;
1256 self.module_headers.insert(result.id, header_json_str);
1257 Ok(result.id.to_string())
1258 }
1259
1260 #[wasm_bindgen(js_name = listModules)]
1262 pub fn list_modules(&self) -> String {
1263 let headers: Vec<serde_json::Value> = self
1264 .module_headers
1265 .values()
1266 .filter_map(|s| serde_json::from_str(s).ok())
1267 .collect();
1268 serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
1269 }
1270
1271 #[wasm_bindgen(js_name = setVariable)]
1274 pub fn set_variable(&mut self, var_id: &str, value_json: &str) -> Result<(), JsValue> {
1275 let var_id: Uuid = var_id
1276 .parse()
1277 .map_err(|_| JsValue::from_str("bad var_id: not a valid UUID"))?;
1278 let value: Value = serde_json::from_str(value_json)
1279 .map_err(|e| JsValue::from_str(&format!("bad value JSON: {e}")))?;
1280 self.variables.borrow_mut().insert(var_id, value);
1281 Ok(())
1282 }
1283
1284 pub fn tick(&mut self, nodes_json: &str) -> Result<String, JsValue> {
1291 let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
1292 .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
1293 if nodes.is_empty() {
1294 return Err(JsValue::from_str("tree has no nodes"));
1295 }
1296
1297 let root_id = nodes[0].id;
1298 let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
1299 let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
1300
1301 let fn_meta = &self.fn_meta;
1302 let root_callable_id = register_node(
1303 &mut *self.inner,
1304 root_id,
1305 &node_index,
1306 fn_meta,
1307 &trace,
1308 &self.variables,
1309 )
1310 .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
1311
1312 let callable_id = CallableId {
1313 id: root_callable_id,
1314 };
1315 let result = Callable::call(&callable_id, &mut *self.inner)
1316 .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
1317 let status = value_to_status(&result);
1318
1319 let trace_json: Vec<serde_json::Value> = trace
1320 .borrow()
1321 .iter()
1322 .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
1323 .collect();
1324
1325 let vars_json: serde_json::Map<String, serde_json::Value> = self
1326 .variables
1327 .borrow()
1328 .iter()
1329 .filter_map(|(id, v)| serde_json::to_value(v).ok().map(|jv| (id.to_string(), jv)))
1330 .collect();
1331
1332 Ok(serde_json::json!({
1333 "status": status,
1334 "trace": trace_json,
1335 "variables": vars_json,
1336 })
1337 .to_string())
1338 }
1339
1340 pub fn run(&mut self, nodes_json: &str) -> Result<String, JsValue> {
1350 let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
1351 .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
1352 if nodes.is_empty() {
1353 return Err(JsValue::from_str("tree has no nodes"));
1354 }
1355
1356 let root_id = nodes[0].id;
1357 let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
1358 let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
1359
1360 let fn_meta = &self.fn_meta;
1361 let root_callable_id = register_node(
1362 &mut *self.inner,
1363 root_id,
1364 &node_index,
1365 fn_meta,
1366 &trace,
1367 &self.variables,
1368 )
1369 .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
1370
1371 let callable_id = CallableId {
1372 id: root_callable_id,
1373 };
1374
1375 let mut last_status = "running";
1376 for _ in 0..10_000 {
1377 let result = Callable::call(&callable_id, &mut *self.inner)
1378 .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
1379 last_status = value_to_status(&result);
1380 if last_status != "running" {
1381 break;
1382 }
1383 }
1384
1385 let trace_json: Vec<serde_json::Value> = trace
1386 .borrow()
1387 .iter()
1388 .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
1389 .collect();
1390
1391 let out = serde_json::json!({
1392 "status": last_status,
1393 "trace": trace_json,
1394 });
1395 Ok(out.to_string())
1396 }
1397}
1398
1399impl Default for BehaviorTreeRunner {
1400 fn default() -> Self {
1401 Self::new()
1402 }
1403}
1404
1405#[wasm_bindgen]
1414pub struct Registry {
1415 entries: HashMap<Uuid, (String, Option<Uuid>)>,
1416}
1417
1418#[derive(serde::Deserialize)]
1419struct RecordEntry {
1420 id: Uuid,
1421 name: String,
1422 parent: Option<Uuid>,
1423}
1424
1425#[wasm_bindgen]
1426impl Registry {
1427 #[wasm_bindgen(constructor)]
1428 pub fn new() -> Registry {
1429 Registry {
1430 entries: HashMap::new(),
1431 }
1432 }
1433
1434 #[wasm_bindgen(js_name = loadRecordsJson)]
1436 pub fn load_records_json(&mut self, json: &str) -> Result<(), JsValue> {
1437 let records: Vec<RecordEntry> = serde_json::from_str(json)
1438 .map_err(|e| JsValue::from_str(&format!("invalid records JSON: {e}")))?;
1439 for r in records {
1440 self.entries.insert(r.id, (r.name, r.parent));
1441 }
1442 Ok(())
1443 }
1444
1445 #[wasm_bindgen(js_name = resolveId)]
1448 pub fn resolve_id(&self, id: &str) -> Option<String> {
1449 let uuid: Uuid = id.parse().ok()?;
1450 self.compute_path(&uuid)
1451 }
1452
1453 fn compute_path(&self, id: &Uuid) -> Option<String> {
1454 let (name, parent) = self.entries.get(id)?;
1455 match parent {
1456 None => Some(name.clone()),
1457 Some(parent_id) if !self.entries.contains_key(parent_id) => Some(name.clone()),
1458 Some(parent_id) => Some(format!("{}.{}", self.compute_path(parent_id)?, name)),
1459 }
1460 }
1461}
1462
1463impl Default for Registry {
1464 fn default() -> Self {
1465 Self::new()
1466 }
1467}
1468
1469#[cfg(test)]
1473mod undrained_tests {
1474 use super::*;
1475 use wasm_bindgen_futures::JsFuture;
1476 use wasm_bindgen_test::*;
1477
1478 wasm_bindgen_test_configure!(run_in_browser);
1479
1480 const KEYS: usize = 200;
1482
1483 fn frame(store: &dyn DataStore, value: f32) {
1485 let mut change = StateChange::new();
1486 for key in 0..KEYS {
1487 change
1488 .set
1489 .insert(Key::new(format!("face/{key}")), Some(Value::F32(value)));
1490 }
1491 store.write(change).expect("write a frame");
1492 }
1493
1494 fn device() -> (AroraWeb, Box<dyn DataStore>) {
1497 let arora = Arora::builder().build().expect("build the device");
1498 let store = arora.store().clone_box();
1499 (AroraWeb::from(arora), store)
1500 }
1501
1502 fn channel_is_empty(web: &AroraWeb) -> bool {
1505 web.changes.borrow().subscription.try_recv().is_none()
1506 }
1507
1508 fn held(web: &AroraWeb, key: usize) -> Option<Value> {
1510 web.changes
1511 .borrow()
1512 .latest
1513 .get(&Key::new(format!("face/{key}")))
1514 .cloned()
1515 .flatten()
1516 }
1517
1518 async fn sleep(ms: i32) {
1519 let timer = js_sys::Promise::new(&mut |resolve, _| {
1520 let set_timeout: js_sys::Function =
1521 js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("setTimeout"))
1522 .expect("a global setTimeout")
1523 .into();
1524 set_timeout
1525 .call2(&JsValue::NULL, &resolve, &JsValue::from(ms))
1526 .expect("setTimeout");
1527 });
1528 JsFuture::from(timer).await.expect("the timer fires");
1529 }
1530
1531 #[wasm_bindgen_test]
1532 fn stepping_without_a_drain_holds_one_value_per_key() {
1533 let (web, store) = device();
1534 for step in 0..1_000 {
1535 frame(&*store, step as f32);
1536 web.step(10.0).expect("step");
1537 assert!(channel_is_empty(&web), "step {step} left changes unfolded");
1538 }
1539 let keys = store.snapshot().storage.len();
1540 assert!(keys >= KEYS, "the store holds the frame's keys");
1541 assert!(
1542 web.changes.borrow().latest.len() <= keys,
1543 "the backlog holds no more than one entry per key"
1544 );
1545 assert_eq!(held(&web, 7), Some(Value::F32(999.0)));
1546 }
1547
1548 #[wasm_bindgen_test]
1549 async fn a_running_device_folds_every_step() {
1550 let (web, store) = device();
1551 let running = web.run(Some(1.0));
1552 for step in 0..50 {
1553 frame(&*store, step as f32);
1554 sleep(5).await;
1556 assert!(channel_is_empty(&web), "step {step} left changes unfolded");
1557 assert_eq!(held(&web, 0), Some(Value::F32(step as f32)));
1558 }
1559 web.stop();
1560 JsFuture::from(running).await.expect("the run stops");
1561 }
1562
1563 #[wasm_bindgen_test]
1564 fn a_page_write_is_folded_at_once() {
1565 let (web, _store) = device();
1566 web.drain_changes().expect("the first drain");
1567 for value in 0..100 {
1568 web.set_value("face/0", &format!(r#"{{"f32": {value}}}"#))
1569 .expect("set a key");
1570 assert!(channel_is_empty(&web));
1571 }
1572 assert_eq!(held(&web, 0), Some(Value::F32(99.0)));
1573 }
1574}