carapace_wasm/boundary.rs
1//! The `wasm_bindgen` boundary for `input_v1` (`CarapaceInputV1`
2//! decode/`Session`) API. First pass: a real JS object/array, deserialized
3//! via `serde-wasm-bindgen`, not yet the transferable-typed-array wire
4//! format intended for `postMessage` — see
5//! `input_v1`'s module doc comment for why that's deferred. This is
6//! enough to drive `decode`/`Session::advance` from real JS (or `pysees`)
7//! code end to end; the zero-copy transfer optimization can replace
8//! `serde-wasm-bindgen`'s JSON-ish decoding later without changing this
9//! module's shape (`decodeInput` in, `WasmSession` methods out).
10//! `Ts<T>` connects signatures to generated TypeScript definitions while
11//! keeping data conversion fallible inside the function body.
12
13use tsify::{Ts, Tsify};
14use wasm_bindgen::prelude::*;
15
16use crate::input_v1::{self, CarapaceInputV1};
17use crate::material_probe::{MaterialProbe, MaterialProbeConfig, MaterialProbeError, MaterialProbeResponse};
18
19/// Opaque handle to a decoded, steppable analysis session — the `Session`
20/// enum itself can't cross the boundary directly, since `wasm_bindgen`
21/// requires an exported type to be a plain struct.
22#[wasm_bindgen]
23pub struct WasmSession(input_v1::Session);
24
25/// Decodes a `CarapaceInputV1`-shaped JS value (see `input_v1`'s table
26/// doc comments for the exact field names — `#[serde(rename_all =
27/// "camelCase")]` throughout) into a steppable [`WasmSession`]. Rejects
28/// with a `{ kind: "...", ... }`-shaped JS error object on any
29/// [`input_v1::DecodeError`], the same tagged shape a Rust caller would
30/// match on.
31#[wasm_bindgen(js_name = decodeInput)]
32pub fn decode_input(value: Ts<CarapaceInputV1>) -> Result<WasmSession, JsValue> {
33 let input: CarapaceInputV1 = value
34 .to_rust()
35 .map_err(|error| JsValue::from_str(&format!("malformed CarapaceInputV1: {error}")))?;
36 let session = input_v1::decode(input).map_err(to_js_error)?;
37 Ok(WasmSession(session))
38}
39
40#[wasm_bindgen]
41impl WasmSession {
42 /// See `input_v1::Session::advance`. Returns a `{ done, stageComplete, stepsTaken,
43 /// loadFactor, error, recorderBatches }` object — `recorderBatches` holds only the samples
44 /// this call produced (results-storage-indexeddb.md's `recorderBatch`, one entry per
45 /// recorder that recorded this call), not the whole run's history. There is no separate
46 /// "samples so far" accessor: a caller that needs the full history accumulates these
47 /// batches itself, same as the planned results-storage worker will.
48 pub fn advance(&mut self, step_budget: u32) -> Result<Ts<input_v1::StepOutcome>, JsValue> {
49 let outcome = self.0.advance(step_budget);
50 outcome
51 .into_ts()
52 .map_err(|error| JsValue::from_str(&error.to_string()))
53 }
54
55 /// Frequencies, mode shapes and participation of every `Modal` stage finished so far
56 /// (`{ stages: ModalStageResult[] }`); empty until a modal stage completes. Read it after
57 /// `advance` reports `done`, or whenever a stage has completed.
58 #[wasm_bindgen(js_name = modalResults)]
59 pub fn modal_results(&self) -> Result<Ts<input_v1::ModalResultsReport>, JsValue> {
60 self.0
61 .modal_results()
62 .into_ts()
63 .map_err(|error| JsValue::from_str(&error.to_string()))
64 }
65
66 /// The `AnalysisSequence` stage id `advance` is currently in (or, once
67 /// done, whichever stage stopped it) — `undefined` once every stage has
68 /// completed.
69 #[wasm_bindgen(js_name = currentStageId)]
70 pub fn current_stage_id(&self) -> Option<String> {
71 self.0.current_stage_id().map(str::to_owned)
72 }
73}
74
75fn to_js_error(error: input_v1::DecodeError) -> JsValue {
76 serde_wasm_bindgen::to_value(&error).unwrap_or_else(|_| {
77 JsValue::from_str("decode error (failed to serialize DecodeError itself)")
78 })
79}
80
81/// Opaque handle to a persistent uniaxial material probe (`material_probe`).
82/// Material state persists across `applyStrain` calls; cancel between points
83/// by simply not calling it again.
84#[wasm_bindgen]
85pub struct WasmMaterialProbe(MaterialProbe);
86
87#[wasm_bindgen(js_name = createMaterialProbe)]
88pub fn create_material_probe(
89 config: Ts<MaterialProbeConfig>,
90) -> Result<WasmMaterialProbe, JsValue> {
91 let config: MaterialProbeConfig = config
92 .to_rust()
93 .map_err(|error| JsValue::from_str(&format!("malformed MaterialProbeConfig: {error}")))?;
94 MaterialProbe::new(&config)
95 .map(WasmMaterialProbe)
96 .map_err(probe_error)
97}
98
99#[wasm_bindgen]
100impl WasmMaterialProbe {
101 /// Imposes the strain and returns `{ strain, stress }`; rejects with a
102 /// `{ kind, ... }` `MaterialProbeError`.
103 #[wasm_bindgen(js_name = applyStrain)]
104 pub fn apply_strain(&mut self, target: f64) -> Result<Ts<MaterialProbeResponse>, JsValue> {
105 let response = self.0.apply_strain(target).map_err(probe_error)?;
106 response
107 .into_ts()
108 .map_err(|error| JsValue::from_str(&error.to_string()))
109 }
110
111 pub fn reset(&mut self) -> Result<(), JsValue> {
112 self.0.reset().map_err(probe_error)
113 }
114}
115
116fn probe_error(error: MaterialProbeError) -> JsValue {
117 serde_wasm_bindgen::to_value(&error)
118 .unwrap_or_else(|_| JsValue::from_str("material probe error"))
119}