Fix signal-add-sub! losing subscribers after remove, fix build pipeline

signal-add-sub! used (append! subscribers f) which returns a new list
for immutable List but discards the result — after signal-remove-sub!
replaces the subscribers list via dict-set!, re-adding subscribers
silently fails. Counter island only worked once (0→1 then stuck).

Fix: use (dict-set! s "subscribers" (append ...)) to explicitly update
the dict field, matching signal-remove-sub!'s pattern.

Build pipeline fixes:
- sx-build-all.sh now bundles spec→dist and recompiles .sxbc bytecode
- compile-modules.js syncs .sx source files alongside .sxbc to wasm/sx/
- Per-file cache busting: wasm, platform JS, and sxbc each get own hash
- bundle.sh adds cssx.sx to dist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 07:36:36 +00:00
parent 5abc947ac7
commit 9ce8659f74
12 changed files with 439 additions and 48 deletions

View File

@@ -1597,6 +1597,8 @@ let http_render_page env path headers =
Keyword "sx-css-classes"; get_shell "sx-css-classes";
Keyword "asset-url"; get_shell "asset-url";
Keyword "wasm-hash"; get_shell "wasm-hash";
Keyword "platform-hash"; get_shell "platform-hash";
Keyword "sxbc-hash"; get_shell "sxbc-hash";
Keyword "inline-css"; get_shell "inline-css";
Keyword "inline-head-js"; get_shell "inline-head-js";
Keyword "init-sx"; get_shell "init-sx";
@@ -1678,6 +1680,20 @@ let file_hash path =
String.sub (Digest.string (In_channel.with_open_bin path In_channel.input_all) |> Digest.to_hex) 0 12
else ""
let sxbc_combined_hash dir =
let sxbc_dir = dir ^ "/sx" in
if Sys.file_exists sxbc_dir && Sys.is_directory sxbc_dir then begin
let files = Array.to_list (Sys.readdir sxbc_dir) in
let sxbc_files = List.filter (fun f -> Filename.check_suffix f ".sxbc") files in
let sorted = List.sort String.compare sxbc_files in
let buf = Buffer.create 65536 in
List.iter (fun f ->
let path = sxbc_dir ^ "/" ^ f in
Buffer.add_string buf (In_channel.with_open_bin path In_channel.input_all)
) sorted;
String.sub (Digest.string (Buffer.contents buf) |> Digest.to_hex) 0 12
end else ""
let read_css_file path =
if Sys.file_exists path then
In_channel.with_open_text path In_channel.input_all
@@ -1726,8 +1742,10 @@ let http_inject_shell_statics env static_dir sx_sxc =
literals, preventing the HTML parser from matching </script>. *)
let component_defs = raw_defs in
let component_hash = Digest.string component_defs |> Digest.to_hex in
(* Compute file hashes for cache busting *)
(* Compute per-file hashes for cache busting *)
let wasm_hash = file_hash (static_dir ^ "/wasm/sx_browser.bc.wasm.js") in
let platform_hash = file_hash (static_dir ^ "/wasm/sx-platform-2.js") in
let sxbc_hash = sxbc_combined_hash (static_dir ^ "/wasm") 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
@@ -1781,6 +1799,8 @@ let http_inject_shell_statics env static_dir sx_sxc =
ignore (env_bind env "__shell-sx-css-classes" (String ""));
ignore (env_bind env "__shell-asset-url" (String "/static"));
ignore (env_bind env "__shell-wasm-hash" (String wasm_hash));
ignore (env_bind env "__shell-platform-hash" (String platform_hash));
ignore (env_bind env "__shell-sxbc-hash" (String sxbc_hash));
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).
@@ -1793,8 +1813,8 @@ let http_inject_shell_statics env static_dir sx_sxc =
SX.renderPage(); \
} \
});"));
Printf.eprintf "[sx-http] Shell statics: defs=%d hash=%s css=%d wasm=%s\n%!"
(String.length component_defs) component_hash (String.length sx_css) wasm_hash
Printf.eprintf "[sx-http] Shell statics: defs=%d hash=%s css=%d wasm=%s platform=%s sxbc=%s\n%!"
(String.length component_defs) component_hash (String.length sx_css) wasm_hash platform_hash sxbc_hash
let http_setup_declarative_stubs env =
(* Stub declarative forms that are metadata-only — no-ops at render time. *)