From eac0fce8f730a235db6a0170cae68d3a32bee935 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 5 Mar 2026 13:04:27 +0000 Subject: [PATCH] Split orchestration from engine into separate adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- shared/static/scripts/sx-browser.js | 415 ++++++------- shared/static/scripts/sx-ref.js | 415 ++++++------- shared/sx/ref/bootstrap_js.py | 64 +- shared/sx/ref/engine.sx | 877 +--------------------------- shared/sx/ref/orchestration.sx | 815 ++++++++++++++++++++++++++ 5 files changed, 1259 insertions(+), 1327 deletions(-) create mode 100644 shared/sx/ref/orchestration.sx diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 417bca8..c4288ca 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -1252,6 +1252,9 @@ // parse-sse-swap var parseSseSwap = function(el) { return sxOr(domGetAttr(el, "sx-sse-swap"), "message"); }; + + // === Transpiled from orchestration === + // _preload-cache var _preloadCache = {}; @@ -1261,15 +1264,15 @@ // dispatch-trigger-events var dispatchTriggerEvents = function(el, headerVal) { return (isSxTruthy(headerVal) ? (function() { var parsed = tryParseJson(headerVal); - return (isSxTruthy((isSxTruthy(parsed) && isDict(parsed))) ? forEach(function(key) { return domDispatch(el, key, dictGet(parsed, key)); }, keys(parsed)) : forEach(function(name) { return (function() { - var n = trim(name); - return (isSxTruthy(!(n == "")) ? domDispatch(el, n, {}) : NIL); + return (isSxTruthy(parsed) ? forEach(function(key) { return domDispatch(el, key, get(parsed, key)); }, keys(parsed)) : forEach(function(name) { return (function() { + var trimmed = trim(name); + return (isSxTruthy(!isEmpty(trimmed)) ? domDispatch(el, trimmed, {}) : NIL); })(); }, split(headerVal, ","))); })() : NIL); }; // init-css-tracking var initCssTracking = function() { return (function() { - var meta = domQuery("meta[name=\"sx-css-classes\"]"); + var meta = domQuery("meta[name=\"sx-css-hash\"]"); return (isSxTruthy(meta) ? (function() { var content = domGetAttr(meta, "content"); return (isSxTruthy(content) ? (_cssHash = content) : NIL); @@ -1278,65 +1281,58 @@ // execute-request var executeRequest = function(el, verbInfo, extraParams) { return (function() { - var currentVerb = getVerbInfo(el); - var verb = (isSxTruthy(currentVerb) ? currentVerb : verbInfo); - var method = get(verb, "method"); - var url = get(verb, "url"); - if (isSxTruthy(!domHasClass(el, "sx-error"))) { - domRemoveAttr(el, "data-sx-retry-ms"); -} + var info = sxOr(verbInfo, getVerbInfo(el)); + return (isSxTruthy(isNil(info)) ? promiseResolve(NIL) : (function() { + var verb = get(info, "method"); + var url = get(info, "url"); return (isSxTruthy((function() { var media = domGetAttr(el, "sx-media"); return (isSxTruthy(media) && !browserMediaMatches(media)); })()) ? promiseResolve(NIL) : (isSxTruthy((function() { - var msg = domGetAttr(el, "sx-confirm"); - return (isSxTruthy(msg) && !browserConfirm(msg)); + var confirmMsg = domGetAttr(el, "sx-confirm"); + return (isSxTruthy(confirmMsg) && !browserConfirm(confirmMsg)); })()) ? promiseResolve(NIL) : (function() { var promptMsg = domGetAttr(el, "sx-prompt"); - var params = extraParams; - return (isSxTruthy(promptMsg) ? (function() { - var promptVal = browserPrompt(promptMsg); - return (isSxTruthy(isNil(promptVal)) ? promiseResolve(NIL) : ((params = sxOr(params, {})), dictSet(params, "promptValue", promptVal), doFetch(el, verb, method, url, params))); -})() : doFetch(el, verb, method, url, params)); + var promptVal = (isSxTruthy(promptMsg) ? browserPrompt(promptMsg) : NIL); + return (isSxTruthy((isSxTruthy(promptMsg) && isNil(promptVal))) ? promiseResolve(NIL) : (isSxTruthy(!validateForRequest(el)) ? promiseResolve(NIL) : doFetch(el, verb, verb, url, (isSxTruthy(promptVal) ? assoc(sxOr(extraParams, {}), "SX-Prompt", promptVal) : extraParams)))); })())); +})()); })(); }; // do-fetch var doFetch = function(el, verb, method, url, extraParams) { return (function() { - var syncAttr = domGetAttr(el, "sx-sync"); - if (isSxTruthy((isSxTruthy(syncAttr) && contains(syncAttr, "replace")))) { + var sync = domGetAttr(el, "sx-sync"); + if (isSxTruthy((sync == "replace"))) { abortPrevious(el); } return (function() { var ctrl = newAbortController(); trackController(el, ctrl); - return (function() { - var headers = buildRequestHeaders(el, loadedComponentNames(), _cssHash); - if (isSxTruthy((isSxTruthy(extraParams) && dictHas(extraParams, "promptValue")))) { - headers["SX-Prompt"] = get(extraParams, "promptValue"); -} - if (isSxTruthy((isSxTruthy(!(method == "GET")) && browserSameOrigin(url)))) { - (function() { - var csrf = csrfToken(); - return (isSxTruthy(csrf) ? dictSet(headers, "X-CSRFToken", csrf) : NIL); -})(); -} return (function() { var bodyInfo = buildRequestBody(el, method, url); - return (function() { - var body = get(bodyInfo, "body"); var finalUrl = get(bodyInfo, "url"); + var body = get(bodyInfo, "body"); var ct = get(bodyInfo, "content-type"); + var headers = buildRequestHeaders(el, loadedComponentNames(), _cssHash); + var csrf = csrfToken(); + if (isSxTruthy(extraParams)) { + { var _c = keys(extraParams); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; headers[k] = get(extraParams, k); } } +} if (isSxTruthy(ct)) { headers["Content-Type"] = ct; } - return (isSxTruthy(!domDispatch(el, "sx:beforeRequest", {["method"]: method, ["url"]: finalUrl})) ? promiseResolve(NIL) : (domAddClass(el, "sx-request"), domSetAttr(el, "aria-busy", "true"), (function() { + if (isSxTruthy(csrf)) { + headers["X-CSRFToken"] = csrf; +} + return (function() { + var cached = preloadCacheGet(_preloadCache, finalUrl); + var optimisticState = applyOptimistic(el); var indicator = showIndicator(el); var disabledElts = disableElements(el); - var preloaded = (isSxTruthy((method == "GET")) ? preloadCacheGet(_preloadCache, finalUrl) : NIL); - return fetchRequest({["url"]: finalUrl, ["method"]: method, ["headers"]: headers, ["body"]: body, ["signal"]: controllerSignal(ctrl), ["preloaded"]: preloaded, ["cross-origin"]: isCrossOrigin(finalUrl)}, function(respOk, status, getHeader, text) { return (clearLoadingState(el, indicator, disabledElts), (isSxTruthy(!respOk) ? (domDispatch(el, "sx:responseError", {["status"]: status}), handleRetry(el, verb, extraParams)) : (domDispatch(el, "sx:afterRequest", {}), handleFetchSuccess(el, finalUrl, verb, extraParams, getHeader, text)))); }, function(err) { return (clearLoadingState(el, indicator, disabledElts), (isSxTruthy(!isAbortError(err)) ? (domDispatch(el, "sx:sendError", {["error"]: err}), handleRetry(el, verb, extraParams)) : NIL)); }); -})())); -})(); + 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(!respOk) ? (domDispatch(el, "sx:responseError", {["status"]: status, ["text"]: 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(!isAbortError(err)) ? domDispatch(el, "sx:requestError", {["error"]: err}) : NIL)); }); })(); })(); })(); @@ -1344,257 +1340,225 @@ // handle-fetch-success var handleFetchSuccess = function(el, url, verb, extraParams, getHeader, text) { return (function() { - var headers = processResponseHeaders(getHeader); - return (isSxTruthy(get(headers, "redirect")) ? browserNavigate(get(headers, "redirect")) : (isSxTruthy((get(headers, "refresh") == "true")) ? browserReload() : (dispatchTriggerEvents(el, get(headers, "trigger")), (function() { - var rawSwap = sxOr(domGetAttr(el, "sx-swap"), DEFAULT_SWAP); - var target = resolveTarget(el); - var selectSel = domGetAttr(el, "sx-select"); - if (isSxTruthy(get(headers, "retarget"))) { - target = sxOr(domQuery(get(headers, "retarget")), target); -} - if (isSxTruthy(get(headers, "reswap"))) { - rawSwap = get(headers, "reswap"); -} - return (function() { - var swap = parseSwapSpec(rawSwap, false); - var ct = sxOr(get(headers, "content-type"), ""); - (isSxTruthy(contains(ct, "text/sx")) ? handleSxResponse(el, target, swap, selectSel, text) : handleHtmlResponse(el, target, swap, selectSel, text)); - if (isSxTruthy(get(headers, "location"))) { - fetchLocation(get(headers, "location")); -} - handleHistory(el, url, headers); - domDispatch(el, "sx:afterSwap", {["target"]: target}); - dispatchTriggerEvents(el, get(headers, "trigger-swap")); - return requestAnimationFrame_(function() { return (domDispatch(el, "sx:afterSettle", {["target"]: target}), dispatchTriggerEvents(el, get(headers, "trigger-settle"))); }); + var respHeaders = processResponseHeaders(getHeader); + (function() { + var newHash = get(respHeaders, "css-hash"); + return (isSxTruthy(newHash) ? (_cssHash = newHash) : NIL); })(); + dispatchTriggerEvents(el, get(respHeaders, "trigger")); + return (isSxTruthy(get(respHeaders, "redirect")) ? browserNavigate(get(respHeaders, "redirect")) : (isSxTruthy(get(respHeaders, "refresh")) ? browserReload() : (isSxTruthy(get(respHeaders, "location")) ? fetchLocation(get(respHeaders, "location")) : (function() { + var targetEl = (isSxTruthy(get(respHeaders, "retarget")) ? domQuery(get(respHeaders, "retarget")) : resolveTarget(el)); + var swapSpec = parseSwapSpec(sxOr(get(respHeaders, "reswap"), domGetAttr(el, "sx-swap")), domHasClass(domBody(), "sx-transitions")); + var swapStyle = get(swapSpec, "style"); + var useTransition = get(swapSpec, "transition"); + var ct = sxOr(get(respHeaders, "content-type"), ""); + (isSxTruthy(contains(ct, "text/sx")) ? handleSxResponse(el, targetEl, text, swapStyle, useTransition) : handleHtmlResponse(el, targetEl, text, swapStyle, useTransition)); + dispatchTriggerEvents(el, get(respHeaders, "trigger-swap")); + handleHistory(el, url, respHeaders); + if (isSxTruthy(get(respHeaders, "trigger-settle"))) { + setTimeout_(function() { return dispatchTriggerEvents(el, get(respHeaders, "trigger-settle")); }, 20); +} + return domDispatch(el, "sx:afterSwap", {["target"]: targetEl, ["swap"]: swapStyle}); })()))); })(); }; // handle-sx-response - var handleSxResponse = function(el, target, swap, selectSel, text) { return (function() { + var handleSxResponse = function(el, target, text, swapStyle, useTransition) { return (function() { var cleaned = stripComponentScripts(text); - var cleaned2 = extractResponseCss(cleaned); return (function() { - var source = trim(cleaned2); - return (isSxTruthy((isSxTruthy(source) && !(source == ""))) ? (function() { - var dom = sxRender(source); + var final = extractResponseCss(cleaned); + return (function() { + var trimmed = trim(final); + return (isSxTruthy(!isEmpty(trimmed)) ? (function() { + var rendered = sxRender(trimmed); var container = domCreateElement("div", NIL); - domAppend(container, dom); + domAppend(container, rendered); processOobSwaps(container, function(t, oob, s) { return swapDomNodes(t, oob, s); }); return (function() { - var selected = (isSxTruthy(selectSel) ? selectFromContainer(container, selectSel) : childrenToFragment(container)); - return (isSxTruthy((isSxTruthy(!(get(swap, "style") == "none")) && target)) ? withTransition(get(swap, "transition"), function() { return (swapDomNodes(target, selected, get(swap, "style")), hoistHeadElements(target)); }) : NIL); + var selectSel = domGetAttr(el, "sx-select"); + var content = (isSxTruthy(selectSel) ? selectFromContainer(container, selectSel) : childrenToFragment(container)); + return withTransition(useTransition, function() { return swapDomNodes(target, content, swapStyle); }); })(); })() : NIL); })(); +})(); })(); }; // handle-html-response - var handleHtmlResponse = function(el, target, swap, selectSel, text) { return (function() { + var handleHtmlResponse = function(el, target, text, swapStyle, useTransition) { return (function() { var doc = domParseHtmlDocument(text); - sxProcessScripts(doc); - processOobSwaps(doc, function(t, oob, s) { return swapHtmlString(t, domOuterHtml(oob), s); }); - return (function() { - var content = (isSxTruthy(selectSel) ? selectHtmlFromDoc(doc, selectSel) : sxOr(domBodyInnerHtml(doc), text)); - return (isSxTruthy((isSxTruthy(!(get(swap, "style") == "none")) && target)) ? withTransition(get(swap, "transition"), function() { return (swapHtmlString(target, content, get(swap, "style")), hoistHeadElements(target)); }) : NIL); -})(); + return (isSxTruthy(doc) ? (function() { + var selectSel = domGetAttr(el, "sx-select"); + return (isSxTruthy(selectSel) ? (function() { + var html = selectHtmlFromDoc(doc, selectSel); + return withTransition(useTransition, function() { return swapHtmlString(target, html, swapStyle); }); +})() : (function() { + var container = domCreateElement("div", NIL); + domSetInnerHtml(container, domBodyInnerHtml(doc)); + processOobSwaps(container, function(t, oob, s) { return swapDomNodes(t, oob, s); }); + hoistHeadElements(container); + return withTransition(useTransition, function() { return swapDomNodes(target, childrenToFragment(container), swapStyle); }); +})()); +})() : NIL); })(); }; // handle-retry - var handleRetry = function(el, verbInfo, extraParams) { return (function() { + var handleRetry = function(el, verb, method, url, extraParams) { return (function() { var retryAttr = domGetAttr(el, "sx-retry"); - return (isSxTruthy(retryAttr) ? (function() { var spec = parseRetrySpec(retryAttr); - var currentMs = sxOr(parseInt_(domGetAttr(el, "data-sx-retry-ms"), 0), get(spec, "start-ms")); - domAddClass(el, "sx-error"); - domRemoveClass(el, "sx-loading"); - return setTimeout_(function() { return (domRemoveClass(el, "sx-error"), domAddClass(el, "sx-loading"), domSetAttr(el, "data-sx-retry-ms", (String(nextRetryMs(currentMs, get(spec, "cap-ms"))))), executeRequest(el, verbInfo, extraParams)); }, currentMs); + return (isSxTruthy(spec) ? (function() { + var currentMs = sxOr(domGetAttr(el, "data-sx-retry-ms"), get(spec, "start-ms")); + return (function() { + var ms = parseInt_(currentMs, get(spec, "start-ms")); + domSetAttr(el, "data-sx-retry-ms", (String(nextRetryMs(ms, get(spec, "cap-ms"))))); + return setTimeout_(function() { return doFetch(el, verb, method, url, extraParams); }, ms); +})(); })() : NIL); })(); }; // bind-triggers var bindTriggers = function(el, verbInfo) { return (function() { - var triggerSpec = domGetAttr(el, "sx-trigger"); - var triggers = (isSxTruthy(triggerSpec) ? parseTriggerSpec(triggerSpec) : defaultTrigger(domTagName(el))); - return forEach(function(trig) { return (function() { - var kind = classifyTrigger(trig); - return (isSxTruthy((kind == "poll")) ? setInterval_(function() { return executeRequest(el, verbInfo, NIL); }, sxOr(get(get(trig, "modifiers"), "interval"), 1000)) : (isSxTruthy((kind == "intersect")) ? observeIntersection(el, function() { return executeRequest(el, verbInfo, NIL); }, get(get(trig, "modifiers"), "once"), get(get(trig, "modifiers"), "delay")) : (isSxTruthy((kind == "load")) ? setTimeout_(function() { return executeRequest(el, verbInfo, NIL); }, 0) : (isSxTruthy((kind == "revealed")) ? observeIntersection(el, function() { return executeRequest(el, verbInfo, NIL); }, true, NIL) : bindEvent(el, verbInfo, trig))))); + var triggers = sxOr(parseTriggerSpec(domGetAttr(el, "sx-trigger")), defaultTrigger(domTagName(el))); + return forEach(function(trigger) { return (function() { + var kind = classifyTrigger(trigger); + var mods = get(trigger, "modifiers"); + return (isSxTruthy((kind == "poll")) ? setInterval_(function() { return executeRequest(el, NIL, NIL); }, get(mods, "interval")) : (isSxTruthy((kind == "intersect")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, false, get(mods, "delay")) : (isSxTruthy((kind == "load")) ? setTimeout_(function() { return executeRequest(el, NIL, NIL); }, sxOr(get(mods, "delay"), 0)) : (isSxTruthy((kind == "revealed")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, true, get(mods, "delay")) : (isSxTruthy((kind == "event")) ? bindEvent(el, get(trigger, "event"), mods, verbInfo) : NIL))))); })(); }, triggers); })(); }; // bind-event - var bindEvent = function(el, verbInfo, trig) { return (function() { - var eventName = get(trig, "event"); - var mods = get(trig, "modifiers"); - var listenTarget = (isSxTruthy(get(mods, "from")) ? sxOr(domQuery(get(mods, "from")), el) : el); + var bindEvent = function(el, eventName, mods, verbInfo) { return (function() { var timer = NIL; var lastVal = NIL; - return domAddListener(listenTarget, eventName, function(e) { return ((isSxTruthy((eventName == "submit")) ? preventDefault_(e) : NIL), (isSxTruthy((isSxTruthy((eventName == "click")) && (domTagName(el) == "A"))) ? preventDefault_(e) : NIL), (isSxTruthy(!validateForRequest(el)) ? domDispatch(el, "sx:validationFailed", {}) : (isSxTruthy((isSxTruthy(get(mods, "changed")) && isSxTruthy(!isNil(elementValue(el))) && (elementValue(el) == lastVal))) ? NIL : ((isSxTruthy(get(mods, "changed")) ? (lastVal = elementValue(el)) : NIL), (function() { - var optState = applyOptimistic(el); - var execFn = function() { return (function() { - var p = executeRequest(el, verbInfo, NIL); - return (isSxTruthy((isSxTruthy(optState) && p)) ? promiseCatch(p, function(_) { return revertOptimistic(optState); }) : NIL); -})(); }; - return (isSxTruthy(get(mods, "delay")) ? (clearTimeout_(timer), (timer = setTimeout_(execFn, get(mods, "delay")))) : execFn()); -})())))); }, {["once"]: get(mods, "once")}); + var listenTarget = (isSxTruthy(get(mods, "from")) ? domQuery(get(mods, "from")) : el); + return (isSxTruthy(listenTarget) ? domAddListener(listenTarget, eventName, function(e) { return (function() { + var shouldFire = true; + if (isSxTruthy(get(mods, "changed"))) { + (function() { + var val = elementValue(el); + return (isSxTruthy((val == lastVal)) ? (shouldFire = false) : (lastVal = val)); +})(); +} + return (isSxTruthy(shouldFire) ? ((isSxTruthy(sxOr((eventName == "submit"), (isSxTruthy((eventName == "click")) && domHasAttr(el, "href")))) ? preventDefault_(e) : NIL), (isSxTruthy(get(mods, "delay")) ? (clearTimeout_(timer), (timer = setTimeout_(function() { return executeRequest(el, verbInfo, NIL); }, get(mods, "delay")))) : executeRequest(el, verbInfo, NIL))) : NIL); +})(); }, (isSxTruthy(get(mods, "once")) ? {["once"]: true} : NIL)) : NIL); })(); }; // post-swap - var postSwap = function(root) { return (activateScripts(root), sxProcessScripts(root), sxHydrate(root), processElements(root)); }; + var postSwap = function(root) { return activateScripts(root); }; // activate-scripts - var activateScripts = function(root) { return (function() { - var dead = domQueryAll(root, "script:not([type]), script[type='text/javascript']"); - return forEach(function(d) { return (function() { - var live = createScriptClone(d); - return domReplaceChild(domParent(d), live, d); -})(); }, dead); -})(); }; + var activateScripts = function(root) { return (isSxTruthy(root) ? (function() { + var scripts = domQueryAll(root, "script"); + return forEach(function(dead) { return (isSxTruthy((isSxTruthy(!domHasAttr(dead, "data-components")) && !domHasAttr(dead, "data-sx-activated"))) ? (function() { + var live = createScriptClone(dead); + domSetAttr(live, "data-sx-activated", "true"); + return domReplaceChild(domParent(dead), live, dead); +})() : NIL); }, scripts); +})() : NIL); }; // process-oob-swaps - var processOobSwaps = function(container, swapFn) { return forEach(function(attr) { return (function() { - var oobEls = domQueryAll(container, (String("[") + String(attr) + String("]"))); + var processOobSwaps = function(container, swapFn) { return (function() { + var oobs = findOobSwaps(container); return forEach(function(oob) { return (function() { - var swapType = sxOr(domGetAttr(oob, attr), "outerHTML"); - var targetId = domId(oob); - domRemoveAttr(oob, attr); - if (isSxTruthy(domParent(oob))) { - domRemoveChild(domParent(oob), oob); -} - return (isSxTruthy(targetId) ? (function() { + var targetId = get(oob, "target-id"); var target = domQueryById(targetId); - return (isSxTruthy(target) ? swapFn(target, oob, swapType) : NIL); -})() : NIL); -})(); }, oobEls); -})(); }, ["sx-swap-oob", "hx-swap-oob"]); }; + var oobEl = get(oob, "element"); + var swapType = get(oob, "swap-type"); + if (isSxTruthy(domParent(oobEl))) { + domRemoveChild(domParent(oobEl), oobEl); +} + return (isSxTruthy(target) ? swapFn(target, oobEl, swapType) : NIL); +})(); }, oobs); +})(); }; // hoist-head-elements - var hoistHeadElements = function(root) { return (function() { - var styles = domQueryAll(root, "style[data-sx-css]"); - var links = domQueryAll(root, "link[rel='stylesheet']"); - { var _c = styles; for (var _i = 0; _i < _c.length; _i++) { var el = _c[_i]; if (isSxTruthy(domParent(el))) { - domRemoveChild(domParent(el), el); -} } } - return forEach(function(el) { return (isSxTruthy(domParent(el)) ? domRemoveChild(domParent(el), el) : NIL); }, links); -})(); }; + var hoistHeadElements = function(container) { return forEach(function(style) { return (isSxTruthy(domParent(style)) ? domRemoveChild(domParent(style), style) : NIL); }, domQueryAll(container, "style[data-sx-css]")); }; // process-boosted - var processBoosted = function(root) { return (function() { - var containers = domQueryAll(root, "[sx-boost]"); - if (isSxTruthy(domMatches(root, "[sx-boost]"))) { - boostDescendants(root); -} - return forEach(boostDescendants, containers); -})(); }; + var processBoosted = function(root) { return forEach(function(container) { return boostDescendants(container); }, domQueryAll(sxOr(root, domBody()), "[sx-boost]")); }; // boost-descendants - var boostDescendants = function(container) { return ((function() { - var links = domQueryAll(container, "a[href]"); - return forEach(function(link) { return (isSxTruthy((isSxTruthy(!isProcessed(link, "boost")) && shouldBoostLink(link))) ? (markProcessed(link, "boost"), bindBoostLink(link, domGetAttr(link, "href")), (isSxTruthy(!domHasAttr(link, "sx-target")) ? domSetAttr(link, "sx-target", "#main-panel") : NIL), (isSxTruthy(!domHasAttr(link, "sx-swap")) ? domSetAttr(link, "sx-swap", "innerHTML") : NIL), (isSxTruthy(!domHasAttr(link, "sx-select")) ? domSetAttr(link, "sx-select", "#main-panel") : NIL)) : NIL); }, links); -})(), (function() { - var forms = domQueryAll(container, "form"); - return forEach(function(form) { return (isSxTruthy((isSxTruthy(!isProcessed(form, "boost")) && shouldBoostForm(form))) ? (markProcessed(form, "boost"), bindBoostForm(form, sxOr(upper(domGetAttr(form, "method")), "GET"), sxOr(domGetAttr(form, "action"), browserLocationHref())), (isSxTruthy(!domHasAttr(form, "sx-target")) ? domSetAttr(form, "sx-target", "#main-panel") : NIL), (isSxTruthy(!domHasAttr(form, "sx-swap")) ? domSetAttr(form, "sx-swap", "innerHTML") : NIL)) : NIL); }, forms); -})()); }; + var boostDescendants = function(container) { return forEach(function(link) { return (isSxTruthy((isSxTruthy(!isProcessed(link, "boost")) && shouldBoostLink(link))) ? (markProcessed(link, "boost"), (isSxTruthy(!domHasAttr(link, "sx-target")) ? domSetAttr(link, "sx-target", "#main-panel") : NIL), (isSxTruthy(!domHasAttr(link, "sx-swap")) ? domSetAttr(link, "sx-swap", "innerHTML") : NIL), (isSxTruthy(!domHasAttr(link, "sx-push-url")) ? domSetAttr(link, "sx-push-url", "true") : NIL), bindBoostLink(link, domGetAttr(link, "href"))) : NIL); }, domQueryAll(container, "a[href]")); }; // process-sse - var processSse = function(root) { return (function() { - var sseEls = domQueryAll(root, "[sx-sse]"); - if (isSxTruthy(domMatches(root, "[sx-sse]"))) { - bindSse(root); -} - return forEach(bindSse, sseEls); -})(); }; + var processSse = function(root) { return forEach(function(el) { return (isSxTruthy(!isProcessed(el, "sse")) ? (markProcessed(el, "sse"), bindSse(el)) : NIL); }, domQueryAll(sxOr(root, domBody()), "[sx-sse]")); }; // bind-sse - var bindSse = function(el) { return (isSxTruthy(!isProcessed(el, "sse")) ? (markProcessed(el, "sse"), (function() { + var bindSse = function(el) { return (function() { var url = domGetAttr(el, "sx-sse"); return (isSxTruthy(url) ? (function() { var source = eventSourceConnect(url, el); - return (function() { - var swapEls = domQueryAll(el, "[sx-sse-swap]"); - if (isSxTruthy(domHasAttr(el, "sx-sse-swap"))) { - bindSseSwap(el, source); -} - return forEach(function(child) { return bindSseSwap(child, source); }, swapEls); -})(); + var eventName = parseSseSwap(el); + return eventSourceListen(source, eventName, function(data) { return bindSseSwap(el, data); }); })() : NIL); -})()) : NIL); }; +})(); }; // bind-sse-swap - var bindSseSwap = function(el, source) { return (function() { - var eventName = parseSseSwap(el); - return eventSourceListen(source, eventName, function(data) { return (function() { - var target = sxOr(resolveTarget(el), el); - var swapStyle = sxOr(domGetAttr(el, "sx-swap"), "innerHTML"); - (isSxTruthy(startsWith(trim(data), "(")) ? (function() { - var dom = sxRender(data); - return swapDomNodes(target, dom, swapStyle); -})() : swapHtmlString(target, data, swapStyle)); - postSwap(target); - return domDispatch(el, "sx:sseMessage", {["data"]: data, ["event"]: eventName}); -})(); }); + var bindSseSwap = function(el, data) { return (function() { + var target = resolveTarget(el); + var swapSpec = parseSwapSpec(domGetAttr(el, "sx-swap"), domHasClass(domBody(), "sx-transitions")); + var swapStyle = get(swapSpec, "style"); + var useTransition = get(swapSpec, "transition"); + var trimmed = trim(data); + return (isSxTruthy(!isEmpty(trimmed)) ? (isSxTruthy(startsWith(trimmed, "(")) ? (function() { + var rendered = sxRender(trimmed); + var container = domCreateElement("div", NIL); + domAppend(container, rendered); + return withTransition(useTransition, function() { return swapDomNodes(target, childrenToFragment(container), swapStyle); }); +})() : withTransition(useTransition, function() { return swapHtmlString(target, trimmed, swapStyle); })) : NIL); })(); }; // bind-inline-handlers - var bindInlineHandlers = function(el) { return (isSxTruthy(!isProcessed(el, "on")) ? (markProcessed(el, "on"), (function() { - var attrs = domAttrList(el); - return forEach(function(attr) { return (function() { + var bindInlineHandlers = function(root) { return forEach(function(el) { return forEach(function(attr) { return (function() { var name = first(attr); - var val = nth(attr, 1); - return (isSxTruthy(startsWith(name, "sx-on:")) ? bindInlineHandler(el, slice(name, 6), val) : NIL); -})(); }, attrs); -})()) : NIL); }; + var body = nth(attr, 1); + return (isSxTruthy(startsWith(name, "sx-on:")) ? (function() { + var eventName = slice(name, 6); + return (isSxTruthy(!isProcessed(el, (String("on:") + String(eventName)))) ? (markProcessed(el, (String("on:") + String(eventName))), bindInlineHandler(el, eventName, body)) : NIL); +})() : NIL); +})(); }, domAttrList(el)); }, domQueryAll(sxOr(root, domBody()), "[sx-on\\:beforeRequest],[sx-on\\:afterRequest],[sx-on\\:afterSwap],[sx-on\\:afterSettle],[sx-on\\:load]")); }; // bind-preload-for - var bindPreloadFor = function(el) { return (isSxTruthy(domHasAttr(el, "sx-preload")) ? (function() { - var mode = sxOr(domGetAttr(el, "sx-preload"), "mousedown"); - var events = (isSxTruthy((mode == "mouseover")) ? ["mouseenter", "focusin"] : ["mousedown", "focusin"]); - var debounceMs = (isSxTruthy((mode == "mouseover")) ? 100 : 0); - return bindPreload(el, events, debounceMs, function() { return (function() { - var verb = getVerbInfo(el); - return (isSxTruthy(verb) ? (function() { - var url = get(verb, "url"); - return (isSxTruthy(isNil(preloadCacheGet(_preloadCache, url))) ? doPreload(url) : NIL); + var bindPreloadFor = function(el) { return (function() { + var preloadAttr = domGetAttr(el, "sx-preload"); + return (isSxTruthy(preloadAttr) ? (function() { + var info = getVerbInfo(el); + return (isSxTruthy(info) ? (function() { + var url = get(info, "url"); + var headers = buildRequestHeaders(el, loadedComponentNames(), _cssHash); + var events = (isSxTruthy((preloadAttr == "mousedown")) ? ["mousedown", "touchstart"] : ["mouseover"]); + var debounceMs = (isSxTruthy((preloadAttr == "mousedown")) ? 0 : 100); + return bindPreload(el, events, debounceMs, function() { return doPreload(url, headers); }); +})() : NIL); })() : NIL); -})(); }); -})() : NIL); }; - - // do-preload - var doPreload = function(url) { return (function() { - var headers = buildRequestHeaders(NIL, loadedComponentNames(), _cssHash); - return fetchPreload(url, headers, _preloadCache); })(); }; + // do-preload + var doPreload = function(url, headers) { return (isSxTruthy(isNil(preloadCacheGet(_preloadCache, url))) ? fetchPreload(url, headers, _preloadCache) : NIL); }; + // VERB_SELECTOR - var VERB_SELECTOR = "[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]"; + var VERB_SELECTOR = (String("[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]")); // process-elements var processElements = function(root) { return (function() { - var root = sxOr(root, domBody()); - return (isSxTruthy(root) ? ((isSxTruthy(domMatches(root, VERB_SELECTOR)) ? processOne(root) : NIL), (function() { - var elements = domQueryAll(root, VERB_SELECTOR); - return forEach(processOne, elements); -})(), processBoosted(root), processSse(root), (function() { - var onEls = domQueryAll(root, "[sx-on\\:beforeRequest],[sx-on\\:afterRequest],[sx-on\\:afterSwap],[sx-on\\:afterSettle],[sx-on\\:responseError]"); - return forEach(bindInlineHandlers, onEls); -})()) : NIL); + var els = domQueryAll(sxOr(root, domBody()), VERB_SELECTOR); + return forEach(function(el) { return (isSxTruthy(!isProcessed(el, "verb")) ? (markProcessed(el, "verb"), processOne(el)) : NIL); }, els); })(); }; // process-one - var processOne = function(el) { return (isSxTruthy(!isProcessed(el, "bound")) ? (isSxTruthy(!sxOr(domHasAttr(el, "sx-disable"), domClosest(el, "[sx-disable]"))) ? (markProcessed(el, "bound"), (function() { + var processOne = function(el) { return (function() { var verbInfo = getVerbInfo(el); - return (isSxTruthy(verbInfo) ? (bindTriggers(el, verbInfo), bindPreloadFor(el)) : NIL); -})()) : NIL) : NIL); }; + return (isSxTruthy(verbInfo) ? (isSxTruthy(!domHasAttr(el, "sx-disable")) ? (bindTriggers(el, verbInfo), bindPreloadFor(el)) : NIL) : NIL); +})(); }; // handle-popstate var handlePopstate = function(scrollY) { return (function() { - var url = browserLocationHref(); var main = domQueryById("main-panel"); - return (isSxTruthy(!main) ? browserReload() : (function() { - var headers = buildRequestHeaders(NIL, loadedComponentNames(), _cssHash); - headers["SX-History-Restore"] = "true"; + var url = browserLocationHref(); + return (isSxTruthy(main) ? (function() { + var headers = buildRequestHeaders(main, loadedComponentNames(), _cssHash); return fetchAndRestore(main, url, headers, scrollY); -})()); +})() : NIL); })(); }; // engine-init @@ -1756,11 +1720,9 @@ // ========================================================================= - // Platform interface — Engine (browser-only) + // Platform interface — Engine pure logic (browser + node compatible) // ========================================================================= - // --- Browser/Network --- - function browserLocationHref() { return typeof location !== "undefined" ? location.href : ""; } @@ -1784,6 +1746,23 @@ } } + 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 interface — Orchestration (browser-only) + // ========================================================================= + + // --- Browser/Network --- + function browserNavigate(url) { if (typeof location !== "undefined") location.assign(url); } @@ -1812,16 +1791,6 @@ 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"]'); @@ -2504,7 +2473,7 @@ executeRequest: typeof executeRequest === "function" ? executeRequest : null, postSwap: typeof postSwap === "function" ? postSwap : null, init: typeof engineInit === "function" ? engineInit : null, - _version: "ref-2.0 (dom+engine, bootstrap-compiled)" + _version: "ref-2.0 (dom+engine+orchestration, bootstrap-compiled)" }; diff --git a/shared/static/scripts/sx-ref.js b/shared/static/scripts/sx-ref.js index 76c06bf..e7dc6ea 100644 --- a/shared/static/scripts/sx-ref.js +++ b/shared/static/scripts/sx-ref.js @@ -1400,6 +1400,9 @@ // parse-sse-swap var parseSseSwap = function(el) { return sxOr(domGetAttr(el, "sx-sse-swap"), "message"); }; + + // === Transpiled from orchestration === + // _preload-cache var _preloadCache = {}; @@ -1409,15 +1412,15 @@ // dispatch-trigger-events var dispatchTriggerEvents = function(el, headerVal) { return (isSxTruthy(headerVal) ? (function() { var parsed = tryParseJson(headerVal); - return (isSxTruthy((isSxTruthy(parsed) && isDict(parsed))) ? forEach(function(key) { return domDispatch(el, key, dictGet(parsed, key)); }, keys(parsed)) : forEach(function(name) { return (function() { - var n = trim(name); - return (isSxTruthy(!(n == "")) ? domDispatch(el, n, {}) : NIL); + return (isSxTruthy(parsed) ? forEach(function(key) { return domDispatch(el, key, get(parsed, key)); }, keys(parsed)) : forEach(function(name) { return (function() { + var trimmed = trim(name); + return (isSxTruthy(!isEmpty(trimmed)) ? domDispatch(el, trimmed, {}) : NIL); })(); }, split(headerVal, ","))); })() : NIL); }; // init-css-tracking var initCssTracking = function() { return (function() { - var meta = domQuery("meta[name=\"sx-css-classes\"]"); + var meta = domQuery("meta[name=\"sx-css-hash\"]"); return (isSxTruthy(meta) ? (function() { var content = domGetAttr(meta, "content"); return (isSxTruthy(content) ? (_cssHash = content) : NIL); @@ -1426,65 +1429,58 @@ // execute-request var executeRequest = function(el, verbInfo, extraParams) { return (function() { - var currentVerb = getVerbInfo(el); - var verb = (isSxTruthy(currentVerb) ? currentVerb : verbInfo); - var method = get(verb, "method"); - var url = get(verb, "url"); - if (isSxTruthy(!domHasClass(el, "sx-error"))) { - domRemoveAttr(el, "data-sx-retry-ms"); -} + var info = sxOr(verbInfo, getVerbInfo(el)); + return (isSxTruthy(isNil(info)) ? promiseResolve(NIL) : (function() { + var verb = get(info, "method"); + var url = get(info, "url"); return (isSxTruthy((function() { var media = domGetAttr(el, "sx-media"); return (isSxTruthy(media) && !browserMediaMatches(media)); })()) ? promiseResolve(NIL) : (isSxTruthy((function() { - var msg = domGetAttr(el, "sx-confirm"); - return (isSxTruthy(msg) && !browserConfirm(msg)); + var confirmMsg = domGetAttr(el, "sx-confirm"); + return (isSxTruthy(confirmMsg) && !browserConfirm(confirmMsg)); })()) ? promiseResolve(NIL) : (function() { var promptMsg = domGetAttr(el, "sx-prompt"); - var params = extraParams; - return (isSxTruthy(promptMsg) ? (function() { - var promptVal = browserPrompt(promptMsg); - return (isSxTruthy(isNil(promptVal)) ? promiseResolve(NIL) : ((params = sxOr(params, {})), dictSet(params, "promptValue", promptVal), doFetch(el, verb, method, url, params))); -})() : doFetch(el, verb, method, url, params)); + var promptVal = (isSxTruthy(promptMsg) ? browserPrompt(promptMsg) : NIL); + return (isSxTruthy((isSxTruthy(promptMsg) && isNil(promptVal))) ? promiseResolve(NIL) : (isSxTruthy(!validateForRequest(el)) ? promiseResolve(NIL) : doFetch(el, verb, verb, url, (isSxTruthy(promptVal) ? assoc(sxOr(extraParams, {}), "SX-Prompt", promptVal) : extraParams)))); })())); +})()); })(); }; // do-fetch var doFetch = function(el, verb, method, url, extraParams) { return (function() { - var syncAttr = domGetAttr(el, "sx-sync"); - if (isSxTruthy((isSxTruthy(syncAttr) && contains(syncAttr, "replace")))) { + var sync = domGetAttr(el, "sx-sync"); + if (isSxTruthy((sync == "replace"))) { abortPrevious(el); } return (function() { var ctrl = newAbortController(); trackController(el, ctrl); - return (function() { - var headers = buildRequestHeaders(el, loadedComponentNames(), _cssHash); - if (isSxTruthy((isSxTruthy(extraParams) && dictHas(extraParams, "promptValue")))) { - headers["SX-Prompt"] = get(extraParams, "promptValue"); -} - if (isSxTruthy((isSxTruthy(!(method == "GET")) && browserSameOrigin(url)))) { - (function() { - var csrf = csrfToken(); - return (isSxTruthy(csrf) ? dictSet(headers, "X-CSRFToken", csrf) : NIL); -})(); -} return (function() { var bodyInfo = buildRequestBody(el, method, url); - return (function() { - var body = get(bodyInfo, "body"); var finalUrl = get(bodyInfo, "url"); + var body = get(bodyInfo, "body"); var ct = get(bodyInfo, "content-type"); + var headers = buildRequestHeaders(el, loadedComponentNames(), _cssHash); + var csrf = csrfToken(); + if (isSxTruthy(extraParams)) { + { var _c = keys(extraParams); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; headers[k] = get(extraParams, k); } } +} if (isSxTruthy(ct)) { headers["Content-Type"] = ct; } - return (isSxTruthy(!domDispatch(el, "sx:beforeRequest", {["method"]: method, ["url"]: finalUrl})) ? promiseResolve(NIL) : (domAddClass(el, "sx-request"), domSetAttr(el, "aria-busy", "true"), (function() { + if (isSxTruthy(csrf)) { + headers["X-CSRFToken"] = csrf; +} + return (function() { + var cached = preloadCacheGet(_preloadCache, finalUrl); + var optimisticState = applyOptimistic(el); var indicator = showIndicator(el); var disabledElts = disableElements(el); - var preloaded = (isSxTruthy((method == "GET")) ? preloadCacheGet(_preloadCache, finalUrl) : NIL); - return fetchRequest({["url"]: finalUrl, ["method"]: method, ["headers"]: headers, ["body"]: body, ["signal"]: controllerSignal(ctrl), ["preloaded"]: preloaded, ["cross-origin"]: isCrossOrigin(finalUrl)}, function(respOk, status, getHeader, text) { return (clearLoadingState(el, indicator, disabledElts), (isSxTruthy(!respOk) ? (domDispatch(el, "sx:responseError", {["status"]: status}), handleRetry(el, verb, extraParams)) : (domDispatch(el, "sx:afterRequest", {}), handleFetchSuccess(el, finalUrl, verb, extraParams, getHeader, text)))); }, function(err) { return (clearLoadingState(el, indicator, disabledElts), (isSxTruthy(!isAbortError(err)) ? (domDispatch(el, "sx:sendError", {["error"]: err}), handleRetry(el, verb, extraParams)) : NIL)); }); -})())); -})(); + 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(!respOk) ? (domDispatch(el, "sx:responseError", {["status"]: status, ["text"]: 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(!isAbortError(err)) ? domDispatch(el, "sx:requestError", {["error"]: err}) : NIL)); }); })(); })(); })(); @@ -1492,257 +1488,225 @@ // handle-fetch-success var handleFetchSuccess = function(el, url, verb, extraParams, getHeader, text) { return (function() { - var headers = processResponseHeaders(getHeader); - return (isSxTruthy(get(headers, "redirect")) ? browserNavigate(get(headers, "redirect")) : (isSxTruthy((get(headers, "refresh") == "true")) ? browserReload() : (dispatchTriggerEvents(el, get(headers, "trigger")), (function() { - var rawSwap = sxOr(domGetAttr(el, "sx-swap"), DEFAULT_SWAP); - var target = resolveTarget(el); - var selectSel = domGetAttr(el, "sx-select"); - if (isSxTruthy(get(headers, "retarget"))) { - target = sxOr(domQuery(get(headers, "retarget")), target); -} - if (isSxTruthy(get(headers, "reswap"))) { - rawSwap = get(headers, "reswap"); -} - return (function() { - var swap = parseSwapSpec(rawSwap, false); - var ct = sxOr(get(headers, "content-type"), ""); - (isSxTruthy(contains(ct, "text/sx")) ? handleSxResponse(el, target, swap, selectSel, text) : handleHtmlResponse(el, target, swap, selectSel, text)); - if (isSxTruthy(get(headers, "location"))) { - fetchLocation(get(headers, "location")); -} - handleHistory(el, url, headers); - domDispatch(el, "sx:afterSwap", {["target"]: target}); - dispatchTriggerEvents(el, get(headers, "trigger-swap")); - return requestAnimationFrame_(function() { return (domDispatch(el, "sx:afterSettle", {["target"]: target}), dispatchTriggerEvents(el, get(headers, "trigger-settle"))); }); + var respHeaders = processResponseHeaders(getHeader); + (function() { + var newHash = get(respHeaders, "css-hash"); + return (isSxTruthy(newHash) ? (_cssHash = newHash) : NIL); })(); + dispatchTriggerEvents(el, get(respHeaders, "trigger")); + return (isSxTruthy(get(respHeaders, "redirect")) ? browserNavigate(get(respHeaders, "redirect")) : (isSxTruthy(get(respHeaders, "refresh")) ? browserReload() : (isSxTruthy(get(respHeaders, "location")) ? fetchLocation(get(respHeaders, "location")) : (function() { + var targetEl = (isSxTruthy(get(respHeaders, "retarget")) ? domQuery(get(respHeaders, "retarget")) : resolveTarget(el)); + var swapSpec = parseSwapSpec(sxOr(get(respHeaders, "reswap"), domGetAttr(el, "sx-swap")), domHasClass(domBody(), "sx-transitions")); + var swapStyle = get(swapSpec, "style"); + var useTransition = get(swapSpec, "transition"); + var ct = sxOr(get(respHeaders, "content-type"), ""); + (isSxTruthy(contains(ct, "text/sx")) ? handleSxResponse(el, targetEl, text, swapStyle, useTransition) : handleHtmlResponse(el, targetEl, text, swapStyle, useTransition)); + dispatchTriggerEvents(el, get(respHeaders, "trigger-swap")); + handleHistory(el, url, respHeaders); + if (isSxTruthy(get(respHeaders, "trigger-settle"))) { + setTimeout_(function() { return dispatchTriggerEvents(el, get(respHeaders, "trigger-settle")); }, 20); +} + return domDispatch(el, "sx:afterSwap", {["target"]: targetEl, ["swap"]: swapStyle}); })()))); })(); }; // handle-sx-response - var handleSxResponse = function(el, target, swap, selectSel, text) { return (function() { + var handleSxResponse = function(el, target, text, swapStyle, useTransition) { return (function() { var cleaned = stripComponentScripts(text); - var cleaned2 = extractResponseCss(cleaned); return (function() { - var source = trim(cleaned2); - return (isSxTruthy((isSxTruthy(source) && !(source == ""))) ? (function() { - var dom = sxRender(source); + var final = extractResponseCss(cleaned); + return (function() { + var trimmed = trim(final); + return (isSxTruthy(!isEmpty(trimmed)) ? (function() { + var rendered = sxRender(trimmed); var container = domCreateElement("div", NIL); - domAppend(container, dom); + domAppend(container, rendered); processOobSwaps(container, function(t, oob, s) { return swapDomNodes(t, oob, s); }); return (function() { - var selected = (isSxTruthy(selectSel) ? selectFromContainer(container, selectSel) : childrenToFragment(container)); - return (isSxTruthy((isSxTruthy(!(get(swap, "style") == "none")) && target)) ? withTransition(get(swap, "transition"), function() { return (swapDomNodes(target, selected, get(swap, "style")), hoistHeadElements(target)); }) : NIL); + var selectSel = domGetAttr(el, "sx-select"); + var content = (isSxTruthy(selectSel) ? selectFromContainer(container, selectSel) : childrenToFragment(container)); + return withTransition(useTransition, function() { return swapDomNodes(target, content, swapStyle); }); })(); })() : NIL); })(); +})(); })(); }; // handle-html-response - var handleHtmlResponse = function(el, target, swap, selectSel, text) { return (function() { + var handleHtmlResponse = function(el, target, text, swapStyle, useTransition) { return (function() { var doc = domParseHtmlDocument(text); - sxProcessScripts(doc); - processOobSwaps(doc, function(t, oob, s) { return swapHtmlString(t, domOuterHtml(oob), s); }); - return (function() { - var content = (isSxTruthy(selectSel) ? selectHtmlFromDoc(doc, selectSel) : sxOr(domBodyInnerHtml(doc), text)); - return (isSxTruthy((isSxTruthy(!(get(swap, "style") == "none")) && target)) ? withTransition(get(swap, "transition"), function() { return (swapHtmlString(target, content, get(swap, "style")), hoistHeadElements(target)); }) : NIL); -})(); + return (isSxTruthy(doc) ? (function() { + var selectSel = domGetAttr(el, "sx-select"); + return (isSxTruthy(selectSel) ? (function() { + var html = selectHtmlFromDoc(doc, selectSel); + return withTransition(useTransition, function() { return swapHtmlString(target, html, swapStyle); }); +})() : (function() { + var container = domCreateElement("div", NIL); + domSetInnerHtml(container, domBodyInnerHtml(doc)); + processOobSwaps(container, function(t, oob, s) { return swapDomNodes(t, oob, s); }); + hoistHeadElements(container); + return withTransition(useTransition, function() { return swapDomNodes(target, childrenToFragment(container), swapStyle); }); +})()); +})() : NIL); })(); }; // handle-retry - var handleRetry = function(el, verbInfo, extraParams) { return (function() { + var handleRetry = function(el, verb, method, url, extraParams) { return (function() { var retryAttr = domGetAttr(el, "sx-retry"); - return (isSxTruthy(retryAttr) ? (function() { var spec = parseRetrySpec(retryAttr); - var currentMs = sxOr(parseInt_(domGetAttr(el, "data-sx-retry-ms"), 0), get(spec, "start-ms")); - domAddClass(el, "sx-error"); - domRemoveClass(el, "sx-loading"); - return setTimeout_(function() { return (domRemoveClass(el, "sx-error"), domAddClass(el, "sx-loading"), domSetAttr(el, "data-sx-retry-ms", (String(nextRetryMs(currentMs, get(spec, "cap-ms"))))), executeRequest(el, verbInfo, extraParams)); }, currentMs); + return (isSxTruthy(spec) ? (function() { + var currentMs = sxOr(domGetAttr(el, "data-sx-retry-ms"), get(spec, "start-ms")); + return (function() { + var ms = parseInt_(currentMs, get(spec, "start-ms")); + domSetAttr(el, "data-sx-retry-ms", (String(nextRetryMs(ms, get(spec, "cap-ms"))))); + return setTimeout_(function() { return doFetch(el, verb, method, url, extraParams); }, ms); +})(); })() : NIL); })(); }; // bind-triggers var bindTriggers = function(el, verbInfo) { return (function() { - var triggerSpec = domGetAttr(el, "sx-trigger"); - var triggers = (isSxTruthy(triggerSpec) ? parseTriggerSpec(triggerSpec) : defaultTrigger(domTagName(el))); - return forEach(function(trig) { return (function() { - var kind = classifyTrigger(trig); - return (isSxTruthy((kind == "poll")) ? setInterval_(function() { return executeRequest(el, verbInfo, NIL); }, sxOr(get(get(trig, "modifiers"), "interval"), 1000)) : (isSxTruthy((kind == "intersect")) ? observeIntersection(el, function() { return executeRequest(el, verbInfo, NIL); }, get(get(trig, "modifiers"), "once"), get(get(trig, "modifiers"), "delay")) : (isSxTruthy((kind == "load")) ? setTimeout_(function() { return executeRequest(el, verbInfo, NIL); }, 0) : (isSxTruthy((kind == "revealed")) ? observeIntersection(el, function() { return executeRequest(el, verbInfo, NIL); }, true, NIL) : bindEvent(el, verbInfo, trig))))); + var triggers = sxOr(parseTriggerSpec(domGetAttr(el, "sx-trigger")), defaultTrigger(domTagName(el))); + return forEach(function(trigger) { return (function() { + var kind = classifyTrigger(trigger); + var mods = get(trigger, "modifiers"); + return (isSxTruthy((kind == "poll")) ? setInterval_(function() { return executeRequest(el, NIL, NIL); }, get(mods, "interval")) : (isSxTruthy((kind == "intersect")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, false, get(mods, "delay")) : (isSxTruthy((kind == "load")) ? setTimeout_(function() { return executeRequest(el, NIL, NIL); }, sxOr(get(mods, "delay"), 0)) : (isSxTruthy((kind == "revealed")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, true, get(mods, "delay")) : (isSxTruthy((kind == "event")) ? bindEvent(el, get(trigger, "event"), mods, verbInfo) : NIL))))); })(); }, triggers); })(); }; // bind-event - var bindEvent = function(el, verbInfo, trig) { return (function() { - var eventName = get(trig, "event"); - var mods = get(trig, "modifiers"); - var listenTarget = (isSxTruthy(get(mods, "from")) ? sxOr(domQuery(get(mods, "from")), el) : el); + var bindEvent = function(el, eventName, mods, verbInfo) { return (function() { var timer = NIL; var lastVal = NIL; - return domAddListener(listenTarget, eventName, function(e) { return ((isSxTruthy((eventName == "submit")) ? preventDefault_(e) : NIL), (isSxTruthy((isSxTruthy((eventName == "click")) && (domTagName(el) == "A"))) ? preventDefault_(e) : NIL), (isSxTruthy(!validateForRequest(el)) ? domDispatch(el, "sx:validationFailed", {}) : (isSxTruthy((isSxTruthy(get(mods, "changed")) && isSxTruthy(!isNil(elementValue(el))) && (elementValue(el) == lastVal))) ? NIL : ((isSxTruthy(get(mods, "changed")) ? (lastVal = elementValue(el)) : NIL), (function() { - var optState = applyOptimistic(el); - var execFn = function() { return (function() { - var p = executeRequest(el, verbInfo, NIL); - return (isSxTruthy((isSxTruthy(optState) && p)) ? promiseCatch(p, function(_) { return revertOptimistic(optState); }) : NIL); -})(); }; - return (isSxTruthy(get(mods, "delay")) ? (clearTimeout_(timer), (timer = setTimeout_(execFn, get(mods, "delay")))) : execFn()); -})())))); }, {["once"]: get(mods, "once")}); + var listenTarget = (isSxTruthy(get(mods, "from")) ? domQuery(get(mods, "from")) : el); + return (isSxTruthy(listenTarget) ? domAddListener(listenTarget, eventName, function(e) { return (function() { + var shouldFire = true; + if (isSxTruthy(get(mods, "changed"))) { + (function() { + var val = elementValue(el); + return (isSxTruthy((val == lastVal)) ? (shouldFire = false) : (lastVal = val)); +})(); +} + return (isSxTruthy(shouldFire) ? ((isSxTruthy(sxOr((eventName == "submit"), (isSxTruthy((eventName == "click")) && domHasAttr(el, "href")))) ? preventDefault_(e) : NIL), (isSxTruthy(get(mods, "delay")) ? (clearTimeout_(timer), (timer = setTimeout_(function() { return executeRequest(el, verbInfo, NIL); }, get(mods, "delay")))) : executeRequest(el, verbInfo, NIL))) : NIL); +})(); }, (isSxTruthy(get(mods, "once")) ? {["once"]: true} : NIL)) : NIL); })(); }; // post-swap - var postSwap = function(root) { return (activateScripts(root), sxProcessScripts(root), sxHydrate(root), processElements(root)); }; + var postSwap = function(root) { return activateScripts(root); }; // activate-scripts - var activateScripts = function(root) { return (function() { - var dead = domQueryAll(root, "script:not([type]), script[type='text/javascript']"); - return forEach(function(d) { return (function() { - var live = createScriptClone(d); - return domReplaceChild(domParent(d), live, d); -})(); }, dead); -})(); }; + var activateScripts = function(root) { return (isSxTruthy(root) ? (function() { + var scripts = domQueryAll(root, "script"); + return forEach(function(dead) { return (isSxTruthy((isSxTruthy(!domHasAttr(dead, "data-components")) && !domHasAttr(dead, "data-sx-activated"))) ? (function() { + var live = createScriptClone(dead); + domSetAttr(live, "data-sx-activated", "true"); + return domReplaceChild(domParent(dead), live, dead); +})() : NIL); }, scripts); +})() : NIL); }; // process-oob-swaps - var processOobSwaps = function(container, swapFn) { return forEach(function(attr) { return (function() { - var oobEls = domQueryAll(container, (String("[") + String(attr) + String("]"))); + var processOobSwaps = function(container, swapFn) { return (function() { + var oobs = findOobSwaps(container); return forEach(function(oob) { return (function() { - var swapType = sxOr(domGetAttr(oob, attr), "outerHTML"); - var targetId = domId(oob); - domRemoveAttr(oob, attr); - if (isSxTruthy(domParent(oob))) { - domRemoveChild(domParent(oob), oob); -} - return (isSxTruthy(targetId) ? (function() { + var targetId = get(oob, "target-id"); var target = domQueryById(targetId); - return (isSxTruthy(target) ? swapFn(target, oob, swapType) : NIL); -})() : NIL); -})(); }, oobEls); -})(); }, ["sx-swap-oob", "hx-swap-oob"]); }; + var oobEl = get(oob, "element"); + var swapType = get(oob, "swap-type"); + if (isSxTruthy(domParent(oobEl))) { + domRemoveChild(domParent(oobEl), oobEl); +} + return (isSxTruthy(target) ? swapFn(target, oobEl, swapType) : NIL); +})(); }, oobs); +})(); }; // hoist-head-elements - var hoistHeadElements = function(root) { return (function() { - var styles = domQueryAll(root, "style[data-sx-css]"); - var links = domQueryAll(root, "link[rel='stylesheet']"); - { var _c = styles; for (var _i = 0; _i < _c.length; _i++) { var el = _c[_i]; if (isSxTruthy(domParent(el))) { - domRemoveChild(domParent(el), el); -} } } - return forEach(function(el) { return (isSxTruthy(domParent(el)) ? domRemoveChild(domParent(el), el) : NIL); }, links); -})(); }; + var hoistHeadElements = function(container) { return forEach(function(style) { return (isSxTruthy(domParent(style)) ? domRemoveChild(domParent(style), style) : NIL); }, domQueryAll(container, "style[data-sx-css]")); }; // process-boosted - var processBoosted = function(root) { return (function() { - var containers = domQueryAll(root, "[sx-boost]"); - if (isSxTruthy(domMatches(root, "[sx-boost]"))) { - boostDescendants(root); -} - return forEach(boostDescendants, containers); -})(); }; + var processBoosted = function(root) { return forEach(function(container) { return boostDescendants(container); }, domQueryAll(sxOr(root, domBody()), "[sx-boost]")); }; // boost-descendants - var boostDescendants = function(container) { return ((function() { - var links = domQueryAll(container, "a[href]"); - return forEach(function(link) { return (isSxTruthy((isSxTruthy(!isProcessed(link, "boost")) && shouldBoostLink(link))) ? (markProcessed(link, "boost"), bindBoostLink(link, domGetAttr(link, "href")), (isSxTruthy(!domHasAttr(link, "sx-target")) ? domSetAttr(link, "sx-target", "#main-panel") : NIL), (isSxTruthy(!domHasAttr(link, "sx-swap")) ? domSetAttr(link, "sx-swap", "innerHTML") : NIL), (isSxTruthy(!domHasAttr(link, "sx-select")) ? domSetAttr(link, "sx-select", "#main-panel") : NIL)) : NIL); }, links); -})(), (function() { - var forms = domQueryAll(container, "form"); - return forEach(function(form) { return (isSxTruthy((isSxTruthy(!isProcessed(form, "boost")) && shouldBoostForm(form))) ? (markProcessed(form, "boost"), bindBoostForm(form, sxOr(upper(domGetAttr(form, "method")), "GET"), sxOr(domGetAttr(form, "action"), browserLocationHref())), (isSxTruthy(!domHasAttr(form, "sx-target")) ? domSetAttr(form, "sx-target", "#main-panel") : NIL), (isSxTruthy(!domHasAttr(form, "sx-swap")) ? domSetAttr(form, "sx-swap", "innerHTML") : NIL)) : NIL); }, forms); -})()); }; + var boostDescendants = function(container) { return forEach(function(link) { return (isSxTruthy((isSxTruthy(!isProcessed(link, "boost")) && shouldBoostLink(link))) ? (markProcessed(link, "boost"), (isSxTruthy(!domHasAttr(link, "sx-target")) ? domSetAttr(link, "sx-target", "#main-panel") : NIL), (isSxTruthy(!domHasAttr(link, "sx-swap")) ? domSetAttr(link, "sx-swap", "innerHTML") : NIL), (isSxTruthy(!domHasAttr(link, "sx-push-url")) ? domSetAttr(link, "sx-push-url", "true") : NIL), bindBoostLink(link, domGetAttr(link, "href"))) : NIL); }, domQueryAll(container, "a[href]")); }; // process-sse - var processSse = function(root) { return (function() { - var sseEls = domQueryAll(root, "[sx-sse]"); - if (isSxTruthy(domMatches(root, "[sx-sse]"))) { - bindSse(root); -} - return forEach(bindSse, sseEls); -})(); }; + var processSse = function(root) { return forEach(function(el) { return (isSxTruthy(!isProcessed(el, "sse")) ? (markProcessed(el, "sse"), bindSse(el)) : NIL); }, domQueryAll(sxOr(root, domBody()), "[sx-sse]")); }; // bind-sse - var bindSse = function(el) { return (isSxTruthy(!isProcessed(el, "sse")) ? (markProcessed(el, "sse"), (function() { + var bindSse = function(el) { return (function() { var url = domGetAttr(el, "sx-sse"); return (isSxTruthy(url) ? (function() { var source = eventSourceConnect(url, el); - return (function() { - var swapEls = domQueryAll(el, "[sx-sse-swap]"); - if (isSxTruthy(domHasAttr(el, "sx-sse-swap"))) { - bindSseSwap(el, source); -} - return forEach(function(child) { return bindSseSwap(child, source); }, swapEls); -})(); + var eventName = parseSseSwap(el); + return eventSourceListen(source, eventName, function(data) { return bindSseSwap(el, data); }); })() : NIL); -})()) : NIL); }; +})(); }; // bind-sse-swap - var bindSseSwap = function(el, source) { return (function() { - var eventName = parseSseSwap(el); - return eventSourceListen(source, eventName, function(data) { return (function() { - var target = sxOr(resolveTarget(el), el); - var swapStyle = sxOr(domGetAttr(el, "sx-swap"), "innerHTML"); - (isSxTruthy(startsWith(trim(data), "(")) ? (function() { - var dom = sxRender(data); - return swapDomNodes(target, dom, swapStyle); -})() : swapHtmlString(target, data, swapStyle)); - postSwap(target); - return domDispatch(el, "sx:sseMessage", {["data"]: data, ["event"]: eventName}); -})(); }); + var bindSseSwap = function(el, data) { return (function() { + var target = resolveTarget(el); + var swapSpec = parseSwapSpec(domGetAttr(el, "sx-swap"), domHasClass(domBody(), "sx-transitions")); + var swapStyle = get(swapSpec, "style"); + var useTransition = get(swapSpec, "transition"); + var trimmed = trim(data); + return (isSxTruthy(!isEmpty(trimmed)) ? (isSxTruthy(startsWith(trimmed, "(")) ? (function() { + var rendered = sxRender(trimmed); + var container = domCreateElement("div", NIL); + domAppend(container, rendered); + return withTransition(useTransition, function() { return swapDomNodes(target, childrenToFragment(container), swapStyle); }); +})() : withTransition(useTransition, function() { return swapHtmlString(target, trimmed, swapStyle); })) : NIL); })(); }; // bind-inline-handlers - var bindInlineHandlers = function(el) { return (isSxTruthy(!isProcessed(el, "on")) ? (markProcessed(el, "on"), (function() { - var attrs = domAttrList(el); - return forEach(function(attr) { return (function() { + var bindInlineHandlers = function(root) { return forEach(function(el) { return forEach(function(attr) { return (function() { var name = first(attr); - var val = nth(attr, 1); - return (isSxTruthy(startsWith(name, "sx-on:")) ? bindInlineHandler(el, slice(name, 6), val) : NIL); -})(); }, attrs); -})()) : NIL); }; + var body = nth(attr, 1); + return (isSxTruthy(startsWith(name, "sx-on:")) ? (function() { + var eventName = slice(name, 6); + return (isSxTruthy(!isProcessed(el, (String("on:") + String(eventName)))) ? (markProcessed(el, (String("on:") + String(eventName))), bindInlineHandler(el, eventName, body)) : NIL); +})() : NIL); +})(); }, domAttrList(el)); }, domQueryAll(sxOr(root, domBody()), "[sx-on\\:beforeRequest],[sx-on\\:afterRequest],[sx-on\\:afterSwap],[sx-on\\:afterSettle],[sx-on\\:load]")); }; // bind-preload-for - var bindPreloadFor = function(el) { return (isSxTruthy(domHasAttr(el, "sx-preload")) ? (function() { - var mode = sxOr(domGetAttr(el, "sx-preload"), "mousedown"); - var events = (isSxTruthy((mode == "mouseover")) ? ["mouseenter", "focusin"] : ["mousedown", "focusin"]); - var debounceMs = (isSxTruthy((mode == "mouseover")) ? 100 : 0); - return bindPreload(el, events, debounceMs, function() { return (function() { - var verb = getVerbInfo(el); - return (isSxTruthy(verb) ? (function() { - var url = get(verb, "url"); - return (isSxTruthy(isNil(preloadCacheGet(_preloadCache, url))) ? doPreload(url) : NIL); + var bindPreloadFor = function(el) { return (function() { + var preloadAttr = domGetAttr(el, "sx-preload"); + return (isSxTruthy(preloadAttr) ? (function() { + var info = getVerbInfo(el); + return (isSxTruthy(info) ? (function() { + var url = get(info, "url"); + var headers = buildRequestHeaders(el, loadedComponentNames(), _cssHash); + var events = (isSxTruthy((preloadAttr == "mousedown")) ? ["mousedown", "touchstart"] : ["mouseover"]); + var debounceMs = (isSxTruthy((preloadAttr == "mousedown")) ? 0 : 100); + return bindPreload(el, events, debounceMs, function() { return doPreload(url, headers); }); +})() : NIL); })() : NIL); -})(); }); -})() : NIL); }; - - // do-preload - var doPreload = function(url) { return (function() { - var headers = buildRequestHeaders(NIL, loadedComponentNames(), _cssHash); - return fetchPreload(url, headers, _preloadCache); })(); }; + // do-preload + var doPreload = function(url, headers) { return (isSxTruthy(isNil(preloadCacheGet(_preloadCache, url))) ? fetchPreload(url, headers, _preloadCache) : NIL); }; + // VERB_SELECTOR - var VERB_SELECTOR = "[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]"; + var VERB_SELECTOR = (String("[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]")); // process-elements var processElements = function(root) { return (function() { - var root = sxOr(root, domBody()); - return (isSxTruthy(root) ? ((isSxTruthy(domMatches(root, VERB_SELECTOR)) ? processOne(root) : NIL), (function() { - var elements = domQueryAll(root, VERB_SELECTOR); - return forEach(processOne, elements); -})(), processBoosted(root), processSse(root), (function() { - var onEls = domQueryAll(root, "[sx-on\\:beforeRequest],[sx-on\\:afterRequest],[sx-on\\:afterSwap],[sx-on\\:afterSettle],[sx-on\\:responseError]"); - return forEach(bindInlineHandlers, onEls); -})()) : NIL); + var els = domQueryAll(sxOr(root, domBody()), VERB_SELECTOR); + return forEach(function(el) { return (isSxTruthy(!isProcessed(el, "verb")) ? (markProcessed(el, "verb"), processOne(el)) : NIL); }, els); })(); }; // process-one - var processOne = function(el) { return (isSxTruthy(!isProcessed(el, "bound")) ? (isSxTruthy(!sxOr(domHasAttr(el, "sx-disable"), domClosest(el, "[sx-disable]"))) ? (markProcessed(el, "bound"), (function() { + var processOne = function(el) { return (function() { var verbInfo = getVerbInfo(el); - return (isSxTruthy(verbInfo) ? (bindTriggers(el, verbInfo), bindPreloadFor(el)) : NIL); -})()) : NIL) : NIL); }; + return (isSxTruthy(verbInfo) ? (isSxTruthy(!domHasAttr(el, "sx-disable")) ? (bindTriggers(el, verbInfo), bindPreloadFor(el)) : NIL) : NIL); +})(); }; // handle-popstate var handlePopstate = function(scrollY) { return (function() { - var url = browserLocationHref(); var main = domQueryById("main-panel"); - return (isSxTruthy(!main) ? browserReload() : (function() { - var headers = buildRequestHeaders(NIL, loadedComponentNames(), _cssHash); - headers["SX-History-Restore"] = "true"; + var url = browserLocationHref(); + return (isSxTruthy(main) ? (function() { + var headers = buildRequestHeaders(main, loadedComponentNames(), _cssHash); return fetchAndRestore(main, url, headers, scrollY); -})()); +})() : NIL); })(); }; // engine-init @@ -1904,11 +1868,9 @@ // ========================================================================= - // Platform interface — Engine (browser-only) + // Platform interface — Engine pure logic (browser + node compatible) // ========================================================================= - // --- Browser/Network --- - function browserLocationHref() { return typeof location !== "undefined" ? location.href : ""; } @@ -1932,6 +1894,23 @@ } } + 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 interface — Orchestration (browser-only) + // ========================================================================= + + // --- Browser/Network --- + function browserNavigate(url) { if (typeof location !== "undefined") location.assign(url); } @@ -1960,16 +1939,6 @@ 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"]'); @@ -2670,7 +2639,7 @@ executeRequest: typeof executeRequest === "function" ? executeRequest : null, postSwap: typeof postSwap === "function" ? postSwap : null, init: typeof engineInit === "function" ? engineInit : null, - _version: "ref-2.0 (dom+engine+html+sx, bootstrap-compiled)" + _version: "ref-2.0 (dom+engine+html+orchestration+sx, bootstrap-compiled)" }; diff --git a/shared/sx/ref/bootstrap_js.py b/shared/sx/ref/bootstrap_js.py index 473375b..16bb415 100644 --- a/shared/sx/ref/bootstrap_js.py +++ b/shared/sx/ref/bootstrap_js.py @@ -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") { diff --git a/shared/sx/ref/engine.sx b/shared/sx/ref/engine.sx index 8e84d9f..1ab88e3 100644 --- a/shared/sx/ref/engine.sx +++ b/shared/sx/ref/engine.sx @@ -1,41 +1,15 @@ ;; ========================================================================== -;; engine.sx — SxEngine specification +;; engine.sx — SxEngine pure logic ;; ;; Fetch/swap/history engine for browser-side SX. Like HTMX but native ;; to the SX rendering pipeline. ;; -;; This file specifies the LOGIC of the engine in s-expressions. -;; Browser-specific APIs (fetch, DOM, history, events) are declared as -;; platform interface at the bottom. +;; This file specifies the pure LOGIC of the engine in s-expressions: +;; parsing trigger specs, morph algorithm, swap dispatch, header building, +;; retry logic, target resolution, etc. ;; -;; The engine processes elements with sx-* attributes: -;; sx-get, sx-post, sx-put, sx-delete, sx-patch — HTTP verb + URL -;; sx-trigger — when to fire (click, submit, change, every 5s, ...) -;; sx-target — where to swap response (#selector, "this", "closest") -;; sx-swap — how to swap (innerHTML, outerHTML, afterend, ...) -;; sx-select — filter response (CSS selector) -;; sx-confirm — confirmation dialog before request -;; sx-prompt — prompt dialog, sends result as SX-Prompt header -;; sx-validate — form validation before request -;; sx-encoding — "json" for JSON body instead of form-encoded -;; sx-params — filter form fields (include, exclude, none) -;; sx-include — include extra inputs from other elements -;; sx-vals — extra key-value pairs to send -;; sx-headers — extra request headers -;; sx-indicator — show/hide loading indicator -;; sx-disabled-elt — disable elements during request -;; sx-push-url — push to browser history -;; sx-replace-url — replace browser history -;; sx-sync — abort previous request ("replace") -;; sx-media — only fire if media query matches -;; sx-preload — preload on mousedown/mouseover -;; sx-boost — auto-boost links and forms in container -;; sx-sse — connect to Server-Sent Events -;; sx-retry — retry on failure (exponential:startMs:capMs) -;; sx-optimistic — optimistic update (remove, disable, add-class:name) -;; sx-preserve — don't morph this element during swap -;; sx-ignore — skip morphing entirely -;; sx-on:* — inline event handlers (beforeRequest, afterSwap, ...) +;; Orchestration (binding events, executing requests, processing elements) +;; lives in orchestration.sx, which depends on this file. ;; ;; Depends on: ;; adapter-dom.sx — render-to-dom (for SX response rendering) @@ -662,834 +636,29 @@ (or (dom-get-attr el "sx-sse-swap") "message"))) -;; ========================================================================== -;; Engine orchestration -;; -;; The following functions define the runtime behavior of the engine: -;; request execution, trigger binding, post-swap lifecycle, boost, SSE, -;; and main processing. Browser-specific mechanics (fetch, addEventListener, -;; IntersectionObserver, EventSource, etc.) are declared as platform -;; interface at the bottom. -;; ========================================================================== - - ;; -------------------------------------------------------------------------- -;; Engine state -;; -------------------------------------------------------------------------- - -(define _preload-cache (dict)) -(define _css-hash "") - - -;; -------------------------------------------------------------------------- -;; Event dispatch helpers -;; -------------------------------------------------------------------------- - -(define dispatch-trigger-events - (fn (el header-val) - ;; Parse and dispatch SX-Trigger header events. - ;; Value: JSON object, JSON string, or comma-separated names. - (when header-val - (let ((parsed (try-parse-json header-val))) - (if (and parsed (dict? parsed)) - (for-each - (fn (key) (dom-dispatch el key (dict-get parsed key))) - (keys parsed)) - (for-each - (fn (name) - (let ((n (trim name))) - (when (not (= n "")) - (dom-dispatch el n (dict))))) - (split header-val ","))))))) - - -;; -------------------------------------------------------------------------- -;; CSS tracking -;; -------------------------------------------------------------------------- - -(define init-css-tracking - (fn () - ;; Read CSS hash from - (let ((meta (dom-query "meta[name=\"sx-css-classes\"]"))) - (when meta - (let ((content (dom-get-attr meta "content"))) - (when content - (set! _css-hash content))))))) - - -;; -------------------------------------------------------------------------- -;; Request execution -;; -------------------------------------------------------------------------- - -(define execute-request - (fn (el verb-info extra-params) - ;; Pre-flight gate logic: media, confirm, prompt. - ;; Returns a promise. - (let ((current-verb (get-verb-info el)) - (verb (if current-verb current-verb verb-info)) - (method (get verb "method")) - (url (get verb "url"))) - ;; Reset retry backoff on fresh requests - (when (not (dom-has-class? el "sx-error")) - (dom-remove-attr el "data-sx-retry-ms")) - ;; Gate: media query - (if (let ((media (dom-get-attr el "sx-media"))) - (and media (not (browser-media-matches? media)))) - (promise-resolve nil) - ;; Gate: confirm dialog - (if (let ((msg (dom-get-attr el "sx-confirm"))) - (and msg (not (browser-confirm msg)))) - (promise-resolve nil) - ;; Gate: prompt dialog - (let ((prompt-msg (dom-get-attr el "sx-prompt")) - (params extra-params)) - (if prompt-msg - (let ((prompt-val (browser-prompt prompt-msg))) - (if (nil? prompt-val) - (promise-resolve nil) - (do - (set! params (or params (dict))) - (dict-set! params "promptValue" prompt-val) - (do-fetch el verb method url params)))) - (do-fetch el verb method url params)))))))) - - -;; -------------------------------------------------------------------------- -;; Fetch pipeline -;; -------------------------------------------------------------------------- - -(define do-fetch - (fn (el verb method url extra-params) - ;; Build request, execute fetch, handle response. - ;; Returns a promise. - (let ((sync-attr (dom-get-attr el "sx-sync"))) - (when (and sync-attr (contains? sync-attr "replace")) - (abort-previous el)) - (let ((ctrl (new-abort-controller))) - (track-controller el ctrl) - (let ((headers (build-request-headers el - (loaded-component-names) _css-hash))) - ;; Prompt header - (when (and extra-params (dict-has? extra-params "promptValue")) - (dict-set! headers "SX-Prompt" - (get extra-params "promptValue"))) - ;; CSRF for mutating same-origin - (when (and (not (= method "GET")) (browser-same-origin? url)) - (let ((csrf (csrf-token))) - (when csrf - (dict-set! headers "X-CSRFToken" csrf)))) - ;; Build request body - (let ((body-info (build-request-body el method url))) - (let ((body (get body-info "body")) - (final-url (get body-info "url")) - (ct (get body-info "content-type"))) - (when ct (dict-set! headers "Content-Type" ct)) - ;; Lifecycle: beforeRequest - (if (not (dom-dispatch el "sx:beforeRequest" - (dict "method" method "url" final-url))) - (promise-resolve nil) - (do - ;; Loading state - (dom-add-class el "sx-request") - (dom-set-attr el "aria-busy" "true") - (let ((indicator (show-indicator el)) - (disabled-elts (disable-elements el)) - (preloaded (if (= method "GET") - (preload-cache-get _preload-cache final-url) - nil))) - ;; Platform fetch with callbacks - (fetch-request - (dict "url" final-url "method" method - "headers" headers "body" body - "signal" (controller-signal ctrl) - "preloaded" preloaded - "cross-origin" (cross-origin? final-url)) - ;; Success: (fn (resp-ok status get-header text) ...) - (fn (resp-ok status get-header text) - (do - (clear-loading-state el indicator disabled-elts) - (if (not resp-ok) - (do - (dom-dispatch el "sx:responseError" - (dict "status" status)) - (handle-retry el verb extra-params)) - (do - (dom-dispatch el "sx:afterRequest" (dict)) - (handle-fetch-success el final-url verb - extra-params get-header text))))) - ;; Error: (fn (err) ...) - (fn (err) - (do - (clear-loading-state el indicator disabled-elts) - (when (not (abort-error? err)) - (do - (dom-dispatch el "sx:sendError" - (dict "error" err)) - (handle-retry el verb extra-params)))))))))))))))) - - -;; -------------------------------------------------------------------------- -;; Response handling -;; -------------------------------------------------------------------------- - -(define handle-fetch-success - (fn (el url verb extra-params get-header text) - ;; Process a successful fetch response. - (let ((headers (process-response-headers get-header))) - ;; Redirect — skip swap - (if (get headers "redirect") - (browser-navigate (get headers "redirect")) - ;; Refresh — skip swap - (if (= (get headers "refresh") "true") - (browser-reload) - (do - ;; Trigger events from header - (dispatch-trigger-events el (get headers "trigger")) - ;; Determine swap target and strategy - (let ((raw-swap (or (dom-get-attr el "sx-swap") DEFAULT_SWAP)) - (target (resolve-target el)) - (select-sel (dom-get-attr el "sx-select"))) - ;; Server overrides - (when (get headers "retarget") - (set! target (or (dom-query (get headers "retarget")) target))) - (when (get headers "reswap") - (set! raw-swap (get headers "reswap"))) - ;; Parse swap spec - (let ((swap (parse-swap-spec raw-swap false)) - (ct (or (get headers "content-type") ""))) - ;; Dispatch by content type - (if (contains? ct "text/sx") - (handle-sx-response el target swap select-sel text) - (handle-html-response el target swap select-sel text)) - ;; SX-Location - (when (get headers "location") - (fetch-location (get headers "location"))) - ;; History - (handle-history el url headers) - ;; After-swap lifecycle - (dom-dispatch el "sx:afterSwap" (dict "target" target)) - (dispatch-trigger-events el (get headers "trigger-swap")) - (request-animation-frame - (fn () - (do - (dom-dispatch el "sx:afterSettle" - (dict "target" target)) - (dispatch-trigger-events el - (get headers "trigger-settle"))))))))))))) - - -;; -------------------------------------------------------------------------- -;; SX response handler -;; -------------------------------------------------------------------------- - -(define handle-sx-response - (fn (el target swap select-sel text) - ;; Process text/sx response: extract components, CSS, render, swap. - (let ((cleaned (strip-component-scripts text)) - (cleaned2 (extract-response-css cleaned))) - (let ((source (trim cleaned2))) - (when (and source (not (= source ""))) - (let ((dom (sx-render source)) - (container (dom-create-element "div" nil))) - (dom-append container dom) - ;; OOB processing on live DOM nodes - (process-oob-swaps container - (fn (t oob s) (swap-dom-nodes t oob s))) - ;; Select filtering - (let ((selected (if select-sel - (select-from-container container select-sel) - (children-to-fragment container)))) - ;; Main swap - (when (and (not (= (get swap "style") "none")) target) - (with-transition (get swap "transition") - (fn () - (do - (swap-dom-nodes target selected (get swap "style")) - (hoist-head-elements target)))))))))))) - - -;; -------------------------------------------------------------------------- -;; HTML response handler -;; -------------------------------------------------------------------------- - -(define handle-html-response - (fn (el target swap select-sel text) - ;; Process HTML response: parse, scripts, OOB, swap. - (let ((doc (dom-parse-html-document text))) - ;; Process sx scripts - (sx-process-scripts doc) - ;; OOB processing - (process-oob-swaps doc - (fn (t oob s) - (swap-html-string t (dom-outer-html oob) s))) - ;; Build content - (let ((content (if select-sel - (select-html-from-doc doc select-sel) - (or (dom-body-inner-html doc) text)))) - ;; Main swap - (when (and (not (= (get swap "style") "none")) target) - (with-transition (get swap "transition") - (fn () - (do - (swap-html-string target content (get swap "style")) - (hoist-head-elements target))))))))) - - -;; -------------------------------------------------------------------------- -;; Retry handling -;; -------------------------------------------------------------------------- - -(define handle-retry - (fn (el verb-info extra-params) - ;; Retry failed request with exponential backoff. - (let ((retry-attr (dom-get-attr el "sx-retry"))) - (when retry-attr - (let ((spec (parse-retry-spec retry-attr)) - (current-ms (or (parse-int - (dom-get-attr el "data-sx-retry-ms") 0) - (get spec "start-ms")))) - (dom-add-class el "sx-error") - (dom-remove-class el "sx-loading") - (set-timeout - (fn () - (do - (dom-remove-class el "sx-error") - (dom-add-class el "sx-loading") - (dom-set-attr el "data-sx-retry-ms" - (str (next-retry-ms current-ms (get spec "cap-ms")))) - (execute-request el verb-info extra-params))) - current-ms)))))) - - -;; -------------------------------------------------------------------------- -;; Trigger binding -;; -------------------------------------------------------------------------- - -(define bind-triggers - (fn (el verb-info) - ;; Parse triggers and bind event handlers. - (let ((trigger-spec (dom-get-attr el "sx-trigger")) - (triggers (if trigger-spec - (parse-trigger-spec trigger-spec) - (default-trigger (dom-tag-name el))))) - (for-each - (fn (trig) - (let ((kind (classify-trigger trig))) - (cond - (= kind "poll") - (set-interval - (fn () (execute-request el verb-info nil)) - (or (get (get trig "modifiers") "interval") 1000)) - (= kind "intersect") - (observe-intersection el - (fn () (execute-request el verb-info nil)) - (get (get trig "modifiers") "once") - (get (get trig "modifiers") "delay")) - (= kind "load") - (set-timeout - (fn () (execute-request el verb-info nil)) 0) - (= kind "revealed") - (observe-intersection el - (fn () (execute-request el verb-info nil)) - true nil) - :else - (bind-event el verb-info trig)))) - triggers)))) - - -;; -------------------------------------------------------------------------- -;; Event binding with modifiers -;; -------------------------------------------------------------------------- - -(define bind-event - (fn (el verb-info trig) - ;; Bind a single event with modifiers (once, delay, changed, from). - (let ((event-name (get trig "event")) - (mods (get trig "modifiers")) - (listen-target (if (get mods "from") - (or (dom-query (get mods "from")) el) - el)) - (timer nil) - (last-val nil)) - (dom-add-listener listen-target event-name - (fn (e) - (do - ;; Prevent defaults - (when (= event-name "submit") (prevent-default e)) - (when (and (= event-name "click") (= (dom-tag-name el) "A")) - (prevent-default e)) - ;; Validation gate - (if (not (validate-for-request el)) - (dom-dispatch el "sx:validationFailed" (dict)) - ;; Changed modifier gate - (if (and (get mods "changed") - (not (nil? (element-value el))) - (= (element-value el) last-val)) - nil - (do - (when (get mods "changed") - (set! last-val (element-value el))) - ;; Apply optimistic update - (let ((opt-state (apply-optimistic el)) - (exec-fn - (fn () - (let ((p (execute-request el verb-info nil))) - (when (and opt-state p) - (promise-catch p - (fn (_) (revert-optimistic opt-state)))))))) - ;; Delay modifier - (if (get mods "delay") - (do - (clear-timeout timer) - (set! timer - (set-timeout exec-fn (get mods "delay")))) - (exec-fn)))))))) - (dict "once" (get mods "once")))))) - - -;; -------------------------------------------------------------------------- -;; Post-swap lifecycle -;; -------------------------------------------------------------------------- - -(define post-swap - (fn (root) - ;; Post-swap: activate scripts, load components, hydrate, bind engine. - (do - (activate-scripts root) - (sx-process-scripts root) - (sx-hydrate root) - (process-elements root)))) - -(define activate-scripts - (fn (root) - ;; Scripts inserted via innerHTML don't execute. - ;; Replace dead scripts with live clones so the browser runs them. - (let ((dead (dom-query-all root - "script:not([type]), script[type='text/javascript']"))) - (for-each - (fn (d) - (let ((live (create-script-clone d))) - (dom-replace-child (dom-parent d) live d))) - dead)))) - - -;; -------------------------------------------------------------------------- -;; Out-of-band swap processing (orchestration variant) -;; -------------------------------------------------------------------------- - -(define process-oob-swaps - (fn (container swap-fn) - ;; Find elements with sx-swap-oob/hx-swap-oob and swap to targets. - (for-each - (fn (attr) - (let ((oob-els (dom-query-all container (str "[" attr "]")))) - (for-each - (fn (oob) - (let ((swap-type (or (dom-get-attr oob attr) "outerHTML")) - (target-id (dom-id oob))) - (dom-remove-attr oob attr) - (when (dom-parent oob) - (dom-remove-child (dom-parent oob) oob)) - (when target-id - (let ((target (dom-query-by-id target-id))) - (when target - (swap-fn target oob swap-type)))))) - oob-els))) - (list "sx-swap-oob" "hx-swap-oob")))) - - -;; -------------------------------------------------------------------------- -;; Head element hoisting -;; -------------------------------------------------------------------------- - -(define hoist-head-elements - (fn (root) - ;; Move