;; ========================================================================== ;; orchestration.sx — Engine orchestration (browser wiring) ;; ;; Binds the pure engine logic to actual browser events, fetch, DOM ;; processing, and lifecycle management. This is the runtime that makes ;; the engine go. ;; ;; Dependency is one-way: orchestration → engine, never reverse. ;; ;; Depends on: ;; engine.sx — parse-trigger-spec, get-verb-info, build-request-headers, ;; process-response-headers, parse-swap-spec, parse-retry-spec, ;; next-retry-ms, resolve-target, apply-optimistic, ;; revert-optimistic, find-oob-swaps, swap-dom-nodes, ;; swap-html-string, morph-children, handle-history, ;; preload-cache-get, preload-cache-set, classify-trigger, ;; should-boost-link?, should-boost-form?, parse-sse-swap, ;; default-trigger, filter-params, PRELOAD_TTL ;; adapter-dom.sx — render-to-dom ;; render.sx — shared registries ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; Engine state ;; -------------------------------------------------------------------------- (define _preload-cache (dict)) (define _css-hash "") ;; -------------------------------------------------------------------------- ;; Event dispatch helpers ;; -------------------------------------------------------------------------- (define dispatch-trigger-events (fn (el header-val) ;; Dispatch events from SX-Trigger / SX-Trigger-After-Swap headers. ;; Value can be JSON object (name → detail) or comma-separated names. (when header-val (let ((parsed (try-parse-json header-val))) (if parsed ;; JSON object: keys are event names, values are detail (for-each (fn (key) (dom-dispatch el key (get parsed key))) (keys parsed)) ;; Comma-separated event names (for-each (fn (name) (let ((trimmed (trim name))) (when (not (empty? trimmed)) (dom-dispatch el trimmed (dict))))) (split header-val ","))))))) ;; -------------------------------------------------------------------------- ;; CSS tracking ;; -------------------------------------------------------------------------- (define init-css-tracking (fn () ;; Read initial CSS hash from meta tag (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 verbInfo extraParams) ;; Gate checks then delegate to do-fetch. ;; verbInfo: dict with "method" and "url" (or nil to read from element). ;; Re-read from element in case attributes were morphed since binding. ;; Returns a promise. (let ((info (or (get-verb-info el) verbInfo))) (if (nil? info) (promise-resolve nil) (let ((verb (get info "method")) (url (get info "url"))) ;; Media query gate (if (let ((media (dom-get-attr el "sx-media"))) (and media (not (browser-media-matches? media)))) (promise-resolve nil) ;; Confirm gate (if (let ((confirm-msg (dom-get-attr el "sx-confirm"))) (and confirm-msg (not (browser-confirm confirm-msg)))) (promise-resolve nil) ;; Prompt (let ((prompt-msg (dom-get-attr el "sx-prompt")) (prompt-val (if prompt-msg (browser-prompt prompt-msg) nil))) (if (and prompt-msg (nil? prompt-val)) (promise-resolve nil) ;; Validation gate (if (not (validate-for-request el)) (promise-resolve nil) (do-fetch el verb verb url (if prompt-val (assoc (or extraParams (dict)) "SX-Prompt" prompt-val) extraParams)))))))))))) (define do-fetch (fn (el verb method url extraParams) ;; Execute the actual fetch. Manages abort, headers, body, loading state. (let ((sync (dom-get-attr el "sx-sync"))) ;; Abort previous if sync mode (when (= sync "replace") (abort-previous el)) (let ((ctrl (new-abort-controller))) (track-controller el ctrl) ;; Build request (let ((body-info (build-request-body el method url)) (final-url (get body-info "url")) (body (get body-info "body")) (ct (get body-info "content-type")) (headers (build-request-headers el (loaded-component-names) _css-hash)) (csrf (csrf-token))) ;; Merge extra params as headers (when extraParams (for-each (fn (k) (dict-set! headers k (get extraParams k))) (keys extraParams))) ;; Content-Type (when ct (dict-set! headers "Content-Type" ct)) ;; CSRF (when csrf (dict-set! headers "X-CSRFToken" csrf)) ;; Preload cache check (let ((cached (preload-cache-get _preload-cache final-url)) (optimistic-state (apply-optimistic el)) (indicator (show-indicator el)) (disabled-elts (disable-elements el))) ;; Loading indicators (dom-add-class el "sx-request") (dom-set-attr el "aria-busy" "true") (dom-dispatch el "sx:beforeRequest" (dict "url" final-url "method" method)) ;; Fetch (fetch-request (dict "url" final-url "method" method "headers" headers "body" body "signal" (controller-signal ctrl) "cross-origin" (cross-origin? final-url) "preloaded" cached) ;; Success callback (fn (resp-ok status get-header text) (do (clear-loading-state el indicator disabled-elts) (revert-optimistic optimistic-state) (if (not resp-ok) (do (dom-dispatch el "sx:responseError" (dict "status" status "text" text)) (handle-retry el verb method final-url extraParams)) (do (dom-dispatch el "sx:afterRequest" (dict "status" status)) (handle-fetch-success el final-url verb extraParams get-header text))))) ;; Error callback (fn (err) (do (clear-loading-state el indicator disabled-elts) (revert-optimistic optimistic-state) (when (not (abort-error? err)) (dom-dispatch el "sx:requestError" (dict "error" err)))))))))))) (define handle-fetch-success (fn (el url verb extraParams get-header text) ;; Route a successful response through the appropriate handler. (let ((resp-headers (process-response-headers get-header))) ;; CSS hash update (let ((new-hash (get resp-headers "css-hash"))) (when new-hash (set! _css-hash new-hash))) ;; Triggers (before swap) (dispatch-trigger-events el (get resp-headers "trigger")) (cond ;; Redirect (get resp-headers "redirect") (browser-navigate (get resp-headers "redirect")) ;; Refresh (get resp-headers "refresh") (browser-reload) ;; Location (SX-Location header) (get resp-headers "location") (fetch-location (get resp-headers "location")) ;; Normal response — route by content type :else (let ((target-el (if (get resp-headers "retarget") (dom-query (get resp-headers "retarget")) (resolve-target el))) (swap-spec (parse-swap-spec (or (get resp-headers "reswap") (dom-get-attr el "sx-swap")) (dom-has-class? (dom-body) "sx-transitions"))) (swap-style (get swap-spec "style")) (use-transition (get swap-spec "transition")) (ct (or (get resp-headers "content-type") ""))) ;; Dispatch by content type (if (contains? ct "text/sx") (handle-sx-response el target-el text swap-style use-transition) (handle-html-response el target-el text swap-style use-transition)) ;; Post-swap triggers (dispatch-trigger-events el (get resp-headers "trigger-swap")) ;; History (handle-history el url resp-headers) ;; Settle triggers (after small delay) (when (get resp-headers "trigger-settle") (set-timeout (fn () (dispatch-trigger-events el (get resp-headers "trigger-settle"))) 20)) ;; Lifecycle event (dom-dispatch el "sx:afterSwap" (dict "target" target-el "swap" swap-style))))))) (define handle-sx-response (fn (el target text swap-style use-transition) ;; Handle SX-format response: strip components, extract CSS, render, swap. (let ((cleaned (strip-component-scripts text))) (let ((final (extract-response-css cleaned))) (let ((trimmed (trim final))) (when (not (empty? trimmed)) (let ((rendered (sx-render trimmed)) (container (dom-create-element "div" nil))) (dom-append container rendered) ;; Process OOB swaps (process-oob-swaps container (fn (t oob s) (swap-dom-nodes t oob s) (sx-hydrate t) (process-elements t))) ;; Select if specified (let ((select-sel (dom-get-attr el "sx-select")) (content (if select-sel (select-from-container container select-sel) (children-to-fragment container)))) ;; Swap (with-transition use-transition (fn () (swap-dom-nodes target content swap-style) (post-swap target))))))))))) (define handle-html-response (fn (el target text swap-style use-transition) ;; Handle HTML-format response: parse, OOB, select, swap. (let ((doc (dom-parse-html-document text))) (when doc (let ((select-sel (dom-get-attr el "sx-select"))) (if select-sel ;; Select from parsed document (let ((html (select-html-from-doc doc select-sel))) (with-transition use-transition (fn () (swap-html-string target html swap-style) (post-swap target)))) ;; Full body content (let ((container (dom-create-element "div" nil))) (dom-set-inner-html container (dom-body-inner-html doc)) ;; Process OOB swaps (process-oob-swaps container (fn (t oob s) (swap-dom-nodes t oob s) (post-swap t))) ;; Hoist head elements (hoist-head-elements container) ;; Swap remaining content (with-transition use-transition (fn () (swap-dom-nodes target (children-to-fragment container) swap-style) (post-swap target)))))))))) ;; -------------------------------------------------------------------------- ;; Retry ;; -------------------------------------------------------------------------- (define handle-retry (fn (el verb method url extraParams) ;; Handle retry on failure if sx-retry is configured (let ((retry-attr (dom-get-attr el "sx-retry")) (spec (parse-retry-spec retry-attr))) (when spec (let ((current-ms (or (dom-get-attr el "data-sx-retry-ms") (get spec "start-ms")))) (let ((ms (parse-int current-ms (get spec "start-ms")))) (dom-set-attr el "data-sx-retry-ms" (str (next-retry-ms ms (get spec "cap-ms")))) (set-timeout (fn () (do-fetch el verb method url extraParams)) ms))))))) ;; -------------------------------------------------------------------------- ;; Trigger binding ;; -------------------------------------------------------------------------- (define bind-triggers (fn (el verbInfo) ;; Bind triggers from sx-trigger attribute (or defaults) (let ((triggers (or (parse-trigger-spec (dom-get-attr el "sx-trigger")) (default-trigger (dom-tag-name el))))) (for-each (fn (trigger) (let ((kind (classify-trigger trigger)) (mods (get trigger "modifiers"))) (cond (= kind "poll") (set-interval (fn () (execute-request el nil nil)) (get mods "interval")) (= kind "intersect") (observe-intersection el (fn () (execute-request el nil nil)) false (get mods "delay")) (= kind "load") (set-timeout (fn () (execute-request el nil nil)) (or (get mods "delay") 0)) (= kind "revealed") (observe-intersection el (fn () (execute-request el nil nil)) true (get mods "delay")) (= kind "event") (bind-event el (get trigger "event") mods verbInfo)))) triggers)))) (define bind-event (fn (el event-name mods verbInfo) ;; Bind a standard DOM event trigger. ;; Handles delay, once, changed, optimistic, preventDefault. (let ((timer nil) (last-val nil) (listen-target (if (get mods "from") (dom-query (get mods "from")) el))) (when listen-target (dom-add-listener listen-target event-name (fn (e) (let ((should-fire true)) ;; Changed modifier: skip if value unchanged (when (get mods "changed") (let ((val (element-value el))) (if (= val last-val) (set! should-fire false) (set! last-val val)))) (when should-fire ;; Prevent default for submit/click on links (when (or (= event-name "submit") (and (= event-name "click") (dom-has-attr? el "href"))) (prevent-default e)) ;; For GET clicks on links, try client-side routing first (let ((is-get-link (and (= event-name "click") (= (get verbInfo "method") "GET") (dom-has-attr? el "href") (not (get mods "delay")))) (client-routed false)) (when is-get-link (log-info (str "sx:route trying " (get verbInfo "url"))) (set! client-routed (try-client-route (url-pathname (get verbInfo "url")) (dom-get-attr el "sx-target")))) (if client-routed (do (browser-push-state (get verbInfo "url")) (browser-scroll-to 0 0)) (do (when is-get-link (log-info (str "sx:route server fetch " (get verbInfo "url")))) (if (get mods "delay") (do (clear-timeout timer) (set! timer (set-timeout (fn () (execute-request el verbInfo nil)) (get mods "delay")))) (execute-request el verbInfo nil)))))))) (if (get mods "once") (dict "once" true) nil)))))) ;; -------------------------------------------------------------------------- ;; Post-swap lifecycle ;; -------------------------------------------------------------------------- (define post-swap (fn (root) ;; Run lifecycle after swap: activate scripts, process SX, hydrate, process (activate-scripts root) (sx-process-scripts root) (sx-hydrate root) (process-elements root))) (define activate-scripts (fn (root) ;; Re-activate scripts in swapped content. ;; Scripts inserted via innerHTML are inert — clone to make them execute. (when root (let ((scripts (dom-query-all root "script"))) (for-each (fn (dead) ;; Skip already-processed or data-components scripts (when (and (not (dom-has-attr? dead "data-components")) (not (dom-has-attr? dead "data-sx-activated"))) (let ((live (create-script-clone dead))) (dom-set-attr live "data-sx-activated" "true") (dom-replace-child (dom-parent dead) live dead)))) scripts))))) ;; -------------------------------------------------------------------------- ;; OOB swap processing ;; -------------------------------------------------------------------------- (define process-oob-swaps (fn (container swap-fn) ;; Find and process out-of-band swaps in container. ;; swap-fn is (fn (target oob-element swap-type) ...). (let ((oobs (find-oob-swaps container))) (for-each (fn (oob) (let ((target-id (get oob "target-id")) (target (dom-query-by-id target-id)) (oob-el (get oob "element")) (swap-type (get oob "swap-type"))) ;; Remove from source container (when (dom-parent oob-el) (dom-remove-child (dom-parent oob-el) oob-el)) ;; Swap into target (when target (swap-fn target oob-el swap-type)))) oobs)))) ;; -------------------------------------------------------------------------- ;; Head element hoisting ;; -------------------------------------------------------------------------- (define hoist-head-elements (fn (container) ;; Move style[data-sx-css] and link[rel=stylesheet] to ;; so they take effect globally. (for-each (fn (style) (when (dom-parent style) (dom-remove-child (dom-parent style) style)) (dom-append-to-head style)) (dom-query-all container "style[data-sx-css]")) (for-each (fn (link) (when (dom-parent link) (dom-remove-child (dom-parent link) link)) (dom-append-to-head link)) (dom-query-all container "link[rel=\"stylesheet\"]")))) ;; -------------------------------------------------------------------------- ;; Boost processing ;; -------------------------------------------------------------------------- (define process-boosted (fn (root) ;; Find [sx-boost] containers and boost their descendants (for-each (fn (container) (boost-descendants container)) (dom-query-all (or root (dom-body)) "[sx-boost]")))) (define boost-descendants (fn (container) ;; Boost links and forms within a container. ;; The sx-boost attribute value is the default target selector ;; for boosted descendants (e.g. sx-boost="#main-panel"). (let ((boost-target (dom-get-attr container "sx-boost"))) (for-each (fn (link) (when (and (not (is-processed? link "boost")) (should-boost-link? link)) (mark-processed! link "boost") ;; Inherit target from boost container if not specified (when (and (not (dom-has-attr? link "sx-target")) boost-target (not (= boost-target "true"))) (dom-set-attr link "sx-target" boost-target)) (when (not (dom-has-attr? link "sx-swap")) (dom-set-attr link "sx-swap" "innerHTML")) (when (not (dom-has-attr? link "sx-push-url")) (dom-set-attr link "sx-push-url" "true")) (bind-client-route-link link (dom-get-attr link "href")))) (dom-query-all container "a[href]")) (for-each (fn (form) (when (and (not (is-processed? form "boost")) (should-boost-form? form)) (mark-processed! form "boost") (let ((method (upper (or (dom-get-attr form "method") "GET"))) (action (or (dom-get-attr form "action") (browser-location-href)))) (when (and (not (dom-has-attr? form "sx-target")) boost-target (not (= boost-target "true"))) (dom-set-attr form "sx-target" boost-target)) (when (not (dom-has-attr? form "sx-swap")) (dom-set-attr form "sx-swap" "innerHTML")) (bind-boost-form form method action)))) (dom-query-all container "form"))))) ;; -------------------------------------------------------------------------- ;; Client-side routing — data cache ;; -------------------------------------------------------------------------- ;; Cache for page data resolved via resolve-page-data. ;; Keyed by "page-name:param1=val1¶m2=val2", value is {data, ts}. ;; Default TTL: 30s. Prevents redundant fetches on back/forward navigation. (define _page-data-cache (dict)) (define _page-data-cache-ttl 30000) ;; 30 seconds in ms (define page-data-cache-key (fn (page-name params) ;; Build a cache key from page name + params. ;; Params are from route matching so order is deterministic. (let ((base page-name)) (if (or (nil? params) (empty? (keys params))) base (let ((parts (list))) (for-each (fn (k) (append! parts (str k "=" (get params k)))) (keys params)) (str base ":" (join "&" parts))))))) (define page-data-cache-get (fn (cache-key) ;; Return cached data if fresh, else nil. (let ((entry (get _page-data-cache cache-key))) (if (nil? entry) nil (if (> (- (now-ms) (get entry "ts")) _page-data-cache-ttl) (do (dict-set! _page-data-cache cache-key nil) nil) (get entry "data")))))) (define page-data-cache-set (fn (cache-key data) ;; Store data with current timestamp. (dict-set! _page-data-cache cache-key {"data" data "ts" (now-ms)}))) ;; -------------------------------------------------------------------------- ;; Client-side routing ;; -------------------------------------------------------------------------- ;; No app-specific nav update here — apps handle sx:clientRoute event. (define swap-rendered-content (fn (target rendered pathname) ;; Swap rendered DOM content into target and run post-processing. ;; Shared by pure and data page client routes. (do (dom-set-text-content target "") (dom-append target rendered) (hoist-head-elements-full target) (process-elements target) (sx-hydrate-elements target) (dom-dispatch target "sx:clientRoute" (dict "pathname" pathname)) (log-info (str "sx:route client " pathname))))) (define resolve-route-target (fn (target-sel) ;; Resolve a target selector to a DOM element, or nil. (if (and target-sel (not (= target-sel "true"))) (dom-query target-sel) nil))) (define try-client-route (fn (pathname target-sel) ;; Try to render a page client-side. Returns true if successful, false otherwise. ;; target-sel is the CSS selector for the swap target (from sx-boost value). ;; For pure pages: renders immediately. For :data pages: fetches data then renders. (let ((match (find-matching-route pathname _page-routes))) (if (nil? match) (do (log-info (str "sx:route no match (" (len _page-routes) " routes) " pathname)) false) (let ((content-src (get match "content")) (closure (or (get match "closure") {})) (params (get match "params")) (page-name (get match "name"))) (if (or (nil? content-src) (empty? content-src)) (do (log-warn (str "sx:route no content for " pathname)) false) (let ((target (resolve-route-target target-sel))) (if (nil? target) (do (log-warn (str "sx:route target not found: " target-sel)) false) (if (get match "has-data") ;; Data page: check cache, else resolve asynchronously (let ((cache-key (page-data-cache-key page-name params)) (cached (page-data-cache-get cache-key))) (if cached ;; Cache hit — render immediately (let ((env (merge closure params cached)) (rendered (try-eval-content content-src env))) (if (nil? rendered) (do (log-warn (str "sx:route cached eval failed for " pathname)) false) (do (log-info (str "sx:route client+cache " pathname)) (swap-rendered-content target rendered pathname) true))) ;; Cache miss — fetch, cache, render (do (log-info (str "sx:route client+data " pathname)) (resolve-page-data page-name params (fn (data) (page-data-cache-set cache-key data) (let ((env (merge closure params data)) (rendered (try-eval-content content-src env))) (if (nil? rendered) (log-warn (str "sx:route data eval failed for " pathname)) (swap-rendered-content target rendered pathname))))) true))) ;; Pure page: render immediately (let ((env (merge closure params)) (rendered (try-eval-content content-src env))) (if (nil? rendered) (do (log-info (str "sx:route server (eval failed) " pathname)) false) (do (swap-rendered-content target rendered pathname) true)))))))))))) (define bind-client-route-link (fn (link href) ;; Bind a boost link with client-side routing. If the route can be ;; rendered client-side (pure page, no :data), do so. Otherwise ;; fall back to standard server fetch via bind-boost-link. (bind-client-route-click link href (fn () ;; Fallback: use standard boost link binding (bind-boost-link link href))))) ;; -------------------------------------------------------------------------- ;; SSE processing ;; -------------------------------------------------------------------------- (define process-sse (fn (root) ;; Find and bind SSE elements (for-each (fn (el) (when (not (is-processed? el "sse")) (mark-processed! el "sse") (bind-sse el))) (dom-query-all (or root (dom-body)) "[sx-sse]")))) (define bind-sse (fn (el) ;; Connect to SSE endpoint and bind swap handler (let ((url (dom-get-attr el "sx-sse"))) (when url (let ((source (event-source-connect url el)) (event-name (parse-sse-swap el))) (event-source-listen source event-name (fn (data) (bind-sse-swap el data)))))))) (define bind-sse-swap (fn (el data) ;; Handle an SSE event: swap data into element (let ((target (resolve-target el)) (swap-spec (parse-swap-spec (dom-get-attr el "sx-swap") (dom-has-class? (dom-body) "sx-transitions"))) (swap-style (get swap-spec "style")) (use-transition (get swap-spec "transition")) (trimmed (trim data))) (when (not (empty? trimmed)) (if (starts-with? trimmed "(") ;; SX response (let ((rendered (sx-render trimmed)) (container (dom-create-element "div" nil))) (dom-append container rendered) (with-transition use-transition (fn () (swap-dom-nodes target (children-to-fragment container) swap-style) (post-swap target)))) ;; HTML response (with-transition use-transition (fn () (swap-html-string target trimmed swap-style) (post-swap target)))))))) ;; -------------------------------------------------------------------------- ;; Inline event handlers ;; -------------------------------------------------------------------------- (define bind-inline-handlers (fn (root) ;; Find elements with sx-on:* attributes and bind handlers (for-each (fn (el) (for-each (fn (attr) (let ((name (first attr)) (body (nth attr 1))) (when (starts-with? name "sx-on:") (let ((event-name (slice name 6))) (when (not (is-processed? el (str "on:" event-name))) (mark-processed! el (str "on:" event-name)) (bind-inline-handler el event-name body)))))) (dom-attr-list el))) (dom-query-all (or root (dom-body)) "[sx-on\\:beforeRequest],[sx-on\\:afterRequest],[sx-on\\:afterSwap],[sx-on\\:afterSettle],[sx-on\\:load]")))) ;; -------------------------------------------------------------------------- ;; Preload ;; -------------------------------------------------------------------------- (define bind-preload-for (fn (el) ;; Bind preload event listeners based on sx-preload attribute (let ((preload-attr (dom-get-attr el "sx-preload"))) (when preload-attr (let ((info (get-verb-info el))) (when info (let ((url (get info "url")) (headers (build-request-headers el (loaded-component-names) _css-hash)) (events (if (= preload-attr "mousedown") (list "mousedown" "touchstart") (list "mouseover"))) (debounce-ms (if (= preload-attr "mousedown") 0 100))) (bind-preload el events debounce-ms (fn () (do-preload url headers)))))))))) (define do-preload (fn (url headers) ;; Execute a preload fetch into the cache (when (nil? (preload-cache-get _preload-cache url)) (fetch-preload url headers _preload-cache)))) ;; -------------------------------------------------------------------------- ;; Main element processing ;; -------------------------------------------------------------------------- (define VERB_SELECTOR (str "[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]")) (define process-elements (fn (root) ;; Find all elements with sx-* verb attributes and process them. (let ((els (dom-query-all (or root (dom-body)) VERB_SELECTOR))) (for-each (fn (el) (when (not (is-processed? el "verb")) (mark-processed! el "verb") (process-one el))) els)) ;; Also process boost, SSE, inline handlers (process-boosted root) (process-sse root) (bind-inline-handlers root))) (define process-one (fn (el) ;; Process a single element with an sx-* verb attribute (let ((verb-info (get-verb-info el))) (when verb-info ;; Check for disabled (when (not (dom-has-attr? el "sx-disable")) (bind-triggers el verb-info) (bind-preload-for el)))))) ;; -------------------------------------------------------------------------- ;; History: popstate handler ;; -------------------------------------------------------------------------- (define handle-popstate (fn (scrollY) ;; Handle browser back/forward navigation. ;; Derive target from the nearest [sx-boost] container. ;; Try client-side route first, fall back to server fetch. (let ((boost-el (dom-query "[sx-boost]")) (url (browser-location-href))) (when boost-el (let ((target-sel (dom-get-attr boost-el "sx-boost")) (target (if (and target-sel (not (= target-sel "true"))) (dom-query target-sel) nil)) (pathname (url-pathname url))) (when target (if (try-client-route pathname target-sel) (browser-scroll-to 0 scrollY) (let ((headers (build-request-headers target (loaded-component-names) _css-hash))) (fetch-and-restore target url headers scrollY))))))))) ;; -------------------------------------------------------------------------- ;; Initialization ;; -------------------------------------------------------------------------- (define engine-init (fn () ;; Initialize: CSS tracking, scripts, hydrate, process. (do (init-css-tracking) (sx-process-scripts nil) (sx-hydrate nil) (process-elements nil)))) ;; -------------------------------------------------------------------------- ;; Platform interface — Orchestration ;; -------------------------------------------------------------------------- ;; ;; From engine.sx (pure logic): ;; parse-trigger-spec, default-trigger, get-verb-info, classify-trigger, ;; build-request-headers, process-response-headers, parse-swap-spec, ;; parse-retry-spec, next-retry-ms, resolve-target, apply-optimistic, ;; revert-optimistic, find-oob-swaps, swap-dom-nodes, swap-html-string, ;; morph-children, handle-history, preload-cache-get, preload-cache-set, ;; should-boost-link?, should-boost-form?, parse-sse-swap, filter-params, ;; PRELOAD_TTL ;; ;; === Promises === ;; (promise-resolve val) → resolved Promise ;; (promise-catch p fn) → p.catch(fn) ;; ;; === Abort controllers === ;; (abort-previous el) → abort + remove controller for element ;; (track-controller el ctrl) → store controller for element ;; (new-abort-controller) → new AbortController() ;; (controller-signal ctrl) → ctrl.signal ;; (abort-error? err) → boolean (err.name === "AbortError") ;; ;; === Timers === ;; (set-timeout fn ms) → timer id ;; (set-interval fn ms) → timer id ;; (clear-timeout id) → void ;; (request-animation-frame fn) → void ;; ;; === Fetch === ;; (fetch-request config success-fn error-fn) → Promise ;; config: dict with url, method, headers, body, signal, preloaded, ;; cross-origin ;; success-fn: (fn (resp-ok status get-header text) ...) ;; error-fn: (fn (err) ...) ;; (fetch-location url) → fetch URL and swap to boost target ;; (fetch-and-restore main url headers scroll-y) → popstate fetch+swap ;; (fetch-preload url headers cache) → preload into cache ;; ;; === Request body === ;; (build-request-body el method url) → dict with body, url, content-type ;; ;; === Loading state === ;; (show-indicator el) → indicator state (or nil) ;; (disable-elements el) → list of disabled elements ;; (clear-loading-state el indicator disabled-elts) → void ;; ;; === DOM extras (beyond adapter-dom.sx) === ;; (dom-query-by-id id) → Element or nil ;; (dom-matches? el sel) → boolean ;; (dom-closest el sel) → Element or nil ;; (dom-body) → document.body ;; (dom-has-class? el cls) → boolean ;; (dom-append-to-head el) → void ;; (dom-parse-html-document text) → parsed document (DOMParser) ;; (dom-outer-html el) → string ;; (dom-body-inner-html doc) → string ;; (dom-tag-name el) → uppercase tag name ;; ;; === Events === ;; (dom-dispatch el name detail) → boolean (dispatchEvent) ;; (dom-add-listener el event fn opts) → void ;; (prevent-default e) → void ;; (element-value el) → el.value or nil ;; ;; === Validation === ;; (validate-for-request el) → boolean ;; ;; === View Transitions === ;; (with-transition enabled fn) → void ;; ;; === IntersectionObserver === ;; (observe-intersection el fn once? delay) → void ;; ;; === EventSource === ;; (event-source-connect url el) → EventSource (with cleanup) ;; (event-source-listen source event fn) → void ;; ;; === Boost bindings === ;; (bind-boost-link el href) → void (click handler + pushState) ;; (bind-boost-form form method action) → void (submit handler) ;; (bind-client-route-click link href fallback-fn) → void (client route click handler) ;; ;; === Inline handlers === ;; (bind-inline-handler el event-name body) → void (new Function) ;; ;; === Preload === ;; (bind-preload el events debounce-ms fn) → void ;; ;; === Processing markers === ;; (mark-processed! el key) → void ;; (is-processed? el key) → boolean ;; ;; === Script handling === ;; (create-script-clone script) → live script Element ;; ;; === SX API (references to Sx/SxRef object) === ;; (sx-render source) → DOM nodes ;; (sx-process-scripts root) → void ;; (sx-hydrate root) → void ;; (loaded-component-names) → list of ~name strings ;; ;; === Response processing === ;; (strip-component-scripts text) → cleaned text ;; (extract-response-css text) → cleaned text ;; (select-from-container el sel) → DocumentFragment ;; (children-to-fragment el) → DocumentFragment ;; (select-html-from-doc doc sel) → HTML string ;; ;; === Parsing === ;; (try-parse-json s) → parsed value or nil ;; ;; === Client-side routing === ;; (try-eval-content source env) → DOM node or nil (catches eval errors) ;; (url-pathname href) → extract pathname from URL string ;; (resolve-page-data name params cb) → void; resolves data for a named page. ;; Platform decides transport (HTTP, cache, IPC, etc). Calls (cb data-dict) ;; when data is available. params is a dict of URL/route parameters. ;; ;; From boot.sx: ;; _page-routes → list of route entries ;; ;; From router.sx: ;; (find-matching-route path routes) → matching entry with params, or nil ;; (parse-route-pattern pattern) → parsed pattern segments ;; ;; === Browser (via engine.sx) === ;; (browser-location-href) → current URL string ;; (browser-navigate url) → void ;; (browser-reload) → void ;; (browser-scroll-to x y) → void ;; (browser-media-matches? query) → boolean ;; (browser-confirm msg) → boolean ;; (browser-prompt msg) → string or nil ;; (csrf-token) → string ;; (cross-origin? url) → boolean ;; (now-ms) → timestamp ms ;; --------------------------------------------------------------------------