Route all rendering through OCaml bridge — render_to_html no longer uses Python async_eval. Fix register_components to parse &key params and &rest children from defcomp forms. Remove all dead sx_ref.py imports. Epoch protocol (prevents pipe desync): - Every command prefixed with (epoch N), all responses tagged with epoch - Both sides discard stale-epoch messages — desync structurally impossible - OCaml main loop discards stale io-responses between commands Consolidate scope primitives into sx_scope.ml: - Single source of truth for scope-push!/pop!/peek, collect!/collected, emit!/emitted, context, and 12 other scope operations - Removes duplicate registrations from sx_server.ml (including bugs where scope-emit! and clear-collected! were registered twice with different impls) - Bind scope prims into env so JIT VM finds them via OP_GLOBAL_GET JIT VM fixes: - Trampoline thunks before passing args to CALL_PRIM - as_list resolves thunks via _sx_trampoline_fn - len handles all value types (Bool, Number, RawHTML, SxExpr, Spread, etc.) Other fixes: - ~cssx/tw signature: (tokens) → (&key tokens) to match callers - Minimal Python evaluator in html.py for sync sx() Jinja function - Python scope primitive stubs (thread-local) for non-OCaml paths - Reader macro resolution via OcamlSync instead of sx_ref.py Tests: 1114 OCaml, 1078 JS, 35 Python regression, 6/6 Playwright SSR Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
105 lines
3.3 KiB
Python
105 lines
3.3 KiB
Python
"""
|
|
Execute defquery / defaction definitions.
|
|
|
|
Unlike fragment handlers (which produce SX markup via ``async_eval_to_sx``),
|
|
query/action defs produce **data** (dicts, lists, scalars) that get
|
|
JSON-serialized by the calling blueprint. Uses ``async_eval()`` with
|
|
the I/O primitive pipeline so ``(service ...)`` calls are awaited inline.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from .types import QueryDef, ActionDef, NIL
|
|
|
|
|
|
async def execute_query(query_def: QueryDef, params: dict[str, str]) -> Any:
|
|
"""Execute a defquery and return a JSON-serializable result.
|
|
|
|
Parameters are bound from request query string args.
|
|
"""
|
|
from .jinja_bridge import get_component_env, _get_request_context
|
|
import os
|
|
|
|
env = dict(get_component_env())
|
|
env.update(query_def.closure)
|
|
|
|
# Bind params from request args (try kebab-case and snake_case)
|
|
for param in query_def.params:
|
|
snake = param.replace("-", "_")
|
|
val = params.get(param, params.get(snake, NIL))
|
|
# Coerce type=int for common patterns
|
|
if isinstance(val, str) and val.lstrip("-").isdigit():
|
|
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)
|
|
|
|
from .async_eval import async_eval
|
|
|
|
ctx = _get_request_context()
|
|
result = await async_eval(query_def.body, env, ctx)
|
|
return _normalize(result)
|
|
|
|
|
|
async def execute_action(action_def: ActionDef, payload: dict[str, Any]) -> Any:
|
|
"""Execute a defaction and return a JSON-serializable result.
|
|
|
|
Parameters are bound from the JSON request body.
|
|
"""
|
|
from .jinja_bridge import get_component_env, _get_request_context
|
|
import os
|
|
|
|
env = dict(get_component_env())
|
|
env.update(action_def.closure)
|
|
|
|
# Bind params from JSON payload (try kebab-case and snake_case)
|
|
for param in action_def.params:
|
|
snake = param.replace("-", "_")
|
|
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)
|
|
|
|
from .async_eval import async_eval
|
|
|
|
ctx = _get_request_context()
|
|
result = await async_eval(action_def.body, env, ctx)
|
|
return _normalize(result)
|
|
|
|
|
|
def _normalize(value: Any) -> Any:
|
|
"""Ensure result is JSON-serializable (strip NIL, convert sets, etc)."""
|
|
if value is NIL or value is None:
|
|
return None
|
|
if isinstance(value, set):
|
|
return list(value)
|
|
return value
|