Fix navigation: deep URL routing, back button, render timeout
- request-handler.sx: replace all dots (not just `.(`) and auto-quote undefined symbols as strings so 3-level URLs like /sx/(geography.(reactive.(examples.counter))) resolve correctly - sx-platform.js: register popstate handler (was missing from manual boot sequence) and fetch full HTML for back/forward navigation - sx_ref.ml: add CEK step limit (10M steps) checked every 4096 steps so runaway renders return 500 instead of blocking the worker forever - Rename test-runner.sx → runner-placeholder.sx to avoid `test-` skip - Playwright config: pin testDir, single worker, ignore worktrees Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,12 @@ let _last_error_kont : value ref = ref Nil
|
|||||||
|
|
||||||
(* === Transpiled from evaluator (frames + eval + CEK) === *)
|
(* === Transpiled from evaluator (frames + eval + CEK) === *)
|
||||||
|
|
||||||
|
(* Per-domain step limit (0 = no limit).
|
||||||
|
Set by the HTTP render worker before each page render.
|
||||||
|
Checked every 4096 CEK steps in cek_run. *)
|
||||||
|
let _step_limit : int Atomic.t = Atomic.make 0
|
||||||
|
let _step_count : int Atomic.t = Atomic.make 0
|
||||||
|
|
||||||
(* make-cek-state *)
|
(* make-cek-state *)
|
||||||
let rec make_cek_state control env kont =
|
let rec make_cek_state control env kont =
|
||||||
(CekState { cs_control = control; cs_env = env; cs_kont = kont; cs_phase = "eval"; cs_value = Nil })
|
(CekState { cs_control = control; cs_env = env; cs_kont = kont; cs_phase = "eval"; cs_value = Nil })
|
||||||
@@ -372,13 +378,23 @@ and sf_provide args env =
|
|||||||
and expand_macro mac raw_args env =
|
and expand_macro mac raw_args env =
|
||||||
(let local = (env_merge ((macro_closure (mac))) (env)) in (let () = ignore ((List.iter (fun pair -> ignore ((env_bind local (sx_to_string (first (pair))) (if sx_truthy ((prim_call "<" [(nth (pair) ((Number 1.0))); (len (raw_args))])) then (nth (raw_args) ((nth (pair) ((Number 1.0))))) else Nil)))) (sx_to_list (List (List.mapi (fun i p -> let i = Number (float_of_int i) in (List [p; i])) (sx_to_list (macro_params (mac)))))); Nil)) in (let () = ignore ((if sx_truthy ((macro_rest_param (mac))) then (env_bind local (sx_to_string (macro_rest_param (mac))) (prim_call "slice" [raw_args; (len ((macro_params (mac))))])) else Nil)) in (trampoline ((eval_expr ((macro_body (mac))) (local)))))))
|
(let local = (env_merge ((macro_closure (mac))) (env)) in (let () = ignore ((List.iter (fun pair -> ignore ((env_bind local (sx_to_string (first (pair))) (if sx_truthy ((prim_call "<" [(nth (pair) ((Number 1.0))); (len (raw_args))])) then (nth (raw_args) ((nth (pair) ((Number 1.0))))) else Nil)))) (sx_to_list (List (List.mapi (fun i p -> let i = Number (float_of_int i) in (List [p; i])) (sx_to_list (macro_params (mac)))))); Nil)) in (let () = ignore ((if sx_truthy ((macro_rest_param (mac))) then (env_bind local (sx_to_string (macro_rest_param (mac))) (prim_call "slice" [raw_args; (len ((macro_params (mac))))])) else Nil)) in (trampoline ((eval_expr ((macro_body (mac))) (local)))))))
|
||||||
|
|
||||||
(* cek-run *)
|
(* cek-run — iterative loop to avoid JS stack overflow in WASM *)
|
||||||
and cek_run state =
|
and cek_run state =
|
||||||
(if sx_truthy ((cek_terminal_p (state))) then (cek_value (state)) else
|
let s = ref state in
|
||||||
try cek_run ((cek_step (state)))
|
(try
|
||||||
with Eval_error msg ->
|
while not (match cek_terminal_p !s with Bool true -> true | _ -> false) do
|
||||||
(if !_last_error_kont = Nil then _last_error_kont := cek_kont state);
|
s := cek_step !s;
|
||||||
raise (Eval_error msg))
|
let n = Atomic.fetch_and_add _step_count 1 in
|
||||||
|
if n land 4095 = 0 then begin
|
||||||
|
let lim = Atomic.get _step_limit in
|
||||||
|
if lim > 0 && n >= lim then
|
||||||
|
raise (Eval_error (Printf.sprintf "Render step limit exceeded (%d steps)" n))
|
||||||
|
end
|
||||||
|
done;
|
||||||
|
cek_value !s
|
||||||
|
with Eval_error msg ->
|
||||||
|
(if !_last_error_kont = Nil then _last_error_kont := cek_kont !s);
|
||||||
|
raise (Eval_error msg))
|
||||||
|
|
||||||
(* cek-step *)
|
(* cek-step *)
|
||||||
and cek_step state =
|
and cek_step state =
|
||||||
|
|||||||
@@ -329,17 +329,17 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
var loaded = 0, bcCount = 0, srcCount = 0;
|
var loaded = 0, bcCount = 0, srcCount = 0;
|
||||||
if (K.beginModuleLoad) K.beginModuleLoad();
|
var inBatch = false;
|
||||||
for (var i = 0; i < files.length; i++) {
|
for (var i = 0; i < files.length; i++) {
|
||||||
|
if (!inBatch && K.beginModuleLoad) { K.beginModuleLoad(); inBatch = true; }
|
||||||
var r = loadBytecodeFile(files[i]);
|
var r = loadBytecodeFile(files[i]);
|
||||||
if (r) { bcCount++; continue; }
|
if (r) { bcCount++; continue; }
|
||||||
// Bytecode not available — end batch, load source, restart batch
|
// Bytecode not available — end batch, load source
|
||||||
if (K.endModuleLoad) K.endModuleLoad();
|
if (inBatch && K.endModuleLoad) { K.endModuleLoad(); inBatch = false; }
|
||||||
r = loadSxFile(files[i]);
|
r = loadSxFile(files[i]);
|
||||||
if (typeof r === "number") { loaded += r; srcCount++; }
|
if (typeof r === "number") { loaded += r; srcCount++; }
|
||||||
if (K.beginModuleLoad) K.beginModuleLoad();
|
|
||||||
}
|
}
|
||||||
if (K.endModuleLoad) K.endModuleLoad();
|
if (inBatch && K.endModuleLoad) K.endModuleLoad();
|
||||||
console.log("[sx-platform] Loaded " + files.length + " files (" + bcCount + " bytecode, " + srcCount + " source, " + loaded + " exprs)");
|
console.log("[sx-platform] Loaded " + files.length + " files (" + bcCount + " bytecode, " + srcCount + " source, " + loaded + " exprs)");
|
||||||
return loaded;
|
return loaded;
|
||||||
}
|
}
|
||||||
@@ -397,6 +397,43 @@
|
|||||||
"hydrated:", !!islands[j]._sxBoundislandhydrated || !!islands[j]["_sxBound" + "island-hydrated"],
|
"hydrated:", !!islands[j]._sxBoundislandhydrated || !!islands[j]["_sxBound" + "island-hydrated"],
|
||||||
"children:", islands[j].children.length);
|
"children:", islands[j].children.length);
|
||||||
}
|
}
|
||||||
|
// Register popstate handler for back/forward navigation.
|
||||||
|
// Fetch HTML (not SX) and extract #main-panel content.
|
||||||
|
window.addEventListener("popstate", function() {
|
||||||
|
var url = location.pathname + location.search;
|
||||||
|
var target = document.querySelector("#main-panel");
|
||||||
|
if (!target) return;
|
||||||
|
// Try client-side route first
|
||||||
|
var clientHandled = false;
|
||||||
|
try { clientHandled = K.eval('(try-client-route "' + url.replace(/"/g, '\\"') + '" "#main-panel")'); } catch(e) {}
|
||||||
|
if (clientHandled) return;
|
||||||
|
// Server fetch — request full HTML (no SX-Request header)
|
||||||
|
fetch(url)
|
||||||
|
.then(function(r) { return r.text(); })
|
||||||
|
.then(function(html) {
|
||||||
|
if (!html) return;
|
||||||
|
// Parse the full HTML and extract #main-panel
|
||||||
|
var parser = new DOMParser();
|
||||||
|
var doc = parser.parseFromString(html, "text/html");
|
||||||
|
var srcPanel = doc.querySelector("#main-panel");
|
||||||
|
var srcNav = doc.querySelector("#sx-nav");
|
||||||
|
if (srcPanel) {
|
||||||
|
target.outerHTML = srcPanel.outerHTML;
|
||||||
|
}
|
||||||
|
// Also update nav if present
|
||||||
|
var navTarget = document.querySelector("#sx-nav");
|
||||||
|
if (srcNav && navTarget) {
|
||||||
|
navTarget.outerHTML = srcNav.outerHTML;
|
||||||
|
}
|
||||||
|
// Re-hydrate
|
||||||
|
var newTarget = document.querySelector("#main-panel");
|
||||||
|
if (newTarget) {
|
||||||
|
try { K.eval("(post-swap (dom-query \"#main-panel\"))"); } catch(e) {}
|
||||||
|
try { K.eval("(sx-hydrate-islands (dom-query \"#main-panel\"))"); } catch(e) {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(e) { console.warn("[sx] popstate fetch error:", e); });
|
||||||
|
});
|
||||||
console.log("[sx] boot done");
|
console.log("[sx] boot done");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
const { defineConfig } = require('playwright/test');
|
const { defineConfig } = require('playwright/test');
|
||||||
|
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
testDir: '.',
|
testDir: __dirname,
|
||||||
|
testMatch: '*.spec.js',
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
retries: 0,
|
retries: 0,
|
||||||
|
workers: 1,
|
||||||
use: {
|
use: {
|
||||||
baseURL: process.env.SX_TEST_URL || 'http://localhost:8013',
|
baseURL: process.env.SX_TEST_URL || 'http://localhost:8013',
|
||||||
headless: true,
|
headless: true,
|
||||||
|
|||||||
@@ -1,16 +1,3 @@
|
|||||||
(define
|
|
||||||
sx-url-to-expr
|
|
||||||
(fn
|
|
||||||
(path)
|
|
||||||
(cond
|
|
||||||
(or (= path "/") (= path "/sx/") (= path "/sx"))
|
|
||||||
"home"
|
|
||||||
(starts-with? path "/sx/")
|
|
||||||
(join " " (split (slice path 4 (len path)) "."))
|
|
||||||
(starts-with? path "/")
|
|
||||||
(join " " (split (slice path 1 (len path)) "."))
|
|
||||||
:else path)))
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
sx-auto-quote
|
sx-auto-quote
|
||||||
(fn
|
(fn
|
||||||
@@ -23,22 +10,17 @@
|
|||||||
:else expr)))
|
:else expr)))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
sx-eval-page
|
sx-expr-to-str
|
||||||
(fn
|
(fn
|
||||||
(path-expr env)
|
(expr)
|
||||||
(cek-try
|
(cond
|
||||||
(fn
|
(string? expr)
|
||||||
()
|
(str "\"" expr "\"")
|
||||||
(let
|
(symbol? expr)
|
||||||
((exprs (sx-parse path-expr)))
|
(symbol-name expr)
|
||||||
(when
|
(list? expr)
|
||||||
(not (empty? exprs))
|
(str "(" (join " " (map sx-expr-to-str expr)) ")")
|
||||||
(let
|
:else (str expr))))
|
||||||
((expr (if (= (len exprs) 1) (first exprs) exprs))
|
|
||||||
(quoted (sx-auto-quote expr env))
|
|
||||||
(callable (if (symbol? quoted) (list quoted) quoted)))
|
|
||||||
(eval-expr callable env)))))
|
|
||||||
(fn (err) nil))))
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
sx-handle-request
|
sx-handle-request
|
||||||
@@ -46,50 +28,48 @@
|
|||||||
(path headers env)
|
(path headers env)
|
||||||
(let
|
(let
|
||||||
((is-ajax (or (has-key? headers "sx-request") (has-key? headers "hx-request")))
|
((is-ajax (or (has-key? headers "sx-request") (has-key? headers "hx-request")))
|
||||||
(path-expr (sx-url-to-expr path))
|
(raw-sx
|
||||||
(page-ast (sx-eval-page path-expr env)))
|
(if
|
||||||
|
(or (= path "/sx/") (= path "/"))
|
||||||
|
"(home)"
|
||||||
|
(slice path 4 (len path))))
|
||||||
|
(page-sx-raw (replace raw-sx "." " "))
|
||||||
|
(page-exprs (sx-parse page-sx-raw))
|
||||||
|
(page-expr (if (empty? page-exprs) nil (first page-exprs)))
|
||||||
|
(page-sx
|
||||||
|
(if
|
||||||
|
(nil? page-expr)
|
||||||
|
page-sx-raw
|
||||||
|
(sx-expr-to-str (sx-auto-quote page-expr env))))
|
||||||
|
(layout-sx (str "(~layouts/doc :path \"" path "\" " page-sx ")")))
|
||||||
(if
|
(if
|
||||||
(nil? page-ast)
|
is-ajax
|
||||||
nil
|
|
||||||
(let
|
(let
|
||||||
((nav-path (if (starts-with? path "/sx/") path (str "/sx" path))))
|
((full-sx (render-to-sx (first (sx-parse layout-sx)) env)))
|
||||||
(cek-try
|
full-sx)
|
||||||
(fn
|
(let
|
||||||
()
|
((body-html (render-to-html (first (sx-parse layout-sx)) env)))
|
||||||
(if
|
(render-to-html
|
||||||
is-ajax
|
(quasiquote
|
||||||
(let
|
(~shared:shell/sx-page-shell
|
||||||
((content (list (make-symbol "~layouts/doc") :path nav-path page-ast)))
|
:title "sx"
|
||||||
(render-to-html content env))
|
:csrf ""
|
||||||
(let
|
:page-sx (unquote layout-sx)
|
||||||
((wrapped (list (make-symbol "~layouts/doc") :path nav-path page-ast))
|
:body-html (unquote body-html)
|
||||||
(full-ast
|
:component-defs (unquote (env-get env "__shell-component-defs"))
|
||||||
(list
|
:component-hash (unquote (env-get env "__shell-component-hash"))
|
||||||
(make-symbol "~shared:layout/app-body")
|
:pages-sx (unquote (env-get env "__shell-pages-sx"))
|
||||||
:content wrapped))
|
:sx-css (unquote (env-get env "__shell-sx-css"))
|
||||||
(body-html (render-to-html full-ast env)))
|
:sx-css-classes (unquote (env-get env "__shell-sx-css-classes"))
|
||||||
(render-to-html
|
:asset-url (unquote (env-get env "__shell-asset-url"))
|
||||||
(list
|
:sx-js-hash (unquote (env-get env "__shell-sx-js-hash"))
|
||||||
(make-symbol "~shared:shell/sx-page-shell")
|
:body-js-hash (unquote (env-get env "__shell-body-js-hash"))
|
||||||
:title "SX"
|
:wasm-hash (unquote (env-get env "__shell-wasm-hash"))
|
||||||
:csrf ""
|
:head-scripts nil
|
||||||
:page-sx (serialize full-ast)
|
:body-scripts nil
|
||||||
:body-html body-html
|
:inline-css nil
|
||||||
:component-defs __shell-component-defs
|
:inline-head-js nil
|
||||||
:component-hash __shell-component-hash
|
:init-sx nil
|
||||||
:pages-sx __shell-pages-sx
|
:use-wasm true
|
||||||
:sx-css __shell-sx-css
|
:meta-html ""))
|
||||||
:sx-css-classes __shell-sx-css-classes
|
env))))))
|
||||||
:asset-url __shell-asset-url
|
|
||||||
:sx-js-hash __shell-sx-js-hash
|
|
||||||
:body-js-hash __shell-body-js-hash
|
|
||||||
:wasm-hash __shell-wasm-hash
|
|
||||||
:head-scripts __shell-head-scripts
|
|
||||||
:body-scripts __shell-body-scripts
|
|
||||||
:inline-css __shell-inline-css
|
|
||||||
:inline-head-js __shell-inline-head-js
|
|
||||||
:init-sx __shell-init-sx
|
|
||||||
:use-wasm true
|
|
||||||
:meta-html "")
|
|
||||||
env))))
|
|
||||||
(fn (err) (str "<h1>Render error</h1><pre>" err "</pre>"))))))))
|
|
||||||
|
|||||||
Reference in New Issue
Block a user