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

@@ -883,7 +883,9 @@ def _get_shell_static() -> dict[str, Any]:
pages_sx=pages_sx,
sx_css=sx_css,
sx_css_classes=sx_css_classes,
wasm_hash=_wasm_hash("sx_browser.bc.js"),
wasm_hash=_wasm_hash("sx_browser.bc.wasm.js"),
platform_hash=_wasm_hash("sx-platform-2.js"),
sxbc_hash=_sxbc_hash(),
asset_url=_ca.config.get("ASSET_URL", "/static"),
inline_css=_shell_cfg.get("inline_css"),
inline_head_js=_shell_cfg.get("inline_head_js"),
@@ -1032,7 +1034,9 @@ def sx_page_streaming_parts(ctx: dict, page_html: str, *,
from quart import current_app
pages_sx = _build_pages_sx(current_app.name)
wasm_hash = _wasm_hash("sx_browser.bc.js")
wasm_hash = _wasm_hash("sx_browser.bc.wasm.js")
platform_hash = _wasm_hash("sx-platform-2.js")
sxbc_hash = _sxbc_hash()
# Shell: head + body with server-rendered HTML (not SX mount script)
shell = (
@@ -1074,7 +1078,7 @@ def sx_page_streaming_parts(ctx: dict, page_html: str, *,
tail = (
_SX_STREAMING_BOOTSTRAP + '\n'
+ f'<script src="{asset_url}/wasm/sx_browser.bc.wasm.js?v={wasm_hash}"></script>\n'
f'<script src="{asset_url}/wasm/sx-platform.js?v={wasm_hash}"></script>\n'
f'<script src="{asset_url}/wasm/sx-platform-2.js?v={platform_hash}" data-sxbc-hash="{sxbc_hash}"></script>\n'
)
return shell, tail
@@ -1124,6 +1128,21 @@ def _wasm_hash(filename: str) -> str:
return _SCRIPT_HASH_CACHE[key]
def _sxbc_hash() -> str:
"""Compute combined MD5 hash of all .sxbc files, cached for process lifetime."""
key = "sxbc/_combined"
if key not in _SCRIPT_HASH_CACHE:
try:
sxbc_dir = Path(__file__).resolve().parent.parent / "static" / "wasm" / "sx"
h = hashlib.md5()
for p in sorted(sxbc_dir.glob("*.sxbc")):
h.update(p.read_bytes())
_SCRIPT_HASH_CACHE[key] = h.hexdigest()[:8]
except OSError:
_SCRIPT_HASH_CACHE[key] = "dev"
return _SCRIPT_HASH_CACHE[key]
def _get_csrf_token() -> str:
"""Get the CSRF token from the current request context."""
try:

View File

@@ -190,7 +190,8 @@ class OcamlBridge:
# Only inject small, safe values as kernel variables.
# Large/complex blobs use placeholder tokens at render time.
for key in ("component_hash", "sx_css_classes", "asset_url",
"sx_js_hash", "body_js_hash", "wasm_hash"):
"sx_js_hash", "body_js_hash", "wasm_hash",
"platform_hash", "sxbc_hash"):
val = static.get(key) or ""
var = f"__shell-{key.replace('_', '-')}"
defn = f'(define {var} "{_escape(str(val))}")'
@@ -269,6 +270,7 @@ class OcamlBridge:
placeholders = {}
static_keys = {"component_hash", "sx_css_classes", "asset_url",
"sx_js_hash", "body_js_hash", "wasm_hash",
"platform_hash", "sxbc_hash",
"head_scripts", "body_scripts"}
# page_source is SX wire format that may contain \" escapes.
# Send via binary blob protocol to avoid double-escaping

View File

@@ -13,6 +13,8 @@
(body-html :as string?)
(asset-url :as string)
(wasm-hash :as string?)
(platform-hash :as string?)
(sxbc-hash :as string?)
(inline-css :as string?)
(inline-head-js :as string?)
(init-sx :as string?))
@@ -74,8 +76,12 @@
:type "text/sx"
:data-mount "#sx-root"
(raw! (or page-sx "")))
(let
((wv (or wasm-hash "0")))
(<>
(script :src (str asset-url "/wasm/sx_browser.bc.wasm.js?v=" wv))
(script :src (str asset-url "/wasm/sx-platform-2.js?v=" wv))))))))
(<>
(script
:src (str
asset-url
"/wasm/sx_browser.bc.wasm.js?v="
(or wasm-hash "0")))
(script
:src (str asset-url "/wasm/sx-platform-2.js?v=" (or platform-hash "0"))
:data-sxbc-hash (or sxbc-hash "0")))))))