SxExpr aser wire format fix + Playwright test infrastructure + blob protocol

Aser serialization: aser-call/fragment now return SxExpr instead of String.
serialize/inspect passes SxExpr through unquoted, preventing the double-
escaping (\" → \\\" ) that broke client-side parsing when aser wire format
was output via raw! into <script> tags. Added make-sx-expr + sx-expr-source
primitives to OCaml and JS hosts.

Binary blob protocol: eval, aser, aser-slot, and sx-page-full now send SX
source as length-prefixed blobs instead of escaped strings. Eliminates pipe
desync from concurrent requests and removes all string-escape round-trips
between Python and OCaml.

Bridge safety: re-entrancy guard (_in_io_handler) raises immediately if an
IO handler tries to call the bridge, preventing silent deadlocks.

Fetch error logging: orchestration.sx error callback now logs method + URL
via log-warn. Platform catches (fetchAndRestore, fetchPreload, bindBoostForm)
also log errors instead of silently swallowing them.

Transpiler fixes: makeEnv, scopePeek, scopeEmit, makeSxExpr added as
platform function definitions + transpiler mappings — were referenced in
transpiled code but never defined as JS functions.

Playwright test infrastructure:
- nav() captures JS errors and fails fast with the actual error message
- Checks for [object Object] rendering artifacts
- New tests: delete-row interaction, full page refresh, back button,
  direct load with fresh context, code block content verification
- Default base URL changed to localhost:8013 (standalone dev server)
- docker-compose.dev-sx.yml: port 8013 exposed for local testing
- test-sx-build.sh: build + unit tests + Playwright smoke tests

Geography content: index page component written (sx/sx/geography/index.sx)
describing OCaml evaluator, wire formats, rendering pipeline, and topic
links. Wiring blocked by aser-expand-component children passing issue.

Tests: 1080/1080 JS, 952/952 OCaml, 66/66 Playwright

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 22:17:43 +00:00
parent 6d73edf297
commit df461beec2
17 changed files with 684 additions and 82 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-19T14:05:23Z";
var SX_VERSION = "2026-03-22T20:34:05Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -111,6 +111,7 @@
if (x._spread) return "spread";
if (x._macro) return "macro";
if (x._raw) return "raw-html";
if (x._sx_expr) return "sx-expr";
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
if (Array.isArray(x)) return "list";
if (typeof x === "object") return "dict";
@@ -510,8 +511,10 @@
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;
var scopeEmit = sxEmit;
var scopePeek = sxEmitted;
PRIMITIVES["scope-emit!"] = scopeEmit;
PRIMITIVES["scope-peek"] = scopePeek;
function isPrimitive(name) { return name in PRIMITIVES; }
@@ -592,6 +595,7 @@
// escape-html and escape-attr are now library functions defined in render.sx
function rawHtmlContent(r) { return r.html; }
function makeRawHtml(s) { return { _raw: true, html: s }; }
function makeSxExpr(s) { return { _sx_expr: true, source: s }; }
function sxExprSource(x) { return x && x.source ? x.source : String(x); }
// Placeholders — overridden by transpiled spec from parser.sx / adapter-sx.sx
@@ -848,7 +852,7 @@
function escapeString(s) {
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
}
function sxExprSource(e) { return typeof e === "string" ? e : String(e); }
function sxExprSource(e) { return typeof e === "string" ? e : (e && e.source ? e.source : String(e)); }
var charFromCode = PRIMITIVES["char-from-code"];
@@ -1646,7 +1650,7 @@ PRIMITIVES["step-sf-deref"] = stepSfDeref;
// cek-call
var cekCall = function(f, args) { return (function() {
var a = (isSxTruthy(isNil(args)) ? [] : args);
return (isSxTruthy(isNil(f)) ? NIL : (isSxTruthy(sxOr(isLambda(f), isCallable(f))) ? cekRun(continueWithCall(f, a, {}, a, [])) : NIL));
return (isSxTruthy(isNil(f)) ? NIL : (isSxTruthy(sxOr(isLambda(f), isCallable(f))) ? cekRun(continueWithCall(f, a, makeEnv(), a, [])) : NIL));
})(); };
PRIMITIVES["cek-call"] = cekCall;
@@ -2655,7 +2659,7 @@ PRIMITIVES["serialize-island-state"] = serializeIslandState;
// render-to-sx
var renderToSx = function(expr, env) { return (function() {
var result = aser(expr, env);
return (isSxTruthy((typeOf(result) == "string")) ? result : serialize(result));
return (isSxTruthy((typeOf(result) == "sx-expr")) ? sxExprSource(result) : (isSxTruthy((typeOf(result) == "string")) ? result : serialize(result)));
})(); };
PRIMITIVES["render-to-sx"] = renderToSx;
@@ -2665,8 +2669,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 (scopeEmit_b("element-attrs", spreadAttrs(expr)), NIL); return expr; })();
return (isSxTruthy(isSpread(result)) ? (scopeEmit_b("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("element-attrs", spreadAttrs(expr)), NIL); return expr; })();
return (isSxTruthy(isSpread(result)) ? (scopeEmit("element-attrs", spreadAttrs(result)), NIL) : result);
})(); };
PRIMITIVES["aser"] = aser;
@@ -2694,9 +2698,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(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(isNil(result)) ? NIL : (isSxTruthy((typeOf(result) == "sx-expr")) ? append_b(parts, sxExprSource(result)) : (isSxTruthy((typeOf(result) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? (isSxTruthy((typeOf(item) == "sx-expr")) ? append_b(parts, sxExprSource(item)) : append_b(parts, serialize(item))) : NIL); }, result) : append_b(parts, serialize(result)))));
})(); } }
return (isSxTruthy(isEmpty(parts)) ? "" : (isSxTruthy((len(parts) == 1)) ? first(parts) : (String("(<> ") + String(join(" ", parts)) + String(")"))));
return (isSxTruthy(isEmpty(parts)) ? "" : (isSxTruthy((len(parts) == 1)) ? makeSxExpr(first(parts)) : makeSxExpr((String("(<> ") + String(join(" ", parts)) + String(")")))));
})(); };
PRIMITIVES["aser-fragment"] = aserFragment;
@@ -2711,14 +2715,14 @@ PRIMITIVES["aser-fragment"] = aserFragment;
var val = aser(nth(args, (i + 1)), env);
if (isSxTruthy(!isSxTruthy(isNil(val)))) {
attrParts.push((String(":") + String(keywordName(arg))));
(isSxTruthy((isSxTruthy((typeOf(val) == "string")) && isSxTruthy((stringLength(val) > 0)) && startsWith(val, "("))) ? append_b(attrParts, val) : append_b(attrParts, serialize(val)));
(isSxTruthy((typeOf(val) == "sx-expr")) ? append_b(attrParts, sxExprSource(val)) : append_b(attrParts, serialize(val)));
}
skip = true;
return (i = (i + 1));
})() : (function() {
var val = aser(arg, env);
if (isSxTruthy(!isSxTruthy(isNil(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))));
(isSxTruthy((typeOf(val) == "sx-expr")) ? append_b(childParts, sxExprSource(val)) : (isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? (isSxTruthy((typeOf(item) == "sx-expr")) ? append_b(childParts, sxExprSource(item)) : append_b(childParts, serialize(item))) : NIL); }, val) : append_b(childParts, serialize(val))));
}
return (i = (i + 1));
})())); } }
@@ -2730,7 +2734,7 @@ PRIMITIVES["aser-fragment"] = aserFragment;
scopePop("element-attrs");
return (function() {
var parts = concat([name], attrParts, childParts);
return (String("(") + String(join(" ", parts)) + String(")"));
return makeSxExpr((String("(") + String(join(" ", parts)) + String(")")));
})();
})(); };
PRIMITIVES["aser-call"] = aserCall;
@@ -3952,7 +3956,7 @@ PRIMITIVES["execute-request"] = executeRequest;
domAddClass(el, "sx-request");
domSetAttr(el, "aria-busy", "true");
domDispatch(el, "sx:beforeRequest", {["url"]: finalUrl, ["method"]: method});
return fetchRequest({["url"]: finalUrl, ["method"]: method, ["headers"]: headers, ["body"]: body, ["signal"]: controllerSignal(ctrl), ["cross-origin"]: isCrossOrigin(finalUrl), ["preloaded"]: cached}, function(respOk, status, getHeader, text) { return (clearLoadingState(el, indicator, disabledElts), revertOptimistic(optimisticState), (isSxTruthy(!isSxTruthy(respOk)) ? (domDispatch(el, "sx:responseError", {["status"]: status, ["text"]: text}), (isSxTruthy((isSxTruthy(text) && (len(text) > 0))) ? handleFetchSuccess(el, finalUrl, verb, extraParams, getHeader, text) : handleRetry(el, verb, method, finalUrl, extraParams))) : (domDispatch(el, "sx:afterRequest", {["status"]: status}), handleFetchSuccess(el, finalUrl, verb, extraParams, getHeader, text)))); }, function(err) { return (clearLoadingState(el, indicator, disabledElts), revertOptimistic(optimisticState), (isSxTruthy(!isSxTruthy(isAbortError(err))) ? domDispatch(el, "sx:requestError", {["error"]: err}) : NIL)); });
return fetchRequest({["url"]: finalUrl, ["method"]: method, ["headers"]: headers, ["body"]: body, ["signal"]: controllerSignal(ctrl), ["cross-origin"]: isCrossOrigin(finalUrl), ["preloaded"]: cached}, function(respOk, status, getHeader, text) { return (clearLoadingState(el, indicator, disabledElts), revertOptimistic(optimisticState), (isSxTruthy(!isSxTruthy(respOk)) ? (domDispatch(el, "sx:responseError", {["status"]: status, ["text"]: text}), (isSxTruthy((isSxTruthy(text) && (len(text) > 0))) ? handleFetchSuccess(el, finalUrl, verb, extraParams, getHeader, text) : handleRetry(el, verb, method, finalUrl, extraParams))) : (domDispatch(el, "sx:afterRequest", {["status"]: status}), handleFetchSuccess(el, finalUrl, verb, extraParams, getHeader, text)))); }, function(err) { return (clearLoadingState(el, indicator, disabledElts), revertOptimistic(optimisticState), (isSxTruthy(!isSxTruthy(isAbortError(err))) ? (logWarn((String("sx:fetch error ") + String(method) + String(" ") + String(finalUrl) + String(" — ") + String(err))), domDispatch(el, "sx:requestError", {["error"]: err})) : NIL)); });
})();
})();
})();
@@ -5866,7 +5870,8 @@ PRIMITIVES["resource"] = resource;
PRIMITIVES["island?"] = isIsland;
PRIMITIVES["make-symbol"] = function(n) { return new Symbol(n); };
PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; };
PRIMITIVES["make-env"] = function() { return merge(componentEnv, PRIMITIVES); };
function makeEnv() { return merge(componentEnv, PRIMITIVES); }
PRIMITIVES["make-env"] = makeEnv;
// localStorage — defined here (before boot) so islands can use at hydration
PRIMITIVES["local-storage-get"] = function(key) {
@@ -6393,7 +6398,10 @@ PRIMITIVES["resource"] = resource;
}
}
});
}).catch(function() { location.reload(); });
}).catch(function(err) {
logWarn("sx:popstate fetch error " + url + " — " + (err && err.message ? err.message : err));
location.reload();
});
}
function fetchStreaming(target, url, headers) {
@@ -6531,7 +6539,9 @@ PRIMITIVES["resource"] = resource;
return resp.text().then(function(text) {
preloadCacheSet(cache, url, text, ct);
});
}).catch(function() { /* ignore */ });
}).catch(function(err) {
logInfo("sx:preload error " + url + " — " + (err && err.message ? err.message : err));
});
}
// --- Request body building ---
@@ -6820,6 +6830,8 @@ PRIMITIVES["resource"] = resource;
var liveAction = form.getAttribute("action") || _action || location.href;
executeRequest(form, { method: liveMethod, url: liveAction }).then(function() {
try { history.pushState({ sxUrl: liveAction, scrollY: window.scrollY }, "", liveAction); } catch (err) {}
}).catch(function(err) {
logWarn("sx:boost form error " + liveMethod + " " + liveAction + " — " + (err && err.message ? err.message : err));
});
});
}

