From 998536f52d8a1b575b51ed464f362724c86b7a7e Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 25 Mar 2026 14:21:51 +0000 Subject: [PATCH] Cache-bust wasm scripts, fix orchestration.sx paren balance - Add wasm_hash (MD5 of sx_browser.bc.js) to shell template - Script tags: /wasm/sx_browser.bc.js?v={hash}, /wasm/sx-platform.js?v={hash} - Pass wasm_hash through helpers.py and ocaml_bridge.py - Fix missing close paren in bind-sse-swap (broke SX parsing) Co-Authored-By: Claude Opus 4.6 (1M context) --- shared/static/wasm/sx/orchestration.sx | 5 +++-- shared/sx/helpers.py | 22 ++++++++++++++++++++-- shared/sx/ocaml_bridge.py | 4 ++-- shared/sx/templates/shell.sx | 11 +++++++++-- web/orchestration.sx | 5 +++-- 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/shared/static/wasm/sx/orchestration.sx b/shared/static/wasm/sx/orchestration.sx index d12e9d57..46267717 100644 --- a/shared/static/wasm/sx/orchestration.sx +++ b/shared/static/wasm/sx/orchestration.sx @@ -300,7 +300,8 @@ (with-transition use-transition (fn () (let ((swap-result (swap-dom-nodes target content swap-style))) - (post-swap (or swap-result target)))))))))))) + (post-swap (or swap-result target))))))))))))) + (define handle-html-response :effects [mutation io] @@ -1083,7 +1084,7 @@ (with-transition use-transition (fn () (swap-html-string target trimmed swap-style) - (post-swap target)))))))) + (post-swap target))))))))) ;; -------------------------------------------------------------------------- diff --git a/shared/sx/helpers.py b/shared/sx/helpers.py index 10211149..95367c7f 100644 --- a/shared/sx/helpers.py +++ b/shared/sx/helpers.py @@ -7,6 +7,7 @@ page elements (headers, search, etc.) from template context. from __future__ import annotations import hashlib +import os from pathlib import Path from typing import Any @@ -884,12 +885,14 @@ def _get_shell_static() -> dict[str, Any]: sx_css_classes=sx_css_classes, sx_js_hash=_script_hash("sx-browser.js"), body_js_hash=_script_hash("body.js"), + wasm_hash=_wasm_hash("sx_browser.bc.js"), asset_url=_ca.config.get("ASSET_URL", "/static"), head_scripts=_shell_cfg.get("head_scripts"), inline_css=_shell_cfg.get("inline_css"), inline_head_js=_shell_cfg.get("inline_head_js"), init_sx=_shell_cfg.get("init_sx"), body_scripts=_shell_cfg.get("body_scripts"), + use_wasm=os.environ.get("SX_USE_WASM") == "1", ) t1 = time.monotonic() @@ -1081,8 +1084,11 @@ def sx_page_streaming_parts(ctx: dict, page_html: str, *, # Tail: bootstrap suspense resolver + scripts + close tail = ( _SX_STREAMING_BOOTSTRAP + '\n' - f'\n' - f'\n' + + (f'\n' + f'\n' + if os.environ.get("SX_USE_WASM") == "1" else + f'\n') + + f'\n' ) return shell, tail @@ -1120,6 +1126,18 @@ def _script_hash(filename: str) -> str: return _SCRIPT_HASH_CACHE[filename] +def _wasm_hash(filename: str) -> str: + """Compute MD5 hash of a static wasm file, cached for process lifetime.""" + key = "wasm/" + filename + if key not in _SCRIPT_HASH_CACHE: + try: + data = (Path(__file__).resolve().parent.parent / "static" / "wasm" / filename).read_bytes() + _SCRIPT_HASH_CACHE[key] = hashlib.md5(data).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: diff --git a/shared/sx/ocaml_bridge.py b/shared/sx/ocaml_bridge.py index 182672d3..2e22281a 100644 --- a/shared/sx/ocaml_bridge.py +++ b/shared/sx/ocaml_bridge.py @@ -190,7 +190,7 @@ 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"): + "sx_js_hash", "body_js_hash", "wasm_hash"): val = static.get(key) or "" var = f"__shell-{key.replace('_', '-')}" defn = f'(define {var} "{_escape(str(val))}")' @@ -268,7 +268,7 @@ class OcamlBridge: "sx_css", "inline_css", "inline_head_js"} placeholders = {} static_keys = {"component_hash", "sx_css_classes", "asset_url", - "sx_js_hash", "body_js_hash", + "sx_js_hash", "body_js_hash", "wasm_hash", "head_scripts", "body_scripts"} # page_source is SX wire format that may contain \" escapes. # Send via binary blob protocol to avoid double-escaping diff --git a/shared/sx/templates/shell.sx b/shared/sx/templates/shell.sx index eb83fb46..1c2ef892 100644 --- a/shared/sx/templates/shell.sx +++ b/shared/sx/templates/shell.sx @@ -17,8 +17,10 @@ (pages-sx :as string?) (page-sx :as string?) (body-html :as string?) (asset-url :as string) (sx-js-hash :as string) (body-js-hash :as string?) + (wasm-hash :as string?) (head-scripts :as list?) (inline-css :as string?) (inline-head-js :as string?) - (init-sx :as string?) (body-scripts :as list?)) + (init-sx :as string?) (body-scripts :as list?) + (use-wasm :as boolean?)) (<> (raw! "") (html :lang "en" @@ -83,7 +85,12 @@ details.group{overflow:hidden}details.group>summary{list-style:none}details.grou (raw! (or pages-sx ""))) (script :type "text/sx" :data-mount "#sx-root" (raw! (or page-sx ""))) - (script :src (str asset-url "/scripts/sx-browser.js?v=" sx-js-hash)) + (if use-wasm + (let ((wv (or wasm-hash "dev"))) + (<> + (script :src (str asset-url "/wasm/sx_browser.bc.js?v=" wv)) + (script :src (str asset-url "/wasm/sx-platform.js?v=" wv)))) + (script :src (str asset-url "/scripts/sx-browser.js?v=" sx-js-hash))) ;; Body scripts — configurable per app ;; Pass a list (even empty) to override defaults; nil = use defaults (if (not (nil? body-scripts)) diff --git a/web/orchestration.sx b/web/orchestration.sx index d12e9d57..46267717 100644 --- a/web/orchestration.sx +++ b/web/orchestration.sx @@ -300,7 +300,8 @@ (with-transition use-transition (fn () (let ((swap-result (swap-dom-nodes target content swap-style))) - (post-swap (or swap-result target)))))))))))) + (post-swap (or swap-result target))))))))))))) + (define handle-html-response :effects [mutation io] @@ -1083,7 +1084,7 @@ (with-transition use-transition (fn () (swap-html-string target trimmed swap-style) - (post-swap target)))))))) + (post-swap target))))))))) ;; --------------------------------------------------------------------------