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)]
314struct StepScheduler {
315 device: Rc<RefCell<Option<Arora>>>,
316 changes: Rc<RefCell<Undrained>>,
317 due: Rc<Cell<bool>>,
320 stepped: Rc<watch::Sender<Stepped>>,
321}
322
323impl StepScheduler {
324 fn new(device: Arora, changes: Rc<RefCell<Undrained>>) -> Self {
325 Self {
326 device: Rc::new(RefCell::new(Some(device))),
327 changes,
328 due: Rc::new(Cell::new(false)),
329 stepped: Rc::new(watch::channel(Stepped::Idle).0),
330 }
331 }
332
333 fn mid_step(&self) -> bool {
335 self.device.try_borrow_mut().is_err()
336 }
337
338 fn request(&self) {
342 if self.due.replace(true) {
343 return;
344 }
345 let scheduler = self.clone();
346 let callback = Closure::once_into_js(move || scheduler.step_if_due());
347 if self.mid_step() {
348 set_timeout(callback.unchecked_ref(), 0);
349 } else {
350 queue_microtask(callback.unchecked_ref());
351 }
352 }
353
354 fn met(&self) {
356 self.due.set(false);
357 }
358
359 fn failed(&self, message: String) {
361 self.stepped.send_replace(Stepped::Failed(message));
362 }
363
364 fn step_if_due(&self) {
365 if !self.due.replace(false) {
366 return;
367 }
368 let Ok(mut parked) = self.device.try_borrow_mut() else {
369 self.request();
372 return;
373 };
374 let Some(device) = parked.as_mut() else {
377 return;
378 };
379 let stepped = device.step(Duration::ZERO);
380 drop(parked);
381 self.changes.borrow_mut().fold();
382 if let Err(e) = stepped {
383 let message = e.to_string();
384 console_error(&format!("arora-web: a step at a zero dt failed: {message}"));
386 self.failed(message);
387 }
388 }
389}
390
391#[wasm_bindgen(js_name = AroraRuntime)]
453pub struct AroraWeb {
454 steps: StepScheduler,
458 stop: RefCell<Option<futures::channel::oneshot::Sender<()>>>,
461 caller: LocalCaller,
462 store: Box<dyn DataStore>,
463 methods: Vec<MethodSignature>,
466 changes: Rc<RefCell<Undrained>>,
469 behavior_error: watch::Receiver<Option<String>>,
472 behavior_error_changes: Rc<RefCell<Option<watch::Receiver<Option<String>>>>>,
476}
477
478impl From<Arora> for AroraWeb {
479 fn from(arora: Arora) -> Self {
483 install_panic_hook();
484 let caller = arora.caller();
485 let store = arora.store().clone_box();
486 let methods = arora.describe_methods(None);
487 let changes = Rc::new(RefCell::new(Undrained::new(store.subscribe())));
488 let behavior_error = arora.behavior_error();
489 let behavior_error_changes = Rc::new(RefCell::new(Some(arora.behavior_error())));
490 Self {
491 steps: StepScheduler::new(arora, changes.clone()),
492 stop: RefCell::new(None),
493 caller,
494 store,
495 methods,
496 changes,
497 behavior_error,
498 behavior_error_changes,
499 }
500 }
501}
502
503#[wasm_bindgen(js_class = "AroraRuntime")]
504impl AroraWeb {
505 #[wasm_bindgen(constructor)]
508 pub fn new() -> Result<AroraWeb, JsValue> {
509 Ok(AroraWeb::from(Arora::builder().build().map_err(|e| {
510 JsValue::from_str(&format!("arora build failed: {e:?}"))
511 })?))
512 }
513
514 pub fn step(&self, dt_ms: f64) -> Result<(), JsValue> {
522 let mut parked = self
523 .steps
524 .device
525 .try_borrow_mut()
526 .map_err(|_| JsValue::from_str("the device is mid-step; call between steps"))?;
527 let device = parked
528 .as_mut()
529 .ok_or_else(|| JsValue::from_str("run() drives the device; step() is unavailable"))?;
530 self.steps.met();
531 let stepped = device.step(Duration::from_secs_f64(dt_ms / 1_000.0));
532 drop(parked);
533 self.changes.borrow_mut().fold();
534 stepped.map_err(|e| {
535 self.steps.failed(e.to_string());
538 JsValue::from_str(&format!("step failed: {e}"))
539 })
540 }
541
542 pub fn run(&self, period_ms: Option<f64>) -> js_sys::Promise {
553 let taken = match self.steps.device.try_borrow_mut() {
554 Ok(mut parked) => parked.take(),
555 Err(_) => {
556 return js_sys::Promise::reject(&JsValue::from_str(
557 "the device is mid-step; call between steps",
558 ))
559 }
560 };
561 let Some(mut device) = taken else {
562 return js_sys::Promise::reject(&JsValue::from_str("the device is already running"));
563 };
564 let steps = self.steps.clone();
565 let changes = self.changes.clone();
566 let (stop_tx, stop_rx) = futures::channel::oneshot::channel::<()>();
567 *self.stop.borrow_mut() = Some(stop_tx);
568 wasm_bindgen_futures::future_to_promise(async move {
569 let period = period_ms
570 .map(|ms| Duration::from_secs_f64(ms / 1_000.0))
571 .unwrap_or(Arora::DEFAULT_STEP_PERIOD);
572 let outcome = {
573 let run = device.run(period);
574 futures::pin_mut!(run);
575 let run = futures::future::poll_fn(|cx| {
578 let polled = run.as_mut().poll(cx);
579 changes.borrow_mut().fold();
580 polled
581 })
582 .fuse();
583 let mut stop_rx = stop_rx.fuse();
584 futures::pin_mut!(run);
585 futures::select_biased! {
586 result = run => Some(result),
587 _ = stop_rx => None,
588 }
589 };
590 steps.device.borrow_mut().replace(device);
595 steps.stepped.send_replace(Stepped::Parked);
596 match outcome {
597 Some(result) => {
599 result.map_err(|e| JsValue::from_str(&format!("run failed: {e}")))?;
600 Ok(JsValue::UNDEFINED)
601 }
602 None => Ok(JsValue::UNDEFINED),
604 }
605 })
606 }
607
608 pub fn stop(&self) {
612 if let Some(stop) = self.stop.borrow_mut().take() {
613 let _ = stop.send(());
614 }
615 }
616
617 #[wasm_bindgen(getter)]
619 pub fn running(&self) -> bool {
620 self.steps
622 .device
623 .try_borrow()
624 .is_ok_and(|parked| parked.is_none())
625 }
626
627 #[wasm_bindgen(getter, js_name = behaviorError)]
632 pub fn behavior_error(&self) -> Option<String> {
633 self.behavior_error.borrow().clone()
634 }
635
636 #[wasm_bindgen(js_name = behaviorErrorChanged)]
642 pub fn behavior_error_changed(&self) -> js_sys::Promise {
643 let Some(mut errors) = self.behavior_error_changes.borrow_mut().take() else {
644 return js_sys::Promise::reject(&JsValue::from_str(
645 "a behaviorErrorChanged await is already pending",
646 ));
647 };
648 let home = self.behavior_error_changes.clone();
649 wasm_bindgen_futures::future_to_promise(async move {
650 let outcome = errors.changed().await;
651 let value = errors.borrow_and_update().clone();
652 home.borrow_mut().replace(errors);
653 outcome.map_err(|_| JsValue::from_str("the device is gone"))?;
654 Ok(match value {
655 Some(message) => JsValue::from_str(&message),
656 None => JsValue::UNDEFINED,
657 })
658 })
659 }
660
661 pub fn call(&self, call_json: &str) -> js_sys::Promise {
666 let call: Call = match serde_json::from_str(call_json) {
667 Ok(call) => call,
668 Err(e) => return reject(format!("invalid call json: {e}")),
669 };
670 let reply = self.call_now(call);
671 promise(async move {
672 let result = reply
673 .await
674 .map_err(|e| JsValue::from_str(&format!("call failed: {e}")))?;
675 let json = serde_json::to_string(&result)
676 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))?;
677 Ok(JsValue::from_str(&json))
678 })
679 }
680
681 pub fn invoke(
699 &self,
700 method: &str,
701 args_json: &str,
702 module_id: Option<String>,
703 ) -> js_sys::Promise {
704 let failed = |e: String| reject(format!("invoke {method} failed: {e}"));
705 let args: HashMap<String, Value> = match serde_json::from_str(args_json) {
706 Ok(args) => args,
707 Err(e) => return reject(format!("invalid args json: {e}")),
708 };
709 let module = match module_id.map(|id| Uuid::parse_str(&id).map_err(|e| (id, e))) {
710 None => None,
711 Some(Ok(module)) => Some(module),
712 Some(Err((id, e))) => return reject(format!("'{id}' is not a module id: {e}")),
713 };
714 let signature = match client::find_method(&self.methods, method, module) {
715 Ok(signature) => signature,
716 Err(e) => return failed(e),
717 };
718 let call = match client::call_of(signature, args) {
719 Ok(call) => call,
720 Err(e) => return failed(e),
721 };
722 let method = method.to_string();
723 let failed = move |e: CallError| JsValue::from_str(&format!("invoke {method} failed: {e}"));
724 if client::task_shaped(&signature.function) {
725 let caller = self.caller.clone();
726 let started = self.complete(async move { caller.spawn(call).await });
727 promise(async move { run_to_js(&started.await.map_err(failed)?) })
728 } else {
729 let returned = self.call_now(call);
730 promise(async move { value_to_js(&returned.await.map_err(failed)?.ret) })
731 }
732 }
733
734 pub fn spawn(&self, call_json: &str) -> js_sys::Promise {
743 let call: Call = match serde_json::from_str(call_json) {
744 Ok(call) => call,
745 Err(e) => return reject(format!("invalid call json: {e}")),
746 };
747 let caller = self.caller.clone();
748 let started = self.complete(async move { caller.spawn(call).await });
749 promise(async move {
750 let handle = started
751 .await
752 .map_err(|e| JsValue::from_str(&format!("spawn failed: {e}")))?;
753 run_to_js(&handle)
754 })
755 }
756
757 pub fn halt(&self, run_id: &str) -> js_sys::Promise {
762 let run = match Uuid::parse_str(run_id) {
763 Ok(run) => TaskId(run),
764 Err(e) => return reject(format!("'{run_id}' is not a run id: {e}")),
765 };
766 let caller = self.caller.clone();
767 let halted = self.complete(async move { caller.halt(run).await });
768 promise(async move {
769 halted
770 .await
771 .map_err(|e| JsValue::from_str(&format!("halt failed: {e}")))?;
772 Ok(JsValue::UNDEFINED)
773 })
774 }
775
776 #[wasm_bindgen(js_name = listKeys)]
783 pub fn list_keys(&self, prefix: Option<String>) -> js_sys::Promise {
784 let keys: Vec<KeyInfo> = arora::list_keys(&*self.store, prefix.as_deref())
785 .into_iter()
786 .map(|(path, meta)| KeyInfo { path, meta })
787 .collect();
788 settled(to_js(&keys))
789 }
790
791 #[wasm_bindgen(js_name = describeMethods)]
800 pub fn describe_methods(&self, prefix: Option<String>) -> js_sys::Promise {
801 let described = self
802 .methods
803 .iter()
804 .filter(|signature| {
805 prefix
806 .as_deref()
807 .is_none_or(|prefix| signature.name.starts_with(prefix))
808 })
809 .map(|signature| {
810 let mut method = serde_json::to_value(client::method_info(signature))
811 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))?;
812 method["module"] = serde_json::Value::String(signature.module_id.to_string());
813 Ok(method)
814 })
815 .collect::<Result<Vec<_>, JsValue>>();
816 settled(described.and_then(|methods| to_js(&methods)))
817 }
818
819 #[wasm_bindgen(js_name = setValue)]
824 pub fn set_value(&self, path: &str, value_json: &str) -> Result<(), JsValue> {
825 let written = store_json::set_value(&*self.store, path, value_json);
826 self.wrote(written)
827 }
828
829 #[wasm_bindgen(js_name = writeValues)]
833 pub fn write_values(&self, values_json: &str) -> Result<(), JsValue> {
834 let written = store_json::write_values(&*self.store, values_json);
835 self.wrote(written)
836 }
837
838 #[wasm_bindgen(js_name = readValues)]
840 pub fn read_values(&self, paths: JsValue) -> Result<JsValue, JsValue> {
841 store_json::read_values(&*self.store, paths)
842 }
843
844 pub fn snapshot(&self) -> Result<JsValue, JsValue> {
846 store_json::snapshot(&*self.store)
847 }
848
849 #[wasm_bindgen(js_name = drainChanges)]
856 pub fn drain_changes(&self) -> Result<JsValue, JsValue> {
857 self.changes.borrow_mut().drain()
858 }
859}
860
861impl AroraWeb {
862 pub fn caller(&self) -> LocalCaller {
869 self.caller.clone()
870 }
871
872 pub fn complete<V: 'static>(
886 &self,
887 reply: impl Future<Output = Result<V, CallError>> + 'static,
888 ) -> impl Future<Output = Result<V, CallError>> + 'static {
889 let mut reply = Box::pin(reply);
890 let first = reply.as_mut().poll(&mut std::task::Context::from_waker(
891 futures::task::noop_waker_ref(),
892 ));
893 let mut stepped = self.steps.stepped.subscribe();
894 let steps = self.steps.clone();
895 if first.is_pending() {
896 steps.request();
897 }
898 async move {
899 if let Poll::Ready(answer) = first {
900 return answer;
901 }
902 loop {
903 {
904 let changed = std::pin::pin!(stepped.changed());
907 if let Either::Left((answer, _)) =
908 futures::future::select(reply.as_mut(), changed).await
909 {
910 return answer;
911 }
912 }
913 let event = stepped.borrow_and_update().clone();
914 match event {
915 Stepped::Idle => {}
916 Stepped::Failed(message) => {
917 return match reply.as_mut().now_or_never() {
918 Some(answer) => answer,
919 None => Err(CallError::Generic {
920 message: format!("step failed: {message}"),
921 }),
922 }
923 }
924 Stepped::Parked => steps.request(),
925 }
926 }
927 }
928 }
929
930 fn wrote(&self, written: Result<(), JsValue>) -> Result<(), JsValue> {
938 self.changes.borrow_mut().fold();
939 if written.is_ok() && !self.steps.mid_step() {
940 self.steps.request();
941 }
942 written
943 }
944
945 fn call_now(
947 &self,
948 call: Call,
949 ) -> impl Future<Output = Result<arora_types::call::CallResult, CallError>> + 'static {
950 let caller = self.caller.clone();
951 self.complete(async move { caller.call(call).await })
952 }
953}
954
955fn reject(message: String) -> js_sys::Promise {
957 js_sys::Promise::reject(&JsValue::from_str(&message))
958}
959
960fn settled(answer: Result<JsValue, JsValue>) -> js_sys::Promise {
962 match answer {
963 Ok(value) => js_sys::Promise::resolve(&value),
964 Err(error) => js_sys::Promise::reject(&error),
965 }
966}
967
968#[wasm_bindgen(js_name = AroraRuntimeBuilder)]
975#[derive(Default)]
976pub struct AroraWebBuilder {
977 inner: AroraBuilder,
978 records: arora::LocalRegistry,
979}
980
981#[wasm_bindgen(js_class = "AroraRuntimeBuilder")]
982impl AroraWebBuilder {
983 #[wasm_bindgen(constructor)]
984 pub fn new() -> AroraWebBuilder {
985 AroraWebBuilder::default()
986 }
987
988 #[wasm_bindgen(js_name = withModule)]
993 pub fn with_module(&mut self, header_json: &str, executable: &[u8]) -> Result<(), JsValue> {
994 let header: Header = serde_json::from_str(header_json)
995 .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
996 let inner = std::mem::take(&mut self.inner);
997 self.inner = inner.with_module(header, executable.to_vec());
998 Ok(())
999 }
1000
1001 #[wasm_bindgen(js_name = withCompiledModule)]
1007 pub fn with_compiled_module(&mut self, module: &CompiledModule) {
1008 let inner = std::mem::take(&mut self.inner);
1009 self.inner = inner.with_compiled_module(&module.inner);
1010 }
1011
1012 #[wasm_bindgen(js_name = withRecords)]
1028 pub fn with_records(
1029 &mut self,
1030 #[wasm_bindgen(unchecked_param_type = "Record<string, string>")] records: JsValue,
1031 ) -> Result<(), JsValue> {
1032 use arora_registry::local_yaml::{load_records, RecordKind};
1033 use futures::FutureExt;
1034
1035 let files: std::collections::BTreeMap<String, String> =
1036 serde_wasm_bindgen::from_value(records).map_err(|e| {
1037 JsValue::from_str(&format!(
1038 "records must map each record file's path to its YAML: {e}"
1039 ))
1040 })?;
1041 let mut entries = Vec::with_capacity(files.len());
1042 for (path, yaml) in &files {
1043 let record = path.split_once('/').and_then(|(directory, file)| {
1044 Some((
1045 RecordKind::from_directory(directory)?,
1046 file.strip_suffix(".yaml")?,
1047 ))
1048 });
1049 let Some((kind, stem)) = record else {
1050 return Err(JsValue::from_str(&format!(
1051 "'{path}' is not a record file's path: <folder|enumeration|structure|module>/<id>[@<version>].yaml"
1052 )));
1053 };
1054 entries.push((kind, stem, yaml.clone()));
1055 }
1056 load_records(entries, &mut self.records)
1057 .now_or_never()
1058 .ok_or_else(|| JsValue::from_str("the records did not load synchronously"))?
1059 .map_err(|e| JsValue::from_str(&format!("the records do not load: {e}")))
1060 }
1061
1062 pub fn build(&mut self) -> Result<AroraWeb, JsValue> {
1065 let arora = std::mem::take(&mut self.inner)
1066 .with_registry(std::mem::take(&mut self.records))
1067 .build()
1068 .map_err(|e| JsValue::from_str(&format!("arora build failed: {e:?}")))?;
1069 Ok(AroraWeb::from(arora))
1070 }
1071}
1072
1073#[wasm_bindgen]
1077pub struct CompiledModule {
1078 inner: arora::CompiledModule,
1079}
1080
1081#[wasm_bindgen]
1082impl CompiledModule {
1083 #[wasm_bindgen(constructor)]
1089 pub fn new(header_json: &str, executable: &[u8]) -> Result<CompiledModule, JsValue> {
1090 install_panic_hook();
1091 let header: Header = serde_json::from_str(header_json)
1092 .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
1093 let inner = arora::CompiledModule::new(header, executable)
1094 .map_err(|e| JsValue::from_str(&format!("module does not compile: {e}")))?;
1095 Ok(CompiledModule { inner })
1096 }
1097}
1098
1099#[wasm_bindgen]
1101pub struct Engine {
1102 inner: std::pin::Pin<Box<arora_engine::engine::Engine>>,
1103 loader: SharedLoaderRc,
1104 function_module: HashMap<Uuid, Uuid>,
1105 module_headers: HashMap<Uuid, String>,
1106}
1107
1108#[wasm_bindgen]
1109impl Engine {
1110 #[wasm_bindgen(constructor)]
1111 pub fn new() -> Engine {
1112 install_panic_hook();
1113 let executor = BrowserExecutor::new();
1114 let loader = executor.shared();
1115 let inner = EngineBuilder::new().add_executor(executor).build();
1116 Engine {
1117 inner,
1118 loader,
1119 function_module: HashMap::new(),
1120 module_headers: HashMap::new(),
1121 }
1122 }
1123
1124 #[wasm_bindgen(js_name = loadModule)]
1131 pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
1132 self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
1133 }
1134
1135 #[wasm_bindgen(js_name = prepareModule)]
1139 pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
1140 prepare_module_impl(self.loader.clone(), header_json, executable)
1141 }
1142
1143 #[wasm_bindgen(js_name = loadPreparedModule)]
1146 pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
1147 self.load_module_inner(header_json, Box::new([]))
1148 }
1149
1150 fn load_module_inner(
1151 &mut self,
1152 header_json: &str,
1153 executable: Box<[u8]>,
1154 ) -> Result<String, JsValue> {
1155 let header: Header = serde_json::from_str(header_json)
1156 .map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
1157 let header_json_str = header_json.to_string();
1158 let loaded = load_module_from_parts(&mut self.inner, header, executable)
1159 .map_err(|e| JsValue::from_str(&format!("load_module failed: {e}")))?;
1160 for fn_id in &loaded.function_ids {
1161 self.function_module.insert(*fn_id, loaded.id);
1162 }
1163 self.module_headers.insert(loaded.id, header_json_str);
1164 Ok(loaded.id.to_string())
1165 }
1166
1167 #[wasm_bindgen(js_name = listModules)]
1169 pub fn list_modules(&self) -> String {
1170 let headers: Vec<serde_json::Value> = self
1171 .module_headers
1172 .values()
1173 .filter_map(|s| serde_json::from_str(s).ok())
1174 .collect();
1175 serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
1176 }
1177
1178 #[wasm_bindgen]
1181 pub fn call(&mut self, call_json: &str) -> Result<String, JsValue> {
1182 let mut call: Call = serde_json::from_str(call_json)
1183 .map_err(|e| JsValue::from_str(&format!("invalid call json: {e}")))?;
1184 if call.module_id.is_none() {
1185 call.module_id = Some(
1187 *self
1188 .function_module
1189 .get(&call.id)
1190 .ok_or_else(|| JsValue::from_str("no module known for function"))?,
1191 );
1192 }
1193 let result = self
1194 .inner
1195 .arora_call(call)
1196 .map_err(|e| JsValue::from_str(&format!("call failed: {e}")))?;
1197 serde_json::to_string(&result)
1198 .map_err(|e| JsValue::from_str(&format!("serialize failed: {e}")))
1199 }
1200}
1201
1202impl Default for Engine {
1203 fn default() -> Self {
1204 Self::new()
1205 }
1206}
1207
1208fn prepare_module_impl(
1212 loader: SharedLoaderRc,
1213 header_json: &str,
1214 executable: Vec<u8>,
1215) -> js_sys::Promise {
1216 let header: Result<Header, _> = serde_json::from_str(header_json);
1217 wasm_bindgen_futures::future_to_promise(async move {
1218 let header = header.map_err(|e| JsValue::from_str(&format!("invalid header json: {e}")))?;
1219 loader.prepare(header.id, executable).await?;
1220 Ok(JsValue::UNDEFINED)
1221 })
1222}
1223
1224const TICK_ID_STRUCT_BYTES: [u8; 16] = [
1234 0x6f, 0x49, 0xe6, 0x50, 0x84, 0xca, 0x48, 0x99, 0xa9, 0xbd, 0x1f, 0x3b, 0xf1, 0x7f, 0xab, 0x51,
1235];
1236const TICK_ID_CALLABLE_FIELD_BYTES: [u8; 16] = [
1238 0x23, 0x79, 0x92, 0xd2, 0x17, 0xd1, 0x45, 0x9f, 0xbc, 0xa1, 0x71, 0x85, 0xfa, 0x6a, 0x69, 0xd7,
1239];
1240const STATUS_SUCCESS_BYTES: [u8; 16] = [
1242 0x76, 0x6e, 0x9e, 0x9a, 0x44, 0x6d, 0x4e, 0x46, 0x83, 0xe6, 0x14, 0xb7, 0xca, 0x10, 0x11, 0x69,
1243];
1244const STATUS_FAILURE_BYTES: [u8; 16] = [
1246 0x24, 0x68, 0xf4, 0x6c, 0xbb, 0x60, 0x42, 0x5c, 0x9a, 0x4d, 0x9a, 0xd3, 0x26, 0xcc, 0xc7, 0xe2,
1247];
1248const STATUS_ENUM_BYTES: [u8; 16] = [
1250 0x32, 0x5a, 0x57, 0x67, 0xe3, 0x44, 0x45, 0x32, 0x86, 0x0e, 0x07, 0x49, 0xbc, 0xf2, 0xe4, 0x28,
1251];
1252const RET_PARAM_BYTES: [u8; 16] = [
1254 0x5f, 0x72, 0x65, 0x74, 0x00, 0x00, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1255];
1256
1257fn value_to_status(v: &Value) -> &'static str {
1258 if let Value::Enumeration(e) = v {
1259 if *e.variant_id.as_bytes() == STATUS_SUCCESS_BYTES {
1260 return "success";
1261 }
1262 if *e.variant_id.as_bytes() == STATUS_FAILURE_BYTES {
1263 return "failure";
1264 }
1265 }
1266 "running"
1267}
1268
1269#[derive(serde::Deserialize, Clone, Debug)]
1272#[serde(rename_all = "snake_case")]
1273enum BtExpression {
1274 VariableId(Uuid),
1275 Value(Value),
1276}
1277
1278#[derive(serde::Deserialize, Debug)]
1280struct BtNode {
1281 id: Uuid,
1282 function: Uuid,
1283 #[serde(default)]
1284 children: Option<Vec<Uuid>>,
1285 #[serde(default)]
1289 arguments: HashMap<Uuid, BtExpression>,
1290}
1291
1292struct FnMeta {
1294 module_id: Uuid,
1295 children_param_id: Option<Uuid>,
1298}
1299
1300struct NodeCallable {
1302 node_id: Uuid,
1303 fn_id: Uuid,
1304 module_id: Uuid,
1305 children_param_id: Option<Uuid>,
1306 children_callable_ids: Vec<u64>,
1307 arguments: HashMap<Uuid, BtExpression>,
1308 variables: Rc<RefCell<HashMap<Uuid, Value>>>,
1309 trace: Rc<RefCell<Vec<(Uuid, &'static str)>>>,
1310}
1311
1312impl Callable for NodeCallable {
1313 fn call(&self, caller: &mut dyn CallBridge) -> Result<Value, arora_engine::call::CallError> {
1314 let tick_id_type = Uuid::from_bytes(TICK_ID_STRUCT_BYTES);
1315 let callable_field = Uuid::from_bytes(TICK_ID_CALLABLE_FIELD_BYTES);
1316 let ret_param_id = Uuid::from_bytes(RET_PARAM_BYTES);
1317
1318 let mut args = Vec::new();
1319
1320 if let Some(children_param_id) = self.children_param_id {
1321 let elements: Vec<StructureWithoutId> = self
1322 .children_callable_ids
1323 .iter()
1324 .map(|&id| StructureWithoutId {
1325 fields: vec![StructureField {
1326 id: callable_field,
1327 value: Box::new(Value::U64(id)),
1328 }],
1329 })
1330 .collect();
1331 args.push(StructureField {
1332 id: children_param_id,
1333 value: Box::new(Value::ArrayStructure {
1334 id: tick_id_type,
1335 elements,
1336 }),
1337 });
1338 }
1339
1340 for (¶m_id, expr) in &self.arguments {
1341 if param_id == ret_param_id {
1342 continue;
1343 }
1344 let value = match expr {
1345 BtExpression::Value(v) => v.clone(),
1346 BtExpression::VariableId(var_id) => self
1347 .variables
1348 .borrow()
1349 .get(var_id)
1350 .cloned()
1351 .unwrap_or(Value::Unit),
1352 };
1353 args.push(StructureField {
1354 id: param_id,
1355 value: Box::new(value),
1356 });
1357 }
1358
1359 let mutable_param_vars: HashMap<Uuid, Uuid> = self
1362 .arguments
1363 .iter()
1364 .filter_map(|(¶m_id, expr)| {
1365 if param_id == ret_param_id {
1366 return None;
1367 }
1368 if let BtExpression::VariableId(var_id) = expr {
1369 Some((param_id, *var_id))
1370 } else {
1371 None
1372 }
1373 })
1374 .collect();
1375
1376 let result = caller.arora_call(Call {
1377 module_id: Some(self.module_id),
1378 id: self.fn_id,
1379 args,
1380 })?;
1381
1382 for mutated in &result.mutated {
1384 if let Some(&var_id) = mutable_param_vars.get(&mutated.id) {
1385 self.variables
1386 .borrow_mut()
1387 .insert(var_id, *mutated.value.clone());
1388 }
1389 }
1390
1391 let has_ret = self.arguments.contains_key(&ret_param_id);
1392 if has_ret {
1393 if let Some(BtExpression::VariableId(var_id)) = self.arguments.get(&ret_param_id) {
1394 self.variables
1395 .borrow_mut()
1396 .insert(*var_id, result.ret.clone());
1397 }
1398 }
1399
1400 let s = if has_ret {
1401 "success"
1402 } else {
1403 value_to_status(&result.ret)
1404 };
1405 self.trace.borrow_mut().push((self.node_id, s));
1406
1407 if has_ret {
1408 Ok(Value::Enumeration(Enumeration {
1409 id: Uuid::from_bytes(STATUS_ENUM_BYTES),
1410 variant_id: Uuid::from_bytes(STATUS_SUCCESS_BYTES),
1411 value: Box::new(Value::Unit),
1412 }))
1413 } else {
1414 Ok(result.ret)
1415 }
1416 }
1417}
1418
1419fn register_node(
1422 engine: &mut dyn CallBridge,
1423 node_id: Uuid,
1424 node_index: &HashMap<Uuid, BtNode>,
1425 fn_meta: &HashMap<Uuid, FnMeta>,
1426 trace: &Rc<RefCell<Vec<(Uuid, &'static str)>>>,
1427 variables: &Rc<RefCell<HashMap<Uuid, Value>>>,
1428) -> Result<u64, String> {
1429 let node = node_index
1430 .get(&node_id)
1431 .ok_or_else(|| format!("node {node_id} not found in tree"))?;
1432 let meta = fn_meta
1433 .get(&node.function)
1434 .ok_or_else(|| format!("function {} not registered in fn_meta", node.function))?;
1435
1436 let children_callable_ids = match &node.children {
1437 None => vec![],
1438 Some(ids) => ids
1439 .iter()
1440 .map(|&child_id| register_node(engine, child_id, node_index, fn_meta, trace, variables))
1441 .collect::<Result<Vec<_>, _>>()?,
1442 };
1443
1444 let callable: Rc<dyn Callable> = Rc::new(NodeCallable {
1445 node_id,
1446 fn_id: node.function,
1447 module_id: meta.module_id,
1448 children_param_id: meta.children_param_id,
1449 children_callable_ids,
1450 arguments: node.arguments.clone(),
1451 variables: variables.clone(),
1452 trace: trace.clone(),
1453 });
1454 let id = engine.arora_register_callable(callable);
1455 Ok(id.id)
1456}
1457
1458#[wasm_bindgen]
1467pub struct BehaviorTreeRunner {
1468 inner: Pin<Box<arora_engine::engine::Engine>>,
1469 loader: SharedLoaderRc,
1470 fn_meta: HashMap<Uuid, FnMeta>,
1471 variables: Rc<RefCell<HashMap<Uuid, Value>>>,
1472 module_headers: HashMap<Uuid, String>,
1473}
1474
1475#[wasm_bindgen]
1476impl BehaviorTreeRunner {
1477 #[wasm_bindgen(constructor)]
1478 pub fn new() -> BehaviorTreeRunner {
1479 install_panic_hook();
1480 let executor = BrowserExecutor::new();
1481 let loader = executor.shared();
1482 let inner = EngineBuilder::new().add_executor(executor).build();
1483 BehaviorTreeRunner {
1484 inner,
1485 loader,
1486 fn_meta: HashMap::new(),
1487 variables: Rc::new(RefCell::new(HashMap::new())),
1488 module_headers: HashMap::new(),
1489 }
1490 }
1491
1492 #[wasm_bindgen(js_name = loadModule)]
1500 pub fn load_module(&mut self, header_json: &str, executable: &[u8]) -> Result<String, JsValue> {
1501 self.load_module_inner(header_json, executable.to_vec().into_boxed_slice())
1502 }
1503
1504 #[wasm_bindgen(js_name = prepareModule)]
1508 pub fn prepare_module(&self, header_json: &str, executable: Vec<u8>) -> js_sys::Promise {
1509 prepare_module_impl(self.loader.clone(), header_json, executable)
1510 }
1511
1512 #[wasm_bindgen(js_name = loadPreparedModule)]
1515 pub fn load_prepared_module(&mut self, header_json: &str) -> Result<String, JsValue> {
1516 self.load_module_inner(header_json, Box::new([]))
1517 }
1518
1519 fn load_module_inner(
1520 &mut self,
1521 header_json: &str,
1522 executable: Box<[u8]>,
1523 ) -> Result<String, JsValue> {
1524 let header: Header = serde_json::from_str(header_json)
1525 .map_err(|e| JsValue::from_str(&format!("invalid header: {e}")))?;
1526 let module_id = header.id;
1527 let header_json_str = header_json.to_string();
1528
1529 for export in &header.exports {
1530 let arora_types::module::low::ExportSymbol::Function(f) = export;
1531 let children_param_id = f.parameters.first().and_then(|p| {
1532 if let arora_types::module::low::TypeRef::Array { id } = &p.ty {
1533 if id == &Uuid::from_bytes(TICK_ID_STRUCT_BYTES) {
1534 Some(p.id)
1535 } else {
1536 None
1537 }
1538 } else {
1539 None
1540 }
1541 });
1542
1543 self.fn_meta.insert(
1544 f.id,
1545 FnMeta {
1546 module_id,
1547 children_param_id,
1548 },
1549 );
1550 }
1551
1552 let result = load_module_from_parts(&mut self.inner, header, executable)
1553 .map_err(|e| JsValue::from_str(&format!("load failed: {e}")))?;
1554 self.module_headers.insert(result.id, header_json_str);
1555 Ok(result.id.to_string())
1556 }
1557
1558 #[wasm_bindgen(js_name = listModules)]
1560 pub fn list_modules(&self) -> String {
1561 let headers: Vec<serde_json::Value> = self
1562 .module_headers
1563 .values()
1564 .filter_map(|s| serde_json::from_str(s).ok())
1565 .collect();
1566 serde_json::to_string(&headers).unwrap_or_else(|_| "[]".to_string())
1567 }
1568
1569 #[wasm_bindgen(js_name = setVariable)]
1572 pub fn set_variable(&mut self, var_id: &str, value_json: &str) -> Result<(), JsValue> {
1573 let var_id: Uuid = var_id
1574 .parse()
1575 .map_err(|_| JsValue::from_str("bad var_id: not a valid UUID"))?;
1576 let value: Value = serde_json::from_str(value_json)
1577 .map_err(|e| JsValue::from_str(&format!("bad value JSON: {e}")))?;
1578 self.variables.borrow_mut().insert(var_id, value);
1579 Ok(())
1580 }
1581
1582 pub fn tick(&mut self, nodes_json: &str) -> Result<String, JsValue> {
1589 let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
1590 .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
1591 if nodes.is_empty() {
1592 return Err(JsValue::from_str("tree has no nodes"));
1593 }
1594
1595 let root_id = nodes[0].id;
1596 let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
1597 let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
1598
1599 let fn_meta = &self.fn_meta;
1600 let root_callable_id = register_node(
1601 &mut *self.inner,
1602 root_id,
1603 &node_index,
1604 fn_meta,
1605 &trace,
1606 &self.variables,
1607 )
1608 .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
1609
1610 let callable_id = CallableId {
1611 id: root_callable_id,
1612 };
1613 let result = Callable::call(&callable_id, &mut *self.inner)
1614 .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
1615 let status = value_to_status(&result);
1616
1617 let trace_json: Vec<serde_json::Value> = trace
1618 .borrow()
1619 .iter()
1620 .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
1621 .collect();
1622
1623 let vars_json: serde_json::Map<String, serde_json::Value> = self
1624 .variables
1625 .borrow()
1626 .iter()
1627 .filter_map(|(id, v)| serde_json::to_value(v).ok().map(|jv| (id.to_string(), jv)))
1628 .collect();
1629
1630 Ok(serde_json::json!({
1631 "status": status,
1632 "trace": trace_json,
1633 "variables": vars_json,
1634 })
1635 .to_string())
1636 }
1637
1638 pub fn run(&mut self, nodes_json: &str) -> Result<String, JsValue> {
1648 let nodes: Vec<BtNode> = serde_json::from_str(nodes_json)
1649 .map_err(|e| JsValue::from_str(&format!("bad nodes JSON: {e}")))?;
1650 if nodes.is_empty() {
1651 return Err(JsValue::from_str("tree has no nodes"));
1652 }
1653
1654 let root_id = nodes[0].id;
1655 let node_index: HashMap<Uuid, BtNode> = nodes.into_iter().map(|n| (n.id, n)).collect();
1656 let trace: Rc<RefCell<Vec<(Uuid, &'static str)>>> = Rc::new(RefCell::new(Vec::new()));
1657
1658 let fn_meta = &self.fn_meta;
1659 let root_callable_id = register_node(
1660 &mut *self.inner,
1661 root_id,
1662 &node_index,
1663 fn_meta,
1664 &trace,
1665 &self.variables,
1666 )
1667 .map_err(|e| JsValue::from_str(&format!("setup error: {e}")))?;
1668
1669 let callable_id = CallableId {
1670 id: root_callable_id,
1671 };
1672
1673 let mut last_status = "running";
1674 for _ in 0..10_000 {
1675 let result = Callable::call(&callable_id, &mut *self.inner)
1676 .map_err(|e| JsValue::from_str(&format!("tick error: {e}")))?;
1677 last_status = value_to_status(&result);
1678 if last_status != "running" {
1679 break;
1680 }
1681 }
1682
1683 let trace_json: Vec<serde_json::Value> = trace
1684 .borrow()
1685 .iter()
1686 .map(|(id, s)| serde_json::json!({ "nodeId": id.to_string(), "status": s }))
1687 .collect();
1688
1689 let out = serde_json::json!({
1690 "status": last_status,
1691 "trace": trace_json,
1692 });
1693 Ok(out.to_string())
1694 }
1695}
1696
1697impl Default for BehaviorTreeRunner {
1698 fn default() -> Self {
1699 Self::new()
1700 }
1701}
1702
1703#[wasm_bindgen]
1712pub struct Registry {
1713 entries: HashMap<Uuid, (String, Option<Uuid>)>,
1714}
1715
1716#[derive(serde::Deserialize)]
1717struct RecordEntry {
1718 id: Uuid,
1719 name: String,
1720 parent: Option<Uuid>,
1721}
1722
1723#[wasm_bindgen]
1724impl Registry {
1725 #[wasm_bindgen(constructor)]
1726 pub fn new() -> Registry {
1727 Registry {
1728 entries: HashMap::new(),
1729 }
1730 }
1731
1732 #[wasm_bindgen(js_name = loadRecordsJson)]
1734 pub fn load_records_json(&mut self, json: &str) -> Result<(), JsValue> {
1735 let records: Vec<RecordEntry> = serde_json::from_str(json)
1736 .map_err(|e| JsValue::from_str(&format!("invalid records JSON: {e}")))?;
1737 for r in records {
1738 self.entries.insert(r.id, (r.name, r.parent));
1739 }
1740 Ok(())
1741 }
1742
1743 #[wasm_bindgen(js_name = resolveId)]
1746 pub fn resolve_id(&self, id: &str) -> Option<String> {
1747 let uuid: Uuid = id.parse().ok()?;
1748 self.compute_path(&uuid)
1749 }
1750
1751 fn compute_path(&self, id: &Uuid) -> Option<String> {
1752 let (name, parent) = self.entries.get(id)?;
1753 match parent {
1754 None => Some(name.clone()),
1755 Some(parent_id) if !self.entries.contains_key(parent_id) => Some(name.clone()),
1756 Some(parent_id) => Some(format!("{}.{}", self.compute_path(parent_id)?, name)),
1757 }
1758 }
1759}
1760
1761impl Default for Registry {
1762 fn default() -> Self {
1763 Self::new()
1764 }
1765}
1766
1767#[cfg(test)]
1771mod undrained_tests {
1772 use super::*;
1773 use wasm_bindgen_futures::JsFuture;
1774 use wasm_bindgen_test::*;
1775
1776 wasm_bindgen_test_configure!(run_in_browser);
1777
1778 const KEYS: usize = 200;
1780
1781 fn frame(store: &dyn DataStore, value: f32) {
1783 let mut change = StateChange::new();
1784 for key in 0..KEYS {
1785 change
1786 .set
1787 .insert(Key::new(format!("face/{key}")), Some(Value::F32(value)));
1788 }
1789 store.write(change).expect("write a frame");
1790 }
1791
1792 fn device() -> (AroraWeb, Box<dyn DataStore>) {
1795 let arora = Arora::builder().build().expect("build the device");
1796 let store = arora.store().clone_box();
1797 (AroraWeb::from(arora), store)
1798 }
1799
1800 fn channel_is_empty(web: &AroraWeb) -> bool {
1803 web.changes.borrow().subscription.try_recv().is_none()
1804 }
1805
1806 fn held(web: &AroraWeb, key: usize) -> Option<Value> {
1808 web.changes
1809 .borrow()
1810 .latest
1811 .get(&Key::new(format!("face/{key}")))
1812 .cloned()
1813 .flatten()
1814 }
1815
1816 async fn sleep(ms: i32) {
1817 let timer = js_sys::Promise::new(&mut |resolve, _| {
1818 let set_timeout: js_sys::Function =
1819 js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("setTimeout"))
1820 .expect("a global setTimeout")
1821 .into();
1822 set_timeout
1823 .call2(&JsValue::NULL, &resolve, &JsValue::from(ms))
1824 .expect("setTimeout");
1825 });
1826 JsFuture::from(timer).await.expect("the timer fires");
1827 }
1828
1829 #[wasm_bindgen_test]
1830 fn stepping_without_a_drain_holds_one_value_per_key() {
1831 let (web, store) = device();
1832 for step in 0..1_000 {
1833 frame(&*store, step as f32);
1834 web.step(10.0).expect("step");
1835 assert!(channel_is_empty(&web), "step {step} left changes unfolded");
1836 }
1837 let keys = store.snapshot().storage.len();
1838 assert!(keys >= KEYS, "the store holds the frame's keys");
1839 assert!(
1840 web.changes.borrow().latest.len() <= keys,
1841 "the backlog holds no more than one entry per key"
1842 );
1843 assert_eq!(held(&web, 7), Some(Value::F32(999.0)));
1844 }
1845
1846 #[wasm_bindgen_test]
1847 async fn a_running_device_folds_every_step() {
1848 let (web, store) = device();
1849 let running = web.run(Some(1.0));
1850 for step in 0..50 {
1851 frame(&*store, step as f32);
1852 sleep(5).await;
1854 assert!(channel_is_empty(&web), "step {step} left changes unfolded");
1855 assert_eq!(held(&web, 0), Some(Value::F32(step as f32)));
1856 }
1857 web.stop();
1858 JsFuture::from(running).await.expect("the run stops");
1859 }
1860
1861 #[wasm_bindgen_test]
1862 fn a_page_write_is_folded_at_once() {
1863 let (web, _store) = device();
1864 web.drain_changes().expect("the first drain");
1865 for value in 0..100 {
1866 web.set_value("face/0", &format!(r#"{{"f32": {value}}}"#))
1867 .expect("set a key");
1868 assert!(channel_is_empty(&web));
1869 }
1870 assert_eq!(held(&web, 0), Some(Value::F32(99.0)));
1871 }
1872}