Fix island state loss on SX navigation + cache busting

Island markers rendered during SX navigation responses had no
data-sx-state attribute, so hydration found empty kwargs and path
was nil in the copyright display. Now adapter-dom.sx serializes
keyword args into data-sx-state on island markers, matching what
adapter-html.sx does for SSR.

Also fix post-swap to use parent element for outerHTML swaps in
SX responses (was using detached old target). Add SX source file
hashes to wasm_hash for proper browser cache busting — changing
any .sx file now busts the cache. Remove stale .sxbc bytecode
cache files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 17:36:27 +00:00
parent aa4c911178
commit 20b3dfb8a0
12 changed files with 2768 additions and 3100 deletions

View File

@@ -713,13 +713,13 @@ let register_jit_hook env =
Sx_ref.jit_call_hook := Some (fun f args ->
match f with
| Lambda l ->
let fn_name = match l.l_name with Some n -> n | None -> "?" in
(match l.l_compiled with
| Some cl when not (Sx_vm.is_jit_failed cl) ->
(* Cached bytecode — run on VM, fall back to CEK on runtime error.
Log once per function name, then stay quiet. Don't disable. *)
(try Some (Sx_vm.call_closure cl args cl.vm_env_ref)
with e ->
let fn_name = match l.l_name with Some n -> n | None -> "?" in
if not (Hashtbl.mem _jit_warned fn_name) then begin
Hashtbl.replace _jit_warned fn_name true;
Printf.eprintf "[jit] %s runtime fallback to CEK: %s\n%!" fn_name (Printexc.to_string e)
@@ -727,7 +727,6 @@ let register_jit_hook env =
None)
| Some _ -> None (* compile failed or disabled — CEK handles *)
| None ->
let fn_name = match l.l_name with Some n -> n | None -> "?" in
if !_jit_compiling then None
else begin
_jit_compiling := true;
@@ -1578,9 +1577,16 @@ let http_inject_shell_statics env static_dir sx_sxc =
let project_dir = try Sys.getenv "SX_PROJECT_DIR" with Not_found ->
Filename.dirname (Filename.dirname static_dir) in
let templates_dir = project_dir ^ "/shared/sx/templates" in
let client_libs = [
templates_dir ^ "/cssx.sx";
] in
(* Client libraries: all .sx files in templates/client-libs/ *)
let client_libs_dir = templates_dir ^ "/client-libs" in
let extra_libs =
if Sys.file_exists client_libs_dir && Sys.is_directory client_libs_dir then
Array.to_list (Sys.readdir client_libs_dir)
|> List.filter (fun f -> Filename.check_suffix f ".sx")
|> List.sort String.compare
|> List.map (fun f -> client_libs_dir ^ "/" ^ f)
else [] in
let client_libs = (templates_dir ^ "/cssx.sx") :: extra_libs in
List.iter (fun path ->
if Sys.file_exists path then begin
let src = In_channel.with_open_text path In_channel.input_all in
@@ -1614,7 +1620,20 @@ let http_inject_shell_statics env static_dir sx_sxc =
(* Compute file hashes for cache busting *)
let sx_js_hash = file_hash (static_dir ^ "/scripts/sx-browser.js") in
let body_js_hash = file_hash (static_dir ^ "/scripts/body.js") in
let wasm_hash = file_hash (static_dir ^ "/wasm/sx_browser.bc.wasm.js") in
(* Include SX source file hashes so browser cache busts when .sx files change *)
let sx_dir = static_dir ^ "/wasm/sx" in
let sx_files_hash =
if Sys.file_exists sx_dir && Sys.is_directory sx_dir then
let entries = Sys.readdir sx_dir in
Array.sort String.compare entries;
let combined = Array.fold_left (fun acc f ->
if Filename.check_suffix f ".sx" then
acc ^ file_hash (sx_dir ^ "/" ^ f)
else acc
) "" entries in
String.sub (Digest.string combined |> Digest.to_hex) 0 12
else "" in
let wasm_hash = file_hash (static_dir ^ "/wasm/sx_browser.bc.wasm.js") ^ sx_files_hash in
(* Read CSS for inline injection *)
let tw_css = read_css_file (static_dir ^ "/styles/tw.css") in
let basics_css = read_css_file (static_dir ^ "/styles/basics.css") in
@@ -1674,16 +1693,7 @@ let http_inject_shell_statics env static_dir sx_sxc =
ignore (env_bind env "__shell-body-scripts" Nil);
ignore (env_bind env "__shell-inline-css" Nil);
ignore (env_bind env "__shell-inline-head-js" Nil);
(* init-sx: trigger client-side render when sx-root is empty (SSR failed).
The boot code hydrates existing islands but doesn't do fresh render.
This script forces a render from page-sx after boot completes. *)
ignore (env_bind env "__shell-init-sx" (String
"document.addEventListener('sx:boot-done', function() { \
var root = document.getElementById('sx-root'); \
if (root && !root.innerHTML.trim() && typeof SX !== 'undefined' && SX.renderPage) { \
SX.renderPage(); \
} \
});"));
ignore (env_bind env "__shell-init-sx" Nil);
Printf.eprintf "[sx-http] Shell statics: defs=%d hash=%s css=%d js=%s wasm=%s\n%!"
(String.length component_defs) component_hash (String.length sx_css) sx_js_hash wasm_hash
@@ -1789,7 +1799,9 @@ let http_setup_page_helpers env =
SxExpr (Printf.sprintf "(pre :class \"text-sm overflow-x-auto\" (code \"%s\"))" escaped)
| _ -> Nil);
(* component-source — stub *)
bind "component-source" (fun _args -> String "")
bind "component-source" (fun _args -> String "");
(* handler-source — stub (returns empty, used by example pages) *)
bind "handler-source" (fun _args -> String "")
let http_mode port =
let env = make_server_env () in
@@ -2003,12 +2015,17 @@ let http_mode port =
in
match work with
| Some (fd, path, headers) ->
(* ~10M steps ≈ 3-5 seconds of CEK evaluation *)
Atomic.set Sx_ref._step_count 0;
Atomic.set Sx_ref._step_limit 10_000_000;
let response =
try
let is_ajax = List.exists (fun (k, _) -> k = "sx-request" || k = "hx-request") headers in
match http_render_page env path headers with
| Some html ->
let resp = http_response ~content_type:"text/html; charset=utf-8" html in
let ct = if is_ajax then "text/sx; charset=utf-8"
else "text/html; charset=utf-8" in
let resp = http_response ~content_type:ct html in
if not is_ajax then Hashtbl.replace response_cache path resp;
resp
| None -> http_response ~status:404 "<h1>Not Found</h1>"
@@ -2016,6 +2033,7 @@ let http_mode port =
Printf.eprintf "[render] Error for %s: %s\n%!" path (Printexc.to_string e);
http_response ~status:500 "<h1>Internal Server Error</h1>"
in
Atomic.set Sx_ref._step_limit 0;
write_response fd response
| None -> ()
done