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:
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||
var SX_VERSION = "2026-03-23T08:59:15Z";
|
||||
var SX_VERSION = "2026-03-23T13:21:57Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -1582,7 +1582,13 @@ PRIMITIVES["step-sf-lambda"] = stepSfLambda;
|
||||
var val = NIL;
|
||||
var body = NIL;
|
||||
(isSxTruthy((isSxTruthy((len(restArgs) >= 2)) && isSxTruthy((typeOf(first(restArgs)) == "keyword")) && (keywordName(first(restArgs)) == "value"))) ? ((val = trampoline(evalExpr(nth(restArgs, 1), env))), (body = slice(restArgs, 2))) : (body = restArgs));
|
||||
return (isSxTruthy(isEmpty(body)) ? makeCekValue(NIL, env, kont) : (isSxTruthy((len(body) == 1)) ? makeCekState(first(body), env, kontPush(makeScopeAccFrame(name, val, [], env), kont)) : makeCekState(first(body), env, kontPush(makeScopeAccFrame(name, val, rest(body), env), kont))));
|
||||
scopePush(name, val);
|
||||
return (function() {
|
||||
var result = NIL;
|
||||
{ var _c = body; for (var _i = 0; _i < _c.length; _i++) { var expr = _c[_i]; result = trampoline(evalExpr(expr, env)); } }
|
||||
scopePop(name);
|
||||
return makeCekValue(result, env, kont);
|
||||
})();
|
||||
})(); };
|
||||
PRIMITIVES["step-sf-scope"] = stepSfScope;
|
||||
|
||||
@@ -1591,7 +1597,13 @@ PRIMITIVES["step-sf-scope"] = stepSfScope;
|
||||
var name = trampoline(evalExpr(first(args), env));
|
||||
var val = trampoline(evalExpr(nth(args, 1), env));
|
||||
var body = slice(args, 2);
|
||||
return (isSxTruthy(isEmpty(body)) ? makeCekValue(NIL, env, kont) : (isSxTruthy((len(body) == 1)) ? makeCekState(first(body), env, kontPush(makeProvideFrame(name, val, [], env), kont)) : makeCekState(first(body), env, kontPush(makeProvideFrame(name, val, rest(body), env), kont))));
|
||||
scopePush(name, val);
|
||||
return (function() {
|
||||
var result = NIL;
|
||||
{ var _c = body; for (var _i = 0; _i < _c.length; _i++) { var expr = _c[_i]; result = trampoline(evalExpr(expr, env)); } }
|
||||
scopePop(name);
|
||||
return makeCekValue(result, env, kont);
|
||||
})();
|
||||
})(); };
|
||||
PRIMITIVES["step-sf-provide"] = stepSfProvide;
|
||||
|
||||
@@ -1599,8 +1611,8 @@ PRIMITIVES["step-sf-provide"] = stepSfProvide;
|
||||
var stepSfContext = function(args, env, kont) { return (function() {
|
||||
var name = trampoline(evalExpr(first(args), env));
|
||||
var defaultVal = (isSxTruthy((len(args) >= 2)) ? trampoline(evalExpr(nth(args, 1), env)) : NIL);
|
||||
var frame = kontFindProvide(kont, name);
|
||||
return (isSxTruthy(frame) ? makeCekValue(get(frame, "value"), env, kont) : (isSxTruthy((len(args) >= 2)) ? makeCekValue(defaultVal, env, kont) : error((String("No provider for: ") + String(name)))));
|
||||
var val = scopePeek(name);
|
||||
return makeCekValue((isSxTruthy(isNil(val)) ? defaultVal : val), env, kont);
|
||||
})(); };
|
||||
PRIMITIVES["step-sf-context"] = stepSfContext;
|
||||
|
||||
@@ -1608,16 +1620,16 @@ PRIMITIVES["step-sf-context"] = stepSfContext;
|
||||
var stepSfEmit = function(args, env, kont) { return (function() {
|
||||
var name = trampoline(evalExpr(first(args), env));
|
||||
var val = trampoline(evalExpr(nth(args, 1), env));
|
||||
var frame = kontFindScopeAcc(kont, name);
|
||||
return (isSxTruthy(frame) ? (append_b(get(frame, "emitted"), val), makeCekValue(NIL, env, kont)) : error((String("No scope for emit!: ") + String(name))));
|
||||
scopeEmit(name, val);
|
||||
return makeCekValue(NIL, env, kont);
|
||||
})(); };
|
||||
PRIMITIVES["step-sf-emit"] = stepSfEmit;
|
||||
|
||||
// step-sf-emitted
|
||||
var stepSfEmitted = function(args, env, kont) { return (function() {
|
||||
var name = trampoline(evalExpr(first(args), env));
|
||||
var frame = kontFindScopeAcc(kont, name);
|
||||
return (isSxTruthy(frame) ? makeCekValue(get(frame, "emitted"), env, kont) : error((String("No scope for emitted: ") + String(name))));
|
||||
var val = scopePeek(name);
|
||||
return makeCekValue((isSxTruthy(isNil(val)) ? [] : val), env, kont);
|
||||
})(); };
|
||||
PRIMITIVES["step-sf-emitted"] = stepSfEmitted;
|
||||
|
||||
@@ -3573,9 +3585,10 @@ PRIMITIVES["get-verb-info"] = getVerbInfo;
|
||||
var targetSel = domGetAttr(el, "sx-target");
|
||||
return (isSxTruthy(targetSel) ? dictSet(headers, "SX-Target", targetSel) : NIL);
|
||||
})();
|
||||
if (isSxTruthy(!isSxTruthy(isEmpty(loadedComponents)))) {
|
||||
headers["SX-Components"] = join(",", loadedComponents);
|
||||
}
|
||||
(function() {
|
||||
var compHash = domGetAttr(domQuery("script[data-components][data-hash]"), "data-hash");
|
||||
return (isSxTruthy(compHash) ? dictSet(headers, "SX-Components-Hash", compHash) : NIL);
|
||||
})();
|
||||
if (isSxTruthy(cssHash)) {
|
||||
headers["SX-Css"] = cssHash;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user