From c69eb9932c34b83e7360c577d399d2cbc10add27 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Mon, 1 Jun 2026 21:35:49 +0200 Subject: [PATCH] fix(js): tolerate malformed bootstrap state Report malformed data-slhx-st with slhx:state-error and continue binding the root with an empty atom store instead of letting state decode failures stop handlers, navigation, or push. req: state/004 req: state/006 req: runtime/001 --- REQUIREMENTS.md | 3 +++ slhx-js/runtime/slhx.js | 9 +++++++-- slhx-js/tests/runtime.rs | 13 +++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 8044e26..02d3a9e 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -640,6 +640,9 @@ what a valid business email is. ### req: state/004 004 SSR roots may carry a `data-slhx-st` base64url-encoded postcard blob on the `data-slhx-root` element. Runtime decodes it into the client atom store. Atoms computed from server state are immediately available to client-side handlers without a round-trip. +### req: state/006 +006 Malformed `data-slhx-st` bootstrap state must not stop the standard runtime from binding roots, handlers, navigation, or push. The runtime reports `slhx:state-error` and continues with an empty atom store for that root. + ### req: state/005 005 Not all state is an Atom. Ordinary Rust fields are preferred unless the value must be independently addressed, bootstrapped, synced, or subscribed. Atoms are explicit resources, not the default state container. diff --git a/slhx-js/runtime/slhx.js b/slhx-js/runtime/slhx.js index f31bc19..88e6bdf 100644 --- a/slhx-js/runtime/slhx.js +++ b/slhx-js/runtime/slhx.js @@ -668,8 +668,13 @@ function bootstrapState(root) { const encoded = root.getAttribute(STATE); if (!encoded) return; - const store = atomStore(root); - for (const atom of decodeAtomState(encoded)) store.set(String(atom.id), atom.bytes); + try { + const store = atomStore(root); + for (const atom of decodeAtomState(encoded)) store.set(String(atom.id), atom.bytes); + } catch (error) { + atomStores.delete(root); + emit(root, "slhx:state-error", String(error)); + } } function decodeAtomState(encoded) { diff --git a/slhx-js/tests/runtime.rs b/slhx-js/tests/runtime.rs index fa8aab8..76863e5 100644 --- a/slhx-js/tests/runtime.rs +++ b/slhx-js/tests/runtime.rs @@ -216,6 +216,19 @@ fn runtime_refuses_partial_updates_on_fingerprint_mismatch() { assert!(source.contains("if (expected && String(batch.fingerprint) !== expected)")); } +#[test] +fn runtime_malformed_bootstrap_state_reports_and_continues() { + // req: state/004 req: state/006 req: runtime/001 + let source = slhx_js::RUNTIME_JS; + + assert!(source.contains("function bootstrapState(root)")); + assert!(source.contains("try {\n const store = atomStore(root);")); + assert!(source.contains("atomStores.delete(root)")); + assert!(source.contains("emit(root, \"slhx:state-error\", String(error))")); + assert!(source.contains("bindRoot(root)")); + assert!(source.contains("bindSse(root)")); +} + #[test] fn runtime_applies_sse_effect_batches_inside_roots() { // req: push/001 req: push/003 req: push/006 req: runtime/001