host+kernel: blog SPA boost works end-to-end on the WASM OCaml kernel (Playwright 4/4)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 43s

Clicking a blog link now fragment-swaps #content with URL push + working back
button, no full reload — the SX-htmx engine driving the same OCaml kernel the
server runs. Six bugs in the source-load + boost path, found by bisecting in
chromium, all fixed:

1. Import double-apply (sx_server.ml x2, sx_browser.ml): the import suspension
   handlers computed `key = library_name_key lib_spec` then called
   `library_loaded_p key` — but library_loaded_p applies library_name_key
   itself, so it ran sx_to_list on a string and crashed ("Expected list, got
   string"). Only unloaded libs suspend, so it only bit lazy imports. Pass the
   spec, not the key.

2. Unloaded-import crash (spec/evaluator.sx + sx_ref.ml library_exports): an
   import of a not-yet-loaded library returned nil exports, and bind-import-set
   did (keys nil) -> crash. Return an empty dict so the import is a graceful
   no-op (lazy symbol resolution covers real usage).

3. value_to_js missing Integer (sx_browser.ml): integers passed to host methods
   were mishandled, so dom-query-all's (host-call node-list "item" i) ignored i
   and returned node 0 for every index — every element aliased the first, so
   only one link ever boosted. Add the Integer -> JS number case.

4. browser-same-origin? rejected relative URLs (browser.sx x2): it only did
   (starts-with? url origin), so "/alpha/" was treated as cross-origin and
   should-boost-link? refused every relative link. Accept scheme-less,
   non-protocol-relative URLs.

5. dom-query-in undefined (orchestration.sx x2): the swap path called a function
   that exists nowhere; it's just dom-query with a container arg.

6. Lazy-deps never loaded under source fallback (sx-platform.js): lazy symbol
   resolution only fires on the VM GLOBAL_GET path, but source-loaded swap
   callbacks run on the CEK and raise instead of lazy-loading, so the post-swap
   hs-boot-subtree!/htmx-boot-subtree! were undefined and aborted URL push.
   Preload the manifest's lazy-deps.

Verified: native host conformance 271/271; lib/host/playwright/spa-check 4/4
(boot, boost, fragment swap + URL push, back button) in real chromium against an
ephemeral durable host server.
This commit is contained in:
2026-06-29 11:09:11 +00:00
parent 05c0a0b01a
commit 689dae7d0c
12 changed files with 12987 additions and 5363 deletions

View File

@@ -643,6 +643,19 @@
loadLibrary(entry.deps[i], loading);
}
// Also eagerly load lazy-deps. Lazy symbol resolution (the _resolve-symbol
// hook) only fires on the VM GLOBAL_GET path, but source-loaded modules run
// their callbacks via the CEK, which raises "Undefined symbol" instead of
// lazy-loading. So when bytecode is unavailable (source fallback), the swap
// post-processing (hs-boot-subtree! / htmx-boot-subtree! in process-elements)
// would fail. Preload them to keep every symbol defined.
var lazyDeps = entry["lazy-deps"] || entry.lazyDeps;
if (lazyDeps) {
for (var li = 0; li < lazyDeps.length; li++) {
loadLibrary(lazyDeps[li], loading);
}
}
// Load entry point itself (boot.sx — not a library, just defines + init)
loadBytecodeFile("sx/" + entry.file) || loadSxFile("sx/" + entry.file.replace(/\.sxbc$/, '.sx'));

View File

@@ -49,7 +49,10 @@
(fn () (host-get (host-get (dom-window) "location") "origin")))
(define
browser-same-origin?
(fn (url) (starts-with? url (browser-location-origin))))
;; A relative URL (no scheme, not protocol-relative "//host") is same-origin
;; by definition; an absolute URL must start with our origin. The old check
;; only did the latter, so it wrongly rejected every relative link ("/x").
(fn (url) (or (starts-with? url (browser-location-origin)) (and (not (string-contains? url "://")) (not (starts-with? url "//"))))))
(define
url-pathname
(fn

View File

@@ -416,7 +416,7 @@
(post-swap t)))
(hoist-head-elements container)
(let
((manifest-el (dom-query-in container "script[data-sx-manifest]")))
((manifest-el (dom-query container "script[data-sx-manifest]")))
(when
manifest-el
(host-call

File diff suppressed because one or more lines are too long

View File

@@ -1792,7 +1792,7 @@
blake2_js_for_wasm_create: blake2_js_for_wasm_create};
}
(globalThis))
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["re-9a0de245",[2]],["unix-100664f4",[2]],["sx-836faede",[2,3,4]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,6]],["dune__exe__Sx_browser-eb5ef4e3",[2,5,7]],["std_exit-10fb8830",[2]],["start-94d492e1",0]],"generated":(b=>{var
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["re-9a0de245",[2]],["unix-100664f4",[2]],["sx-8edb1a1d",[2,3,4]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,6]],["dune__exe__Sx_browser-4878f9e1",[2,5,7]],["std_exit-10fb8830",[2]],["start-8c705527",0]],"generated":(b=>{var
c=b,a=b?.module?.export||b;return{"env":{"caml_ba_kind_of_typed_array":()=>{throw new
Error("caml_ba_kind_of_typed_array not implemented")},"caml_exn_with_js_backtrace":()=>{throw new
Error("caml_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new