View File

@@ -41,6 +41,7 @@ class OcamlBridge:
self._binary = binary or os.environ.get("SX_OCAML_BIN") or _DEFAULT_BIN
self._proc: asyncio.subprocess.Process | None = None
self._lock = asyncio.Lock()
self._in_io_handler = False # re-entrancy guard
self._started = False
self._components_loaded = False
self._helpers_injected = False
@@ -123,7 +124,8 @@ class OcamlBridge:
"""
await self._ensure_components()
async with self._lock:
await self._send(f'(eval "{_escape(source)}")')
await self._send('(eval-blob)')
await self._send_blob(source)
return await self._read_until_ok(ctx)
async def render(
@@ -141,7 +143,8 @@ class OcamlBridge:
"""Evaluate SX and return SX wire format, handling io-requests."""
await self._ensure_components()
async with self._lock:
await self._send(f'(aser "{_escape(source)}")')
await self._send('(aser-blob)')
await self._send_blob(source)
return await self._read_until_ok(ctx)
async def aser_slot(self, source: str, ctx: dict[str, Any] | None = None) -> str:
@@ -156,7 +159,8 @@ class OcamlBridge:
# a separate lock acquisition could let another coroutine
# interleave commands between injection and aser-slot.
await self._inject_helpers_locked()
await self._send(f'(aser-slot "{_escape(source)}")')
await self._send('(aser-slot-blob)')
await self._send_blob(source)
return await self._read_until_ok(ctx)
_shell_statics_injected: bool = False
@@ -227,7 +231,10 @@ class OcamlBridge:
static_keys = {"component_hash", "sx_css_classes", "asset_url",
"sx_js_hash", "body_js_hash",
"head_scripts", "body_scripts"}
parts = [f'(sx-page-full "{_escape(page_source)}"']
# page_source is SX wire format that may contain \" escapes.
# Send via binary blob protocol to avoid double-escaping
# through the SX string parser round-trip.
parts = ['(sx-page-full-blob']
for key, val in shell_kwargs.items():
k = key.replace("_", "-")
if key in PLACEHOLDER_KEYS:
@@ -248,6 +255,8 @@ class OcamlBridge:
parts.append(")")
cmd = "".join(parts)
await self._send(cmd)
# Send page source as binary blob (avoids string-escape issues)
await self._send_blob(page_source)
html = await self._read_until_ok(ctx)
# Splice in large blobs
for token, blob in placeholders.items():
@@ -473,11 +482,30 @@ class OcamlBridge:
async def _send(self, line: str) -> None:
"""Write a line to the subprocess stdin and flush."""
if self._in_io_handler:
raise OcamlBridgeError(
f"Re-entrant bridge call from IO handler: {line[:80]}. "
f"IO handlers must not call the bridge — use Python-only code."
)
assert self._proc and self._proc.stdin
_logger.debug("SEND: %s", line[:120])
self._proc.stdin.write((line + "\n").encode())
await self._proc.stdin.drain()
async def _send_blob(self, data: str) -> None:
"""Send a length-prefixed binary blob to the subprocess.
Protocol: sends "(blob N)\\n" followed by exactly N bytes, then "\\n".
The OCaml side reads the length, then reads exactly N bytes.
This avoids string-escape round-trip issues for SX wire format.
"""
assert self._proc and self._proc.stdin
encoded = data.encode()
self._proc.stdin.write(f"(blob {len(encoded)})\n".encode())
self._proc.stdin.write(encoded)
self._proc.stdin.write(b"\n")
await self._proc.stdin.drain()
async def _readline(self) -> str:
"""Read a line from the subprocess stdout."""
assert self._proc and self._proc.stdout
@@ -574,7 +602,24 @@ class OcamlBridge:
line: str,
ctx: dict[str, Any] | None,
) -> Any:
"""Dispatch an io-request to the appropriate Python handler."""
"""Dispatch an io-request to the appropriate Python handler.
IO handlers MUST NOT call the bridge (eval/aser/render) — doing so
would deadlock since the lock is already held. The _in_io_handler
flag triggers an immediate error if this rule is violated.
"""
self._in_io_handler = True
try:
return await self._dispatch_io(line, ctx)
finally:
self._in_io_handler = False
async def _dispatch_io(
self,
line: str,
ctx: dict[str, Any] | None,
) -> Any:
"""Inner dispatch for IO requests."""
from .parser import parse_all
# Parse the io-request

View File

@@ -181,6 +181,47 @@ def load_page_dir(directory: str, service_name: str) -> list[PageDef]:
# Page execution
# ---------------------------------------------------------------------------
def _wrap_with_env(expr: Any, env: dict) -> str:
"""Serialize an expression wrapped with let-bindings from env.
Injects page env values (URL params, data results) as let-bindings
so the OCaml kernel can evaluate the expression with those bindings.
Only injects non-component, non-callable values that pages add dynamically.
"""
from .parser import serialize
from .ocaml_bridge import _serialize_for_ocaml
from .types import Symbol, Keyword, NIL
body = serialize(expr)
bindings = []
for k, v in env.items():
# Skip component definitions — already loaded in kernel
if k.startswith("~") or callable(v):
continue
# Skip env keys that are component-env infrastructure
if isinstance(v, (type, type(None))) and v is not None:
continue
# Serialize the value
if v is NIL or v is None:
sv = "nil"
elif isinstance(v, bool):
sv = "true" if v else "false"
elif isinstance(v, (int, float)):
sv = str(int(v)) if isinstance(v, float) and v == int(v) else str(v)
elif isinstance(v, str):
sv = _serialize_for_ocaml(v)
elif isinstance(v, (list, dict)):
sv = _serialize_for_ocaml(v)
else:
# Component, Lambda, etc — skip, already in kernel
continue
bindings.append(f"({k} {sv})")
if not bindings:
return body
return f"(let ({' '.join(bindings)}) {body})"
async def _eval_slot(expr: Any, env: dict, ctx: Any) -> str:
"""Evaluate a page slot expression and return an sx source string.
@@ -188,6 +229,15 @@ async def _eval_slot(expr: Any, env: dict, ctx: Any) -> str:
the result as SX wire format, not HTML.
"""
import os
if os.environ.get("SX_USE_OCAML") == "1":
from .ocaml_bridge import get_bridge
from .parser import serialize
bridge = await get_bridge()
# Wrap expression with let-bindings for env values that pages
# inject (URL params, data results, etc.)
sx_text = _wrap_with_env(expr, env)
service = ctx.get("_helper_service", "") if isinstance(ctx, dict) else ""
return await bridge.aser_slot(sx_text, ctx={"_helper_service": service})
if os.environ.get("SX_USE_REF") == "1":
from .ref.async_eval_ref import async_eval_slot_to_sx
else:
@@ -248,12 +298,19 @@ async def execute_page(
6. Branch: full_page_sx() vs oob_page_sx() based on is_htmx_request()
"""
from .jinja_bridge import get_component_env, _get_request_context
from .async_eval import async_eval
from .page import get_template_context
from .helpers import full_page_sx, oob_page_sx, sx_response
from .layouts import get_layout
from shared.browser.app.utils.htmx import is_htmx_request
_use_ocaml = os.environ.get("SX_USE_OCAML") == "1"
if _use_ocaml:
from .ocaml_bridge import get_bridge
from .parser import serialize, parse_all
from .ocaml_bridge import _serialize_for_ocaml
else:
from .async_eval import async_eval
if url_params is None:
url_params = {}
@@ -275,7 +332,19 @@ async def execute_page(
# Evaluate :data expression if present
_multi_stream_content = None
if page_def.data_expr is not None:
data_result = await async_eval(page_def.data_expr, env, ctx)
if _use_ocaml:
bridge = await get_bridge()
sx_text = _wrap_with_env(page_def.data_expr, env)
ocaml_ctx = {"_helper_service": service_name}
raw = await bridge.eval(sx_text, ctx=ocaml_ctx)
# Parse result back to Python dict/value
if raw:
parsed = parse_all(raw)
data_result = parsed[0] if parsed else {}
else:
data_result = {}
else:
data_result = await async_eval(page_def.data_expr, env, ctx)
if hasattr(data_result, '__aiter__'):
# Multi-stream: consume generator, eval :content per chunk,
# combine into shell with resolved suspense slots.
@@ -358,7 +427,18 @@ async def execute_page(
k = raw[i]
if isinstance(k, SxKeyword) and i + 1 < len(raw):
raw_val = raw[i + 1]
resolved = await async_eval(raw_val, env, ctx)
if _use_ocaml:
bridge = await get_bridge()
sx_text = _wrap_with_env(raw_val, env)
ocaml_ctx = {"_helper_service": service_name}
raw_result = await bridge.eval(sx_text, ctx=ocaml_ctx)
if raw_result:
parsed = parse_all(raw_result)
resolved = parsed[0] if parsed else None
else:
resolved = None
else:
resolved = await async_eval(raw_val, env, ctx)
layout_kwargs[k.name.replace("-", "_")] = resolved
i += 2
else:

View File

@@ -21,10 +21,6 @@ async def execute_query(query_def: QueryDef, params: dict[str, str]) -> Any:
"""
from .jinja_bridge import get_component_env, _get_request_context
import os
if os.environ.get("SX_USE_REF") == "1":
from .ref.async_eval_ref import async_eval
else:
from .async_eval import async_eval
env = dict(get_component_env())
env.update(query_def.closure)
@@ -38,6 +34,26 @@ async def execute_query(query_def: QueryDef, params: dict[str, str]) -> Any:
val = int(val)
env[param] = val
if os.environ.get("SX_USE_OCAML") == "1":
from .ocaml_bridge import get_bridge
from .parser import serialize, parse_all
from .pages import _wrap_with_env
bridge = await get_bridge()
sx_text = _wrap_with_env(query_def.body, env)
ctx = {"_helper_service": ""}
raw = await bridge.eval(sx_text, ctx=ctx)
if raw:
parsed = parse_all(raw)
result = parsed[0] if parsed else None
else:
result = None
return _normalize(result)
if os.environ.get("SX_USE_REF") == "1":
from .ref.async_eval_ref import async_eval
else:
from .async_eval import async_eval
ctx = _get_request_context()
result = await async_eval(query_def.body, env, ctx)
return _normalize(result)
@@ -50,10 +66,6 @@ async def execute_action(action_def: ActionDef, payload: dict[str, Any]) -> Any:
"""
from .jinja_bridge import get_component_env, _get_request_context
import os
if os.environ.get("SX_USE_REF") == "1":
from .ref.async_eval_ref import async_eval
else:
from .async_eval import async_eval
env = dict(get_component_env())
env.update(action_def.closure)
@@ -64,6 +76,26 @@ async def execute_action(action_def: ActionDef, payload: dict[str, Any]) -> Any:
val = payload.get(param, payload.get(snake, NIL))
env[param] = val
if os.environ.get("SX_USE_OCAML") == "1":
from .ocaml_bridge import get_bridge
from .parser import serialize, parse_all
from .pages import _wrap_with_env
bridge = await get_bridge()
sx_text = _wrap_with_env(action_def.body, env)
ctx = {"_helper_service": ""}
raw = await bridge.eval(sx_text, ctx=ctx)
if raw:
parsed = parse_all(raw)
result = parsed[0] if parsed else None
else:
result = None
return _normalize(result)
if os.environ.get("SX_USE_REF") == "1":
from .ref.async_eval_ref import async_eval
else:
from .async_eval import async_eval
ctx = _get_request_context()
result = await async_eval(action_def.body, env, ctx)
return _normalize(result)