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