OCaml evaluator for page dispatch + handler aser, 83/83 Playwright tests
Major architectural change: page function dispatch and handler execution
now go through the OCaml kernel instead of the Python bootstrapped evaluator.
OCaml integration:
- Page dispatch: bridge.eval() evaluates SX URL expressions (geography, marshes, etc.)
- Handler aser: bridge.aser() serializes handler responses as SX wire format
- _ensure_components loads all .sx files into OCaml kernel (spec, web adapter, handlers)
- defhandler/defpage registered as no-op special forms so handler files load
- helper IO primitive dispatches to Python page helpers + IO handlers
- ok-raw response format for SX wire format (no double-escaping)
- Natural list serialization in eval (no (list ...) wrapper)
- Clean pipe: _read_until_ok always sends io-response on error
SX adapter (aser):
- scope-emit!/scope-peek aliases to avoid CEK special form conflict
- aser-fragment/aser-call: strings starting with "(" pass through unserialized
- Registered cond-scheme?, is-else-clause?, primitive?, get-primitive in kernel
- random-int, parse-int as kernel primitives; json-encode, into via IO bridge
Handler migration:
- All IO calls converted to (helper "name" args...) pattern
- request-arg, request-form, state-get, state-set!, now, component-source etc.
- Fixed bare (effect ...) in island bodies leaking disposer functions as text
- Fixed lower-case → lower, ~search-results → ~examples/search-results
Reactive islands:
- sx-hydrate-islands called after client-side navigation swap
- force-dispose-islands-in for outerHTML swaps (clears hydration markers)
- clear-processed! platform primitive for re-hydration
Content restructuring:
- Design, event bridge, named stores, phase 2 consolidated into reactive overview
- Marshes split into overview + 5 example sub-pages
- Nav links use sx-get/sx-target for client-side navigation
Playwright test suite (sx/tests/test_demos.py):
- 83 tests covering hypermedia demos, reactive islands, marshes, spec explorer
- Server-side rendering, handler interactions, island hydration, navigation
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-17T17:31:51Z";
|
||||
var SX_VERSION = "2026-03-18T13:07:01Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -509,6 +509,9 @@
|
||||
PRIMITIVES["context"] = sxContext;
|
||||
PRIMITIVES["emit!"] = sxEmit;
|
||||
PRIMITIVES["emitted"] = sxEmitted;
|
||||
// Aliases for aser adapter (avoids CEK special form conflict on server)
|
||||
PRIMITIVES["scope-emit!"] = sxEmit;
|
||||
PRIMITIVES["scope-peek"] = sxEmitted;
|
||||
|
||||
|
||||
function isPrimitive(name) { return name in PRIMITIVES; }
|
||||
@@ -2659,8 +2662,8 @@ return (function() {
|
||||
var result = (function() { var _m = typeOf(expr); if (_m == "number") return expr; if (_m == "string") return expr; if (_m == "boolean") return expr; if (_m == "nil") return NIL; if (_m == "symbol") return (function() {
|
||||
var name = symbolName(expr);
|
||||
return (isSxTruthy(envHas(env, name)) ? envGet(env, name) : (isSxTruthy(isPrimitive(name)) ? getPrimitive(name) : (isSxTruthy((name == "true")) ? true : (isSxTruthy((name == "false")) ? false : (isSxTruthy((name == "nil")) ? NIL : error((String("Undefined symbol: ") + String(name))))))));
|
||||
})(); if (_m == "keyword") return keywordName(expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : aserList(expr, env)); if (_m == "spread") return (sxEmit("element-attrs", spreadAttrs(expr)), NIL); return expr; })();
|
||||
return (isSxTruthy(isSpread(result)) ? (sxEmit("element-attrs", spreadAttrs(result)), NIL) : result);
|
||||
})(); if (_m == "keyword") return keywordName(expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : aserList(expr, env)); if (_m == "spread") return (scopeEmit_b("element-attrs", spreadAttrs(expr)), NIL); return expr; })();
|
||||
return (isSxTruthy(isSpread(result)) ? (scopeEmit_b("element-attrs", spreadAttrs(result)), NIL) : result);
|
||||
})(); };
|
||||
PRIMITIVES["aser"] = aser;
|
||||
|
||||
@@ -2684,9 +2687,9 @@ PRIMITIVES["aser-list"] = aserList;
|
||||
var parts = [];
|
||||
{ var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() {
|
||||
var result = aser(c, env);
|
||||
return (isSxTruthy((typeOf(result) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, result) : (isSxTruthy(!isSxTruthy(isNil(result))) ? append_b(parts, serialize(result)) : NIL));
|
||||
return (isSxTruthy(isNil(result)) ? NIL : (isSxTruthy((isSxTruthy((typeOf(result) == "string")) && isSxTruthy((stringLength(result) > 0)) && startsWith(result, "("))) ? append_b(parts, result) : (isSxTruthy((typeOf(result) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? (isSxTruthy((isSxTruthy((typeOf(item) == "string")) && isSxTruthy((stringLength(item) > 0)) && startsWith(item, "("))) ? append_b(parts, item) : append_b(parts, serialize(item))) : NIL); }, result) : append_b(parts, serialize(result)))));
|
||||
})(); } }
|
||||
return (isSxTruthy(isEmpty(parts)) ? "" : (String("(<> ") + String(join(" ", parts)) + String(")")));
|
||||
return (isSxTruthy(isEmpty(parts)) ? "" : (isSxTruthy((len(parts) == 1)) ? first(parts) : (String("(<> ") + String(join(" ", parts)) + String(")"))));
|
||||
})(); };
|
||||
PRIMITIVES["aser-fragment"] = aserFragment;
|
||||
|
||||
@@ -2708,11 +2711,11 @@ PRIMITIVES["aser-fragment"] = aserFragment;
|
||||
})() : (function() {
|
||||
var val = aser(arg, env);
|
||||
if (isSxTruthy(!isSxTruthy(isNil(val)))) {
|
||||
(isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(childParts, serialize(item)) : NIL); }, val) : append_b(childParts, serialize(val)));
|
||||
(isSxTruthy((isSxTruthy((typeOf(val) == "string")) && isSxTruthy((stringLength(val) > 0)) && startsWith(val, "("))) ? append_b(childParts, val) : (isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? (isSxTruthy((isSxTruthy((typeOf(item) == "string")) && isSxTruthy((stringLength(item) > 0)) && startsWith(item, "("))) ? append_b(childParts, item) : append_b(childParts, serialize(item))) : NIL); }, val) : append_b(childParts, serialize(val))));
|
||||
}
|
||||
return (i = (i + 1));
|
||||
})())); } }
|
||||
{ var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; { var _c = keys(spreadDict); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; (function() {
|
||||
{ var _c = scopePeek("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; { var _c = keys(spreadDict); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; (function() {
|
||||
var v = dictGet(spreadDict, k);
|
||||
attrParts.push((String(":") + String(k)));
|
||||
return append_b(attrParts, serialize(v));
|
||||
@@ -3975,7 +3978,7 @@ return processElements(t); });
|
||||
return (function() {
|
||||
var selectSel = domGetAttr(el, "sx-select");
|
||||
var content = (isSxTruthy(selectSel) ? selectFromContainer(container, selectSel) : childrenToFragment(container));
|
||||
disposeIslandsIn(target);
|
||||
(isSxTruthy((swapStyle == "outerHTML")) ? forceDisposeIslandsIn(target) : disposeIslandsIn(target));
|
||||
return withTransition(useTransition, function() { swapDomNodes(target, content, swapStyle);
|
||||
return postSwap(target); });
|
||||
})();
|
||||
@@ -4062,7 +4065,8 @@ PRIMITIVES["bind-triggers"] = bindTriggers;
|
||||
PRIMITIVES["bind-event"] = bindEvent;
|
||||
|
||||
// post-swap
|
||||
var postSwap = function(root) { activateScripts(root);
|
||||
var postSwap = function(root) { logInfo((String("post-swap: root=") + String((isSxTruthy(root) ? domTagName(root) : "nil"))));
|
||||
activateScripts(root);
|
||||
sxProcessScripts(root);
|
||||
sxHydrate(root);
|
||||
sxHydrateIslands(root);
|
||||
@@ -4331,7 +4335,7 @@ PRIMITIVES["offline-aware-mutation"] = offlineAwareMutation;
|
||||
PRIMITIVES["current-page-layout"] = currentPageLayout;
|
||||
|
||||
// swap-rendered-content
|
||||
var swapRenderedContent = function(target, rendered, pathname) { return (disposeIslandsIn(target), domSetTextContent(target, ""), domAppend(target, rendered), hoistHeadElementsFull(target), processElements(target), sxHydrateElements(target), runPostRenderHooks(), domDispatch(target, "sx:clientRoute", {["pathname"]: pathname}), logInfo((String("sx:route client ") + String(pathname)))); };
|
||||
var swapRenderedContent = function(target, rendered, pathname) { return (disposeIslandsIn(target), domSetTextContent(target, ""), domAppend(target, rendered), hoistHeadElementsFull(target), processElements(target), sxHydrateElements(target), sxHydrateIslands(target), runPostRenderHooks(), domDispatch(target, "sx:clientRoute", {["pathname"]: pathname}), logInfo((String("sx:route client ") + String(pathname)))); };
|
||||
PRIMITIVES["swap-rendered-content"] = swapRenderedContent;
|
||||
|
||||
// resolve-route-target
|
||||
@@ -4696,7 +4700,8 @@ PRIMITIVES["process-page-scripts"] = processPageScripts;
|
||||
// sx-hydrate-islands
|
||||
var sxHydrateIslands = function(root) { return (function() {
|
||||
var els = domQueryAll(sxOr(root, domBody()), "[data-sx-island]");
|
||||
return forEach(function(el) { return (isSxTruthy(!isSxTruthy(isProcessed(el, "island-hydrated"))) ? (markProcessed(el, "island-hydrated"), hydrateIsland(el)) : NIL); }, els);
|
||||
logInfo((String("sx-hydrate-islands: ") + String(len(els)) + String(" island(s) in ") + String((isSxTruthy(root) ? "subtree" : "document"))));
|
||||
return forEach(function(el) { return (isSxTruthy(isProcessed(el, "island-hydrated")) ? logInfo((String(" skip (already hydrated): ") + String(domGetAttr(el, "data-sx-island")))) : (logInfo((String(" hydrating: ") + String(domGetAttr(el, "data-sx-island")))), markProcessed(el, "island-hydrated"), hydrateIsland(el))); }, els);
|
||||
})(); };
|
||||
PRIMITIVES["sx-hydrate-islands"] = sxHydrateIslands;
|
||||
|
||||
@@ -4729,10 +4734,11 @@ PRIMITIVES["sx-hydrate-islands"] = sxHydrateIslands;
|
||||
PRIMITIVES["hydrate-island"] = hydrateIsland;
|
||||
|
||||
// dispose-island
|
||||
var disposeIsland = function(el) { return (function() {
|
||||
var disposeIsland = function(el) { (function() {
|
||||
var disposers = domGetData(el, "sx-disposers");
|
||||
return (isSxTruthy(disposers) ? (forEach(function(d) { return (isSxTruthy(isCallable(d)) ? d() : NIL); }, disposers), domSetData(el, "sx-disposers", NIL)) : NIL);
|
||||
})(); };
|
||||
})();
|
||||
return clearProcessed(el, "island-hydrated"); };
|
||||
PRIMITIVES["dispose-island"] = disposeIsland;
|
||||
|
||||
// dispose-islands-in
|
||||
@@ -4745,6 +4751,13 @@ PRIMITIVES["dispose-island"] = disposeIsland;
|
||||
})() : NIL); };
|
||||
PRIMITIVES["dispose-islands-in"] = disposeIslandsIn;
|
||||
|
||||
// force-dispose-islands-in
|
||||
var forceDisposeIslandsIn = function(root) { return (isSxTruthy(root) ? (function() {
|
||||
var islands = domQueryAll(root, "[data-sx-island]");
|
||||
return (isSxTruthy((isSxTruthy(islands) && !isSxTruthy(isEmpty(islands)))) ? (logInfo((String("force-disposing ") + String(len(islands)) + String(" island(s)"))), forEach(disposeIsland, islands)) : NIL);
|
||||
})() : NIL); };
|
||||
PRIMITIVES["force-dispose-islands-in"] = forceDisposeIslandsIn;
|
||||
|
||||
// *pre-render-hooks*
|
||||
var _preRenderHooks_ = [];
|
||||
PRIMITIVES["*pre-render-hooks*"] = _preRenderHooks_;
|
||||
@@ -6949,6 +6962,7 @@ PRIMITIVES["resource"] = resource;
|
||||
|
||||
function markProcessed(el, key) { el[PROCESSED + key] = true; }
|
||||
function isProcessed(el, key) { return !!el[PROCESSED + key]; }
|
||||
function clearProcessed(el, key) { delete el[PROCESSED + key]; }
|
||||
|
||||
// --- Script cloning ---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user