From 1a3ee40e0d8037e5cb0b1afa8b753e3ed987022f Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 25 Mar 2026 14:45:04 +0000 Subject: [PATCH] Event buffering during hydration gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicks on island elements before hydration completes are captured and replayed after boot finishes: - shell.sx: inline script (capture phase) buffers clicks on [data-sx-island] elements that aren't hydrated yet into window._sxQ - boot.sx: after hydration + process-elements, replays buffered clicks by calling target.click() on elements still connected to the DOM This makes SSR islands feel interactive immediately — the user can click a button while the SX kernel is still loading/hydrating, and the action fires as soon as the handler is wired up. Co-Authored-By: Claude Opus 4.6 (1M context) --- shared/static/wasm/sx/boot.sx | 18 +++++++++++++++++- shared/sx/templates/shell.sx | 2 ++ web/boot.sx | 18 +++++++++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/shared/static/wasm/sx/boot.sx b/shared/static/wasm/sx/boot.sx index 31dd4691..767400e0 100644 --- a/shared/static/wasm/sx/boot.sx +++ b/shared/static/wasm/sx/boot.sx @@ -492,7 +492,23 @@ (process-elements nil) ;; Wire up popstate for back/forward navigation (dom-listen (dom-window) "popstate" - (fn (e) (handle-popstate 0)))))) + (fn (e) (handle-popstate 0))) + ;; Replay buffered clicks from hydration gap + (let ((queued (host-get (dom-window) "_sxQ"))) + (when queued + (let ((arr (host-call (host-global "Array") "from" queued))) + (let ((n (host-get arr "length"))) + (when (> n 0) + (log-info (str "replaying " n " buffered click(s)")) + (let loop ((i 0)) + (when (< i n) + (let ((entry (host-call arr "at" i))) + (when entry + (let ((target (host-get entry "t"))) + (when (and target (host-get target "isConnected")) + (host-call target "click"))))) + (loop (+ i 1))))))) + (host-set! (dom-window) "_sxQ" nil)))))) ;; -------------------------------------------------------------------------- diff --git a/shared/sx/templates/shell.sx b/shared/sx/templates/shell.sx index 1c2ef892..cedf396d 100644 --- a/shared/sx/templates/shell.sx +++ b/shared/sx/templates/shell.sx @@ -76,6 +76,8 @@ details.group{overflow:hidden}details.group>summary{list-style:none}details.grou (body :class "bg-stone-50 text-stone-900" ;; Server-rendered HTML — visible immediately before JS loads (div :id "sx-root" (raw! (or body-html ""))) + ;; Event buffer — captures clicks during hydration gap, replayed after boot + (script (raw! "document.addEventListener('click',function(e){var i=e.target.closest('[data-sx-island]');if(i&&!i._sxBoundislandHydrated){e.stopPropagation();(window._sxQ=window._sxQ||[]).push({t:e.target,ts:Date.now()})}},true)")) (script :type "text/sx" :data-components true :data-hash component-hash (raw! (or component-defs ""))) (when init-sx diff --git a/web/boot.sx b/web/boot.sx index 31dd4691..767400e0 100644 --- a/web/boot.sx +++ b/web/boot.sx @@ -492,7 +492,23 @@ (process-elements nil) ;; Wire up popstate for back/forward navigation (dom-listen (dom-window) "popstate" - (fn (e) (handle-popstate 0)))))) + (fn (e) (handle-popstate 0))) + ;; Replay buffered clicks from hydration gap + (let ((queued (host-get (dom-window) "_sxQ"))) + (when queued + (let ((arr (host-call (host-global "Array") "from" queued))) + (let ((n (host-get arr "length"))) + (when (> n 0) + (log-info (str "replaying " n " buffered click(s)")) + (let loop ((i 0)) + (when (< i n) + (let ((entry (host-call arr "at" i))) + (when entry + (let ((target (host-get entry "t"))) + (when (and target (host-get target "isConnected")) + (host-call target "click"))))) + (loop (+ i 1))))))) + (host-set! (dom-window) "_sxQ" nil)))))) ;; --------------------------------------------------------------------------