feat(wasm): validate client event and state ABI

req: client_local/005\nreq: client_local/006\nreq: client_local/007\nreq: client_local/008\nreq: client_local/009\nreq: client_local/010\nreq: client_local/014
This commit is contained in:
slhx agent
2026-07-13 12:42:31 +02:00
parent 40643e360d
commit 38eae79a2f
9 changed files with 246 additions and 24 deletions
+99 -1
View File
@@ -10,6 +10,65 @@ pub use wasm_bindgen::prelude::wasm_bindgen;
#[doc(hidden)]
pub use wasm_bindgen::*;
pub const CLIENT_EVENT_ABI_VERSION: u32 = 1;
pub const CLIENT_STATE_ABI_VERSION: u32 = 1;
/// Versioned browser event accepted by client-local handlers.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClientEvent {
pub kind: String,
pub value: Option<String>,
pub checked: Option<bool>,
pub key: Option<String>,
}
/// Explicit root-owned state passed to a client-local handler.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClientState {
pub encoded: String,
}
/// Validates primitive wasm-bindgen values before application code runs.
///
/// Primitive arguments keep JavaScript from owning a second binary codec. The
/// ordinary effect result remains postcard-encoded by `hemx-core`.
#[doc(hidden)]
#[allow(clippy::too_many_arguments)]
pub fn decode_client_inputs(
event_version: u32,
kind: String,
value: Option<String>,
checked: Option<bool>,
key: Option<String>,
state_version: u32,
encoded_state: String,
) -> Result<(ClientEvent, ClientState), String> {
if event_version != CLIENT_EVENT_ABI_VERSION {
return Err(format!(
"unsupported client-local event ABI version {event_version}; expected {CLIENT_EVENT_ABI_VERSION}"
));
}
if kind.is_empty() {
return Err("invalid client-local event payload: event kind is empty".to_owned());
}
if state_version != CLIENT_STATE_ABI_VERSION {
return Err(format!(
"unsupported client-local state ABI version {state_version}; expected {CLIENT_STATE_ABI_VERSION}"
));
}
Ok((
ClientEvent {
kind,
value,
checked,
key,
},
ClientState {
encoded: encoded_state,
},
))
}
/// Encodes a client handler result with the ordinary hemx effect wire format.
///
/// Keeping this conversion here gives generated WASM exports one ABI boundary
@@ -21,9 +80,48 @@ pub fn encode_handler_effect(effect: impl IntoEffect, fingerprint: BuildFingerpr
#[cfg(test)]
mod tests {
use super::encode_handler_effect;
use super::{decode_client_inputs, encode_handler_effect, ClientEvent, ClientState};
use hemx_core::{BuildFingerprint, EffectBatch, Slot};
#[test]
fn client_inputs_are_typed_and_versioned() {
assert_eq!(
decode_client_inputs(
1,
"click".to_owned(),
None,
None,
None,
1,
"count=3".to_owned(),
),
Ok((
ClientEvent {
kind: "click".to_owned(),
value: None,
checked: None,
key: None,
},
ClientState {
encoded: "count=3".to_owned(),
},
))
); // req: client_local/005 req: client_local/007
assert_eq!(
decode_client_inputs(
1,
"click".to_owned(),
None,
None,
None,
2,
"count=3".to_owned(),
)
.expect_err("reject unknown state ABI"),
"unsupported client-local state ABI version 2; expected 1"
); // req: client_local/008
}
#[test]
fn client_handler_uses_the_ordinary_effect_wire_format() {
let fingerprint = BuildFingerprint(17);