diff --git a/hosts/ocaml/lib/sx_primitives.ml b/hosts/ocaml/lib/sx_primitives.ml index c1c17cb6..1dc86f91 100644 --- a/hosts/ocaml/lib/sx_primitives.ml +++ b/hosts/ocaml/lib/sx_primitives.ml @@ -667,6 +667,25 @@ let () = | _ -> raise (Eval_error "error: 1 arg")); (* client? — false by default (server); sx_browser.ml sets _is_client := true *) register "client?" (fun _args -> Bool !_is_client); + (* Named stores — global mutable registry, bypasses env scoping issues *) + let store_registry : (string, value) Hashtbl.t = Hashtbl.create 16 in + register "def-store" (fun args -> + match args with + | [String name; init_fn] -> + if not (Hashtbl.mem store_registry name) then begin + let store = !_sx_trampoline_fn (!_sx_call_fn init_fn []) in + Hashtbl.replace store_registry name store + end; + (match Hashtbl.find_opt store_registry name with Some v -> v | None -> Nil) + | _ -> raise (Eval_error "def-store: expected (name init-fn)")); + register "use-store" (fun args -> + match args with + | [String name] -> + (match Hashtbl.find_opt store_registry name with + | Some v -> v + | None -> raise (Eval_error ("Store not found: " ^ name))) + | _ -> raise (Eval_error "use-store: expected (name)")); + register "clear-stores" (fun _args -> Hashtbl.clear store_registry; Nil); register "apply" (fun args -> let call f a = match f with diff --git a/shared/static/wasm/sx-platform.js b/shared/static/wasm/sx-platform.js index 6a1cdc0b..5e64c5d9 100644 --- a/shared/static/wasm/sx-platform.js +++ b/shared/static/wasm/sx-platform.js @@ -199,7 +199,6 @@ /** * Deserialize type-tagged JSON constant back to JS value for loadModule. - * (Legacy — used for .sxbc.json fallback) */ function deserializeConstant(c) { if (!c || !c.t) return null; @@ -229,61 +228,35 @@ } /** - * Try loading a pre-compiled bytecode module. - * Tries .sxbc (s-expression) first, falls back to .sxbc.json (legacy). + * Try loading a pre-compiled .sxbc.json bytecode module. * Returns true on success, null on failure (caller falls back to .sx source). */ function loadBytecodeFile(path) { - // Try .sxbc (s-expression format) first - var sxbcPath = path.replace(/\.sx$/, '.sxbc'); - var url = _baseUrl + sxbcPath + _cacheBust; + var bcPath = path.replace(/\.sx$/, '.sxbc.json'); + var url = _baseUrl + bcPath + _cacheBust; try { var xhr = new XMLHttpRequest(); xhr.open("GET", url, false); xhr.send(); - if (xhr.status === 200 && xhr.responseText.indexOf("(sxbc") === 0) { - // Parse with the SX kernel — returns the (sxbc ...) form - var parsed = K.eval('(first (sx-parse ' + JSON.stringify(xhr.responseText) + '))'); - // (sxbc version hash (code ...)) — third element is the module - var module = K.eval('(nth (sx-parse ' + JSON.stringify(xhr.responseText) + ') 0)'); - // Use evalSxbc which understands the sxbc format - var result = K.eval('(load-sxbc (first (sx-parse ' + JSON.stringify(xhr.responseText) + ')))'); - if (typeof result === 'string' && result.indexOf('Error') === 0) { - console.warn("[sx-platform] sxbc FAIL " + path + ":", result); - } else { - return true; - } - } - } catch(e) { - // Fall through to JSON - } + if (xhr.status !== 200) return null; - // Fallback: .sxbc.json (legacy format) - var bcPath = path.replace(/\.sx$/, '.sxbc.json'); - url = _baseUrl + bcPath + _cacheBust; - try { - var xhr2 = new XMLHttpRequest(); - xhr2.open("GET", url, false); - xhr2.send(); - if (xhr2.status !== 200) return null; - - var json = JSON.parse(xhr2.responseText); + var json = JSON.parse(xhr.responseText); if (!json.module || json.magic !== 'SXBC') return null; - var module2 = { + var module = { _type: 'dict', bytecode: { _type: 'list', items: json.module.bytecode }, constants: { _type: 'list', items: json.module.constants.map(deserializeConstant) }, }; - var result2 = K.loadModule(module2); - if (typeof result2 === 'string' && result2.indexOf('Error') === 0) { - console.warn("[sx-platform] bytecode FAIL " + path + ":", result2); + var result = K.loadModule(module); + if (typeof result === 'string' && result.indexOf('Error') === 0) { + console.warn("[sx-platform] bytecode FAIL " + path + ":", result); return null; } + console.log("[sx-platform] ok " + path + " (bytecode)"); return true; } catch(e) { - console.error("[sx-platform] bytecode EXCEPTION " + path + ":", e); return null; } } @@ -356,23 +329,18 @@ ]; var loaded = 0, bcCount = 0, srcCount = 0; - var t0 = performance.now(); - var forceSrc = (typeof window !== "undefined" && window.location.search.indexOf("nosxbc") !== -1); - if (!forceSrc && K.beginModuleLoad) K.beginModuleLoad(); + if (K.beginModuleLoad) K.beginModuleLoad(); for (var i = 0; i < files.length; i++) { - if (!forceSrc) { - var r = loadBytecodeFile(files[i]); - if (r) { bcCount++; continue; } - } + var r = loadBytecodeFile(files[i]); + if (r) { bcCount++; continue; } // Bytecode not available — end batch, load source, restart batch - if (!forceSrc && K.endModuleLoad) K.endModuleLoad(); + if (K.endModuleLoad) K.endModuleLoad(); r = loadSxFile(files[i]); if (typeof r === "number") { loaded += r; srcCount++; } - if (!forceSrc && K.beginModuleLoad) K.beginModuleLoad(); + if (K.beginModuleLoad) K.beginModuleLoad(); } - if (!forceSrc && K.endModuleLoad) K.endModuleLoad(); - var elapsed = Math.round(performance.now() - t0); - console.log("[sx-platform] Loaded " + files.length + " files (" + bcCount + " bytecode, " + srcCount + " source, " + loaded + " exprs) in " + elapsed + "ms"); + if (K.endModuleLoad) K.endModuleLoad(); + console.log("[sx-platform] Loaded " + files.length + " files (" + bcCount + " bytecode, " + srcCount + " source, " + loaded + " exprs)"); return loaded; } @@ -390,33 +358,46 @@ engine: function() { return K.engine(); }, // Boot entry point (called by auto-init or manually) init: function() { - if (typeof K.eval !== "function") return; - var steps = [ - '(log-info (str "sx-browser " SX_VERSION))', - '(init-css-tracking)', - '(process-page-scripts)', - '(process-sx-scripts nil)', - '(sx-hydrate-elements nil)', - '(sx-hydrate-islands nil)', - '(run-post-render-hooks)', - '(process-elements nil)', - '(dom-listen (dom-window) "popstate" (fn (e) (handle-popstate 0)))' - ]; - var failures = []; - for (var i = 0; i < steps.length; i++) { - try { - var r = K.eval(steps[i]); - if (typeof r === "string" && r.indexOf("Error") === 0) { - console.error("[sx] boot step " + i + " FAILED: " + steps[i].substring(0, 60), r); - failures.push({ step: i, expr: steps[i], error: r }); - } - } catch(e) { - console.error("[sx] boot step " + i + " THREW: " + steps[i].substring(0, 60), e); - failures.push({ step: i, expr: steps[i], error: String(e) }); + if (typeof K.eval === "function") { + // Check boot-init exists + // Step through boot manually + console.log("[sx] init-css-tracking..."); + K.eval("(init-css-tracking)"); + console.log("[sx] process-page-scripts..."); + K.eval("(process-page-scripts)"); + console.log("[sx] routes after pages:", K.eval("(len _page-routes)")); + console.log("[sx] process-sx-scripts..."); + K.eval("(process-sx-scripts nil)"); + console.log("[sx] sx-hydrate-elements..."); + K.eval("(sx-hydrate-elements nil)"); + console.log("[sx] sx-hydrate-islands..."); + K.eval("(sx-hydrate-islands nil)"); + console.log("[sx] process-elements..."); + K.eval("(process-elements nil)"); + // Debug islands + console.log("[sx] ~home/stepper defined?", K.eval("(type-of ~home/stepper)")); + console.log("[sx] ~layouts/header defined?", K.eval("(type-of ~layouts/header)")); + // Island count (JS-side, avoids VM overhead) + console.log("[sx] manual island query:", document.querySelectorAll("[data-sx-island]").length); + // Try hydrating again + console.log("[sx] retry hydrate-islands..."); + K.eval("(sx-hydrate-islands nil)"); + // Check if links are boosted + var links = document.querySelectorAll("a[href]"); + var boosted = 0; + for (var i = 0; i < links.length; i++) { + if (links[i]._sxBoundboost) boosted++; } - } - if (failures.length > 0) { - console.error("[sx] BOOT FAILED: " + failures.length + " step(s) errored:", failures.map(function(f) { return "step " + f.step + ": " + f.error; }).join("; ")); + console.log("[sx] boosted links:", boosted, "/", links.length); + // Check island state + var islands = document.querySelectorAll("[data-sx-island]"); + console.log("[sx] islands:", islands.length); + for (var j = 0; j < islands.length; j++) { + console.log("[sx] island:", islands[j].getAttribute("data-sx-island"), + "hydrated:", !!islands[j]._sxBoundislandhydrated || !!islands[j]["_sxBound" + "island-hydrated"], + "children:", islands[j].children.length); + } + console.log("[sx] boot done"); } } }; @@ -429,8 +410,8 @@ var _doInit = function() { loadWebStack(); Sx.init(); - // JIT disabled for debugging - // setTimeout(function() { K.eval('(enable-jit!)'); }, 0); + // Enable JIT after all boot code has run + setTimeout(function() { K.eval('(enable-jit!)'); }, 0); }; if (document.readyState === "loading") { diff --git a/shared/static/wasm/sx_browser.bc.wasm.assets/sx-6f7dfa09.wasm b/shared/static/wasm/sx_browser.bc.wasm.assets/sx-6f7dfa09.wasm new file mode 100644 index 00000000..4fa5c0c1 Binary files /dev/null and b/shared/static/wasm/sx_browser.bc.wasm.assets/sx-6f7dfa09.wasm differ diff --git a/shared/static/wasm/sx_browser.bc.wasm.js b/shared/static/wasm/sx_browser.bc.wasm.js index d18ee436..4efdab08 100644 --- a/shared/static/wasm/sx_browser.bc.wasm.js +++ b/shared/static/wasm/sx_browser.bc.wasm.js @@ -1792,7 +1792,7 @@ blake2_js_for_wasm_create: blake2_js_for_wasm_create}; } (globalThis)) -({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-951e6734",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-6b156118",[2,3,5]],["std_exit-10fb8830",[2]],["start-80fdb768",0]],"generated":(b=>{var +({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-6f7dfa09",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-6b156118",[2,3,5]],["std_exit-10fb8830",[2]],["start-80fdb768",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 diff --git a/tests/playwright/isomorphic.spec.js b/tests/playwright/isomorphic.spec.js index 45d5a101..bcffe216 100644 --- a/tests/playwright/isomorphic.spec.js +++ b/tests/playwright/isomorphic.spec.js @@ -177,7 +177,9 @@ test.describe('Isomorphic SSR', () => { }); test.fixme('navigation preserves header island state', async ({ page }) => { - // BUG: def-store state not persisting — *store-registry* likely reset during SPA nav component reload + // BUG: client? primitive works from K.eval but not during render-to-dom hydration. + // Needs JIT/env investigation — the pre-existing JIT "Not callable: nil" bug + // prevents primitives registered after boot from being called during component eval. await page.goto(BASE_URL + '/sx/', { waitUntil: 'networkidle' }); // Wait for header island to hydrate diff --git a/web/web-signals.sx b/web/web-signals.sx index 14d4280b..93eadb7c 100644 --- a/web/web-signals.sx +++ b/web/web-signals.sx @@ -35,25 +35,9 @@ ;; Named stores — page-level signal containers ;; -------------------------------------------------------------------------- -(define *store-registry* (dict)) - -(define def-store :effects [mutation] - (fn ((name :as string) (init-fn :as lambda)) - (let ((registry *store-registry*)) - (when (not (has-key? registry name)) - (set! *store-registry* (assoc registry name (cek-call init-fn nil)))) - (get *store-registry* name)))) - -(define use-store :effects [] - (fn ((name :as string)) - (if (has-key? *store-registry* name) - (get *store-registry* name) - (error (str "Store not found: " name - ". Call (def-store ...) before (use-store ...)."))))) - -(define clear-stores :effects [mutation] - (fn () - (set! *store-registry* (dict)))) +;; def-store, use-store, clear-stores are now OCaml primitives +;; (sx_primitives.ml) with a global mutable registry that survives +;; env scoping across bytecode modules and island hydration. ;; --------------------------------------------------------------------------