Isomorphic SSR: server renders HTML body, client takes over with SX
Server now renders page content as HTML inside <div id="sx-root">, visible immediately before JavaScript loads. The SX source is still included in a <script data-mount="#sx-root"> tag for client hydration. SSR pipeline: after aser produces the SX wire format, parse and render-to-html it (~17ms for a 22KB page). Islands with reactive state gracefully fall back to empty — client hydrates them. Supporting changes: - Load signals.sx into OCaml kernel (reactive primitives for island SSR) - Add cek-call and context to kernel env (needed by signals/deref) - Island-aware component accessors in sx_types.ml - render-to-html handles Island values (renders as component with fallback) - Fix 431 (Request Header Fields Too Large): replace SX-Components header (full component name list) with SX-Components-Hash (12 chars) - CORS allow SX-Components-Hash header Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -425,7 +425,7 @@ let make_server_env () =
|
||||
(* context — scope lookup. The CEK handles this as a special form
|
||||
by walking continuation frames, but compiled VM code needs it as
|
||||
a function that reads from the scope_stacks hashtable. *)
|
||||
bind "sx-context" (fun args ->
|
||||
let context_impl = NativeFn ("context", fun args ->
|
||||
match args with
|
||||
| [String name] | [String name; _] ->
|
||||
let stack = try Hashtbl.find scope_stacks name with Not_found -> [] in
|
||||
@@ -433,7 +433,9 @@ let make_server_env () =
|
||||
| v :: _, _ -> v
|
||||
| [], [_; default_val] -> default_val
|
||||
| [], _ -> Nil)
|
||||
| _ -> Nil);
|
||||
| _ -> Nil) in
|
||||
ignore (env_bind env "sx-context" context_impl);
|
||||
ignore (env_bind env "context" context_impl);
|
||||
|
||||
(* qq-expand-runtime — quasiquote expansion at runtime.
|
||||
The bytecode compiler emits CALL_PRIM "qq-expand-runtime" for
|
||||
@@ -464,6 +466,12 @@ let make_server_env () =
|
||||
| [fn_val; List call_args] ->
|
||||
Sx_ref.eval_expr (List (fn_val :: call_args)) (Env env)
|
||||
| _ -> raise (Eval_error "call-lambda: expected (fn args env?)"));
|
||||
bind "cek-call" (fun args ->
|
||||
match args with
|
||||
| [fn_val; List call_args] -> Sx_ref.cek_call fn_val (List call_args)
|
||||
| [fn_val; Nil] -> Sx_ref.cek_call fn_val (List [])
|
||||
| [fn_val] -> Sx_ref.cek_call fn_val (List [])
|
||||
| _ -> Nil);
|
||||
bind "expand-macro" (fun args ->
|
||||
match args with
|
||||
| [Macro m; List macro_args; Env e] ->
|
||||
@@ -1072,6 +1080,22 @@ let rec dispatch env cmd =
|
||||
in
|
||||
let body_final = flush_batched_io body_str in
|
||||
let t2 = Unix.gettimeofday () in
|
||||
(* Phase 1b: render the aser'd SX to HTML for isomorphic SSR.
|
||||
The aser output is flat (all components expanded, just HTML tags),
|
||||
so render-to-html is cheap — no component lookups needed. *)
|
||||
let body_html =
|
||||
try
|
||||
let body_exprs = Sx_parser.parse_all body_final in
|
||||
let body_expr = match body_exprs with
|
||||
| [e] -> e | [] -> Nil | _ -> List (Symbol "<>" :: body_exprs)
|
||||
in
|
||||
Sx_render.render_to_html body_expr env
|
||||
with e ->
|
||||
Printf.eprintf "[ssr] render-to-html failed: %s\n%!" (Printexc.to_string e);
|
||||
"" (* fallback: client renders from SX source. Islands with
|
||||
reactive state may fail SSR — client hydrates them. *)
|
||||
in
|
||||
let t2b = Unix.gettimeofday () in
|
||||
(* Phase 2: render shell with body + all kwargs.
|
||||
Resolve symbol references (e.g. __shell-component-defs) to their
|
||||
values from the env — these were pre-injected by the bridge. *)
|
||||
@@ -1082,13 +1106,15 @@ let rec dispatch env cmd =
|
||||
with _ -> try Sx_primitives.get_primitive s with _ -> v)
|
||||
| _ -> v
|
||||
) shell_kwargs in
|
||||
let shell_args = Keyword "page-sx" :: String body_final :: resolved_kwargs in
|
||||
let shell_args = Keyword "page-sx" :: String body_final
|
||||
:: Keyword "body-html" :: String body_html
|
||||
:: resolved_kwargs in
|
||||
let shell_call = List (Symbol "~shared:shell/sx-page-shell" :: shell_args) in
|
||||
let html = Sx_render.render_to_html shell_call env in
|
||||
let t3 = Unix.gettimeofday () in
|
||||
Printf.eprintf "[sx-page-full] aser=%.3fs io=%.3fs shell=%.3fs total=%.3fs body=%d html=%d\n%!"
|
||||
(t1 -. t0) (t2 -. t1) (t3 -. t2) (t3 -. t0)
|
||||
(String.length body_final) (String.length html);
|
||||
Printf.eprintf "[sx-page-full] aser=%.3fs io=%.3fs ssr=%.3fs shell=%.3fs total=%.3fs body=%d ssr=%d html=%d\n%!"
|
||||
(t1 -. t0) (t2 -. t1) (t2b -. t2) (t3 -. t2b) (t3 -. t0)
|
||||
(String.length body_final) (String.length body_html) (String.length html);
|
||||
send_ok_string html
|
||||
with
|
||||
| Eval_error msg ->
|
||||
|
||||
@@ -256,6 +256,18 @@ and render_list_to_html head args env =
|
||||
let v = env_get env name in
|
||||
(match v with
|
||||
| Component _ -> render_component v args env
|
||||
| Island i ->
|
||||
(* Islands: render initial HTML server-side (like React SSR).
|
||||
Log failures so we can fix them. *)
|
||||
(try
|
||||
let c = { c_name = i.i_name; c_params = i.i_params;
|
||||
c_has_children = i.i_has_children; c_body = i.i_body;
|
||||
c_closure = i.i_closure; c_affinity = "client";
|
||||
c_compiled = None } in
|
||||
render_component (Component c) args env
|
||||
with e ->
|
||||
Printf.eprintf "[ssr-island] ~%s FAILED: %s\n%!" i.i_name (Printexc.to_string e);
|
||||
"")
|
||||
| Macro m ->
|
||||
let expanded = expand_macro m args env in
|
||||
do_render_to_html expanded env
|
||||
|
||||
@@ -347,26 +347,32 @@ let set_lambda_name l n = match l with
|
||||
|
||||
let component_name = function
|
||||
| Component c -> String c.c_name
|
||||
| Island i -> String i.i_name
|
||||
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
||||
|
||||
let component_params = function
|
||||
| Component c -> List (List.map (fun s -> String s) c.c_params)
|
||||
| Island i -> List (List.map (fun s -> String s) i.i_params)
|
||||
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
||||
|
||||
let component_body = function
|
||||
| Component c -> c.c_body
|
||||
| Island i -> i.i_body
|
||||
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
||||
|
||||
let component_closure = function
|
||||
| Component c -> Env c.c_closure
|
||||
| Island i -> Env i.i_closure
|
||||
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
||||
|
||||
let component_has_children = function
|
||||
| Component c -> Bool c.c_has_children
|
||||
| Island i -> Bool i.i_has_children
|
||||
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
||||
|
||||
let component_affinity = function
|
||||
| Component c -> String c.c_affinity
|
||||
| Island _ -> String "client"
|
||||
| _ -> String "auto"
|
||||
|
||||
let macro_params = function
|
||||
|
||||
Reference in New Issue
Block a user