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, LocalCaller, MethodSignature, TaskId};
61use arora_bridge::client::{self, KeyInfo};
62use arora_types::call::CallError;
63use arora_types::data::{DataStore, Key, StateChange, Subscription};
64use futures::future::Either;
65use futures::FutureExt;
66use std::cell::Cell;
67use std::future::Future;
68use std::task::Poll;
69use std::time::Duration;
70use tokio::sync::watch;
71
72fn promise(future: impl Future<Output = Result<JsValue, JsValue>> + 'static) -> js_sys::Promise {
76 let mut future = Box::pin(future);
77 let mut cx = std::task::Context::from_waker(futures::task::noop_waker_ref());
78 match future.as_mut().poll(&mut cx) {
79 std::task::Poll::Ready(Ok(value)) => js_sys::Promise::resolve(&value),
80 std::task::Poll::Ready(Err(error)) => js_sys::Promise::reject(&error),
81 std::task::Poll::Pending => wasm_bindgen_futures::future_to_promise(future),
82 }
83}
84
85fn to_js(value: &impl serde::Serialize) -> Result<JsValue, JsValue> {
88 value
89 .serialize(&serde_wasm_bindgen::Serializer::json_compatible())
90 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
91}
92
93fn value_to_js(value: &Value) -> Result<JsValue, JsValue> {
96 let json = serde_json::to_value(value)
97 .map_err(|e| JsValue::from_str(&format!("serialize value failed: {e}")))?;
98 to_js(&json)
99}
100
101fn run_to_js(handle: &arora::TaskHandle) -> Result<JsValue, JsValue> {
104 let spawned = arora_behavior::interpreter_module::encode_spawn_result(handle);
105 let run = client::run_of(&spawned).map_err(|e| JsValue::from_str(&e))?;
106 to_js(&run)
107}
108
109pub mod store_json {
115 use super::*;
116
117 pub fn set_value(store: &dyn DataStore, path: &str, value_json: &str) -> Result<(), JsValue> {
120 let value: Value = serde_json::from_str(value_json)
121 .map_err(|e| JsValue::from_str(&format!("invalid value json for {path}: {e}")))?;
122 store
123 .write(StateChange::set(path, value))
124 .map_err(|e| JsValue::from_str(&format!("write {path} failed: {e}")))
125 }
126
127 pub fn write_values(store: &dyn DataStore, values_json: &str) -> Result<(), JsValue> {
131 let map: serde_json::Map<String, serde_json::Value> = serde_json::from_str(values_json)
132 .map_err(|e| JsValue::from_str(&format!("invalid values json: {e}")))?;
133 let mut change = StateChange::new();
134 for (path, raw) in map {
135 let value: Value = serde_json::from_value(raw)
136 .map_err(|e| JsValue::from_str(&format!("invalid value for {path}: {e}")))?;
137 change.set.insert(Key::new(path), Some(value));
138 }
139 store
140 .write(change)
141 .map_err(|e| JsValue::from_str(&format!("write failed: {e}")))
142 }
143
144 pub fn read_values(store: &dyn DataStore, paths: JsValue) -> Result<JsValue, JsValue> {
147 let paths: Vec<String> = serde_wasm_bindgen::from_value(paths)
148 .map_err(|e| JsValue::from_str(&format!("paths must be a string[]: {e}")))?;
149 let keys: Vec<Key> = paths.iter().map(Key::new).collect();
150 let values = store.read(&keys);
151 let mut out = serde_json::Map::with_capacity(paths.len());
152 for (path, value) in paths.into_iter().zip(values) {
153 out.insert(path, value_to_json(value)?);
154 }
155 to_js_object(out)
156 }
157
158 pub fn snapshot(store: &dyn DataStore) -> Result<JsValue, JsValue> {
161 let state = store.snapshot();
162 let mut out = serde_json::Map::with_capacity(state.storage.len());
163 for (key, value) in state.storage {
164 out.insert(key.path, value_to_json(value)?);
165 }
166 to_js_object(out)
167 }
168
169 pub fn drain_changes(changes: &Subscription) -> Result<JsValue, JsValue> {
180 let mut latest = HashMap::new();
181 while let Some(change) = changes.try_recv() {
182 fold_change(&mut latest, change);
183 }
184 latest_to_js(latest.drain())
185 }
186
187 pub(crate) fn fold_change(latest: &mut HashMap<Key, Option<Value>>, change: StateChange) {
190 latest.extend(change.set);
191 latest.extend(change.unset.into_iter().map(|key| (key, None)));
192 }
193
194 pub(crate) fn latest_to_js(
197 latest: impl Iterator<Item = (Key, Option<Value>)>,
198 ) -> Result<JsValue, JsValue> {
199 let mut out = serde_json::Map::new();
200 for (key, value) in latest {
201 out.insert(key.path, value_to_json(value)?);
202 }
203 to_js_object(out)
204 }
205
206 fn to_js_object(out: serde_json::Map<String, serde_json::Value>) -> Result<JsValue, JsValue> {
211 use serde::Serialize;
212 serde_json::Value::Object(out)
213 .serialize(&serde_wasm_bindgen::Serializer::json_compatible())
214 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
215 }
216
217 fn value_to_json(value: Option<Value>) -> Result<serde_json::Value, JsValue> {
219 match value {
220 Some(v) => serde_json::to_value(v)
221 .map_err(|e| JsValue::from_str(&format!("serialize value failed: {e}"))),
222 None => Ok(serde_json::Value::Null),
223 }
224 }
225}
226
227struct Undrained {
239 subscription: Subscription,
240 latest: HashMap<Key, Option<Value>>,
241}
242
243impl Undrained {
244 fn new(subscription: Subscription) -> Self {
245 Self {
246 subscription,
247 latest: HashMap::new(),
248 }
249 }
250
251 fn fold(&mut self) {
253 while let Some(change) = self.subscription.try_recv() {
254 store_json::fold_change(&mut self.latest, change);
255 }
256 }
257
258 fn drain(&mut self) -> Result<JsValue, JsValue> {
261 self.fold();
262 store_json::latest_to_js(self.latest.drain())
263 }
264}
265
266#[wasm_bindgen]
267extern "C" {
268 #[wasm_bindgen(js_name = queueMicrotask)]
270 fn queue_microtask(callback: &js_sys::Function);
271
272 #[wasm_bindgen(js_name = setTimeout)]
274 fn set_timeout(callback: &js_sys::Function, ms: i32);
275
276 #[wasm_bindgen(js_namespace = console, js_name = error)]
277 fn console_error(message: &str);
278}
279
280#[derive(Clone, Debug)]
283enum Stepped {
284 Idle,
286 Failed(String),
290 Parked,
294}
295
296#[derive(Clone)]
315struct StepScheduler {
316 device: Rc<RefCell<Option<Arora>>>,
317 changes: Rc<RefCell<Undrained>>,
318 due: Rc<Cell<bool>>,
321 stepped: Rc<watch::Sender<Stepped>>,
322}
323
324impl StepScheduler {
325 fn new(device: Arora, changes: Rc<RefCell<Undrained>>) -> Self {
326 Self {
327 device: Rc::new(RefCell::new(Some(device))),
328 changes,
329 due: Rc::new(Cell::new(false)),
330 stepped: Rc::new(watch::channel(Stepped::Idle).0),
331 }
332 }
333
334 fn mid_step(&self) -> bool {
336 self.device.try_borrow_mut().is_err()
337 }
338
339 fn request(&self) {
343 if self.due.replace(true) {
344 return;
345 }
346 let scheduler = self.clone();
347 let callback = Closure::once_into_js(move || scheduler.step_if_due());
348 if self.mid_step() {
349 set_timeout(callback.unchecked_ref(), 0);
350 } else {
351 queue_microtask(callback.unchecked_ref());
352 }
353 }
354
355 fn met(&self) {
357 self.due.set(false);
358 }
359
360 fn failed(&self, message: String) {
362 self.stepped.send_replace(Stepped::Failed(message));
363 }
364
365 fn step_if_due(&self) {
366 if !self.due.replace(false) {
367 return;
368 }
369 let Ok(mut parked) = self.device.try_borrow_mut() else {
370 self.request();
373 return;
374 };
375 let Some(device) = parked.as_mut() else {
378 return;
379 };
380 let stepped = device.step(Duration::ZERO);
381 drop(parked);
382 self.changes.borrow_mut().fold();
383 if let Err(e) = stepped {
384 let message = e.to_string();
385 console_error(&format!("arora-web: a step at a zero dt failed: {message}"));
387 self.failed(message);
388 }
389 }
390}
391
392#[wasm_bindgen(js_name = AroraRuntime)]
456pub struct AroraWeb {
457 steps: StepScheduler,
461 stop: RefCell<Option<futures::channel::oneshot::Sender<()>>>,
464 caller: LocalCaller,
465 store: Box<dyn DataStore>,
466 methods: Vec<MethodSignature>,
469 changes: Rc<RefCell<Undrained>>,
472 behavior_error: watch::Receiver<Option<String>>,
475 behavior_error_changes: Rc<RefCell<Option<watch::Receiver<Option<String>>>>>,
479}
480
481impl From<Arora> for AroraWeb {
482 fn from(arora: Arora) -> Self {
486 install_panic_hook();
487 let caller = arora.caller();
488 let store = arora.store().clone_box();
489 let methods = arora.describe_methods(None);
490 let changes = Rc::new(RefCell::new(Undrained::new(store.subscribe())));
491 let behavior_error = arora.behavior_error();
492 let behavior_error_changes = Rc::new(RefCell::new(Some(arora.behavior_error())));
493 Self {
494 steps: StepScheduler::new(arora, changes.clone()),
495 stop: RefCell::new(None),
496 caller,
497 store,
498 methods,
499 changes,
500 behavior_error,
501 behavior_error_changes,
502 }
503 }
504}
505
506#[wasm_bindgen(js_class = "AroraRuntime")]
507impl AroraWeb {
508 #[wasm_bindgen(constructor)]
511 pub fn new() -> Result<AroraWeb, JsValue> {
512 Ok(AroraWeb::from(Arora::builder().build().map_err(|e| {
513 JsValue::from_str(&format!("arora build failed: {e:?}"))
514 })?))
515 }
516
517 pub fn step(&self, dt_ms: f64) -> Result<(), JsValue> {
525 let mut parked = self
526 .steps
527 .device
528 .try_borrow_mut()
529 .map_err(|_| JsValue::from_str("the device is mid-step; call between steps"))?;
530 let device = parked
531 .as_mut()
532 .ok_or_else(|| JsValue::from_str("run() drives the device; step() is unavailable"))?;
533 self.steps.met();
534 let stepped = device.step(Duration::from_secs_f64(dt_ms / 1_000.0));
535 drop(parked);
536 self.changes.borrow_mut().fold();
537 stepped.map_err(|e| {
538 self.steps.failed(e.to_string());
541 JsValue::from_str(&format!("step failed: {e}"))
542 })
543 }
544
545 pub fn run(&self, period_ms: Option<f64>) -> js_sys::Promise {
556 let taken = match self.steps.device.try_borrow_mut() {
557 Ok(mut parked) => parked.take(),
558 Err(_) => {
559 return js_sys::Promise::reject(&JsValue::from_str(
560 "the device is mid-step; call between steps",
561 ))
562 }
563 };
564 let Some(mut device) = taken else {
565 return js_sys::Promise::reject(&JsValue::from_str("the device is already running"));
566 };
567 let steps = self.steps.clone();
568 let changes = self.changes.clone();
569 let (stop_tx, stop_rx) = futures::channel::oneshot::channel::<()>();
570 *self.stop.borrow_mut() = Some(stop_tx);
571 wasm_bindgen_futures::future_to_promise(async move {
572 let period = period_ms
573 .map(|ms| Duration::from_secs_f64(ms / 1_000.0))
574 .unwrap_or(Arora::DEFAULT_STEP_PERIOD);
575 let outcome = {
576 let run = device.run(period);
577 futures::pin_mut!(run);
578 let run = futures::future::poll_fn(|cx| {
581 let polled = run.as_mut().poll(cx);
582 changes.borrow_mut().fold();
583 polled
584 })
585 .fuse();
586 let mut stop_rx = stop_rx.fuse();
587 futures::pin_mut!(run);
588 futures::select_biased! {
589 result = run => Some(result),
590 _ = stop_rx => None,
591 }
592 };
593 steps.device.borrow_mut().replace(device);
598 steps.stepped.send_replace(Stepped::Parked);
599 match outcome {
600 Some(result) => {
602 result.map_err(|e| JsValue::from_str(&format!("run failed: {e}")))?;
603 Ok(JsValue::UNDEFINED)
604 }
605 None => Ok(JsValue::UNDEFINED),
607 }
608 })
609 }
610
611 pub fn stop(&self) {
615 if let Some(stop) = self.stop.borrow_mut().take() {
616 let _ = stop.send(());
617 }
618 }
619
620 #[wasm_bindgen(getter)]
622 pub fn running(&self) -> bool {
623 self.steps
625 .device
626 .try_borrow()
627 .is_ok_and(|parked| parked.is_none())
628 }
629
630 #[wasm_bindgen(getter, js_name = behaviorError)]
635 pub fn behavior_error(&self) -> Option<String> {
636 self.behavior_error.borrow().clone()
637 }
638
639 #[wasm_bindgen(js_name = behaviorErrorChanged)]
645 pub fn behavior_error_changed(&self) -> js_sys::Promise {
646 let Some(mut errors) = self.behavior_error_changes.borrow_mut().take() else {
647 return js_sys::Promise::reject(&JsValue::from_str(
648 "a behaviorErrorChanged await is already pending",
649 ));
650 };
651 let home = self.behavior_error_changes.clone();
652 wasm_bindgen_futures::future_to_promise(async move {
653 let outcome = errors.changed().await;
654 let value = errors.borrow_and_update().clone();
655 home.borrow_mut().replace(errors);
656 outcome.map_err(|_| JsValue::from_str("the device is gone"))?;
657 Ok(match value {
658 Some(message) => JsValue::from_str(&message),
659 None => JsValue::UNDEFINED,
660 })
661 })
662 }
663
664 pub fn call(&self, call_json: &str) -> js_sys::Promise {
669 let call: Call = match serde_json::from_str(call_json) {
670 Ok(call) => call,
671 Err(e) => return reject(format!("invalid call json: {e}")),
672 };
673 let reply = self.call_now(call);
674 promise(async move {
675 let result = reply
676 .await
677 .map_err(|e| JsValue::from_str(&format!("call failed: {e}")))?;
678 let json = serde_json::to_string(&result)
679 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))?;
680 Ok(JsValue::from_str(&json))
681 })
682 }
683
684 pub fn invoke(
702 &self,
703 method: &str,
704 args_json: &str,
705 module_id: Option<String>,
706 ) -> js_sys::Promise {
707 let failed = |e: String| reject(format!("invoke {method} failed: {e}"));
708 let args: HashMap<String, Value> = match serde_json::from_str(args_json) {
709 Ok(args) => args,
710 Err(e) => return reject(format!("invalid args json: {e}")),
711 };
712 let module = match module_id.map(|id| Uuid::parse_str(&id).map_err(|e| (id, e))) {
713 None => None,
714 Some(Ok(module)) => Some(module),
715 Some(Err((id, e))) => return reject(format!("'{id}' is not a module id: {e}")),
716 };
717 let signature = match client::find_method(&self.methods, method, module) {
718 Ok(signature) => signature,
719 Err(e) => return failed(e),
720 };
721 let call = match client::call_of(signature, args) {
722 Ok(call) => call,
723 Err(e) => return failed(e),
724 };
725 let method = method.to_string();
726 let failed = move |e: CallError| JsValue::from_str(&format!("invoke {method} failed: {e}"));
727 if client::task_shaped(&signature.function) {
728 let caller = self.caller.clone();
729 let started = self.complete(async move { caller.spawn(call).await });
730 promise(async move { run_to_js(&started.await.map_err(failed)?) })
731 } else {
732 let returned = self.call_now(call);
733 promise(async move { value_to_js(&returned.await.map_err(failed)?.ret) })
734 }
735 }
736
737 pub fn spawn(&self, call_json: &str) -> js_sys::Promise {
746 let call: Call = match serde_json::from_str(call_json) {
747 Ok(call) => call,
748 Err(e) => return reject(format!("invalid call json: {e}")),
749 };
750 let caller = self.caller.clone();
751 let started = self.complete(async move { caller.spawn(call).await });
752 promise(async move {
753 let handle = started
754 .await
755 .map_err(|e| JsValue::from_str(&format!("spawn failed: {e}")))?;
756 run_to_js(&handle)
757 })
758 }
759
760 pub fn halt(&self, run_id: &str) -> js_sys::Promise {
765 let run = match Uuid::parse_str(run_id) {
766 Ok(run) => TaskId(run),
767 Err(e) => return reject(format!("'{run_id}' is not a run id: {e}")),
768 };
769 let caller = self.caller.clone();
770 let halted = self.complete(async move { caller.halt(run).await });
771 promise(async move {
772 halted
773 .await
774 .map_err(|e| JsValue::from_str(&format!("halt failed: {e}")))?;
775 Ok(JsValue::UNDEFINED)
776 })
777 }
778
779 #[wasm_bindgen(js_name = listKeys)]
786 pub fn list_keys(&self, prefix: Option<String>) -> js_sys::Promise {
787 let keys: Vec<KeyInfo> = arora::list_keys(&*self.store, prefix.as_deref())
788 .into_iter()
789 .map(|(path, meta)| KeyInfo { path, meta })
790 .collect();
791 settled(to_js(&keys))
792 }
793
794 #[wasm_bindgen(js_name = describeMethods)]
803 pub fn describe_methods(&self, prefix: Option<String>) -> js_sys::Promise {
804 let described = self
805 .methods
806 .iter()
807 .filter(|signature| {
808 prefix
809 .as_deref()
810 .is_none_or(|prefix| signature.name.starts_with(prefix))
811 })
812 .map(|signature| {
813 let mut method = serde_json::to_value(client::method_info(signature))
814 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))?;
815 method["module"] = serde_json::Value::String(signature.module_id.to_string());
816 Ok(method)
817 })
818 .collect::<Result<Vec<_>, JsValue>>();
819 settled(described.and_then(|methods| to_js(&methods)))
820 }
821
822 #[wasm_bindgen(js_name = setValue)]
827 pub fn set_value(&self, path: &str, value_json: &str) -> Result<(), JsValue> {
828 let written = store_json::set_value(&*self.store, path, value_json);
829 self.wrote(written)
830 }
831
832 #[wasm_bindgen(js_name = writeValues)]
836 pub fn write_values(&self, values_json: &str) -> Result<(), JsValue> {
837 let written = store_json::write_values(&*self.store, values_json);
838 self.wrote(written)
839 }
840
841 #[wasm_bindgen(js_name = readValues)]
843 pub fn read_values(&self, paths: JsValue) -> Result<JsValue, JsValue> {
844 store_json::read_values(&*self.store, paths)
845 }
846
847 pub fn snapshot(&self) -> Result<JsValue, JsValue> {
849 store_json::snapshot(&*self.store)
850 }
851
852 #[wasm_bindgen(js_name = drainChanges)]
859 pub fn drain_changes(&self) -> Result<JsValue, JsValue> {
860 self.changes.borrow_mut().drain()
861 }
862}
863
864impl AroraWeb {
865 pub fn caller(&self) -> LocalCaller {
872 self.caller.clone()
873 }
874
875 pub fn complete<V: 'static>(
889 &self,
890 reply: impl Future<Output = Result<V, CallError>> + 'static,
891 ) -> impl Future<Output = Result<V, CallError>> + 'static {
892 let mut reply = Box::pin(reply);
893 let first = reply.as_mut().poll(&mut std::task::Context::from_waker(
894 futures::task::noop_waker_ref(),
895 ));
896 let mut stepped = self.steps.stepped.subscribe();
897 let steps = self.steps.clone();
898 if first.is_pending() {
899 steps.request();
900 }
901 async move {
902 if let Poll::Ready(answer) = first {
903 return answer;
904 }
905 loop {
906 {
907 let changed = std::pin::pin!(stepped.changed());
910 if let Either::Left((answer, _)) =
911 futures::future::select(reply.as_mut(), changed).await
912 {
913 return answer;
914 }
915 }
916 let event = stepped.borrow_and_update().clone();
917 match event {
918 Stepped::Idle => {}
919 Stepped::Failed(message) => {
920 return match reply.as_mut().now_or_never() {
921 Some(answer) => answer,
922 None => Err(CallError::Generic {
923 message: format!("step failed: {message}"),
924 }),
925 }
926 }
927 Stepped::Parked => steps.request(),
928 }
929 }
930 }
931 }
932
933 pub fn request_step(&self) {
945 self.steps.request();
946 }
947
948 fn wrote(&self, written: Result<(), JsValue>) -> Result<(), JsValue> {
956 self.changes.borrow_mut().fold();
957 if written.is_ok() && !self.steps.mid_step() {
958 self.steps.request();
959 }
960 written
961 }
962
963 fn call_now(
965 &self,
966 call: Call,
967 ) -> impl Future<Output = Result<arora_types::call::CallResult, CallError>> + 'static {
968 let caller = self.caller.clone();
969 self.complete(async move { caller.call(call).await })
970 }
971}
972
973fn reject(message: String) -> js_sys::Promise {
975 js_sys::Promise::reject(&JsValue::from_str(&message))
976}
977
978fn settled(answer: Result<JsValue, JsValue>) -> js_sys::Promise {
980 match answer {
981 Ok(value) => js_sys::Promise::resolve(&value),
982 Err(error) => js_sys::Promise::reject(&error),
983 }
984}
985
986#[wasm_bindgen(js_name = AroraRuntimeBuilder)]
993#[derive(Default)]
994pub struct AroraWebBuilder {
995 inner: AroraBuilder,
996 records: arora::LocalRegistry,
997}
998
999#[wasm_bindgen(js_class = "AroraRuntimeBuilder")]
1000impl AroraWebBuilder {
1001 #[wasm_bindgen(constructor)]
1002 pub fn new() -> AroraWebBuilder {
1003 AroraWebBuilder::default()
1004 }
1005
1006 #[wasm_bindgen(js_name = withModule)]
1011 pub fn with_module(&mut self, header_json: &str, executable: &[u8]) -> Result<(), JsValue> {
1012 let header: Header = serde_json::from_str(header_json)
1013 .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
1014 let inner = std::mem::take(&mut self.inner);
1015 self.inner = inner.with_module(header, executable.to_vec());
1016 Ok(())
1017 }
1018
1019 #[wasm_bindgen(js_name = withCompiledModule)]
1025 pub fn with_compiled_module(&mut self, module: &CompiledModule) {
1026 let inner = std::mem::take(&mut self.inner);
1027 self.inner = inner.with_compiled_module(&module.inner);
1028 }
1029
1030 #[wasm_bindgen(js_name = withRecords)]
1046 pub fn with_records(
1047 &mut self,
1048 #[wasm_bindgen(unchecked_param_type = "Record<string, string>")] records: JsValue,
1049 ) -> Result<(), JsValue> {
1050 use arora_registry::local_yaml::{load_records, RecordKind};
1051 use futures::FutureExt;
1052
1053 let files: std::collections::BTreeMap<String, String> =
1054 serde_wasm_bindgen::from_value(records).map_err(|e| {
1055 JsValue::from_str(&format!(
1056 "records must map each record file's path to its YAML: {e}"
1057 ))
1058 })?;
1059 let mut entries = Vec::with_capacity(files.len());
1060 for (path, yaml) in &files {
1061 let record = path.split_once('/').and_then(|(directory, file)| {
1062 Some((
1063 RecordKind::from_directory(directory)?,
1064 file.strip_suffix(".yaml")?,
1065 ))
1066 });
1067 let Some((kind, stem)) = record else {
1068 return Err(JsValue::from_str(&format!(
1069 "'{path}' is not a record file's path: <folder|enumeration|structure|module>/<id>[@<version>].yaml"
1070 )));
1071 };
1072 entries.push((kind, stem, yaml.clone()));
1073 }
1074 load_records(entries, &mut self.records)
1075 .now_or_never()
1076 .ok_or_else(|| JsValue::from_str("the records did not load synchronously"))?
1077 .map_err(|e| JsValue::from_str(&format!("the records do not load: {e}")))
1078 }
1079
1080 pub fn build(&mut self) -> Result<AroraWeb, JsValue> {
1083 let arora = std::mem::take(&mut self.inner)
1084 .with_registry(std::mem::take(&mut self.records))
1085 .build()
1086 .map_err(|e| JsValue::from_str(&format!("arora build failed: {e:?}")))?;
1087 Ok(AroraWeb::from(arora))
1088 }
1089}
1090
1091#[wasm_bindgen]
1095pub struct CompiledModule {
1096 inner: arora::CompiledModule,
1097}
1098
1099#[wasm_bindgen]
1100impl CompiledModule {
1101 #[wasm_bindgen(constructor)]
1107 pub fn new(header_json: &str, executable: &[u8]) -> Result<CompiledModule, JsValue> {
1108 install_panic_hook();
1109 let header: Header = serde_json::from_str(header_json)
1110 .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
1111 let inner = arora::CompiledModule::new(header, executable)
1112 .map_err(|e| JsValue::from_str(&format!("module does not compile: {e}")))?;
1113 Ok(CompiledModule { inner })
1114 }
1115}
1116
1117#[wasm_bindgen]
1119pub struct Engine {
1120 inner: std::pin::Pin<Box<arora_engine::engine::Engine>>,
1121 loader: SharedLoaderRc,
1122 function_module: HashMap<Uuid, Uuid>,
1123 module_headers: HashMap<Uuid, String>,
1124}
1125
1126#[wasm_bindgen]
1127impl Engine {
1128 #[wasm_bindgen(constructor)]
1129 pub fn new() -> Engine {
1130 install_panic_hook();
1131 let executor = BrowserExecutor::new();
1132 let loader = executor.shared();
1133 let inner = EngineBuilder::new().add_executor(executor).build();
1134 Engine {
1135 inner,
1136 loader,
1137 function_module: HashMap::new(),
1138 module_headers: HashMap::new(),
1139 }
1140 }
1141
1142 #[wasm_bindgen(js_name = loadModule)]
1149 pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
1150 self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
1151 }
1152
1153 #[wasm_bindgen(js_name = prepareModule)]
1157 pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
1158 prepare_module_impl(self.loader.clone(), header_json, executable)
1159 }
1160
1161 #[wasm_bindgen(js_name = loadPreparedModule)]
1164 pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
1165 self.load_module_inner(header_json, Box::new([]))
1166 }
1167
1168 fn load_module_inner(
1169 &mut self,
1170 header_json: &str,
1171 executable: Box<[u8]>,
1172 ) -> Result<String, JsValue> {
1173 let header: Header = serde_json::from_str(header_json)
1174 .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
1175 let header_json_str = header_json.to_string();
1176 let loaded = load_module_from_parts(&mut self.inner, header, executable)
1177 .map_err(|e| JsValue::from_str(&format!("load_module failed: {e}")))?;
1178 for fn_id in &loaded.function_ids {
1179 self.function_module.insert(*fn_id, loaded.id);
1180 }
1181 self.module_headers.insert(loaded.id, header_json_str);
1182 Ok(loaded.id.to_string())
1183 }
1184
1185 #[wasm_bindgen(js_name = listModules)]
1187 pub fn list_modules(&self) -> String {
1188 let headers: Vec<serde_json::Value> = self
1189 .module_headers
1190 .values()
1191 .filter_map(|s| serde_json::from_str(s).ok())
1192 .collect();
1193 serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
1194 }
1195
1196 #[wasm_bindgen]
1199 pub fn call(&mut self, call_json: &str) -> Result<String, JsValue> {
1200 let mut call: Call = serde_json::from_str(call_json)
1201 .map_err(|e| JsValue::from_str(&format!("invalid call json: {e}")))?;
1202 if call.module_id.is_none() {
1203 call.module_id = Some(
1205 *self
1206 .function_module
1207 .get(&call.id)
1208 .ok_or_else(|| JsValue::from_str("no module known for function"))?,
1209 );
1210 }
1211 let result = self
1212 .inner
1213 .arora_call(call)
1214 .map_err(|e| JsValue::from_str(&format!("call failed: {e}")))?;
1215 serde_json::to_string(&result)
1216 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
1217 }
1218}
1219
1220impl Default for Engine {
1221 fn default() -> Self {
1222 Self::new()
1223 }
1224}
1225
1226fn prepare_module_impl(
1230 loader: SharedLoaderRc,
1231 header_json: &str,
1232 executable: Vec<u8>,
1233) -> js_sys::Promise {
1234 let header: Result<Header, _> = serde_json::from_str(header_json);
1235 wasm_bindgen_futures::future_to_promise(async move {
1236 let header = header.map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
1237 loader.prepare(header.id, executable).await?;
1238 Ok(JsValue::UNDEFINED)
1239 })
1240}
1241
1242const TICK_ID_STRUCT_BYTES: [u8; 16] = [
1252 0x6f, 0x49, 0xe6, 0x50, 0x84, 0xca, 0x48, 0x99, 0xa9, 0xbd, 0x1f, 0x3b, 0xf1, 0x7f, 0xab, 0x51,
1253];
1254const TICK_ID_CALLABLE_FIELD_BYTES: [u8; 16] = [
1256 0x23, 0x79, 0x92, 0xd2, 0x17, 0xd1, 0x45, 0x9f, 0xbc, 0xa1, 0x71, 0x85, 0xfa, 0x6a, 0x69, 0xd7,
1257];
1258const STATUS_SUCCESS_BYTES: [u8; 16] = [
1260 0x76, 0x6e, 0x9e, 0x9a, 0x44, 0x6d, 0x4e, 0x46, 0x83, 0xe6, 0x14, 0xb7, 0xca, 0x10, 0x11, 0x69,
1261];
1262const STATUS_FAILURE_BYTES: [u8; 16] = [
1264 0x24, 0x68, 0xf4, 0x6c, 0xbb, 0x60, 0x42, 0x5c, 0x9a, 0x4d, 0x9a, 0xd3, 0x26, 0xcc, 0xc7, 0xe2,
1265];
1266const STATUS_ENUM_BYTES: [u8; 16] = [
1268 0x32, 0x5a, 0x57, 0x67, 0xe3, 0x44, 0x45, 0x32, 0x86, 0x0e, 0x07, 0x49, 0xbc, 0xf2, 0xe4, 0x28,
1269];
1270const RET_PARAM_BYTES: [u8; 16] = [
1272 0x5f, 0x72, 0x65, 0x74, 0x00, 0x00, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1273];
1274
1275fn value_to_status(v: &Value) -> &'static str {
1276 if let Value::Enumeration(e) = v {
1277 if *e.variant_id.as_bytes() == STATUS_SUCCESS_BYTES {
1278 return "success";
1279 }
1280 if *e.variant_id.as_bytes() == STATUS_FAILURE_BYTES {
1281 return "failure";
1282 }
1283 }
1284 "running"
1285}
1286
1287#[derive(serde::Deserialize, Clone, Debug)]
1290#[serde(rename_all = "snake_case")]
1291enum BtExpression {
1292 VariableId(Uuid),
1293 Value(Value),
1294}
1295
1296#[derive(serde::Deserialize, Debug)]
1298struct BtNode {
1299 id: Uuid,
1300 function: Uuid,
1301 #[serde(default)]
1302 children: Option<Vec<Uuid>>,
1303 #[serde(default)]
1307 arguments: HashMap<Uuid, BtExpression>,
1308}
1309
1310struct FnMeta {
1312 module_id: Uuid,
1313 children_param_id: Option<Uuid>,
1316}
1317
1318struct NodeCallable {
1320 node_id: Uuid,
1321 fn_id: Uuid,
1322 module_id: Uuid,
1323 children_param_id: Option<Uuid>,
1324 children_callable_ids: Vec<u64>,
1325 arguments: HashMap<Uuid, BtExpression>,
1326 variables: Rc<RefCell<HashMap<Uuid, Value>>>,
1327 trace: Rc<RefCell<Vec<(Uuid, &'static str)>>>,
1328}
1329
1330impl Callable for NodeCallable {
1331 fn call(&self, caller: &mut dyn CallBridge) -> Result<Value, arora_engine::call::CallError> {
1332 let tick_id_type = Uuid::from_bytes(TICK_ID_STRUCT_BYTES);
1333 let callable_field = Uuid::from_bytes(TICK_ID_CALLABLE_FIELD_BYTES);
1334 let ret_param_id = Uuid::from_bytes(RET_PARAM_BYTES);
1335
1336 let mut args = Vec::new();
1337
1338 if let Some(children_param_id) = self.children_param_id {
1339 let elements: Vec<StructureWithoutId> = self
1340 .children_callable_ids
1341 .iter()
1342 .map(|&id| StructureWithoutId {
1343 fields: vec![StructureField {
1344 id: callable_field,
1345 value: Box::new(Value::U64(id)),
1346 }],
1347 })
1348 .collect();
1349 args.push(StructureField {
1350 id: children_param_id,
1351 value: Box::new(Value::ArrayStructure {
1352 id: tick_id_type,
1353 elements,
1354 }),
1355 });
1356 }
1357
1358 for (¶m_id, expr) in &self.arguments {
1359 if param_id == ret_param_id {
1360 continue;
1361 }
1362 let value = match expr {
1363 BtExpression::Value(v) => v.clone(),
1364 BtExpression::VariableId(var_id) => self
1365 .variables
1366 .borrow()
1367 .get(var_id)
1368 .cloned()
1369 .unwrap_or(Value::Unit),
1370 };
1371 args.push(StructureField {
1372 id: param_id,
1373 value: Box::new(value),
1374 });
1375 }
1376
1377 let mutable_param_vars: HashMap<Uuid, Uuid> = self
1380 .arguments
1381 .iter()
1382 .filter_map(|(¶m_id, expr)| {
1383 if param_id == ret_param_id {
1384 return None;
1385 }
1386 if let BtExpression::VariableId(var_id) = expr {
1387 Some((param_id, *var_id))
1388 } else {
1389 None
1390 }
1391 })
1392 .collect();
1393
1394 let result = caller.arora_call(Call {
1395 module_id: Some(self.module_id),
1396 id: self.fn_id,
1397 args,
1398 })?;
1399
1400 for mutated in &result.mutated {
1402 if let Some(&var_id) = mutable_param_vars.get(&mutated.id) {
1403 self.variables
1404 .borrow_mut()
1405 .insert(var_id, *mutated.value.clone());
1406 }
1407 }
1408
1409 let has_ret = self.arguments.contains_key(&ret_param_id);
1410 if has_ret {
1411 if let Some(BtExpression::VariableId(var_id)) = self.arguments.get(&ret_param_id) {
1412 self.variables
1413 .borrow_mut()
1414 .insert(*var_id, result.ret.clone());
1415 }
1416 }
1417
1418 let s = if has_ret {
1419 "success"
1420 } else {
1421 value_to_status(&result.ret)
1422 };
1423 self.trace.borrow_mut().push((self.node_id, s));
1424
1425 if has_ret {
1426 Ok(Value::Enumeration(Enumeration {
1427 id: Uuid::from_bytes(STATUS_ENUM_BYTES),
1428 variant_id: Uuid::from_bytes(STATUS_SUCCESS_BYTES),
1429 value: Box::new(Value::Unit),
1430 }))
1431 } else {
1432 Ok(result.ret)
1433 }
1434 }
1435}
1436
1437fn register_node(
1440 engine: &mut dyn CallBridge,
1441 node_id: Uuid,
1442 node_index: &HashMap<Uuid, BtNode>,
1443 fn_meta: &HashMap<Uuid, FnMeta>,
1444 trace: &Rc<RefCell<Vec<(Uuid, &'static str)>>>,
1445 variables: &Rc<RefCell<HashMap<Uuid, Value>>>,
1446) -> Result<u64, String> {
1447 let node = node_index
1448 .get(&node_id)
1449 .ok_or_else(|| format!("node {node_id} not found in tree"))?;
1450 let meta = fn_meta
1451 .get(&node.function)
1452 .ok_or_else(|| format!("function {} not registered in fn_meta", node.function))?;
1453
1454 let children_callable_ids = match &node.children {
1455 None => vec![],
1456 Some(ids) => ids
1457 .iter()
1458 .map(|&child_id| register_node(engine, child_id, node_index, fn_meta, trace, variables))
1459 .collect::<Result<Vec<_>, _>>()?,
1460 };
1461
1462 let callable: Rc<dyn Callable> = Rc::new(NodeCallable {
1463 node_id,
1464 fn_id: node.function,
1465 module_id: meta.module_id,
1466 children_param_id: meta.children_param_id,
1467 children_callable_ids,
1468 arguments: node.arguments.clone(),
1469 variables: variables.clone(),
1470 trace: trace.clone(),
1471 });
1472 let id = engine.arora_register_callable(callable);
1473 Ok(id.id)
1474}
1475
1476#[wasm_bindgen]
1485pub struct BehaviorTreeRunner {
1486 inner: Pin<Box<arora_engine::engine::Engine>>,
1487 loader: SharedLoaderRc,
1488 fn_meta: HashMap<Uuid, FnMeta>,
1489 variables: Rc<RefCell<HashMap<Uuid, Value>>>,
1490 module_headers: HashMap<Uuid, String>,
1491}
1492
1493#[wasm_bindgen]
1494impl BehaviorTreeRunner {
1495 #[wasm_bindgen(constructor)]
1496 pub fn new() -> BehaviorTreeRunner {
1497 install_panic_hook();
1498 let executor = BrowserExecutor::new();
1499 let loader = executor.shared();
1500 let inner = EngineBuilder::new().add_executor(executor).build();
1501 BehaviorTreeRunner {
1502 inner,
1503 loader,
1504 fn_meta: HashMap::new(),
1505 variables: Rc::new(RefCell::new(HashMap::new())),
1506 module_headers: HashMap::new(),
1507 }
1508 }
1509
1510 #[wasm_bindgen(js_name = loadModule)]
1518 pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
1519 self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
1520 }
1521
1522 #[wasm_bindgen(js_name = prepareModule)]
1526 pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
1527 prepare_module_impl(self.loader.clone(), header_json, executable)
1528 }
1529
1530 #[wasm_bindgen(js_name = loadPreparedModule)]
1533 pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
1534 self.load_module_inner(header_json, Box::new([]))
1535 }
1536
1537 fn load_module_inner(
1538 &mut self,
1539 header_json: &str,
1540 executable: Box<[u8]>,
1541 ) -> Result<String, JsValue> {
1542 let header: Header = serde_json::from_str(header_json)
1543 .map_err(|e| JsValue::from_str(&format!("invalid header: {e}")))?;
1544 let module_id = header.id;
1545 let header_json_str = header_json.to_string();
1546
1547 for export in &header.exports {
1548 let arora_types::module::low::ExportSymbol::Function(f) = export;
1549 let children_param_id = f.parameters.first().and_then(|p| {
1550 if let arora_types::module::low::TypeRef::Array { id } = &p.ty {
1551 if id == &Uuid::from_bytes(TICK_ID_STRUCT_BYTES) {
1552 Some(p.id)
1553 } else {
1554 None
1555 }
1556 } else {
1557 None
1558 }
1559 });
1560
1561 self.fn_meta.insert(
1562 f.id,
1563 FnMeta {
1564 module_id,
1565 children_param_id,
1566 },
1567 );
1568 }
1569
1570 let result = load_module_from_parts(&mut self.inner, header, executable)
1571 .map_err(|e| JsValue::from_str(&format!("load failed: {e}")))?;
1572 self.module_headers.insert(result.id, header_json_str);
1573 Ok(result.id.to_string())
1574 }
1575
1576 #[wasm_bindgen(js_name = listModules)]
1578 pub fn list_modules(&self) -> String {
1579 let headers: Vec<serde_json::Value> = self
1580 .module_headers
1581 .values()
1582 .filter_map(|s| serde_json::from_str(s).ok())
1583 .collect();
1584 serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
1585 }
1586
1587 #[wasm_bindgen(js_name = setVariable)]
1590 pub fn set_variable(&mut self, var_id: &str, value_json: &str) -> Result<(), JsValue> {
1591 let var_id: Uuid = var_id
1592 .parse()
1593 .map_err(|_| JsValue::from_str("bad var_id: not a valid UUID"))?;
1594 let value: Value = serde_json::from_str(value_json)
1595 .map_err(|e| JsValue::from_str(&format!("bad value JSON: {e}")))?;
1596 self.variables.borrow_mut().insert(var_id, value);
1597 Ok(())
1598 }
1599
1600 pub fn tick(&mut self, nodes_json: &str) -> Result<String, JsValue> {
1607 let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
1608 .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
1609 if nodes.is_empty() {
1610 return Err(JsValue::from_str("tree has no nodes"));
1611 }
1612
1613 let root_id = nodes[0].id;
1614 let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
1615 let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
1616
1617 let fn_meta = &self.fn_meta;
1618 let root_callable_id = register_node(
1619 &mut *self.inner,
1620 root_id,
1621 &node_index,
1622 fn_meta,
1623 &trace,
1624 &self.variables,
1625 )
1626 .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
1627
1628 let callable_id = CallableId {
1629 id: root_callable_id,
1630 };
1631 let result = Callable::call(&callable_id, &mut *self.inner)
1632 .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
1633 let status = value_to_status(&result);
1634
1635 let trace_json: Vec<serde_json::Value> = trace
1636 .borrow()
1637 .iter()
1638 .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
1639 .collect();
1640
1641 let vars_json: serde_json::Map<String, serde_json::Value> = self
1642 .variables
1643 .borrow()
1644 .iter()
1645 .filter_map(|(id, v)| serde_json::to_value(v).ok().map(|jv| (id.to_string(), jv)))
1646 .collect();
1647
1648 Ok(serde_json::json!({
1649 "status": status,
1650 "trace": trace_json,
1651 "variables": vars_json,
1652 })
1653 .to_string())
1654 }
1655
1656 pub fn run(&mut self, nodes_json: &str) -> Result<String, JsValue> {
1666 let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
1667 .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
1668 if nodes.is_empty() {
1669 return Err(JsValue::from_str("tree has no nodes"));
1670 }
1671
1672 let root_id = nodes[0].id;
1673 let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
1674 let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
1675
1676 let fn_meta = &self.fn_meta;
1677 let root_callable_id = register_node(
1678 &mut *self.inner,
1679 root_id,
1680 &node_index,
1681 fn_meta,
1682 &trace,
1683 &self.variables,
1684 )
1685 .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
1686
1687 let callable_id = CallableId {
1688 id: root_callable_id,
1689 };
1690
1691 let mut last_status = "running";
1692 for _ in 0..10_000 {
1693 let result = Callable::call(&callable_id, &mut *self.inner)
1694 .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
1695 last_status = value_to_status(&result);
1696 if last_status != "running" {
1697 break;
1698 }
1699 }
1700
1701 let trace_json: Vec<serde_json::Value> = trace
1702 .borrow()
1703 .iter()
1704 .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
1705 .collect();
1706
1707 let out = serde_json::json!({
1708 "status": last_status,
1709 "trace": trace_json,
1710 });
1711 Ok(out.to_string())
1712 }
1713}
1714
1715impl Default for BehaviorTreeRunner {
1716 fn default() -> Self {
1717 Self::new()
1718 }
1719}
1720
1721#[wasm_bindgen]
1730pub struct Registry {
1731 entries: HashMap<Uuid, (String, Option<Uuid>)>,
1732}
1733
1734#[derive(serde::Deserialize)]
1735struct RecordEntry {
1736 id: Uuid,
1737 name: String,
1738 parent: Option<Uuid>,
1739}
1740
1741#[wasm_bindgen]
1742impl Registry {
1743 #[wasm_bindgen(constructor)]
1744 pub fn new() -> Registry {
1745 Registry {
1746 entries: HashMap::new(),
1747 }
1748 }
1749
1750 #[wasm_bindgen(js_name = loadRecordsJson)]
1752 pub fn load_records_json(&mut self, json: &str) -> Result<(), JsValue> {
1753 let records: Vec<RecordEntry> = serde_json::from_str(json)
1754 .map_err(|e| JsValue::from_str(&format!("invalid records JSON: {e}")))?;
1755 for r in records {
1756 self.entries.insert(r.id, (r.name, r.parent));
1757 }
1758 Ok(())
1759 }
1760
1761 #[wasm_bindgen(js_name = resolveId)]
1764 pub fn resolve_id(&self, id: &str) -> Option<String> {
1765 let uuid: Uuid = id.parse().ok()?;
1766 self.compute_path(&uuid)
1767 }
1768
1769 fn compute_path(&self, id: &Uuid) -> Option<String> {
1770 let (name, parent) = self.entries.get(id)?;
1771 match parent {
1772 None => Some(name.clone()),
1773 Some(parent_id) if !self.entries.contains_key(parent_id) => Some(name.clone()),
1774 Some(parent_id) => Some(format!("{}.{}", self.compute_path(parent_id)?, name)),
1775 }
1776 }
1777}
1778
1779impl Default for Registry {
1780 fn default() -> Self {
1781 Self::new()
1782 }
1783}
1784
1785#[cfg(test)]
1789mod undrained_tests {
1790 use super::*;
1791 use wasm_bindgen_futures::JsFuture;
1792 use wasm_bindgen_test::*;
1793
1794 wasm_bindgen_test_configure!(run_in_browser);
1795
1796 const KEYS: usize = 200;
1798
1799 fn frame(store: &dyn DataStore, value: f32) {
1801 let mut change = StateChange::new();
1802 for key in 0..KEYS {
1803 change
1804 .set
1805 .insert(Key::new(format!("face/{key}")), Some(Value::F32(value)));
1806 }
1807 store.write(change).expect("write a frame");
1808 }
1809
1810 fn device() -> (AroraWeb, Box<dyn DataStore>) {
1813 let arora = Arora::builder().build().expect("build the device");
1814 let store = arora.store().clone_box();
1815 (AroraWeb::from(arora), store)
1816 }
1817
1818 fn channel_is_empty(web: &AroraWeb) -> bool {
1821 web.changes.borrow().subscription.try_recv().is_none()
1822 }
1823
1824 fn held(web: &AroraWeb, key: usize) -> Option<Value> {
1826 web.changes
1827 .borrow()
1828 .latest
1829 .get(&Key::new(format!("face/{key}")))
1830 .cloned()
1831 .flatten()
1832 }
1833
1834 async fn sleep(ms: i32) {
1835 let timer = js_sys::Promise::new(&mut |resolve, _| {
1836 let set_timeout: js_sys::Function =
1837 js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("setTimeout"))
1838 .expect("a global setTimeout")
1839 .into();
1840 set_timeout
1841 .call2(&JsValue::NULL, &resolve, &JsValue::from(ms))
1842 .expect("setTimeout");
1843 });
1844 JsFuture::from(timer).await.expect("the timer fires");
1845 }
1846
1847 #[wasm_bindgen_test]
1848 fn stepping_without_a_drain_holds_one_value_per_key() {
1849 let (web, store) = device();
1850 for step in 0..1_000 {
1851 frame(&*store, step as f32);
1852 web.step(10.0).expect("step");
1853 assert!(channel_is_empty(&web), "step {step} left changes unfolded");
1854 }
1855 let keys = store.snapshot().storage.len();
1856 assert!(keys >= KEYS, "the store holds the frame's keys");
1857 assert!(
1858 web.changes.borrow().latest.len() <= keys,
1859 "the backlog holds no more than one entry per key"
1860 );
1861 assert_eq!(held(&web, 7), Some(Value::F32(999.0)));
1862 }
1863
1864 #[wasm_bindgen_test]
1865 async fn a_running_device_folds_every_step() {
1866 let (web, store) = device();
1867 let running = web.run(Some(1.0));
1868 for step in 0..50 {
1869 frame(&*store, step as f32);
1870 sleep(5).await;
1872 assert!(channel_is_empty(&web), "step {step} left changes unfolded");
1873 assert_eq!(held(&web, 0), Some(Value::F32(step as f32)));
1874 }
1875 web.stop();
1876 JsFuture::from(running).await.expect("the run stops");
1877 }
1878
1879 #[wasm_bindgen_test]
1880 fn a_page_write_is_folded_at_once() {
1881 let (web, _store) = device();
1882 web.drain_changes().expect("the first drain");
1883 for value in 0..100 {
1884 web.set_value("face/0", &format!(r#"{{"f32": {value}}}"#))
1885 .expect("set a key");
1886 assert!(channel_is_empty(&web));
1887 }
1888 assert_eq!(held(&web, 0), Some(Value::F32(99.0)));
1889 }
1890}