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
+11 -1
View File
@@ -45,6 +45,16 @@ export interface AtomSnapshot {
bytes: Uint8Array;
}
export type ClientHandler = (
eventVersion: number,
eventKind: string,
eventValue: string | undefined,
eventChecked: boolean | undefined,
eventKey: string | undefined,
stateVersion: number,
encodedState: string,
) => Uint8Array | Promise<Uint8Array>;
export interface HemxRuntime {
readonly runtimeAbiVersion: number;
roots(): Element[];
@@ -54,7 +64,7 @@ export interface HemxRuntime {
decodeBatch(buffer: ArrayBuffer): EffectBatch;
atomValue(root: Element | ParentNode | null | undefined, id: number): Uint8Array | undefined;
decodeAtomState(encoded: string): AtomSnapshot[];
registerClientHandler(name: string, handler: () => Uint8Array | Promise<Uint8Array>): void;
registerClientHandler(name: string, handler: ClientHandler): void;
}
declare global {
+31 -6
View File
@@ -238,13 +238,34 @@
});
}
async function runClient(el) {
async function runClient(el, event) {
const name = el.getAttribute("data-hemx-client");
const root = rootOf(el);
const handler = clientHandlers.get(name);
if (!handler) throw new Error(`unknown client-local hemx handler: ${name}`);
const wire = await handler();
if (!(wire instanceof Uint8Array)) throw new Error(`client-local hemx handler ${name} returned an invalid effect batch`);
applyBatch(wire, rootOf(el));
showError(el, null);
showPending(el, true);
try {
if (!handler) throw new Error(`unknown client-local hemx handler: ${name}`);
const stateVersion = Number(root.getAttribute("data-hemx-client-state-version") || "1");
const wire = await handler(
1,
event.type,
"value" in el ? String(el.value) : undefined,
"checked" in el ? Boolean(el.checked) : undefined,
event.key || undefined,
stateVersion,
root.getAttribute(STATE) || "",
);
if (!(wire instanceof Uint8Array)) throw new Error(`client-local hemx handler ${name} returned an invalid effect batch`);
applyBatch(wire, root);
} catch (error) {
const fallback = el.hasAttribute("data-hemx-client-fallback");
showError(el, error);
emit(root, "hemx:client-error", { handler: name, message: String(error), fallback });
if (fallback) await send(el, event.type, el);
} finally {
showPending(el, false);
}
}
async function send(el, eventName, source = el) {
@@ -682,7 +703,11 @@
if (direct && defaultEvent(direct) === "click") {
event.preventDefault();
if (direct.hasAttribute("data-hemx-client")) {
runClient(direct).catch((error) => emit(root, "hemx:client-error", String(error)));
runClient(direct, event).catch((error) => emit(root, "hemx:client-error", {
handler: direct.getAttribute("data-hemx-client"),
message: String(error),
fallback: false,
}));
} else {
schedule(direct, name);
}