DOM-preserving hydration — SSR DOM stays, event listeners attach in place

Scope-based cursor walks the existing SSR DOM during island hydration
instead of creating new elements and calling replaceChildren. The
hydration scope (sx-hydrating) propagates through define-library via
scope-push!/peek/pop!, solving the env isolation that broke the
previous set!-based approach.

Changes:
- adapter-dom.sx: hydrating?, hydrate-next-node, hydrate-enter/exit-element
  helpers. render-to-dom reuses text nodes. render-dom-element reuses
  elements by tag match, skips dom-append. reactive-text/cek-reactive-text
  reuse existing text nodes. render-dom-fragment/lake/marsh skip append.
  dispatch-render-form (if/when/cond) injects markers into existing DOM.
- boot.sx: hydrate-island pushes cursor scope, skips replaceChildren.
  On mismatch error, falls back to full re-render.

Result: zero DOM destruction, zero visual flash, event listeners
attached to original SSR elements. Stepper clicks verified working.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 11:46:41 +00:00
parent 89ffb02b20
commit a2a4d17d53
7 changed files with 2855 additions and 2534 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -330,13 +330,29 @@
(if (dict-has? kwargs p) (dict-get kwargs p) nil)))
(component-params comp))
(let
((body-dom (cek-try (fn () (with-island-scope (fn (disposable) (append! disposers disposable)) (fn () (render-to-dom (component-body comp) local nil)))) (fn (err) (log-warn (str "hydrate-island FAILED: " comp-name " — " err)) (let ((error-el (dom-create-element "div" nil))) (dom-set-attr error-el "class" "sx-island-error") (dom-set-attr error-el "style" "padding:8px;margin:4px 0;border:1px solid #ef4444;border-radius:4px;background:#fef2f2;color:#b91c1c;font-family:monospace;font-size:12px;white-space:pre-wrap") (dom-set-text-content error-el (str "Island error: " comp-name "\n" err)) error-el)))))
(host-call el "replaceChildren" body-dom)
((cursor (dict "parent" el "index" 0)))
(scope-push! "sx-hydrating" cursor)
(cek-try
(fn
()
(with-island-scope
(fn (disposable) (append! disposers disposable))
(fn () (render-to-dom (component-body comp) local nil))))
(fn
(err)
(scope-pop! "sx-hydrating")
(log-warn
(str "hydrate fallback: " comp-name " — " err))
(let
((fallback (cek-try (fn () (with-island-scope (fn (d) (append! disposers d)) (fn () (render-to-dom (component-body comp) local nil)))) (fn (err2) (let ((e (dom-create-element "div" nil))) (dom-set-text-content e (str "Island error: " comp-name "\n" err2)) e)))))
(host-call el "replaceChildren" fallback)
nil)))
(scope-pop! "sx-hydrating")
(dom-set-data el "sx-disposers" disposers)
(set-timeout (fn () (process-elements el)) 0)
(log-info
(str
"hydrated island: "
"hydrated island: ~"
comp-name
" ("
(len disposers)

File diff suppressed because one or more lines are too long

View File

@@ -536,6 +536,7 @@
"SVG_NS",
"MATH_NS",
"island-scope?",
"hydrating?",
"contains-deref?",
"dom-on",
"render-to-dom",

File diff suppressed because it is too large Load Diff

View File

@@ -330,13 +330,29 @@
(if (dict-has? kwargs p) (dict-get kwargs p) nil)))
(component-params comp))
(let
((body-dom (cek-try (fn () (with-island-scope (fn (disposable) (append! disposers disposable)) (fn () (render-to-dom (component-body comp) local nil)))) (fn (err) (log-warn (str "hydrate-island FAILED: " comp-name " — " err)) (let ((error-el (dom-create-element "div" nil))) (dom-set-attr error-el "class" "sx-island-error") (dom-set-attr error-el "style" "padding:8px;margin:4px 0;border:1px solid #ef4444;border-radius:4px;background:#fef2f2;color:#b91c1c;font-family:monospace;font-size:12px;white-space:pre-wrap") (dom-set-text-content error-el (str "Island error: " comp-name "\n" err)) error-el)))))
(host-call el "replaceChildren" body-dom)
((cursor (dict "parent" el "index" 0)))
(scope-push! "sx-hydrating" cursor)
(cek-try
(fn
()
(with-island-scope
(fn (disposable) (append! disposers disposable))
(fn () (render-to-dom (component-body comp) local nil))))
(fn
(err)
(scope-pop! "sx-hydrating")
(log-warn
(str "hydrate fallback: " comp-name " — " err))
(let
((fallback (cek-try (fn () (with-island-scope (fn (d) (append! disposers d)) (fn () (render-to-dom (component-body comp) local nil)))) (fn (err2) (let ((e (dom-create-element "div" nil))) (dom-set-text-content e (str "Island error: " comp-name "\n" err2)) e)))))
(host-call el "replaceChildren" fallback)
nil)))
(scope-pop! "sx-hydrating")
(dom-set-data el "sx-disposers" disposers)
(set-timeout (fn () (process-elements el)) 0)
(log-info
(str
"hydrated island: "
"hydrated island: ~"
comp-name
" ("
(len disposers)