Split orchestration from engine into separate adapter

engine.sx now contains only pure logic: parsing, morph, swap, headers,
retry, target resolution, etc. orchestration.sx contains the browser
wiring: request execution, trigger binding, SSE, boost, post-swap
lifecycle, and init. Dependency is one-way: orchestration → engine.

Bootstrap compiler gains "orchestration" as a separate adapter with
deps on engine+dom. Engine-only builds get morph/swap without the
full browser runtime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 13:04:27 +00:00
parent d4b23aae4c
commit eac0fce8f7
5 changed files with 1259 additions and 1327 deletions

View File

@@ -705,14 +705,15 @@ def extract_defines(source: str) -> list[tuple[str, list]]:
ADAPTER_FILES = {
"html": ("adapter-html.sx", "adapter-html"),
"sx": ("adapter-sx.sx", "adapter-sx"),
"dom": ("adapter-dom.sx", "adapter-dom"),
"engine": ("engine.sx", "engine"),
"html": ("adapter-html.sx", "adapter-html"),
"sx": ("adapter-sx.sx", "adapter-sx"),
"dom": ("adapter-dom.sx", "adapter-dom"),
"engine": ("engine.sx", "engine"),
"orchestration": ("orchestration.sx","orchestration"),
}
# Dependencies: engine requires dom
ADAPTER_DEPS = {"engine": ["dom"]}
# Dependencies: orchestration requires engine+dom, engine requires dom
ADAPTER_DEPS = {"engine": ["dom"], "orchestration": ["engine", "dom"]}
def compile_ref_to_js(adapters: list[str] | None = None) -> str:
@@ -728,8 +729,9 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
# Platform JS blocks keyed by adapter name
adapter_platform = {
"dom": PLATFORM_DOM_JS,
"engine": PLATFORM_ENGINE_JS,
"dom": PLATFORM_DOM_JS,
"engine": PLATFORM_ENGINE_PURE_JS,
"orchestration": PLATFORM_ORCHESTRATION_JS,
}
# Resolve adapter set
@@ -750,7 +752,7 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
("eval.sx", "eval"),
("render.sx", "render (core)"),
]
for name in ("html", "sx", "dom", "engine"):
for name in ("html", "sx", "dom", "engine", "orchestration"):
if name in adapter_set:
sx_files.append(ADAPTER_FILES[name])
@@ -769,6 +771,7 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
has_sx = "sx" in adapter_set
has_dom = "dom" in adapter_set
has_engine = "engine" in adapter_set
has_orch = "orchestration" in adapter_set
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
parts = []
@@ -784,12 +787,12 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
# Platform JS for selected adapters
if not has_dom:
parts.append("\n var _hasDom = false;\n")
for name in ("dom", "engine"):
for name in ("dom", "engine", "orchestration"):
if name in adapter_set and name in adapter_platform:
parts.append(adapter_platform[name])
parts.append(fixups_js(has_html, has_sx, has_dom))
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, adapter_label))
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, adapter_label))
parts.append(EPILOGUE)
return "\n".join(parts)
@@ -1441,13 +1444,11 @@ PLATFORM_DOM_JS = """
function domTagName(el) { return el && el.tagName ? el.tagName : ""; }
"""
PLATFORM_ENGINE_JS = """
PLATFORM_ENGINE_PURE_JS = """
// =========================================================================
// Platform interface — Engine (browser-only)
// Platform interface — Engine pure logic (browser + node compatible)
// =========================================================================
// --- Browser/Network ---
function browserLocationHref() {
return typeof location !== "undefined" ? location.href : "";
}
@@ -1471,6 +1472,24 @@ PLATFORM_ENGINE_JS = """
}
}
function nowMs() { return Date.now(); }
function parseHeaderValue(s) {
if (!s) return null;
try {
if (s.charAt(0) === "{" && s.charAt(1) === ":") return parse(s);
return JSON.parse(s);
} catch (e) { return null; }
}
"""
PLATFORM_ORCHESTRATION_JS = """
// =========================================================================
// Platform interface — Orchestration (browser-only)
// =========================================================================
// --- Browser/Network ---
function browserNavigate(url) {
if (typeof location !== "undefined") location.assign(url);
}
@@ -1499,16 +1518,6 @@ PLATFORM_ENGINE_JS = """
return r === null ? NIL : r;
}
function nowMs() { return Date.now(); }
function parseHeaderValue(s) {
if (!s) return null;
try {
if (s.charAt(0) === "{" && s.charAt(1) === ":") return parse(s);
return JSON.parse(s);
} catch (e) { return null; }
}
function csrfToken() {
if (!_hasDom) return NIL;
var m = document.querySelector('meta[name="csrf-token"]');
@@ -2059,7 +2068,7 @@ def fixups_js(has_html, has_sx, has_dom):
return "\n".join(lines)
def public_api_js(has_html, has_sx, has_dom, has_engine, adapter_label):
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, adapter_label):
# Parser is always included
parser = r'''
// =========================================================================
@@ -2255,6 +2264,7 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, adapter_label):
api_lines.append(' morphNode: typeof morphNode === "function" ? morphNode : null,')
api_lines.append(' morphChildren: typeof morphChildren === "function" ? morphChildren : null,')
api_lines.append(' swapDomNodes: typeof swapDomNodes === "function" ? swapDomNodes : null,')
if has_orch:
api_lines.append(' process: typeof processElements === "function" ? processElements : null,')
api_lines.append(' executeRequest: typeof executeRequest === "function" ? executeRequest : null,')
api_lines.append(' postSwap: typeof postSwap === "function" ? postSwap : null,')
@@ -2263,7 +2273,7 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, adapter_label):
api_lines.append(f' _version: "{version}"')
api_lines.append(' };')
api_lines.append('')
if has_engine:
if has_orch:
api_lines.append('''
// --- Popstate listener ---
if (typeof window !== "undefined") {