Enforce root-owned adapter lifecycles

This commit is contained in:
tmk241
2026-09-01 09:43:38 +02:00
parent 53f0a5db1a
commit 9e12b1eb46
5 changed files with 124 additions and 139 deletions
+39 -13
View File
@@ -14,10 +14,10 @@ fn runtime_exposes_debug_api_before_startup_side_effects() {
.find("if (document.readyState === \"loading\") document.addEventListener(\"DOMContentLoaded\", start)")
.expect("runtime starts after declaration");
assert!(api < start);
assert!(source.contains("try {\n bootstrapState(root);"));
assert!(source.contains("emit(root, \"hemx:state-error\", String(error));"));
assert!(source.contains("try {\n bindRoot(root);"));
assert!(source.contains("emit(root, \"hemx:bind-error\", String(error));"));
assert!(source.contains("roots().forEach(bindEnhancedRoot)"));
assert!(source.contains("bindAdapter(root, \"events\", () => bindRoot(root))"));
assert!(source.contains("bindAdapter(root, \"sse\", () => bindSse(root))"));
assert!(source.contains("emit(root, `hemx:${name}-error`, String(error))"));
}
#[test]
@@ -113,7 +113,7 @@ fn runtime_targets_generated_resources_not_response_selectors() {
assert!(source.contains("function firstElement(scope, predicate)"));
assert!(source.contains("function generatedResource(el, id)"));
assert!(source.contains("return firstElement(scope, (el) => attrEquals(el, \"data-hemx-key\", key) && withinGeneratedResource(el, scope, id))"));
assert!(source.contains("if (node.hasAttribute && node.hasAttribute(ROOT)) continue"));
assert!(source.contains("node.hasAttribute(ROOT) || node.hasAttribute(ISLAND)"));
assert!(source.contains("if (!applyOp(scope, op)) return"));
assert!(!source.contains("canApplyOp"));
assert!(source.contains("if (op.position === \"first\") target.prepend"));
@@ -158,7 +158,7 @@ fn runtime_supports_revealed_scheduling() {
assert!(source.contains("window.addEventListener(\"pageshow\", restoreRevealed)"));
assert!(source.contains("revealed.delete(el)"));
assert!(source.contains("record.addedNodes.forEach((node) =>"));
assert!(source.contains("descendantRoots(node).forEach(bindRoot)"));
assert!(source.contains("descendantRoots(node).forEach(bindEnhancedRoot)"));
assert!(source.contains("const owner = rootOf(node.parentElement)"));
assert!(source.contains("bindPolling(owner)"));
assert!(source.contains("bindRevealed(owner)"));
@@ -360,15 +360,13 @@ fn runtime_refuses_partial_updates_on_fingerprint_mismatch() {
}
#[test]
fn runtime_malformed_bootstrap_state_reports_and_continues() {
fn runtime_has_no_mirrored_client_application_store() {
let source = hemx_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, \"hemx:state-error\", String(error))"));
assert!(source.contains("bindRoot(root)"));
assert!(source.contains("bindSse(root)"));
assert!(!source.contains("atomStore"));
assert!(!source.contains("bootstrapState"));
assert!(!source.contains("data-hemx-st"));
assert!(!source.contains("decodeAtomState"));
}
#[test]
@@ -405,6 +403,34 @@ fn runtime_applies_binary_websocket_effect_batches_inside_roots() {
assert!(source.contains("if (socket) socket.close()"));
}
#[test]
fn runtime_binds_adapters_once_and_cleans_removed_fragments() {
let source = hemx_js::RUNTIME_JS;
assert!(source.contains("const boundRoots = new WeakSet()"));
assert!(source.contains("if (boundRoots.has(root)) return;"));
assert!(source.contains("if (rootOf(event.target) !== root) return;"));
assert!(source.contains("if (rootOf(el) !== root) return;"));
assert!(source.contains("descendantRoots(node).forEach(bindEnhancedRoot)"));
assert!(source.contains("function bindAdapter(root, name, bind)"));
assert!(source.contains("cleanupRemovedFragment(node)"));
assert!(source.contains("source.close()"));
assert!(source.contains("socket.close()"));
assert!(source.contains("observer.disconnect()"));
assert!(source.contains("stopDescendantPolling(node)"));
assert!(source.contains("boundRoots.delete(root)"));
}
#[test]
fn runtime_preserves_explicit_island_subtrees() {
let source = hemx_js::RUNTIME_JS;
assert!(source.contains("const ISLAND = \"data-hemx-island\""));
assert!(source.contains("node.hasAttribute(ROOT) || node.hasAttribute(ISLAND)"));
assert!(source.contains("preserveIslands(target, nodes)"));
assert!(source.contains("island.replaceWith(preserved)"));
}
#[test]
fn runtime_page_swaps_lowered_slot_ids() {
let source = hemx_js::RUNTIME_JS;