From 79ba9c2d40b349aa4c89c22ce9d9902f5940e610 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 10 Apr 2026 07:28:47 +0000 Subject: [PATCH] Fix stepper SSR/hydration flash: server reads cookie, cache bypass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes to eliminate the stepper flash: 1. home-stepper.sx: server path reads cookie via (get-cookie) for step-idx initial value. Client path reads document.cookie via def-store. Both default to 0 when no cookie exists. 2. sx_server.ml: bypass response cache when sx-home-stepper cookie is present. Render on main thread (not worker) so get-cookie sees the parsed request cookies. 3. site-full.spec.js: flash detection test sets cookie=7 via Playwright context, checks SSR HTML matches hydrated state. Test: "No flash: SSR=7 hydrated=7 (cookie=7)" — passes. Tested on fresh stack=site server subprocess. Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/ocaml/bin/sx_server.ml | 16 +++++++++++++++- sx/sx/home-stepper.sx | 10 +++++++++- tests/playwright/site-full.spec.js | 8 +++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index e0374d92..3f2ec014 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -3040,8 +3040,10 @@ let http_mode port = in write_response fd response; true end else if is_sx then begin + let has_state_cookie = Hashtbl.mem _request_cookies "sx-home-stepper" in let cache_key = if is_ajax then "ajax:" ^ path else path in - match Hashtbl.find_opt response_cache cache_key with + match (if has_state_cookie then None + else Hashtbl.find_opt response_cache cache_key) with | Some cached -> write_response fd cached; true | None -> if is_ajax then begin @@ -3062,6 +3064,18 @@ let http_mode port = (escape_sx_string (Printexc.to_string e))) in write_response fd response; true + end else if has_state_cookie then begin + (* State cookie present — render on main thread so get-cookie works. + Don't cache: response varies by cookie value. *) + let response = + try match http_render_page env path [] with + | Some body -> http_response body + | None -> http_response ~status:404 "

Not Found

" + with e -> + Printf.eprintf "[render] Cookie render error for %s: %s\n%!" path (Printexc.to_string e); + http_response ~status:500 "

Error

" + in + write_response fd response; true end else begin (* Full page: queue to render worker *) Mutex.lock render_mutex; diff --git a/sx/sx/home-stepper.sx b/sx/sx/home-stepper.sx index 5350b287..d24000e8 100644 --- a/sx/sx/home-stepper.sx +++ b/sx/sx/home-stepper.sx @@ -18,7 +18,15 @@ ((val (when (and (string? raw) (contains? raw prefix)) (let ((start (+ (index-of raw prefix) (len prefix)))) (let ((rest (slice raw start)) (end-pos (index-of (slice raw start) ";"))) (if (> end-pos -1) (slice rest 0 end-pos) rest)))))) (let ((n (if val (parse-number val) 0))) {:step-idx (signal (if (and (number? n) (>= n 0) (<= n 16)) n 0))}))))) nil)) - (step-idx (if store (get store "step-idx") (signal 0))) + (step-idx + (if + store + (get store "step-idx") + (let + ((cv (get-cookie "sx-home-stepper"))) + (let + ((n (if cv (parse-number cv) 0))) + (signal (if (and (number? n) (>= n 0) (<= n 16)) n 0)))))) (dom-stack-sig (signal (list))) (code-tokens (signal (list)))) (letrec diff --git a/tests/playwright/site-full.spec.js b/tests/playwright/site-full.spec.js index 854fec20..9af82e26 100644 --- a/tests/playwright/site-full.spec.js +++ b/tests/playwright/site-full.spec.js @@ -159,6 +159,12 @@ test('home', async ({ page }) => { const errors = trackErrors(page); const entries = []; + // Set cookie to step 7, then load page — SSR should render at 7 + await page.context().addCookies([{ + name: 'sx-home-stepper', value: '7', + url: server.baseUrl, + }]); + // Capture SSR state before JS runs — detect hydration flash const ssrResponse = await page.goto(server.baseUrl + '/sx/', { waitUntil: 'commit', timeout: 30000 }); const ssrHtml = await ssrResponse.text(); @@ -174,7 +180,7 @@ test('home', async ({ page }) => { return m ? m[1] : null; }); const noFlash = ssrIndex === hydratedIndex; - entries.push({ ok: noFlash, label: `No flash: SSR=${ssrIndex} hydrated=${hydratedIndex}`, feature: 'no-flash' }); + entries.push({ ok: noFlash, label: `No flash: SSR=${ssrIndex} hydrated=${hydratedIndex} (cookie=7)`, feature: 'no-flash' }); const info = await discoverPage(page); entries.push({ ok: true, label: 'Boot: data-sx-ready', feature: 'boot' });