Fix stepper SSR/hydration flash: server reads cookie, cache bypass
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) <noreply@anthropic.com>
This commit is contained in:
@@ -3040,8 +3040,10 @@ let http_mode port =
|
|||||||
in
|
in
|
||||||
write_response fd response; true
|
write_response fd response; true
|
||||||
end else if is_sx then begin
|
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
|
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
|
| Some cached -> write_response fd cached; true
|
||||||
| None ->
|
| None ->
|
||||||
if is_ajax then begin
|
if is_ajax then begin
|
||||||
@@ -3062,6 +3064,18 @@ let http_mode port =
|
|||||||
(escape_sx_string (Printexc.to_string e)))
|
(escape_sx_string (Printexc.to_string e)))
|
||||||
in
|
in
|
||||||
write_response fd response; true
|
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 "<h1>Not Found</h1>"
|
||||||
|
with e ->
|
||||||
|
Printf.eprintf "[render] Cookie render error for %s: %s\n%!" path (Printexc.to_string e);
|
||||||
|
http_response ~status:500 "<h1>Error</h1>"
|
||||||
|
in
|
||||||
|
write_response fd response; true
|
||||||
end else begin
|
end else begin
|
||||||
(* Full page: queue to render worker *)
|
(* Full page: queue to render worker *)
|
||||||
Mutex.lock render_mutex;
|
Mutex.lock render_mutex;
|
||||||
|
|||||||
@@ -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))))))
|
((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))})))))
|
(let ((n (if val (parse-number val) 0))) {:step-idx (signal (if (and (number? n) (>= n 0) (<= n 16)) n 0))})))))
|
||||||
nil))
|
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)))
|
(dom-stack-sig (signal (list)))
|
||||||
(code-tokens (signal (list))))
|
(code-tokens (signal (list))))
|
||||||
(letrec
|
(letrec
|
||||||
|
|||||||
@@ -159,6 +159,12 @@ test('home', async ({ page }) => {
|
|||||||
const errors = trackErrors(page);
|
const errors = trackErrors(page);
|
||||||
const entries = [];
|
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
|
// Capture SSR state before JS runs — detect hydration flash
|
||||||
const ssrResponse = await page.goto(server.baseUrl + '/sx/', { waitUntil: 'commit', timeout: 30000 });
|
const ssrResponse = await page.goto(server.baseUrl + '/sx/', { waitUntil: 'commit', timeout: 30000 });
|
||||||
const ssrHtml = await ssrResponse.text();
|
const ssrHtml = await ssrResponse.text();
|
||||||
@@ -174,7 +180,7 @@ test('home', async ({ page }) => {
|
|||||||
return m ? m[1] : null;
|
return m ? m[1] : null;
|
||||||
});
|
});
|
||||||
const noFlash = ssrIndex === hydratedIndex;
|
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);
|
const info = await discoverPage(page);
|
||||||
entries.push({ ok: true, label: 'Boot: data-sx-ready', feature: 'boot' });
|
entries.push({ ok: true, label: 'Boot: data-sx-ready', feature: 'boot' });
|
||||||
|
|||||||
Reference in New Issue
Block a user