Fix navigation: outerHTML swap, island markers, host handle equality, dispose

Navigation pipeline now works end-to-end:
- outerHTML swap uses dom-replace-child instead of morph-node (morph has
  a CEK continuation issue with nested for-each that needs separate fix)
- swap-dom-nodes returns the new element for outerHTML so post-swap
  hydrates the correct (new) DOM, not the detached old element
- sx-render uses marker mode: islands rendered as empty span[data-sx-island]
  markers, hydrated by post-swap. Prevents duplicate content from island
  body expansion + SX response nav rows.
- dispose-island (singular) called on old island before morph, not just
  dispose-islands-in (which only disposes sub-islands)

OCaml runtime:
- safe_eq: Dict equality checks __host_handle for DOM node identity
  (js_to_value creates new Dict wrappers per call, breaking physical ==)
- contains?: same host handle check
- to_string: trampoline thunks (fixes <thunk> display)
- as_number: trampoline thunks (fixes arithmetic on leaked thunks)

DOM platform:
- dom-remove, dom-attr-list (name/value pairs), dom-child-list (SX list),
  dom-is-active-element?, dom-is-input-element?, dom-is-child-of?, dom-on

All 5 reactive-nav Playwright tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 13:19:29 +00:00
parent 07bbcaf1bb
commit eacde62806
10 changed files with 2352 additions and 30 deletions

View File

@@ -156,7 +156,12 @@ let () =
(List lb | ListRef { contents = lb }) ->
List.length la = List.length lb &&
List.for_all2 safe_eq la lb
(* Dict/Lambda/Component/Island/Signal/NativeFn: physical only *)
(* Dict: check __host_handle for DOM node identity *)
| Dict a, Dict b ->
(match Hashtbl.find_opt a "__host_handle", Hashtbl.find_opt b "__host_handle" with
| Some (Number ha), Some (Number hb) -> ha = hb
| _ -> false)
(* Lambda/Component/Island/Signal/NativeFn: physical only *)
| _ -> false
in
register "=" (fun args ->
@@ -430,6 +435,10 @@ let () =
| Nil, Nil -> true
| Symbol x, Symbol y -> x = y
| Keyword x, Keyword y -> x = y
| Dict a, Dict b ->
(match Hashtbl.find_opt a "__host_handle", Hashtbl.find_opt b "__host_handle" with
| Some (Number ha), Some (Number hb) -> ha = hb
| _ -> false)
| _ -> false)
in
Bool (List.exists (fun x -> safe_eq x item) l)