aser_slot migration: single-pass expansion, pipe desync fix, _render_to_sx
Three fixes completing the aser_slot migration: 1. Single-pass full-page rendering: eval_sx_url builds layout+content AST and aser_slots it in ONE call — avoids double-aser where re-parsed content hits "Undefined symbol: title/deref" errors. 2. Pipe desync fix: _inject_helpers_locked runs INSIDE the aser_slot lock acquisition (not as a separate lock). Prevents interleaved commands from other coroutines between injection and aser-slot. 3. _render_to_sx uses aser_slot (not aser): layout wrappers like oob_page_sx contain re-parsed content from earlier aser_slot calls. Regular aser fails on symbols that were bound during the earlier expansion. aser_slot handles them correctly. HTMX path: aser_slot the content, then oob_page_sx wraps it. Full page path: build (~shared:layout/app-body :content wrapped_ast), aser_slot in one pass, pass directly to sx_page. New Playwright tests: test_navigate_geography_to_reactive, test_direct_load_reactive_page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -416,7 +416,10 @@ async def _render_to_sx(__name: str, **kwargs: Any) -> str:
|
||||
from .parser import serialize
|
||||
bridge = await get_bridge()
|
||||
sx_text = serialize(ast)
|
||||
return SxExpr(await bridge.aser(sx_text))
|
||||
# aser_slot (not aser) — layout wrappers contain re-parsed
|
||||
# content from earlier aser_slot calls. Regular aser fails on
|
||||
# symbols like `title` that were bound during the earlier expansion.
|
||||
return SxExpr(await bridge.aser_slot(sx_text))
|
||||
|
||||
if os.environ.get("SX_USE_REF") == "1":
|
||||
from .ref.async_eval_ref import async_eval_to_sx
|
||||
|
||||
@@ -149,45 +149,46 @@ class OcamlBridge:
|
||||
slots where component bodies need server-side IO evaluation.
|
||||
"""
|
||||
await self._ensure_components()
|
||||
await self._ensure_helpers()
|
||||
async with self._lock:
|
||||
# Inject helpers inside the lock to avoid pipe desync —
|
||||
# a separate lock acquisition could let another coroutine
|
||||
# interleave commands between injection and aser-slot.
|
||||
await self._inject_helpers_locked()
|
||||
self._send(f'(aser-slot "{_escape(source)}")')
|
||||
return await self._read_until_ok(ctx)
|
||||
|
||||
async def _ensure_helpers(self) -> None:
|
||||
"""Lazily inject page helpers into the kernel as IO proxies."""
|
||||
async def _inject_helpers_locked(self) -> None:
|
||||
"""Inject page helpers into the kernel. MUST be called with lock held."""
|
||||
if self._helpers_injected:
|
||||
return
|
||||
self._helpers_injected = True
|
||||
try:
|
||||
from .pages import get_page_helpers
|
||||
import inspect
|
||||
helpers = get_page_helpers("sx")
|
||||
if not helpers:
|
||||
self._helpers_injected = False # retry later
|
||||
self._helpers_injected = False
|
||||
return
|
||||
async with self._lock:
|
||||
for name, fn in helpers.items():
|
||||
if callable(fn) and not name.startswith("~"):
|
||||
# Determine arity from Python function signature
|
||||
import inspect
|
||||
try:
|
||||
sig = inspect.signature(fn)
|
||||
nargs = sum(1 for p in sig.parameters.values()
|
||||
if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD))
|
||||
except (ValueError, TypeError):
|
||||
nargs = 2
|
||||
nargs = max(nargs, 1) # at least 1 arg
|
||||
param_names = " ".join(chr(97 + i) for i in range(nargs))
|
||||
arg_list = " ".join(chr(97 + i) for i in range(nargs))
|
||||
sx_def = f'(define {name} (fn ({param_names}) (helper "{name}" {arg_list})))'
|
||||
try:
|
||||
self._send(f'(load-source "{_escape(sx_def)}")')
|
||||
await self._read_until_ok(ctx=None)
|
||||
except OcamlBridgeError:
|
||||
pass
|
||||
_logger.info("Injected %d page helpers into OCaml kernel",
|
||||
sum(1 for n, f in helpers.items()
|
||||
if callable(f) and not n.startswith("~")))
|
||||
count = 0
|
||||
for name, fn in helpers.items():
|
||||
if callable(fn) and not name.startswith("~"):
|
||||
try:
|
||||
sig = inspect.signature(fn)
|
||||
nargs = sum(1 for p in sig.parameters.values()
|
||||
if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD))
|
||||
except (ValueError, TypeError):
|
||||
nargs = 2
|
||||
nargs = max(nargs, 1)
|
||||
param_names = " ".join(chr(97 + i) for i in range(nargs))
|
||||
arg_list = " ".join(chr(97 + i) for i in range(nargs))
|
||||
sx_def = f'(define {name} (fn ({param_names}) (helper "{name}" {arg_list})))'
|
||||
try:
|
||||
self._send(f'(load-source "{_escape(sx_def)}")')
|
||||
await self._read_until_ok(ctx=None)
|
||||
count += 1
|
||||
except OcamlBridgeError:
|
||||
pass
|
||||
_logger.info("Injected %d page helpers into OCaml kernel", count)
|
||||
except Exception as e:
|
||||
_logger.warning("Helper injection failed: %s", e)
|
||||
self._helpers_injected = False
|
||||
|
||||
Reference in New Issue
Block a user