Skip to main content

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}