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
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:
@@ -537,8 +537,10 @@ and resolve_io_request request =
|
||||
| "import" ->
|
||||
(* Resolve library locally — load the .sx file *)
|
||||
let lib_spec = Sx_runtime.get_val request (String "library") in
|
||||
let key = Sx_ref.library_name_key lib_spec in
|
||||
if Sx_types.sx_truthy (Sx_ref.library_loaded_p key) then
|
||||
(* library_loaded_p takes the library SPEC and computes the key itself —
|
||||
passing an already-computed key string double-applies library_name_key
|
||||
and crashes (sx_to_list on a string). *)
|
||||
if Sx_types.sx_truthy (Sx_ref.library_loaded_p lib_spec) then
|
||||
(* Already loaded — just resume *)
|
||||
Nil
|
||||
else begin
|
||||
@@ -1815,8 +1817,9 @@ let rec dispatch env cmd =
|
||||
| _ -> "" in
|
||||
let response = if op = "import" then begin
|
||||
let lib_spec = Sx_runtime.get_val request (String "library") in
|
||||
let key = Sx_ref.library_name_key lib_spec in
|
||||
if Sx_types.sx_truthy (Sx_ref.library_loaded_p key) then Nil
|
||||
(* pass the SPEC, not a pre-computed key — library_loaded_p applies
|
||||
library_name_key itself (a key string would crash sx_to_list). *)
|
||||
if Sx_types.sx_truthy (Sx_ref.library_loaded_p lib_spec) then Nil
|
||||
else begin
|
||||
(match resolve_library_path lib_spec with
|
||||
| Some path -> load_library_file path | None -> ());
|
||||
|
||||
@@ -73,6 +73,7 @@ let rec value_to_js (v : value) : Js.Unsafe.any =
|
||||
| Nil -> Js.Unsafe.inject Js.null
|
||||
| Bool b -> Js.Unsafe.inject (Js.bool b)
|
||||
| Number n -> Js.Unsafe.inject (Js.number_of_float n)
|
||||
| Integer n -> Js.Unsafe.inject (Js.number_of_float (float_of_int n))
|
||||
| String s -> Js.Unsafe.inject (Js.string s)
|
||||
| RawHTML s -> Js.Unsafe.inject (Js.string s)
|
||||
| Symbol s ->
|
||||
@@ -329,8 +330,9 @@ let handle_import_suspension request =
|
||||
let lib_spec = match request with
|
||||
| Dict d -> (match Hashtbl.find_opt d "library" with Some v -> v | _ -> Nil)
|
||||
| _ -> Nil in
|
||||
let key = Sx_ref.library_name_key lib_spec in
|
||||
if Sx_types.sx_truthy (Sx_ref.library_loaded_p key) then
|
||||
(* library_loaded_p takes the SPEC and applies library_name_key itself —
|
||||
passing a pre-computed key string double-applies it and crashes. *)
|
||||
if Sx_types.sx_truthy (Sx_ref.library_loaded_p lib_spec) then
|
||||
Some Nil (* Already loaded — resume immediately *)
|
||||
else
|
||||
None (* Not loaded — JS platform must fetch it *)
|
||||
|
||||
@@ -404,7 +404,7 @@ and library_loaded_p spec =
|
||||
|
||||
(* library-exports *)
|
||||
and library_exports spec =
|
||||
(get ((get (_library_registry_) ((library_name_key (spec))))) ((String "exports")))
|
||||
(let entry = (get (_library_registry_) ((library_name_key (spec)))) in (if sx_truthy (entry) then (get (entry) ((String "exports"))) else (Dict (Hashtbl.create 0))))
|
||||
|
||||
(* register-library *)
|
||||
and register_library spec exports =
|
||||
|
||||
@@ -33,8 +33,10 @@ test.describe('blog SPA', () => {
|
||||
test('links inside #content get boosted', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitReady(page);
|
||||
// the engine marks a boosted element with data-sx-bound containing "boost"
|
||||
await expect(page.locator(POSTLINK).first()).toHaveAttribute('data-sx-bound', /boost/, { timeout: 15000 });
|
||||
// the engine marks a boosted link with the _sxBoundboost JS property
|
||||
await expect
|
||||
.poll(() => page.locator(POSTLINK).first().evaluate((a) => !!a._sxBoundboost), { timeout: 15000 })
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test('clicking a link does a fragment swap — no full reload, URL updates', async ({ page }) => {
|
||||
|
||||
@@ -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'));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -476,7 +476,9 @@
|
||||
library-exports
|
||||
(fn
|
||||
(spec)
|
||||
(get (get *library-registry* (library-name-key spec)) "exports")))
|
||||
(let
|
||||
((entry (get *library-registry* (library-name-key spec))))
|
||||
(if entry (get entry "exports") (dict)))))
|
||||
|
||||
(define
|
||||
register-library
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user