diff --git a/shared/static/wasm/sx/engine.sx b/shared/static/wasm/sx/engine.sx index d8cb0eb0..e66deaff 100644 --- a/shared/static/wasm/sx/engine.sx +++ b/shared/static/wasm/sx/engine.sx @@ -1,821 +1,770 @@ -;; ========================================================================== -;; 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 pure LOGIC of the engine in s-expressions: -;; parsing trigger specs, morph algorithm, swap dispatch, header building, -;; retry logic, target resolution, etc. -;; -;; 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) -;; render.sx — shared registries -;; ========================================================================== - - -;; -------------------------------------------------------------------------- -;; Constants -;; -------------------------------------------------------------------------- - (define ENGINE_VERBS (list "get" "post" "put" "delete" "patch")) + (define DEFAULT_SWAP "outerHTML") - -;; -------------------------------------------------------------------------- -;; Trigger parsing -;; -------------------------------------------------------------------------- -;; Parses the sx-trigger attribute value into a list of trigger descriptors. -;; Each descriptor is a dict with "event" and "modifiers" keys. - -(define parse-time :effects [] - (fn ((s :as string)) - ;; Parse time string: "2s" → 2000, "500ms" → 500 - ;; Uses nested if (not cond) because cond misclassifies 2-element - ;; function calls like (nil? s) as scheme-style ((test body)) clauses. - (if (nil? s) 0 - (if (ends-with? s "ms") (parse-int s 0) - (if (ends-with? s "s") (* (parse-int (replace s "s" "") 0) 1000) +(define + parse-time + :effects () + (fn + ((s :as string)) + (if + (nil? s) + 0 + (if + (ends-with? s "ms") + (parse-int s 0) + (if + (ends-with? s "s") + (* (parse-int (replace s "s" "") 0) 1000) (parse-int s 0)))))) - -(define parse-trigger-spec :effects [] - (fn ((spec :as string)) - ;; Parse "click delay:500ms once,change" → list of trigger descriptors - (if (nil? spec) +(define + parse-trigger-spec + :effects () + (fn + ((spec :as string)) + (if + (nil? spec) nil - (let ((raw-parts (split spec ","))) + (let + ((raw-parts (split spec ","))) (filter (fn (x) (not (nil? x))) (map - (fn ((part :as string)) - (let ((tokens (split (trim part) " "))) - (if (empty? tokens) + (fn + ((part :as string)) + (let + ((tokens (split (trim part) " "))) + (if + (empty? tokens) nil - (if (and (= (first tokens) "every") (>= (len tokens) 2)) - ;; Polling trigger + (if + (and (= (first tokens) "every") (>= (len tokens) 2)) (dict - "event" "every" - "modifiers" (dict "interval" (parse-time (nth tokens 1)))) - ;; Normal trigger with optional modifiers - (let ((mods (dict))) + "event" + "every" + "modifiers" + (dict "interval" (parse-time (nth tokens 1)))) + (let + ((mods (dict))) (for-each - (fn ((tok :as string)) + (fn + ((tok :as string)) (cond (= tok "once") - (dict-set! mods "once" true) + (dict-set! mods "once" true) (= tok "changed") - (dict-set! mods "changed" true) + (dict-set! mods "changed" true) (starts-with? tok "delay:") - (dict-set! mods "delay" - (parse-time (slice tok 6))) + (dict-set! + mods + "delay" + (parse-time (slice tok 6))) (starts-with? tok "from:") - (dict-set! mods "from" - (slice tok 5)))) + (dict-set! mods "from" (slice tok 5)))) (rest tokens)) (dict "event" (first tokens) "modifiers" mods)))))) raw-parts)))))) - -(define default-trigger :effects [] - (fn ((tag-name :as string)) - ;; Default trigger for element type +(define + default-trigger + :effects () + (fn + ((tag-name :as string)) (cond (= tag-name "FORM") - (list (dict "event" "submit" "modifiers" (dict))) - (or (= tag-name "INPUT") - (= tag-name "SELECT") - (= tag-name "TEXTAREA")) - (list (dict "event" "change" "modifiers" (dict))) - :else - (list (dict "event" "click" "modifiers" (dict)))))) + (list (dict "event" "submit" "modifiers" (dict))) + (or + (= tag-name "INPUT") + (= tag-name "SELECT") + (= tag-name "TEXTAREA")) + (list (dict "event" "change" "modifiers" (dict))) + :else (list (dict "event" "click" "modifiers" (dict)))))) +(define + get-verb-info + :effects (io) + (fn + (el) + (let + ((result nil)) + (for-each + (fn + (verb) + (when + (not result) + (let + ((url (dom-get-attr el (str "sx-" verb)))) + (when + url + (set! result (dict "method" (upper verb) "url" url)))))) + ENGINE_VERBS) + result))) -;; -------------------------------------------------------------------------- -;; Verb extraction -;; -------------------------------------------------------------------------- - -(define get-verb-info :effects [io] - (fn (el) - ;; Check element for sx-get, sx-post, etc. Returns (dict "method" "url") or nil. - (some - (fn ((verb :as string)) - (let ((url (dom-get-attr el (str "sx-" verb)))) - (if url - (dict "method" (upper verb) "url" url) - nil))) - ENGINE_VERBS))) - - -;; -------------------------------------------------------------------------- -;; Request header building -;; -------------------------------------------------------------------------- - -(define build-request-headers :effects [io] - (fn (el (loaded-components :as list) (css-hash :as string)) - ;; Build the SX request headers dict - (let ((headers (dict - "SX-Request" "true" - "SX-Current-URL" (browser-location-href)))) - ;; Target selector - (let ((target-sel (dom-get-attr el "sx-target"))) - (when target-sel - (dict-set! headers "SX-Target" target-sel))) - - ;; Send component hash instead of full name list to avoid 431 - ;; (Request Header Fields Too Large) with many loaded components. - ;; Server uses hash to decide whether to send component definitions. - (let ((comp-hash (dom-get-attr - (dom-query "script[data-components][data-hash]") - "data-hash"))) - (when comp-hash - (dict-set! headers "SX-Components-Hash" comp-hash))) - - ;; CSS class hash - (when css-hash - (dict-set! headers "SX-Css" css-hash)) - - ;; Extra headers from sx-headers attribute - (let ((extra-h (dom-get-attr el "sx-headers"))) - (when extra-h - (let ((parsed (parse-header-value extra-h))) - (when parsed +(define + build-request-headers + :effects (io) + (fn + (el (loaded-components :as list) (css-hash :as string)) + (let + ((headers (dict "SX-Request" "true" "SX-Current-URL" (browser-location-href)))) + (let + ((target-sel (dom-get-attr el "sx-target"))) + (when target-sel (dict-set! headers "SX-Target" target-sel))) + (let + ((comp-hash (dom-get-attr (dom-query "script[data-components][data-hash]") "data-hash"))) + (when comp-hash (dict-set! headers "SX-Components-Hash" comp-hash))) + (when css-hash (dict-set! headers "SX-Css" css-hash)) + (let + ((extra-h (dom-get-attr el "sx-headers"))) + (when + extra-h + (let + ((parsed (parse-header-value extra-h))) + (when + parsed (for-each - (fn ((key :as string)) (dict-set! headers key (str (get parsed key)))) + (fn + ((key :as string)) + (dict-set! headers key (str (get parsed key)))) (keys parsed)))))) - headers))) - -;; -------------------------------------------------------------------------- -;; Response header processing -;; -------------------------------------------------------------------------- - -(define process-response-headers :effects [] - (fn ((get-header :as lambda)) - ;; Extract all SX response header directives into a dict. - ;; get-header is (fn (name) → string or nil). +(define + process-response-headers + :effects () + (fn + ((get-header :as lambda)) (dict - "redirect" (get-header "SX-Redirect") - "refresh" (get-header "SX-Refresh") - "trigger" (get-header "SX-Trigger") - "retarget" (get-header "SX-Retarget") - "reswap" (get-header "SX-Reswap") - "location" (get-header "SX-Location") - "replace-url" (get-header "SX-Replace-Url") - "css-hash" (get-header "SX-Css-Hash") - "trigger-swap" (get-header "SX-Trigger-After-Swap") - "trigger-settle" (get-header "SX-Trigger-After-Settle") - "content-type" (get-header "Content-Type") - "cache-invalidate" (get-header "SX-Cache-Invalidate") - "cache-update" (get-header "SX-Cache-Update")))) + "redirect" + (get-header "SX-Redirect") + "refresh" + (get-header "SX-Refresh") + "trigger" + (get-header "SX-Trigger") + "retarget" + (get-header "SX-Retarget") + "reswap" + (get-header "SX-Reswap") + "location" + (get-header "SX-Location") + "replace-url" + (get-header "SX-Replace-Url") + "css-hash" + (get-header "SX-Css-Hash") + "trigger-swap" + (get-header "SX-Trigger-After-Swap") + "trigger-settle" + (get-header "SX-Trigger-After-Settle") + "content-type" + (get-header "Content-Type") + "cache-invalidate" + (get-header "SX-Cache-Invalidate") + "cache-update" + (get-header "SX-Cache-Update")))) - -;; -------------------------------------------------------------------------- -;; Swap specification parsing -;; -------------------------------------------------------------------------- - -(define parse-swap-spec :effects [] - (fn ((raw-swap :as string) (global-transitions? :as boolean)) - ;; Parse "innerHTML transition:true" → dict with style + transition flag - (let ((parts (split (or raw-swap DEFAULT_SWAP) " ")) - (style (first parts)) - (use-transition global-transitions?)) +(define + parse-swap-spec + :effects () + (fn + ((raw-swap :as string) (global-transitions? :as boolean)) + (let + ((parts (split (or raw-swap DEFAULT_SWAP) " ")) + (style (first parts)) + (use-transition global-transitions?)) (for-each - (fn ((p :as string)) + (fn + ((p :as string)) (cond - (= p "transition:true") (set! use-transition true) - (= p "transition:false") (set! use-transition false))) + (= p "transition:true") + (set! use-transition true) + (= p "transition:false") + (set! use-transition false))) (rest parts)) (dict "style" style "transition" use-transition)))) - -;; -------------------------------------------------------------------------- -;; Retry logic -;; -------------------------------------------------------------------------- - -(define parse-retry-spec :effects [] - (fn ((retry-attr :as string)) - ;; Parse "exponential:1000:30000" → spec dict or nil - (if (nil? retry-attr) +(define + parse-retry-spec + :effects () + (fn + ((retry-attr :as string)) + (if + (nil? retry-attr) nil - (let ((parts (split retry-attr ":"))) + (let + ((parts (split retry-attr ":"))) (dict - "strategy" (first parts) - "start-ms" (parse-int (nth parts 1) 1000) - "cap-ms" (parse-int (nth parts 2) 30000)))))) + "strategy" + (first parts) + "start-ms" + (parse-int (nth parts 1) 1000) + "cap-ms" + (parse-int (nth parts 2) 30000)))))) - -(define next-retry-ms :effects [] - (fn ((current-ms :as number) (cap-ms :as number)) - ;; Exponential backoff: double current, cap at max +(define + next-retry-ms + :effects () + (fn + ((current-ms :as number) (cap-ms :as number)) (min (* current-ms 2) cap-ms))) - -;; -------------------------------------------------------------------------- -;; Form parameter filtering -;; -------------------------------------------------------------------------- - -(define filter-params :effects [] - (fn ((params-spec :as string) (all-params :as list)) - ;; Filter form parameters by sx-params spec. - ;; all-params is a list of (key value) pairs. - ;; Returns filtered list of (key value) pairs. - ;; Uses nested if (not cond) — see parse-time comment. - (if (nil? params-spec) all-params - (if (= params-spec "none") (list) - (if (= params-spec "*") all-params - (if (starts-with? params-spec "not ") - (let ((excluded (map trim (split (slice params-spec 4) ",")))) +(define + filter-params + :effects () + (fn + ((params-spec :as string) (all-params :as list)) + (if + (nil? params-spec) + all-params + (if + (= params-spec "none") + (list) + (if + (= params-spec "*") + all-params + (if + (starts-with? params-spec "not ") + (let + ((excluded (map trim (split (slice params-spec 4) ",")))) (filter (fn ((p :as list)) (not (contains? excluded (first p)))) all-params)) - (let ((allowed (map trim (split params-spec ",")))) + (let + ((allowed (map trim (split params-spec ",")))) (filter (fn ((p :as list)) (contains? allowed (first p))) all-params)))))))) - -;; -------------------------------------------------------------------------- -;; Target resolution -;; -------------------------------------------------------------------------- - -(define resolve-target :effects [io] - (fn (el) - ;; Resolve the swap target for an element - (let ((sel (dom-get-attr el "sx-target"))) +(define + resolve-target + :effects (io) + (fn + (el) + (let + ((sel (dom-get-attr el "sx-target"))) (cond - (or (nil? sel) (= sel "this")) el - (= sel "closest") (dom-parent el) + (or (nil? sel) (= sel "this")) + el + (= sel "closest") + (dom-parent el) :else (dom-query sel))))) - -;; -------------------------------------------------------------------------- -;; Optimistic updates -;; -------------------------------------------------------------------------- - -(define apply-optimistic :effects [mutation io] - (fn (el) - ;; Apply optimistic update preview. Returns state for reverting, or nil. - (let ((directive (dom-get-attr el "sx-optimistic"))) - (if (nil? directive) +(define + apply-optimistic + :effects (mutation io) + (fn + (el) + (let + ((directive (dom-get-attr el "sx-optimistic"))) + (if + (nil? directive) nil - (let ((target (or (resolve-target el) el)) - (state (dict "target" target "directive" directive))) + (let + ((target (or (resolve-target el) el)) + (state (dict "target" target "directive" directive))) (cond (= directive "remove") - (do - (dict-set! state "opacity" (dom-get-style target "opacity")) - (dom-set-style target "opacity" "0") - (dom-set-style target "pointer-events" "none")) + (do + (dict-set! state "opacity" (dom-get-style target "opacity")) + (dom-set-style target "opacity" "0") + (dom-set-style target "pointer-events" "none")) (= directive "disable") - (do - (dict-set! state "disabled" (dom-get-prop target "disabled")) - (dom-set-prop target "disabled" true)) + (do + (dict-set! state "disabled" (dom-get-prop target "disabled")) + (dom-set-prop target "disabled" true)) (starts-with? directive "add-class:") - (let ((cls (slice directive 10))) - (dict-set! state "add-class" cls) - (dom-add-class target cls))) + (let + ((cls (slice directive 10))) + (dict-set! state "add-class" cls) + (dom-add-class target cls))) state))))) - -(define revert-optimistic :effects [mutation io] - (fn ((state :as dict)) - ;; Revert an optimistic update - (when state - (let ((target (get state "target")) - (directive (get state "directive"))) +(define + revert-optimistic + :effects (mutation io) + (fn + ((state :as dict)) + (when + state + (let + ((target (get state "target")) (directive (get state "directive"))) (cond (= directive "remove") - (do - (dom-set-style target "opacity" (or (get state "opacity") "")) - (dom-set-style target "pointer-events" "")) + (do + (dom-set-style target "opacity" (or (get state "opacity") "")) + (dom-set-style target "pointer-events" "")) (= directive "disable") - (dom-set-prop target "disabled" (or (get state "disabled") false)) + (dom-set-prop target "disabled" (or (get state "disabled") false)) (get state "add-class") - (dom-remove-class target (get state "add-class"))))))) + (dom-remove-class target (get state "add-class"))))))) - -;; -------------------------------------------------------------------------- -;; Out-of-band swap identification -;; -------------------------------------------------------------------------- - -(define find-oob-swaps :effects [mutation io] - (fn (container) - ;; Find elements marked for out-of-band swapping. - ;; Returns list of (dict "element" el "swap-type" type "target-id" id). - (let ((results (list))) +(define + find-oob-swaps + :effects (mutation io) + (fn + (container) + (let + ((results (list))) (for-each - (fn ((attr :as string)) - (let ((oob-els (dom-query-all container (str "[" attr "]")))) + (fn + ((attr :as string)) + (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))) + (fn + (oob) + (let + ((swap-type (or (dom-get-attr oob attr) "outerHTML")) + (target-id (dom-id oob))) (dom-remove-attr oob attr) - (when target-id - (append! results - (dict "element" oob - "swap-type" swap-type - "target-id" target-id))))) + (when + target-id + (append! + results + (dict + "element" + oob + "swap-type" + swap-type + "target-id" + target-id))))) oob-els))) (list "sx-swap-oob" "hx-swap-oob")) results))) - -;; -------------------------------------------------------------------------- -;; DOM morph algorithm -;; -------------------------------------------------------------------------- -;; Lightweight reconciler: patches oldNode to match newNode in-place, -;; preserving event listeners, focus, scroll position, and form state -;; on keyed (id) elements. - -(define morph-node :effects [mutation io] - (fn (old-node new-node) - ;; Morph old-node to match new-node, preserving listeners/state. +(define + morph-node + :effects (mutation io) + (fn + (old-node new-node) (cond - ;; sx-preserve / sx-ignore → skip - (or (dom-has-attr? old-node "sx-preserve") - (dom-has-attr? old-node "sx-ignore")) - nil - - ;; Hydrated island → preserve reactive state, morph lakes. - ;; If old and new are the same island (by name), keep the old DOM - ;; with its live signals, effects, and event listeners intact. - ;; But recurse into data-sx-lake slots so the server can update - ;; non-reactive content within the island. - (and (dom-has-attr? old-node "data-sx-island") - (is-processed? old-node "island-hydrated") - (dom-has-attr? new-node "data-sx-island") - (= (dom-get-attr old-node "data-sx-island") - (dom-get-attr new-node "data-sx-island"))) - (morph-island-children old-node new-node) - - ;; Different node type or tag → replace wholesale - (or (not (= (dom-node-type old-node) (dom-node-type new-node))) - (not (= (dom-node-name old-node) (dom-node-name new-node)))) - (dom-replace-child (dom-parent old-node) - (dom-clone new-node) old-node) - - ;; Text/comment nodes → update content + (or + (dom-has-attr? old-node "sx-preserve") + (dom-has-attr? old-node "sx-ignore")) + nil + (and + (dom-has-attr? old-node "data-sx-island") + (is-processed? old-node "island-hydrated") + (dom-has-attr? new-node "data-sx-island") + (= + (dom-get-attr old-node "data-sx-island") + (dom-get-attr new-node "data-sx-island"))) + (morph-island-children old-node new-node) + (or + (not (= (dom-node-type old-node) (dom-node-type new-node))) + (not (= (dom-node-name old-node) (dom-node-name new-node)))) + (dom-replace-child (dom-parent old-node) (dom-clone new-node) old-node) (or (= (dom-node-type old-node) 3) (= (dom-node-type old-node) 8)) - (when (not (= (dom-text-content old-node) (dom-text-content new-node))) - (dom-set-text-content old-node (dom-text-content new-node))) - - ;; Element nodes → sync attributes, then recurse children + (when + (not (= (dom-text-content old-node) (dom-text-content new-node))) + (dom-set-text-content old-node (dom-text-content new-node))) (= (dom-node-type old-node) 1) - (do - ;; If the island name changed, clear hydration flag so the new - ;; island gets re-hydrated after morph. The DOM element is reused - ;; (same tag) but the island content is completely different. - (when (and (dom-has-attr? old-node "data-sx-island") - (dom-has-attr? new-node "data-sx-island") - (not (= (dom-get-attr old-node "data-sx-island") - (dom-get-attr new-node "data-sx-island")))) - ;; Dispose the old island itself (not just sub-islands) - ;; to tear down reactive effects before morphing content - (dispose-island old-node) - (dispose-islands-in old-node)) - (sync-attrs old-node new-node) - ;; Skip morphing focused input to preserve user's in-progress edits - (when (not (and (dom-is-active-element? old-node) - (dom-is-input-element? old-node))) - (morph-children old-node new-node)))))) + (do + (when + (and + (dom-has-attr? old-node "data-sx-island") + (dom-has-attr? new-node "data-sx-island") + (not + (= + (dom-get-attr old-node "data-sx-island") + (dom-get-attr new-node "data-sx-island")))) + (dispose-island old-node) + (dispose-islands-in old-node)) + (sync-attrs old-node new-node) + (when + (not + (and + (dom-is-active-element? old-node) + (dom-is-input-element? old-node))) + (morph-children old-node new-node)))))) - -(define sync-attrs :effects [mutation io] - (fn (old-el new-el) - ;; Sync attributes from new to old, but skip reactively managed attrs. - ;; data-sx-reactive-attrs="style,class" means those attrs are owned by - ;; signal effects and must not be overwritten by the morph. - (let ((ra-str (or (dom-get-attr old-el "data-sx-reactive-attrs") "")) - (reactive-attrs (if (empty? ra-str) (list) (split ra-str ",")))) - ;; Add/update attributes from new, skip reactive ones +(define + sync-attrs + :effects (mutation io) + (fn + (old-el new-el) + (let + ((ra-str (or (dom-get-attr old-el "data-sx-reactive-attrs") "")) + (reactive-attrs (if (empty? ra-str) (list) (split ra-str ",")))) (for-each - (fn ((attr :as list)) - (let ((name (first attr)) - (val (nth attr 1))) - (when (and (not (= (dom-get-attr old-el name) val)) - (not (contains? reactive-attrs name))) + (fn + ((attr :as list)) + (let + ((name (first attr)) (val (nth attr 1))) + (when + (and + (not (= (dom-get-attr old-el name) val)) + (not (contains? reactive-attrs name))) (dom-set-attr old-el name val)))) (dom-attr-list new-el)) - ;; Remove attributes not in new, skip reactive + marker attrs (for-each - (fn ((attr :as list)) - (let ((aname (first attr))) - (when (and (not (dom-has-attr? new-el aname)) - (not (contains? reactive-attrs aname)) - (not (= aname "data-sx-reactive-attrs"))) + (fn + ((attr :as list)) + (let + ((aname (first attr))) + (when + (and + (not (dom-has-attr? new-el aname)) + (not (contains? reactive-attrs aname)) + (not (= aname "data-sx-reactive-attrs"))) (dom-remove-attr old-el aname)))) (dom-attr-list old-el))))) - -(define morph-children :effects [mutation io] - (fn (old-parent new-parent) - ;; Reconcile children of old-parent to match new-parent. - ;; Keyed elements (with id) are matched and moved in-place. - (let ((old-kids (dom-child-list old-parent)) - (new-kids (dom-child-list new-parent)) - ;; Build ID map of old children for keyed matching - (old-by-id (reduce - (fn ((acc :as dict) kid) - (let ((id (dom-id kid))) - (if id (do (dict-set! acc id kid) acc) acc))) - (dict) old-kids)) - (oi 0)) - - ;; Walk new children, morph/insert/append +(define + morph-children + :effects (mutation io) + (fn + (old-parent new-parent) + (let + ((old-kids (dom-child-list old-parent)) + (new-kids (dom-child-list new-parent)) + (old-by-id + (reduce + (fn + ((acc :as dict) kid) + (let + ((id (dom-id kid))) + (if id (do (dict-set! acc id kid) acc) acc))) + (dict) + old-kids)) + (oi 0)) (for-each - (fn (new-child) - (let ((match-id (dom-id new-child)) - (match-by-id (if match-id (dict-get old-by-id match-id) nil))) + (fn + (new-child) + (let + ((match-id (dom-id new-child)) + (match-by-id (if match-id (dict-get old-by-id match-id) nil))) (cond - ;; Keyed match — move into position if needed, then morph (and match-by-id (not (nil? match-by-id))) - (do - (when (and (< oi (len old-kids)) - (not (= match-by-id (nth old-kids oi)))) - (dom-insert-before old-parent match-by-id - (if (< oi (len old-kids)) (nth old-kids oi) nil))) - (morph-node match-by-id new-child) - (set! oi (inc oi))) - - ;; Positional match + (do + (when + (and + (< oi (len old-kids)) + (not (= match-by-id (nth old-kids oi)))) + (dom-insert-before + old-parent + match-by-id + (if (< oi (len old-kids)) (nth old-kids oi) nil))) + (morph-node match-by-id new-child) + (set! oi (inc oi))) (< oi (len old-kids)) - (let ((old-child (nth old-kids oi))) - (if (and (dom-id old-child) (not match-id)) - ;; Old has ID, new doesn't — insert new before old - (dom-insert-before old-parent - (dom-clone new-child) old-child) - ;; Normal positional morph - (do - (morph-node old-child new-child) - (set! oi (inc oi))))) - - ;; Extra new children — append - :else - (dom-append old-parent (dom-clone new-child))))) + (let + ((old-child (nth old-kids oi))) + (if + (and (dom-id old-child) (not match-id)) + (dom-insert-before + old-parent + (dom-clone new-child) + old-child) + (do (morph-node old-child new-child) (set! oi (inc oi))))) + :else (dom-append old-parent (dom-clone new-child))))) new-kids) - - ;; Remove leftover old children (for-each - (fn ((i :as number)) - (when (>= i oi) - (let ((leftover (nth old-kids i))) - (when (and (dom-is-child-of? leftover old-parent) - (not (dom-has-attr? leftover "sx-preserve")) - (not (dom-has-attr? leftover "sx-ignore"))) + (fn + ((i :as number)) + (when + (>= i oi) + (let + ((leftover (nth old-kids i))) + (when + (and + (dom-is-child-of? leftover old-parent) + (not (dom-has-attr? leftover "sx-preserve")) + (not (dom-has-attr? leftover "sx-ignore"))) (dom-remove-child old-parent leftover))))) (range oi (len old-kids)))))) - -;; -------------------------------------------------------------------------- -;; morph-island-children — deep morph into hydrated islands via lakes -;; -------------------------------------------------------------------------- -;; -;; Level 2-3 island morphing: the server can update non-reactive content -;; within hydrated islands by morphing data-sx-lake slots. -;; -;; The island's reactive DOM (signals, effects, event listeners) is preserved. -;; Only lake slots — explicitly marked server territory — receive new content. -;; -;; This is the Hegelian synthesis made concrete: -;; - Islands = client subjectivity (reactive state, preserved) -;; - Lakes = server substance (content, morphed) -;; - The morph = Aufhebung (cancellation/preservation/elevation of both) - -(define morph-island-children :effects [mutation io] - (fn (old-island new-island) - ;; Find all lake and marsh slots in both old and new islands - (let ((old-lakes (dom-query-all old-island "[data-sx-lake]")) - (new-lakes (dom-query-all new-island "[data-sx-lake]")) - (old-marshes (dom-query-all old-island "[data-sx-marsh]")) - (new-marshes (dom-query-all new-island "[data-sx-marsh]"))) - ;; Build ID→element maps for new lakes and marshes - (let ((new-lake-map (dict)) - (new-marsh-map (dict))) +(define + morph-island-children + :effects (mutation io) + (fn + (old-island new-island) + (let + ((old-lakes (dom-query-all old-island "[data-sx-lake]")) + (new-lakes (dom-query-all new-island "[data-sx-lake]")) + (old-marshes (dom-query-all old-island "[data-sx-marsh]")) + (new-marshes (dom-query-all new-island "[data-sx-marsh]"))) + (let + ((new-lake-map (dict)) (new-marsh-map (dict))) (for-each - (fn (lake) - (let ((id (dom-get-attr lake "data-sx-lake"))) + (fn + (lake) + (let + ((id (dom-get-attr lake "data-sx-lake"))) (when id (dict-set! new-lake-map id lake)))) new-lakes) (for-each - (fn (marsh) - (let ((id (dom-get-attr marsh "data-sx-marsh"))) + (fn + (marsh) + (let + ((id (dom-get-attr marsh "data-sx-marsh"))) (when id (dict-set! new-marsh-map id marsh)))) new-marshes) - ;; Morph each old lake from its new counterpart (for-each - (fn (old-lake) - (let ((id (dom-get-attr old-lake "data-sx-lake"))) - (let ((new-lake (dict-get new-lake-map id))) - (when new-lake + (fn + (old-lake) + (let + ((id (dom-get-attr old-lake "data-sx-lake"))) + (let + ((new-lake (dict-get new-lake-map id))) + (when + new-lake (sync-attrs old-lake new-lake) (morph-children old-lake new-lake))))) old-lakes) - ;; Morph each old marsh from its new counterpart (for-each - (fn (old-marsh) - (let ((id (dom-get-attr old-marsh "data-sx-marsh"))) - (let ((new-marsh (dict-get new-marsh-map id))) - (when new-marsh - (morph-marsh old-marsh new-marsh old-island))))) + (fn + (old-marsh) + (let + ((id (dom-get-attr old-marsh "data-sx-marsh"))) + (let + ((new-marsh (dict-get new-marsh-map id))) + (when new-marsh (morph-marsh old-marsh new-marsh old-island))))) old-marshes) - ;; Process data-sx-signal attributes — server writes to named stores (process-signal-updates new-island))))) - -;; -------------------------------------------------------------------------- -;; morph-marsh — re-evaluate server content in island's reactive scope -;; -------------------------------------------------------------------------- -;; -;; Marshes are zones inside islands where server content is re-evaluated by -;; the island's reactive evaluator. During morph, the new content is parsed -;; as SX and rendered in the island's signal context. If the marsh has a -;; :transform function, it reshapes the content before evaluation. - -(define morph-marsh :effects [mutation io] - (fn (old-marsh new-marsh island-el) - (let ((transform (dom-get-data old-marsh "sx-marsh-transform")) - (env (dom-get-data old-marsh "sx-marsh-env")) - (new-html (dom-inner-html new-marsh))) - (if (and env new-html (not (empty? new-html))) - ;; Parse new content as SX and re-evaluate in island scope - (let ((parsed (parse new-html))) - (let ((sx-content (if transform (cek-call transform (list parsed)) parsed))) - ;; Dispose old reactive bindings in this marsh +(define + morph-marsh + :effects (mutation io) + (fn + (old-marsh new-marsh island-el) + (let + ((transform (dom-get-data old-marsh "sx-marsh-transform")) + (env (dom-get-data old-marsh "sx-marsh-env")) + (new-html (dom-inner-html new-marsh))) + (if + (and env new-html (not (empty? new-html))) + (let + ((parsed (parse new-html))) + (let + ((sx-content (if transform (cek-call transform (list parsed)) parsed))) (dispose-marsh-scope old-marsh) - ;; Evaluate the SX in a new marsh scope — creates new reactive bindings - (with-marsh-scope old-marsh - (fn () - (let ((new-dom (render-to-dom sx-content env nil))) - ;; Replace marsh children + (with-marsh-scope + old-marsh + (fn + () + (let + ((new-dom (render-to-dom sx-content env nil))) (dom-remove-children-after old-marsh nil) (dom-append old-marsh new-dom)))))) - ;; Fallback: morph like a lake (do (sync-attrs old-marsh new-marsh) (morph-children old-marsh new-marsh)))))) - -;; -------------------------------------------------------------------------- -;; process-signal-updates — server responses write to named store signals -;; -------------------------------------------------------------------------- -;; -;; Elements with data-sx-signal="name:value" trigger signal writes. -;; After processing, the attribute is removed (consumed). -;; -;; Values are JSON-parsed: "7" → 7, "\"hello\"" → "hello", "true" → true. - -(define process-signal-updates :effects [mutation io] - (fn (root) - (let ((signal-els (dom-query-all root "[data-sx-signal]"))) +(define + process-signal-updates + :effects (mutation io) + (fn + (root) + (let + ((signal-els (dom-query-all root "[data-sx-signal]"))) (for-each - (fn (el) - (let ((spec (dom-get-attr el "data-sx-signal"))) - (when spec - (let ((colon-idx (index-of spec ":"))) - (when (> colon-idx 0) - (let ((store-name (slice spec 0 colon-idx)) - (raw-value (slice spec (+ colon-idx 1)))) - (let ((parsed (json-parse raw-value))) + (fn + (el) + (let + ((spec (dom-get-attr el "data-sx-signal"))) + (when + spec + (let + ((colon-idx (index-of spec ":"))) + (when + (> colon-idx 0) + (let + ((store-name (slice spec 0 colon-idx)) + (raw-value (slice spec (+ colon-idx 1)))) + (let + ((parsed (json-parse raw-value))) (reset! (use-store store-name) parsed)) (dom-remove-attr el "data-sx-signal"))))))) signal-els)))) - -;; -------------------------------------------------------------------------- -;; Swap dispatch -;; -------------------------------------------------------------------------- - -(define swap-dom-nodes :effects [mutation io] - (fn (target new-nodes (strategy :as string)) - ;; Execute a swap strategy on live DOM nodes. - ;; new-nodes is typically a DocumentFragment or Element. - (case strategy +(define + swap-dom-nodes + :effects (mutation io) + (fn + (target new-nodes (strategy :as string)) + (case + strategy "innerHTML" - (if (dom-is-fragment? new-nodes) - (morph-children target new-nodes) - (let ((wrapper (dom-create-element "div" nil))) - (dom-append wrapper new-nodes) - (morph-children target wrapper))) - + (if + (dom-is-fragment? new-nodes) + (morph-children target new-nodes) + (let + ((wrapper (dom-create-element "div" nil))) + (dom-append wrapper new-nodes) + (morph-children target wrapper))) "outerHTML" - (let ((parent (dom-parent target)) - (new-el (dom-clone new-nodes))) - (if (dom-is-fragment? new-nodes) - ;; Fragment — replace target with fragment children - (let ((fc (dom-first-child new-nodes))) - (if fc - (do - (set! new-el (dom-clone fc)) - (dom-replace-child parent new-el target) - (let ((sib (dom-next-sibling fc))) - (insert-remaining-siblings parent new-el sib))) - (dom-remove-child parent target))) - ;; Element — replace target with new element - (dom-replace-child parent new-el target)) - ;; Return the new element so post-swap can hydrate it - new-el) - + (let + ((parent (dom-parent target)) (new-el (dom-clone new-nodes))) + (if + (dom-is-fragment? new-nodes) + (let + ((fc (dom-first-child new-nodes))) + (if + fc + (do + (set! new-el (dom-clone fc)) + (dom-replace-child parent new-el target) + (let + ((sib (dom-next-sibling fc))) + (insert-remaining-siblings parent new-el sib))) + (dom-remove-child parent target))) + (dom-replace-child parent new-el target)) + new-el) "afterend" - (dom-insert-after target new-nodes) - + (dom-insert-after target new-nodes) "beforeend" - (dom-append target new-nodes) - + (dom-append target new-nodes) "afterbegin" - (dom-prepend target new-nodes) - + (dom-prepend target new-nodes) "beforebegin" - (dom-insert-before (dom-parent target) new-nodes target) - + (dom-insert-before (dom-parent target) new-nodes target) "delete" - (dom-remove-child (dom-parent target) target) - + (dom-remove-child (dom-parent target) target) "none" - nil + nil + :else (if + (dom-is-fragment? new-nodes) + (morph-children target new-nodes) + (let + ((wrapper (dom-create-element "div" nil))) + (dom-append wrapper new-nodes) + (morph-children target wrapper)))))) - ;; Default = innerHTML - :else - (if (dom-is-fragment? new-nodes) - (morph-children target new-nodes) - (let ((wrapper (dom-create-element "div" nil))) - (dom-append wrapper new-nodes) - (morph-children target wrapper)))))) - - -(define insert-remaining-siblings :effects [mutation io] - (fn (parent ref-node sib) - ;; Insert sibling chain after ref-node - (when sib - (let ((next (dom-next-sibling sib))) +(define + insert-remaining-siblings + :effects (mutation io) + (fn + (parent ref-node sib) + (when + sib + (let + ((next (dom-next-sibling sib))) (dom-insert-after ref-node sib) (insert-remaining-siblings parent sib next))))) - -;; -------------------------------------------------------------------------- -;; String-based swap (fallback for HTML responses) -;; -------------------------------------------------------------------------- - -(define swap-html-string :effects [mutation io] - (fn (target (html :as string) (strategy :as string)) - ;; Execute a swap strategy using an HTML string (DOMParser pipeline). - (case strategy +(define + swap-html-string + :effects (mutation io) + (fn + (target (html :as string) (strategy :as string)) + (case + strategy "innerHTML" - (dom-set-inner-html target html) + (dom-set-inner-html target html) "outerHTML" - (let ((parent (dom-parent target))) - (dom-insert-adjacent-html target "afterend" html) - (dom-remove-child parent target) - parent) - "afterend" + (let + ((parent (dom-parent target))) (dom-insert-adjacent-html target "afterend" html) + (dom-remove-child parent target) + parent) + "afterend" + (dom-insert-adjacent-html target "afterend" html) "beforeend" - (dom-insert-adjacent-html target "beforeend" html) + (dom-insert-adjacent-html target "beforeend" html) "afterbegin" - (dom-insert-adjacent-html target "afterbegin" html) + (dom-insert-adjacent-html target "afterbegin" html) "beforebegin" - (dom-insert-adjacent-html target "beforebegin" html) + (dom-insert-adjacent-html target "beforebegin" html) "delete" - (dom-remove-child (dom-parent target) target) + (dom-remove-child (dom-parent target) target) "none" - nil - :else - (dom-set-inner-html target html)))) + nil + :else (dom-set-inner-html target html)))) - -;; -------------------------------------------------------------------------- -;; History management -;; -------------------------------------------------------------------------- - -(define handle-history :effects [io] - (fn (el (url :as string) (resp-headers :as dict)) - ;; Process history push/replace based on element attrs and response headers - (let ((push-url (dom-get-attr el "sx-push-url")) - (replace-url (dom-get-attr el "sx-replace-url")) - (hdr-replace (get resp-headers "replace-url"))) +(define + handle-history + :effects (io) + (fn + (el (url :as string) (resp-headers :as dict)) + (let + ((push-url (dom-get-attr el "sx-push-url")) + (replace-url (dom-get-attr el "sx-replace-url")) + (hdr-replace (get resp-headers "replace-url"))) (cond - ;; Server override hdr-replace - (browser-replace-state hdr-replace) - ;; Client push + (browser-replace-state hdr-replace) (and push-url (not (= push-url "false"))) - (browser-push-state - (if (= push-url "true") url push-url)) - ;; Client replace + (browser-push-state (if (= push-url "true") url push-url)) (and replace-url (not (= replace-url "false"))) - (browser-replace-state - (if (= replace-url "true") url replace-url)))))) + (browser-replace-state (if (= replace-url "true") url replace-url)))))) +(define PRELOAD_TTL 30000) -;; -------------------------------------------------------------------------- -;; Preload cache -;; -------------------------------------------------------------------------- - -(define PRELOAD_TTL 30000) ;; 30 seconds - -(define preload-cache-get :effects [mutation] - (fn ((cache :as dict) (url :as string)) - ;; Get and consume a cached preload response. - ;; Returns (dict "text" ... "content-type" ...) or nil. - (let ((entry (dict-get cache url))) - (if (nil? entry) +(define + preload-cache-get + :effects (mutation) + (fn + ((cache :as dict) (url :as string)) + (let + ((entry (dict-get cache url))) + (if + (nil? entry) nil - (if (> (- (now-ms) (get entry "timestamp")) PRELOAD_TTL) + (if + (> (- (now-ms) (get entry "timestamp")) PRELOAD_TTL) (do (dict-delete! cache url) nil) (do (dict-delete! cache url) entry)))))) - -(define preload-cache-set :effects [mutation] - (fn ((cache :as dict) (url :as string) (text :as string) (content-type :as string)) - ;; Store a preloaded response - (dict-set! cache url +(define + preload-cache-set + :effects (mutation) + (fn + ((cache :as dict) + (url :as string) + (text :as string) + (content-type :as string)) + (dict-set! + cache + url (dict "text" text "content-type" content-type "timestamp" (now-ms))))) - -;; -------------------------------------------------------------------------- -;; Trigger dispatch table -;; -------------------------------------------------------------------------- -;; Maps trigger event names to binding strategies. -;; This is the logic; actual browser event binding is platform interface. - -(define classify-trigger :effects [] - (fn ((trigger :as dict)) - ;; Classify a parsed trigger descriptor for binding. - ;; Returns one of: "poll", "intersect", "load", "revealed", "event" - (let ((event (get trigger "event"))) +(define + classify-trigger + :effects () + (fn + ((trigger :as dict)) + (let + ((event (get trigger "event"))) (cond - (= event "every") "poll" - (= event "intersect") "intersect" - (= event "load") "load" - (= event "revealed") "revealed" - :else "event")))) + (= event "every") + "poll" + (= event "intersect") + "intersect" + (= event "load") + "load" + (= event "revealed") + "revealed" + :else "event")))) +(define + should-boost-link? + :effects (io) + (fn + (link) + (let + ((href (dom-get-attr link "href"))) + (and + href + (not (starts-with? href "#")) + (not (starts-with? href "javascript:")) + (not (starts-with? href "mailto:")) + (browser-same-origin? href) + (not (dom-has-attr? link "sx-get")) + (not (dom-has-attr? link "sx-post")) + (not (dom-has-attr? link "sx-disable")))))) -;; -------------------------------------------------------------------------- -;; Boost logic -;; -------------------------------------------------------------------------- +(define + should-boost-form? + :effects (io) + (fn + (form) + (and + (not (dom-has-attr? form "sx-get")) + (not (dom-has-attr? form "sx-post")) + (not (dom-has-attr? form "sx-disable"))))) -(define should-boost-link? :effects [io] - (fn (link) - ;; Whether a link inside an sx-boost container should be boosted - (let ((href (dom-get-attr link "href"))) - (and href - (not (starts-with? href "#")) - (not (starts-with? href "javascript:")) - (not (starts-with? href "mailto:")) - (browser-same-origin? href) - (not (dom-has-attr? link "sx-get")) - (not (dom-has-attr? link "sx-post")) - (not (dom-has-attr? link "sx-disable")))))) - - -(define should-boost-form? :effects [io] - (fn (form) - ;; Whether a form inside an sx-boost container should be boosted - (and (not (dom-has-attr? form "sx-get")) - (not (dom-has-attr? form "sx-post")) - (not (dom-has-attr? form "sx-disable"))))) - - -;; -------------------------------------------------------------------------- -;; SSE event classification -;; -------------------------------------------------------------------------- - -(define parse-sse-swap :effects [io] - (fn (el) - ;; Parse sx-sse-swap attribute - ;; Returns event name to listen for (default "message") - (or (dom-get-attr el "sx-sse-swap") "message"))) - - -;; -------------------------------------------------------------------------- -;; Platform interface — Engine (pure logic) -;; -------------------------------------------------------------------------- -;; -;; From adapter-dom.sx: -;; dom-get-attr, dom-set-attr, dom-remove-attr, dom-has-attr?, dom-attr-list -;; dom-query, dom-query-all, dom-id, dom-parent, dom-first-child, -;; dom-next-sibling, dom-child-list, dom-node-type, dom-node-name, -;; dom-text-content, dom-set-text-content, dom-is-fragment?, -;; dom-is-child-of?, dom-is-active-element?, dom-is-input-element?, -;; dom-create-element, dom-append, dom-prepend, dom-insert-before, -;; dom-insert-after, dom-remove-child, dom-replace-child, dom-clone, -;; dom-get-style, dom-set-style, dom-get-prop, dom-set-prop, -;; dom-add-class, dom-remove-class, dom-set-inner-html, -;; dom-insert-adjacent-html -;; -;; Browser/Network: -;; (browser-location-href) → current URL string -;; (browser-same-origin? url) → boolean -;; (browser-push-state url) → void (history.pushState) -;; (browser-replace-state url) → void (history.replaceState) -;; -;; Parsing: -;; (parse-header-value s) → parsed dict from header string -;; (now-ms) → current timestamp in milliseconds -;; -------------------------------------------------------------------------- +(define + parse-sse-swap + :effects (io) + (fn (el) (or (dom-get-attr el "sx-sse-swap") "message"))) diff --git a/shared/static/wasm/sx/engine.sxbc.json b/shared/static/wasm/sx/engine.sxbc.json index e233d7c6..bc73a481 100644 --- a/shared/static/wasm/sx/engine.sxbc.json +++ b/shared/static/wasm/sx/engine.sxbc.json @@ -1 +1 @@ -{"magic":"SXBC","version":1,"hash":"66f0b10ce08346df","module":{"bytecode":[1,2,0,1,3,0,1,4,0,1,5,0,1,6,0,52,1,0,5,128,0,0,5,1,8,0,128,7,0,5,51,10,0,128,9,0,5,51,12,0,128,11,0,5,51,14,0,128,13,0,5,51,16,0,128,15,0,5,51,18,0,128,17,0,5,51,20,0,128,19,0,5,51,22,0,128,21,0,5,51,24,0,128,23,0,5,51,26,0,128,25,0,5,51,28,0,128,27,0,5,51,30,0,128,29,0,5,51,32,0,128,31,0,5,51,34,0,128,33,0,5,51,36,0,128,35,0,5,51,38,0,128,37,0,5,51,40,0,128,39,0,5,51,42,0,128,41,0,5,51,44,0,128,43,0,5,51,46,0,128,45,0,5,51,48,0,128,47,0,5,51,50,0,128,49,0,5,51,52,0,128,51,0,5,51,54,0,128,53,0,5,51,56,0,128,55,0,5,1,58,0,128,57,0,5,51,60,0,128,59,0,5,51,62,0,128,61,0,5,51,64,0,128,63,0,5,51,66,0,128,65,0,5,51,68,0,128,67,0,5,51,70,0,128,69,0,50],"constants":[{"t":"s","v":"ENGINE_VERBS"},{"t":"s","v":"list"},{"t":"s","v":"get"},{"t":"s","v":"post"},{"t":"s","v":"put"},{"t":"s","v":"delete"},{"t":"s","v":"patch"},{"t":"s","v":"DEFAULT_SWAP"},{"t":"s","v":"outerHTML"},{"t":"s","v":"parse-time"},{"t":"code","v":{"bytecode":[16,0,52,0,0,1,33,6,0,1,1,0,32,74,0,16,0,1,3,0,52,2,0,2,33,12,0,16,0,1,1,0,52,4,0,2,32,50,0,16,0,1,5,0,52,2,0,2,33,29,0,16,0,1,5,0,1,8,0,52,7,0,3,1,1,0,52,4,0,2,1,9,0,52,6,0,2,32,9,0,16,0,1,1,0,52,4,0,2,50],"constants":[{"t":"s","v":"nil?"},{"t":"n","v":0},{"t":"s","v":"ends-with?"},{"t":"s","v":"ms"},{"t":"s","v":"parse-int"},{"t":"s","v":"s"},{"t":"s","v":"*"},{"t":"s","v":"replace"},{"t":"s","v":""},{"t":"n","v":1000}],"arity":1}},{"t":"s","v":"parse-trigger-spec"},{"t":"code","v":{"bytecode":[16,0,52,0,0,1,33,4,0,2,32,27,0,16,0,1,2,0,52,1,0,2,17,1,51,4,0,51,6,0,16,1,52,5,0,2,52,3,0,2,50],"constants":[{"t":"s","v":"nil?"},{"t":"s","v":"split"},{"t":"s","v":","},{"t":"s","v":"filter"},{"t":"code","v":{"bytecode":[16,0,52,1,0,1,52,0,0,1,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"nil?"}],"arity":1}},{"t":"s","v":"map"},{"t":"code","v":{"bytecode":[16,0,52,1,0,1,1,2,0,52,0,0,2,17,1,16,1,52,3,0,1,33,4,0,2,32,111,0,16,1,52,5,0,1,1,6,0,52,4,0,2,6,33,14,0,5,16,1,52,8,0,1,1,9,0,52,7,0,2,33,37,0,1,11,0,1,6,0,1,12,0,1,13,0,20,14,0,16,1,1,16,0,52,15,0,2,48,1,52,10,0,2,52,10,0,4,32,40,0,52,10,0,0,17,2,51,18,0,1,2,16,1,52,19,0,1,52,17,0,2,5,1,11,0,16,1,52,5,0,1,1,12,0,16,2,52,10,0,4,50],"constants":[{"t":"s","v":"split"},{"t":"s","v":"trim"},{"t":"s","v":" "},{"t":"s","v":"empty?"},{"t":"s","v":"="},{"t":"s","v":"first"},{"t":"s","v":"every"},{"t":"s","v":">="},{"t":"s","v":"len"},{"t":"n","v":2},{"t":"s","v":"dict"},{"t":"s","v":"event"},{"t":"s","v":"modifiers"},{"t":"s","v":"interval"},{"t":"s","v":"parse-time"},{"t":"s","v":"nth"},{"t":"n","v":1},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[16,0,1,1,0,52,0,0,2,33,13,0,18,0,1,1,0,3,52,2,0,3,32,97,0,16,0,1,3,0,52,0,0,2,33,13,0,18,0,1,3,0,3,52,2,0,3,32,72,0,16,0,1,5,0,52,4,0,2,33,26,0,18,0,1,6,0,20,7,0,16,0,1,9,0,52,8,0,2,48,1,52,2,0,3,32,34,0,16,0,1,10,0,52,4,0,2,33,21,0,18,0,1,11,0,16,0,1,12,0,52,8,0,2,52,2,0,3,32,1,0,2,50],"constants":[{"t":"s","v":"="},{"t":"s","v":"once"},{"t":"s","v":"dict-set!"},{"t":"s","v":"changed"},{"t":"s","v":"starts-with?"},{"t":"s","v":"delay:"},{"t":"s","v":"delay"},{"t":"s","v":"parse-time"},{"t":"s","v":"slice"},{"t":"n","v":6},{"t":"s","v":"from:"},{"t":"s","v":"from"},{"t":"n","v":5}],"arity":1,"upvalue-count":1}},{"t":"s","v":"rest"}],"arity":1}}],"arity":1}},{"t":"s","v":"default-trigger"},{"t":"code","v":{"bytecode":[16,0,1,1,0,52,0,0,2,33,24,0,1,4,0,1,5,0,1,6,0,52,3,0,0,52,3,0,4,52,2,0,1,32,85,0,16,0,1,7,0,52,0,0,2,6,34,24,0,5,16,0,1,8,0,52,0,0,2,6,34,10,0,5,16,0,1,9,0,52,0,0,2,33,24,0,1,4,0,1,10,0,1,6,0,52,3,0,0,52,3,0,4,52,2,0,1,32,21,0,1,4,0,1,11,0,1,6,0,52,3,0,0,52,3,0,4,52,2,0,1,50],"constants":[{"t":"s","v":"="},{"t":"s","v":"FORM"},{"t":"s","v":"list"},{"t":"s","v":"dict"},{"t":"s","v":"event"},{"t":"s","v":"submit"},{"t":"s","v":"modifiers"},{"t":"s","v":"INPUT"},{"t":"s","v":"SELECT"},{"t":"s","v":"TEXTAREA"},{"t":"s","v":"change"},{"t":"s","v":"click"}],"arity":1}},{"t":"s","v":"get-verb-info"},{"t":"code","v":{"bytecode":[51,1,0,1,0,20,2,0,52,0,0,2,50],"constants":[{"t":"s","v":"some"},{"t":"code","v":{"bytecode":[20,0,0,18,0,1,2,0,16,0,52,1,0,2,48,2,17,1,16,1,33,21,0,1,4,0,16,0,52,5,0,1,1,6,0,16,1,52,3,0,4,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"str"},{"t":"s","v":"sx-"},{"t":"s","v":"dict"},{"t":"s","v":"method"},{"t":"s","v":"upper"},{"t":"s","v":"url"}],"arity":1,"upvalue-count":1}},{"t":"s","v":"ENGINE_VERBS"}],"arity":1}},{"t":"s","v":"build-request-headers"},{"t":"code","v":{"bytecode":[1,1,0,1,2,0,1,3,0,20,4,0,48,0,52,0,0,4,17,3,20,5,0,16,0,1,6,0,48,2,17,4,16,4,33,14,0,16,3,1,8,0,16,4,52,7,0,3,32,1,0,2,5,20,5,0,20,9,0,1,10,0,48,1,1,11,0,48,2,17,4,16,4,33,14,0,16,3,1,12,0,16,4,52,7,0,3,32,1,0,2,5,16,2,33,14,0,16,3,1,13,0,16,2,52,7,0,3,32,1,0,2,5,20,5,0,16,0,1,14,0,48,2,17,4,16,4,33,38,0,20,15,0,16,4,48,1,17,5,16,5,33,20,0,51,17,0,1,3,1,5,16,5,52,18,0,1,52,16,0,2,32,1,0,2,32,1,0,2,5,16,3,50],"constants":[{"t":"s","v":"dict"},{"t":"s","v":"SX-Request"},{"t":"s","v":"true"},{"t":"s","v":"SX-Current-URL"},{"t":"s","v":"browser-location-href"},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"sx-target"},{"t":"s","v":"dict-set!"},{"t":"s","v":"SX-Target"},{"t":"s","v":"dom-query"},{"t":"s","v":"script[data-components][data-hash]"},{"t":"s","v":"data-hash"},{"t":"s","v":"SX-Components-Hash"},{"t":"s","v":"SX-Css"},{"t":"s","v":"sx-headers"},{"t":"s","v":"parse-header-value"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[18,0,16,0,18,1,16,0,52,2,0,2,52,1,0,1,52,0,0,3,50],"constants":[{"t":"s","v":"dict-set!"},{"t":"s","v":"str"},{"t":"s","v":"get"}],"arity":1,"upvalue-count":2}},{"t":"s","v":"keys"}],"arity":3}},{"t":"s","v":"process-response-headers"},{"t":"code","v":{"bytecode":[1,1,0,16,0,1,2,0,48,1,1,3,0,16,0,1,4,0,48,1,1,5,0,16,0,1,6,0,48,1,1,7,0,16,0,1,8,0,48,1,1,9,0,16,0,1,10,0,48,1,1,11,0,16,0,1,12,0,48,1,1,13,0,16,0,1,14,0,48,1,1,15,0,16,0,1,16,0,48,1,1,17,0,16,0,1,18,0,48,1,1,19,0,16,0,1,20,0,48,1,1,21,0,16,0,1,22,0,48,1,1,23,0,16,0,1,24,0,48,1,1,25,0,16,0,1,26,0,48,1,52,0,0,26,50],"constants":[{"t":"s","v":"dict"},{"t":"s","v":"redirect"},{"t":"s","v":"SX-Redirect"},{"t":"s","v":"refresh"},{"t":"s","v":"SX-Refresh"},{"t":"s","v":"trigger"},{"t":"s","v":"SX-Trigger"},{"t":"s","v":"retarget"},{"t":"s","v":"SX-Retarget"},{"t":"s","v":"reswap"},{"t":"s","v":"SX-Reswap"},{"t":"s","v":"location"},{"t":"s","v":"SX-Location"},{"t":"s","v":"replace-url"},{"t":"s","v":"SX-Replace-Url"},{"t":"s","v":"css-hash"},{"t":"s","v":"SX-Css-Hash"},{"t":"s","v":"trigger-swap"},{"t":"s","v":"SX-Trigger-After-Swap"},{"t":"s","v":"trigger-settle"},{"t":"s","v":"SX-Trigger-After-Settle"},{"t":"s","v":"content-type"},{"t":"s","v":"Content-Type"},{"t":"s","v":"cache-invalidate"},{"t":"s","v":"SX-Cache-Invalidate"},{"t":"s","v":"cache-update"},{"t":"s","v":"SX-Cache-Update"}],"arity":1}},{"t":"s","v":"parse-swap-spec"},{"t":"code","v":{"bytecode":[16,0,6,34,4,0,5,20,1,0,1,2,0,52,0,0,2,17,2,16,2,52,3,0,1,17,3,16,1,17,4,51,5,0,1,4,16,2,52,6,0,1,52,4,0,2,5,1,8,0,16,3,1,9,0,16,4,52,7,0,4,50],"constants":[{"t":"s","v":"split"},{"t":"s","v":"DEFAULT_SWAP"},{"t":"s","v":" "},{"t":"s","v":"first"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[16,0,1,1,0,52,0,0,2,33,6,0,3,19,0,32,19,0,16,0,1,2,0,52,0,0,2,33,6,0,4,19,0,32,1,0,2,50],"constants":[{"t":"s","v":"="},{"t":"s","v":"transition:true"},{"t":"s","v":"transition:false"}],"arity":1,"upvalue-count":1}},{"t":"s","v":"rest"},{"t":"s","v":"dict"},{"t":"s","v":"style"},{"t":"s","v":"transition"}],"arity":2}},{"t":"s","v":"parse-retry-spec"},{"t":"code","v":{"bytecode":[16,0,52,0,0,1,33,4,0,2,32,62,0,16,0,1,2,0,52,1,0,2,17,1,1,4,0,16,1,52,5,0,1,1,6,0,16,1,1,9,0,52,8,0,2,1,10,0,52,7,0,2,1,11,0,16,1,1,12,0,52,8,0,2,1,13,0,52,7,0,2,52,3,0,6,50],"constants":[{"t":"s","v":"nil?"},{"t":"s","v":"split"},{"t":"s","v":":"},{"t":"s","v":"dict"},{"t":"s","v":"strategy"},{"t":"s","v":"first"},{"t":"s","v":"start-ms"},{"t":"s","v":"parse-int"},{"t":"s","v":"nth"},{"t":"n","v":1},{"t":"n","v":1000},{"t":"s","v":"cap-ms"},{"t":"n","v":2},{"t":"n","v":30000}],"arity":1}},{"t":"s","v":"next-retry-ms"},{"t":"code","v":{"bytecode":[16,0,1,2,0,52,1,0,2,16,1,52,0,0,2,50],"constants":[{"t":"s","v":"min"},{"t":"s","v":"*"},{"t":"n","v":2}],"arity":2}},{"t":"s","v":"filter-params"},{"t":"code","v":{"bytecode":[16,0,52,0,0,1,33,5,0,16,1,32,116,0,16,0,1,2,0,52,1,0,2,33,7,0,52,3,0,0,32,97,0,16,0,1,4,0,52,1,0,2,33,5,0,16,1,32,80,0,16,0,1,6,0,52,5,0,2,33,39,0,20,8,0,16,0,1,11,0,52,10,0,2,1,12,0,52,9,0,2,52,7,0,2,17,2,51,14,0,1,2,16,1,52,13,0,2,32,29,0,20,8,0,16,0,1,12,0,52,9,0,2,52,7,0,2,17,2,51,15,0,1,2,16,1,52,13,0,2,50],"constants":[{"t":"s","v":"nil?"},{"t":"s","v":"="},{"t":"s","v":"none"},{"t":"s","v":"list"},{"t":"s","v":"*"},{"t":"s","v":"starts-with?"},{"t":"s","v":"not "},{"t":"s","v":"map"},{"t":"s","v":"trim"},{"t":"s","v":"split"},{"t":"s","v":"slice"},{"t":"n","v":4},{"t":"s","v":","},{"t":"s","v":"filter"},{"t":"code","v":{"bytecode":[18,0,16,0,52,2,0,1,52,1,0,2,52,0,0,1,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"contains?"},{"t":"s","v":"first"}],"arity":1,"upvalue-count":1}},{"t":"code","v":{"bytecode":[18,0,16,0,52,1,0,1,52,0,0,2,50],"constants":[{"t":"s","v":"contains?"},{"t":"s","v":"first"}],"arity":1,"upvalue-count":1}}],"arity":2}},{"t":"s","v":"resolve-target"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,16,1,52,2,0,1,6,34,10,0,5,16,1,1,4,0,52,3,0,2,33,5,0,16,0,32,29,0,16,1,1,5,0,52,3,0,2,33,10,0,20,6,0,16,0,49,1,32,7,0,20,7,0,16,1,49,1,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"sx-target"},{"t":"s","v":"nil?"},{"t":"s","v":"="},{"t":"s","v":"this"},{"t":"s","v":"closest"},{"t":"s","v":"dom-parent"},{"t":"s","v":"dom-query"}],"arity":1}},{"t":"s","v":"apply-optimistic"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,16,1,52,2,0,1,33,4,0,2,32,191,0,20,3,0,16,0,48,1,6,34,3,0,5,16,0,17,2,1,5,0,16,2,1,6,0,16,1,52,4,0,4,17,3,16,1,1,8,0,52,7,0,2,33,50,0,16,3,1,10,0,20,11,0,16,2,1,10,0,48,2,52,9,0,3,5,20,12,0,16,2,1,10,0,1,13,0,48,3,5,20,12,0,16,2,1,14,0,1,15,0,48,3,32,94,0,16,1,1,16,0,52,7,0,2,33,34,0,16,3,1,17,0,20,18,0,16,2,1,17,0,48,2,52,9,0,3,5,20,19,0,16,2,1,17,0,3,48,3,32,48,0,16,1,1,21,0,52,20,0,2,33,35,0,16,1,1,23,0,52,22,0,2,17,4,16,3,1,24,0,16,4,52,9,0,3,5,20,25,0,16,2,16,4,48,2,32,1,0,2,5,16,3,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"sx-optimistic"},{"t":"s","v":"nil?"},{"t":"s","v":"resolve-target"},{"t":"s","v":"dict"},{"t":"s","v":"target"},{"t":"s","v":"directive"},{"t":"s","v":"="},{"t":"s","v":"remove"},{"t":"s","v":"dict-set!"},{"t":"s","v":"opacity"},{"t":"s","v":"dom-get-style"},{"t":"s","v":"dom-set-style"},{"t":"s","v":"0"},{"t":"s","v":"pointer-events"},{"t":"s","v":"none"},{"t":"s","v":"disable"},{"t":"s","v":"disabled"},{"t":"s","v":"dom-get-prop"},{"t":"s","v":"dom-set-prop"},{"t":"s","v":"starts-with?"},{"t":"s","v":"add-class:"},{"t":"s","v":"slice"},{"t":"n","v":10},{"t":"s","v":"add-class"},{"t":"s","v":"dom-add-class"}],"arity":1}},{"t":"s","v":"revert-optimistic"},{"t":"code","v":{"bytecode":[16,0,33,153,0,16,0,1,1,0,52,0,0,2,17,1,16,0,1,2,0,52,0,0,2,17,2,16,2,1,4,0,52,3,0,2,33,44,0,20,5,0,16,1,1,6,0,16,0,1,6,0,52,0,0,2,6,34,4,0,5,1,7,0,48,3,5,20,5,0,16,1,1,8,0,1,7,0,49,3,32,72,0,16,2,1,9,0,52,3,0,2,33,28,0,20,10,0,16,1,1,11,0,16,0,1,11,0,52,0,0,2,6,34,2,0,5,4,49,3,32,32,0,16,0,1,12,0,52,0,0,2,33,19,0,20,13,0,16,1,16,0,1,12,0,52,0,0,2,49,2,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"get"},{"t":"s","v":"target"},{"t":"s","v":"directive"},{"t":"s","v":"="},{"t":"s","v":"remove"},{"t":"s","v":"dom-set-style"},{"t":"s","v":"opacity"},{"t":"s","v":""},{"t":"s","v":"pointer-events"},{"t":"s","v":"disable"},{"t":"s","v":"dom-set-prop"},{"t":"s","v":"disabled"},{"t":"s","v":"add-class"},{"t":"s","v":"dom-remove-class"}],"arity":1}},{"t":"s","v":"find-oob-swaps"},{"t":"code","v":{"bytecode":[52,0,0,0,17,1,51,2,0,1,0,1,1,1,3,0,1,4,0,52,0,0,2,52,1,0,2,5,16,1,50],"constants":[{"t":"s","v":"list"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,18,0,1,2,0,16,0,1,3,0,52,1,0,3,48,2,17,1,51,5,0,1,0,0,1,16,1,52,4,0,2,50],"constants":[{"t":"s","v":"dom-query-all"},{"t":"s","v":"str"},{"t":"s","v":"["},{"t":"s","v":"]"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,16,0,18,0,48,2,6,34,4,0,5,1,1,0,17,1,20,2,0,16,0,48,1,17,2,20,3,0,16,0,18,0,48,2,5,16,2,33,29,0,20,4,0,18,1,1,6,0,16,0,1,7,0,16,1,1,8,0,16,2,52,5,0,6,49,2,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"outerHTML"},{"t":"s","v":"dom-id"},{"t":"s","v":"dom-remove-attr"},{"t":"s","v":"append!"},{"t":"s","v":"dict"},{"t":"s","v":"element"},{"t":"s","v":"swap-type"},{"t":"s","v":"target-id"}],"arity":1,"upvalue-count":2}}],"arity":1,"upvalue-count":2}},{"t":"s","v":"sx-swap-oob"},{"t":"s","v":"hx-swap-oob"}],"arity":1}},{"t":"s","v":"morph-node"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,6,34,11,0,5,20,0,0,16,0,1,2,0,48,2,33,4,0,2,32,137,1,20,0,0,16,0,1,3,0,48,2,6,33,55,0,5,20,4,0,16,0,1,5,0,48,2,6,33,40,0,5,20,0,0,16,1,1,3,0,48,2,6,33,25,0,5,20,7,0,16,0,1,3,0,48,2,20,7,0,16,1,1,3,0,48,2,52,6,0,2,33,12,0,20,8,0,16,0,16,1,49,2,32,53,1,20,10,0,16,0,48,1,20,10,0,16,1,48,1,52,6,0,2,52,9,0,1,6,34,23,0,5,20,11,0,16,0,48,1,20,11,0,16,1,48,1,52,6,0,2,52,9,0,1,33,24,0,20,12,0,20,13,0,16,0,48,1,20,14,0,16,1,48,1,16,0,49,3,32,233,0,20,10,0,16,0,48,1,1,15,0,52,6,0,2,6,34,15,0,5,20,10,0,16,0,48,1,1,16,0,52,6,0,2,33,46,0,20,17,0,16,0,48,1,20,17,0,16,1,48,1,52,6,0,2,52,9,0,1,33,17,0,20,18,0,16,0,20,17,0,16,1,48,1,49,2,32,1,0,2,32,151,0,20,10,0,16,0,48,1,1,19,0,52,6,0,2,33,133,0,20,0,0,16,0,1,3,0,48,2,6,33,44,0,5,20,0,0,16,1,1,3,0,48,2,6,33,29,0,5,20,7,0,16,0,1,3,0,48,2,20,7,0,16,1,1,3,0,48,2,52,6,0,2,52,9,0,1,33,18,0,20,20,0,16,0,48,1,5,20,21,0,16,0,48,1,32,1,0,2,5,20,22,0,16,0,16,1,48,2,5,20,23,0,16,0,48,1,6,33,8,0,5,20,24,0,16,0,48,1,52,9,0,1,33,12,0,20,25,0,16,0,16,1,49,2,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"dom-has-attr?"},{"t":"s","v":"sx-preserve"},{"t":"s","v":"sx-ignore"},{"t":"s","v":"data-sx-island"},{"t":"s","v":"is-processed?"},{"t":"s","v":"island-hydrated"},{"t":"s","v":"="},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"morph-island-children"},{"t":"s","v":"not"},{"t":"s","v":"dom-node-type"},{"t":"s","v":"dom-node-name"},{"t":"s","v":"dom-replace-child"},{"t":"s","v":"dom-parent"},{"t":"s","v":"dom-clone"},{"t":"n","v":3},{"t":"n","v":8},{"t":"s","v":"dom-text-content"},{"t":"s","v":"dom-set-text-content"},{"t":"n","v":1},{"t":"s","v":"dispose-island"},{"t":"s","v":"dispose-islands-in"},{"t":"s","v":"sync-attrs"},{"t":"s","v":"dom-is-active-element?"},{"t":"s","v":"dom-is-input-element?"},{"t":"s","v":"morph-children"}],"arity":2}},{"t":"s","v":"sync-attrs"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,6,34,4,0,5,1,2,0,17,2,16,2,52,3,0,1,33,7,0,52,4,0,0,32,9,0,16,2,1,6,0,52,5,0,2,17,3,51,8,0,1,0,1,3,20,9,0,16,1,48,1,52,7,0,2,5,51,10,0,1,1,1,3,1,0,20,9,0,16,0,48,1,52,7,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx-reactive-attrs"},{"t":"s","v":""},{"t":"s","v":"empty?"},{"t":"s","v":"list"},{"t":"s","v":"split"},{"t":"s","v":","},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[16,0,52,0,0,1,17,1,16,0,1,2,0,52,1,0,2,17,2,20,5,0,18,0,16,1,48,2,16,2,52,4,0,2,52,3,0,1,6,33,13,0,5,18,1,16,1,52,6,0,2,52,3,0,1,33,14,0,20,7,0,18,0,16,1,16,2,49,3,32,1,0,2,50],"constants":[{"t":"s","v":"first"},{"t":"s","v":"nth"},{"t":"n","v":1},{"t":"s","v":"not"},{"t":"s","v":"="},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"contains?"},{"t":"s","v":"dom-set-attr"}],"arity":1,"upvalue-count":2}},{"t":"s","v":"dom-attr-list"},{"t":"code","v":{"bytecode":[16,0,52,0,0,1,17,1,20,2,0,18,0,16,1,48,2,52,1,0,1,6,33,31,0,5,18,1,16,1,52,3,0,2,52,1,0,1,6,33,14,0,5,16,1,1,5,0,52,4,0,2,52,1,0,1,33,12,0,20,6,0,18,2,16,1,49,2,32,1,0,2,50],"constants":[{"t":"s","v":"first"},{"t":"s","v":"not"},{"t":"s","v":"dom-has-attr?"},{"t":"s","v":"contains?"},{"t":"s","v":"="},{"t":"s","v":"data-sx-reactive-attrs"},{"t":"s","v":"dom-remove-attr"}],"arity":1,"upvalue-count":3}}],"arity":2}},{"t":"s","v":"morph-children"},{"t":"code","v":{"bytecode":[20,0,0,16,0,48,1,17,2,20,0,0,16,1,48,1,17,3,51,2,0,52,3,0,0,16,2,52,1,0,3,17,4,1,4,0,17,5,51,6,0,1,4,1,5,1,2,1,0,16,3,52,5,0,2,5,51,7,0,1,5,1,2,1,0,16,5,16,2,52,9,0,1,52,8,0,2,52,5,0,2,50],"constants":[{"t":"s","v":"dom-child-list"},{"t":"s","v":"reduce"},{"t":"code","v":{"bytecode":[20,0,0,16,1,48,1,17,2,16,2,33,16,0,16,0,16,2,16,1,52,1,0,3,5,16,0,32,2,0,16,0,50],"constants":[{"t":"s","v":"dom-id"},{"t":"s","v":"dict-set!"}],"arity":2}},{"t":"s","v":"dict"},{"t":"n","v":0},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,16,0,48,1,17,1,16,1,33,11,0,18,0,16,1,52,1,0,2,32,1,0,2,17,2,16,2,6,33,11,0,5,16,2,52,3,0,1,52,2,0,1,33,100,0,18,1,18,2,52,5,0,1,52,4,0,2,6,33,19,0,5,16,2,18,2,18,1,52,7,0,2,52,6,0,2,52,2,0,1,33,39,0,20,8,0,18,3,16,2,18,1,18,2,52,5,0,1,52,4,0,2,33,11,0,18,2,18,1,52,7,0,2,32,1,0,2,48,3,32,1,0,2,5,20,9,0,16,2,16,0,48,2,5,18,1,52,10,0,1,19,1,32,100,0,18,1,18,2,52,5,0,1,52,4,0,2,33,71,0,18,2,18,1,52,7,0,2,17,3,20,0,0,16,3,48,1,6,33,7,0,5,16,1,52,2,0,1,33,19,0,20,8,0,18,3,20,11,0,16,0,48,1,16,3,49,3,32,18,0,20,9,0,16,3,16,0,48,2,5,18,1,52,10,0,1,19,1,32,14,0,20,12,0,18,3,20,11,0,16,0,48,1,49,2,50],"constants":[{"t":"s","v":"dom-id"},{"t":"s","v":"dict-get"},{"t":"s","v":"not"},{"t":"s","v":"nil?"},{"t":"s","v":"<"},{"t":"s","v":"len"},{"t":"s","v":"="},{"t":"s","v":"nth"},{"t":"s","v":"dom-insert-before"},{"t":"s","v":"morph-node"},{"t":"s","v":"inc"},{"t":"s","v":"dom-clone"},{"t":"s","v":"dom-append"}],"arity":1,"upvalue-count":4}},{"t":"code","v":{"bytecode":[16,0,18,0,52,0,0,2,33,76,0,18,1,16,0,52,1,0,2,17,1,20,2,0,16,1,18,2,48,2,6,33,34,0,5,20,4,0,16,1,1,5,0,48,2,52,3,0,1,6,33,15,0,5,20,4,0,16,1,1,6,0,48,2,52,3,0,1,33,12,0,20,7,0,18,2,16,1,49,2,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":">="},{"t":"s","v":"nth"},{"t":"s","v":"dom-is-child-of?"},{"t":"s","v":"not"},{"t":"s","v":"dom-has-attr?"},{"t":"s","v":"sx-preserve"},{"t":"s","v":"sx-ignore"},{"t":"s","v":"dom-remove-child"}],"arity":1,"upvalue-count":3}},{"t":"s","v":"range"},{"t":"s","v":"len"}],"arity":2}},{"t":"s","v":"morph-island-children"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,2,20,0,0,16,1,1,1,0,48,2,17,3,20,0,0,16,0,1,2,0,48,2,17,4,20,0,0,16,1,1,2,0,48,2,17,5,52,3,0,0,17,6,52,3,0,0,17,7,51,5,0,1,6,16,3,52,4,0,2,5,51,6,0,1,7,16,5,52,4,0,2,5,51,7,0,1,6,16,2,52,4,0,2,5,51,8,0,1,7,1,0,16,4,52,4,0,2,5,20,9,0,16,1,49,1,50],"constants":[{"t":"s","v":"dom-query-all"},{"t":"s","v":"[data-sx-lake]"},{"t":"s","v":"[data-sx-marsh]"},{"t":"s","v":"dict"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,16,1,33,13,0,18,0,16,1,16,0,52,2,0,3,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx-lake"},{"t":"s","v":"dict-set!"}],"arity":1,"upvalue-count":1}},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,16,1,33,13,0,18,0,16,1,16,0,52,2,0,3,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx-marsh"},{"t":"s","v":"dict-set!"}],"arity":1,"upvalue-count":1}},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,18,0,16,1,52,2,0,2,17,2,16,2,33,22,0,20,3,0,16,0,16,2,48,2,5,20,4,0,16,0,16,2,49,2,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx-lake"},{"t":"s","v":"dict-get"},{"t":"s","v":"sync-attrs"},{"t":"s","v":"morph-children"}],"arity":1,"upvalue-count":1}},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,18,0,16,1,52,2,0,2,17,2,16,2,33,14,0,20,3,0,16,0,16,2,18,1,49,3,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx-marsh"},{"t":"s","v":"dict-get"},{"t":"s","v":"morph-marsh"}],"arity":1,"upvalue-count":2}},{"t":"s","v":"process-signal-updates"}],"arity":2}},{"t":"s","v":"morph-marsh"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,3,20,0,0,16,0,1,2,0,48,2,17,4,20,3,0,16,1,48,1,17,5,16,4,6,33,18,0,5,16,5,6,33,11,0,5,16,5,52,5,0,1,52,4,0,1,33,61,0,20,6,0,16,5,48,1,17,6,16,3,33,16,0,20,7,0,16,3,16,6,52,8,0,1,48,2,32,2,0,16,6,17,7,20,9,0,16,0,48,1,5,20,10,0,16,0,51,11,0,1,7,1,4,1,0,49,2,32,19,0,20,12,0,16,0,16,1,48,2,5,20,13,0,16,0,16,1,49,2,50],"constants":[{"t":"s","v":"dom-get-data"},{"t":"s","v":"sx-marsh-transform"},{"t":"s","v":"sx-marsh-env"},{"t":"s","v":"dom-inner-html"},{"t":"s","v":"not"},{"t":"s","v":"empty?"},{"t":"s","v":"parse"},{"t":"s","v":"cek-call"},{"t":"s","v":"list"},{"t":"s","v":"dispose-marsh-scope"},{"t":"s","v":"with-marsh-scope"},{"t":"code","v":{"bytecode":[20,0,0,18,0,18,1,2,48,3,17,0,20,1,0,18,2,2,48,2,5,20,2,0,18,2,16,0,49,2,50],"constants":[{"t":"s","v":"render-to-dom"},{"t":"s","v":"dom-remove-children-after"},{"t":"s","v":"dom-append"}],"upvalue-count":3}},{"t":"s","v":"sync-attrs"},{"t":"s","v":"morph-children"}],"arity":3}},{"t":"s","v":"process-signal-updates"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,51,3,0,16,1,52,2,0,2,50],"constants":[{"t":"s","v":"dom-query-all"},{"t":"s","v":"[data-sx-signal]"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,16,1,33,94,0,16,1,1,3,0,52,2,0,2,17,2,16,2,1,5,0,52,4,0,2,33,67,0,16,1,1,5,0,16,2,52,6,0,3,17,3,16,1,16,2,1,8,0,52,7,0,2,52,6,0,2,17,4,20,9,0,16,4,48,1,17,5,20,10,0,20,11,0,16,3,48,1,16,5,48,2,5,20,12,0,16,0,1,1,0,49,2,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx-signal"},{"t":"s","v":"index-of"},{"t":"s","v":":"},{"t":"s","v":">"},{"t":"n","v":0},{"t":"s","v":"slice"},{"t":"s","v":"+"},{"t":"n","v":1},{"t":"s","v":"json-parse"},{"t":"s","v":"reset!"},{"t":"s","v":"use-store"},{"t":"s","v":"dom-remove-attr"}],"arity":1}}],"arity":1}},{"t":"s","v":"swap-dom-nodes"},{"t":"code","v":{"bytecode":[16,2,6,1,0,0,52,1,0,2,33,56,0,5,20,2,0,16,1,48,1,33,12,0,20,3,0,16,0,16,1,49,2,32,30,0,20,4,0,1,5,0,2,48,2,17,3,20,6,0,16,3,16,1,48,2,5,20,3,0,16,0,16,3,49,2,32,73,1,6,1,7,0,52,1,0,2,33,117,0,5,20,8,0,16,0,48,1,17,3,20,9,0,16,1,48,1,17,4,20,2,0,16,1,48,1,33,71,0,20,10,0,16,1,48,1,17,5,16,5,33,45,0,20,9,0,16,5,48,1,17,4,5,20,11,0,16,3,16,4,16,0,48,3,5,20,12,0,16,5,48,1,17,6,20,13,0,16,3,16,4,16,6,48,3,32,9,0,20,14,0,16,3,16,0,48,2,32,11,0,20,11,0,16,3,16,4,16,0,48,3,5,16,4,32,201,0,6,1,15,0,52,1,0,2,33,13,0,5,20,16,0,16,0,16,1,49,2,32,177,0,6,1,17,0,52,1,0,2,33,13,0,5,20,6,0,16,0,16,1,49,2,32,153,0,6,1,18,0,52,1,0,2,33,13,0,5,20,19,0,16,0,16,1,49,2,32,129,0,6,1,20,0,52,1,0,2,33,20,0,5,20,21,0,20,8,0,16,0,48,1,16,1,16,0,49,3,32,98,0,6,1,22,0,52,1,0,2,33,18,0,5,20,14,0,20,8,0,16,0,48,1,16,0,49,2,32,69,0,6,1,23,0,52,1,0,2,33,5,0,5,2,32,53,0,5,20,2,0,16,1,48,1,33,12,0,20,3,0,16,0,16,1,49,2,32,30,0,20,4,0,1,5,0,2,48,2,17,3,20,6,0,16,3,16,1,48,2,5,20,3,0,16,0,16,3,49,2,50],"constants":[{"t":"s","v":"innerHTML"},{"t":"s","v":"="},{"t":"s","v":"dom-is-fragment?"},{"t":"s","v":"morph-children"},{"t":"s","v":"dom-create-element"},{"t":"s","v":"div"},{"t":"s","v":"dom-append"},{"t":"s","v":"outerHTML"},{"t":"s","v":"dom-parent"},{"t":"s","v":"dom-clone"},{"t":"s","v":"dom-first-child"},{"t":"s","v":"dom-replace-child"},{"t":"s","v":"dom-next-sibling"},{"t":"s","v":"insert-remaining-siblings"},{"t":"s","v":"dom-remove-child"},{"t":"s","v":"afterend"},{"t":"s","v":"dom-insert-after"},{"t":"s","v":"beforeend"},{"t":"s","v":"afterbegin"},{"t":"s","v":"dom-prepend"},{"t":"s","v":"beforebegin"},{"t":"s","v":"dom-insert-before"},{"t":"s","v":"delete"},{"t":"s","v":"none"}],"arity":3}},{"t":"s","v":"insert-remaining-siblings"},{"t":"code","v":{"bytecode":[16,2,33,33,0,20,0,0,16,2,48,1,17,3,20,1,0,16,1,16,2,48,2,5,20,2,0,16,0,16,2,16,3,49,3,32,1,0,2,50],"constants":[{"t":"s","v":"dom-next-sibling"},{"t":"s","v":"dom-insert-after"},{"t":"s","v":"insert-remaining-siblings"}],"arity":3}},{"t":"s","v":"swap-html-string"},{"t":"code","v":{"bytecode":[16,2,6,1,0,0,52,1,0,2,33,13,0,5,20,2,0,16,0,16,1,49,2,32,212,0,6,1,3,0,52,1,0,2,33,38,0,5,20,4,0,16,0,48,1,17,3,20,5,0,16,0,1,6,0,16,1,48,3,5,20,7,0,16,3,16,0,48,2,5,16,3,32,163,0,6,1,6,0,52,1,0,2,33,16,0,5,20,5,0,16,0,1,6,0,16,1,49,3,32,136,0,6,1,8,0,52,1,0,2,33,16,0,5,20,5,0,16,0,1,8,0,16,1,49,3,32,109,0,6,1,9,0,52,1,0,2,33,16,0,5,20,5,0,16,0,1,9,0,16,1,49,3,32,82,0,6,1,10,0,52,1,0,2,33,16,0,5,20,5,0,16,0,1,10,0,16,1,49,3,32,55,0,6,1,11,0,52,1,0,2,33,18,0,5,20,7,0,20,4,0,16,0,48,1,16,0,49,2,32,26,0,6,1,12,0,52,1,0,2,33,5,0,5,2,32,10,0,5,20,2,0,16,0,16,1,49,2,50],"constants":[{"t":"s","v":"innerHTML"},{"t":"s","v":"="},{"t":"s","v":"dom-set-inner-html"},{"t":"s","v":"outerHTML"},{"t":"s","v":"dom-parent"},{"t":"s","v":"dom-insert-adjacent-html"},{"t":"s","v":"afterend"},{"t":"s","v":"dom-remove-child"},{"t":"s","v":"beforeend"},{"t":"s","v":"afterbegin"},{"t":"s","v":"beforebegin"},{"t":"s","v":"delete"},{"t":"s","v":"none"}],"arity":3}},{"t":"s","v":"handle-history"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,3,20,0,0,16,0,1,2,0,48,2,17,4,16,2,1,4,0,52,3,0,2,17,5,16,5,33,10,0,20,5,0,16,5,49,1,32,101,0,16,3,6,33,14,0,5,16,3,1,8,0,52,7,0,2,52,6,0,1,33,27,0,20,9,0,16,3,1,10,0,52,7,0,2,33,5,0,16,1,32,2,0,16,3,49,1,32,51,0,16,4,6,33,14,0,5,16,4,1,8,0,52,7,0,2,52,6,0,1,33,27,0,20,5,0,16,4,1,10,0,52,7,0,2,33,5,0,16,1,32,2,0,16,4,49,1,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"sx-push-url"},{"t":"s","v":"sx-replace-url"},{"t":"s","v":"get"},{"t":"s","v":"replace-url"},{"t":"s","v":"browser-replace-state"},{"t":"s","v":"not"},{"t":"s","v":"="},{"t":"s","v":"false"},{"t":"s","v":"browser-push-state"},{"t":"s","v":"true"}],"arity":3}},{"t":"s","v":"PRELOAD_TTL"},{"t":"n","v":30000},{"t":"s","v":"preload-cache-get"},{"t":"code","v":{"bytecode":[16,0,16,1,52,0,0,2,17,2,16,2,52,1,0,1,33,4,0,2,32,52,0,20,4,0,48,0,16,2,1,6,0,52,5,0,2,52,3,0,2,20,7,0,52,2,0,2,33,13,0,16,0,16,1,52,8,0,2,5,2,32,11,0,16,0,16,1,52,8,0,2,5,16,2,50],"constants":[{"t":"s","v":"dict-get"},{"t":"s","v":"nil?"},{"t":"s","v":">"},{"t":"s","v":"-"},{"t":"s","v":"now-ms"},{"t":"s","v":"get"},{"t":"s","v":"timestamp"},{"t":"s","v":"PRELOAD_TTL"},{"t":"s","v":"dict-delete!"}],"arity":2}},{"t":"s","v":"preload-cache-set"},{"t":"code","v":{"bytecode":[16,0,16,1,1,2,0,16,2,1,3,0,16,3,1,4,0,20,5,0,48,0,52,1,0,6,52,0,0,3,50],"constants":[{"t":"s","v":"dict-set!"},{"t":"s","v":"dict"},{"t":"s","v":"text"},{"t":"s","v":"content-type"},{"t":"s","v":"timestamp"},{"t":"s","v":"now-ms"}],"arity":4}},{"t":"s","v":"classify-trigger"},{"t":"code","v":{"bytecode":[16,0,1,1,0,52,0,0,2,17,1,16,1,1,3,0,52,2,0,2,33,6,0,1,4,0,32,57,0,16,1,1,5,0,52,2,0,2,33,6,0,1,5,0,32,39,0,16,1,1,6,0,52,2,0,2,33,6,0,1,6,0,32,21,0,16,1,1,7,0,52,2,0,2,33,6,0,1,7,0,32,3,0,1,1,0,50],"constants":[{"t":"s","v":"get"},{"t":"s","v":"event"},{"t":"s","v":"="},{"t":"s","v":"every"},{"t":"s","v":"poll"},{"t":"s","v":"intersect"},{"t":"s","v":"load"},{"t":"s","v":"revealed"}],"arity":1}},{"t":"s","v":"should-boost-link?"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,16,1,6,33,119,0,5,16,1,1,4,0,52,3,0,2,52,2,0,1,6,33,101,0,5,16,1,1,5,0,52,3,0,2,52,2,0,1,6,33,83,0,5,16,1,1,6,0,52,3,0,2,52,2,0,1,6,33,65,0,5,20,7,0,16,1,48,1,6,33,53,0,5,20,8,0,16,0,1,9,0,48,2,52,2,0,1,6,33,34,0,5,20,8,0,16,0,1,10,0,48,2,52,2,0,1,6,33,15,0,5,20,8,0,16,0,1,11,0,48,2,52,2,0,1,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"href"},{"t":"s","v":"not"},{"t":"s","v":"starts-with?"},{"t":"s","v":"#"},{"t":"s","v":"javascript:"},{"t":"s","v":"mailto:"},{"t":"s","v":"browser-same-origin?"},{"t":"s","v":"dom-has-attr?"},{"t":"s","v":"sx-get"},{"t":"s","v":"sx-post"},{"t":"s","v":"sx-disable"}],"arity":1}},{"t":"s","v":"should-boost-form?"},{"t":"code","v":{"bytecode":[20,1,0,16,0,1,2,0,48,2,52,0,0,1,6,33,34,0,5,20,1,0,16,0,1,3,0,48,2,52,0,0,1,6,33,15,0,5,20,1,0,16,0,1,4,0,48,2,52,0,0,1,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"dom-has-attr?"},{"t":"s","v":"sx-get"},{"t":"s","v":"sx-post"},{"t":"s","v":"sx-disable"}],"arity":1}},{"t":"s","v":"parse-sse-swap"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,6,34,4,0,5,1,2,0,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"sx-sse-swap"},{"t":"s","v":"message"}],"arity":1}}]}} \ No newline at end of file +{"magic":"SXBC","version":1,"hash":"0d3ce0ff69929e45","module":{"bytecode":[1,2,0,1,3,0,1,4,0,1,5,0,1,6,0,52,1,0,5,128,0,0,5,1,8,0,128,7,0,5,51,10,0,128,9,0,5,51,12,0,128,11,0,5,51,14,0,128,13,0,5,51,16,0,128,15,0,5,51,18,0,128,17,0,5,51,20,0,128,19,0,5,51,22,0,128,21,0,5,51,24,0,128,23,0,5,51,26,0,128,25,0,5,51,28,0,128,27,0,5,51,30,0,128,29,0,5,51,32,0,128,31,0,5,51,34,0,128,33,0,5,51,36,0,128,35,0,5,51,38,0,128,37,0,5,51,40,0,128,39,0,5,51,42,0,128,41,0,5,51,44,0,128,43,0,5,51,46,0,128,45,0,5,51,48,0,128,47,0,5,51,50,0,128,49,0,5,51,52,0,128,51,0,5,51,54,0,128,53,0,5,51,56,0,128,55,0,5,1,58,0,128,57,0,5,51,60,0,128,59,0,5,51,62,0,128,61,0,5,51,64,0,128,63,0,5,51,66,0,128,65,0,5,51,68,0,128,67,0,5,51,70,0,128,69,0,50],"constants":[{"t":"s","v":"ENGINE_VERBS"},{"t":"s","v":"list"},{"t":"s","v":"get"},{"t":"s","v":"post"},{"t":"s","v":"put"},{"t":"s","v":"delete"},{"t":"s","v":"patch"},{"t":"s","v":"DEFAULT_SWAP"},{"t":"s","v":"outerHTML"},{"t":"s","v":"parse-time"},{"t":"code","v":{"bytecode":[16,0,52,0,0,1,33,6,0,1,1,0,32,74,0,16,0,1,3,0,52,2,0,2,33,12,0,16,0,1,1,0,52,4,0,2,32,50,0,16,0,1,5,0,52,2,0,2,33,29,0,16,0,1,5,0,1,8,0,52,7,0,3,1,1,0,52,4,0,2,1,9,0,52,6,0,2,32,9,0,16,0,1,1,0,52,4,0,2,50],"constants":[{"t":"s","v":"nil?"},{"t":"n","v":0},{"t":"s","v":"ends-with?"},{"t":"s","v":"ms"},{"t":"s","v":"parse-int"},{"t":"s","v":"s"},{"t":"s","v":"*"},{"t":"s","v":"replace"},{"t":"s","v":""},{"t":"n","v":1000}],"arity":1}},{"t":"s","v":"parse-trigger-spec"},{"t":"code","v":{"bytecode":[16,0,52,0,0,1,33,4,0,2,32,27,0,16,0,1,2,0,52,1,0,2,17,1,51,4,0,51,6,0,16,1,52,5,0,2,52,3,0,2,50],"constants":[{"t":"s","v":"nil?"},{"t":"s","v":"split"},{"t":"s","v":","},{"t":"s","v":"filter"},{"t":"code","v":{"bytecode":[16,0,52,1,0,1,52,0,0,1,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"nil?"}],"arity":1}},{"t":"s","v":"map"},{"t":"code","v":{"bytecode":[16,0,52,1,0,1,1,2,0,52,0,0,2,17,1,16,1,52,3,0,1,33,4,0,2,32,111,0,16,1,52,5,0,1,1,6,0,52,4,0,2,6,33,14,0,5,16,1,52,8,0,1,1,9,0,52,7,0,2,33,37,0,1,11,0,1,6,0,1,12,0,1,13,0,20,14,0,16,1,1,16,0,52,15,0,2,48,1,52,10,0,2,52,10,0,4,32,40,0,52,10,0,0,17,2,51,18,0,1,2,16,1,52,19,0,1,52,17,0,2,5,1,11,0,16,1,52,5,0,1,1,12,0,16,2,52,10,0,4,50],"constants":[{"t":"s","v":"split"},{"t":"s","v":"trim"},{"t":"s","v":" "},{"t":"s","v":"empty?"},{"t":"s","v":"="},{"t":"s","v":"first"},{"t":"s","v":"every"},{"t":"s","v":">="},{"t":"s","v":"len"},{"t":"n","v":2},{"t":"s","v":"dict"},{"t":"s","v":"event"},{"t":"s","v":"modifiers"},{"t":"s","v":"interval"},{"t":"s","v":"parse-time"},{"t":"s","v":"nth"},{"t":"n","v":1},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[16,0,1,1,0,52,0,0,2,33,13,0,18,0,1,1,0,3,52,2,0,3,32,97,0,16,0,1,3,0,52,0,0,2,33,13,0,18,0,1,3,0,3,52,2,0,3,32,72,0,16,0,1,5,0,52,4,0,2,33,26,0,18,0,1,6,0,20,7,0,16,0,1,9,0,52,8,0,2,48,1,52,2,0,3,32,34,0,16,0,1,10,0,52,4,0,2,33,21,0,18,0,1,11,0,16,0,1,12,0,52,8,0,2,52,2,0,3,32,1,0,2,50],"constants":[{"t":"s","v":"="},{"t":"s","v":"once"},{"t":"s","v":"dict-set!"},{"t":"s","v":"changed"},{"t":"s","v":"starts-with?"},{"t":"s","v":"delay:"},{"t":"s","v":"delay"},{"t":"s","v":"parse-time"},{"t":"s","v":"slice"},{"t":"n","v":6},{"t":"s","v":"from:"},{"t":"s","v":"from"},{"t":"n","v":5}],"arity":1,"upvalue-count":1}},{"t":"s","v":"rest"}],"arity":1}}],"arity":1}},{"t":"s","v":"default-trigger"},{"t":"code","v":{"bytecode":[16,0,1,1,0,52,0,0,2,33,24,0,1,4,0,1,5,0,1,6,0,52,3,0,0,52,3,0,4,52,2,0,1,32,85,0,16,0,1,7,0,52,0,0,2,6,34,24,0,5,16,0,1,8,0,52,0,0,2,6,34,10,0,5,16,0,1,9,0,52,0,0,2,33,24,0,1,4,0,1,10,0,1,6,0,52,3,0,0,52,3,0,4,52,2,0,1,32,21,0,1,4,0,1,11,0,1,6,0,52,3,0,0,52,3,0,4,52,2,0,1,50],"constants":[{"t":"s","v":"="},{"t":"s","v":"FORM"},{"t":"s","v":"list"},{"t":"s","v":"dict"},{"t":"s","v":"event"},{"t":"s","v":"submit"},{"t":"s","v":"modifiers"},{"t":"s","v":"INPUT"},{"t":"s","v":"SELECT"},{"t":"s","v":"TEXTAREA"},{"t":"s","v":"change"},{"t":"s","v":"click"}],"arity":1}},{"t":"s","v":"get-verb-info"},{"t":"code","v":{"bytecode":[2,17,1,51,1,0,1,1,1,0,20,2,0,52,0,0,2,5,16,1,50],"constants":[{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[18,0,52,0,0,1,33,50,0,20,1,0,18,1,1,3,0,16,0,52,2,0,2,48,2,17,1,16,1,33,23,0,1,5,0,16,0,52,6,0,1,1,7,0,16,1,52,4,0,4,19,0,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"str"},{"t":"s","v":"sx-"},{"t":"s","v":"dict"},{"t":"s","v":"method"},{"t":"s","v":"upper"},{"t":"s","v":"url"}],"arity":1,"upvalue-count":2}},{"t":"s","v":"ENGINE_VERBS"}],"arity":1}},{"t":"s","v":"build-request-headers"},{"t":"code","v":{"bytecode":[1,1,0,1,2,0,1,3,0,20,4,0,48,0,52,0,0,4,17,3,20,5,0,16,0,1,6,0,48,2,17,4,16,4,33,14,0,16,3,1,8,0,16,4,52,7,0,3,32,1,0,2,5,20,5,0,20,9,0,1,10,0,48,1,1,11,0,48,2,17,4,16,4,33,14,0,16,3,1,12,0,16,4,52,7,0,3,32,1,0,2,5,16,2,33,14,0,16,3,1,13,0,16,2,52,7,0,3,32,1,0,2,5,20,5,0,16,0,1,14,0,48,2,17,4,16,4,33,38,0,20,15,0,16,4,48,1,17,5,16,5,33,20,0,51,17,0,1,3,1,5,16,5,52,18,0,1,52,16,0,2,32,1,0,2,32,1,0,2,5,16,3,50],"constants":[{"t":"s","v":"dict"},{"t":"s","v":"SX-Request"},{"t":"s","v":"true"},{"t":"s","v":"SX-Current-URL"},{"t":"s","v":"browser-location-href"},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"sx-target"},{"t":"s","v":"dict-set!"},{"t":"s","v":"SX-Target"},{"t":"s","v":"dom-query"},{"t":"s","v":"script[data-components][data-hash]"},{"t":"s","v":"data-hash"},{"t":"s","v":"SX-Components-Hash"},{"t":"s","v":"SX-Css"},{"t":"s","v":"sx-headers"},{"t":"s","v":"parse-header-value"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[18,0,16,0,18,1,16,0,52,2,0,2,52,1,0,1,52,0,0,3,50],"constants":[{"t":"s","v":"dict-set!"},{"t":"s","v":"str"},{"t":"s","v":"get"}],"arity":1,"upvalue-count":2}},{"t":"s","v":"keys"}],"arity":3}},{"t":"s","v":"process-response-headers"},{"t":"code","v":{"bytecode":[1,1,0,16,0,1,2,0,48,1,1,3,0,16,0,1,4,0,48,1,1,5,0,16,0,1,6,0,48,1,1,7,0,16,0,1,8,0,48,1,1,9,0,16,0,1,10,0,48,1,1,11,0,16,0,1,12,0,48,1,1,13,0,16,0,1,14,0,48,1,1,15,0,16,0,1,16,0,48,1,1,17,0,16,0,1,18,0,48,1,1,19,0,16,0,1,20,0,48,1,1,21,0,16,0,1,22,0,48,1,1,23,0,16,0,1,24,0,48,1,1,25,0,16,0,1,26,0,48,1,52,0,0,26,50],"constants":[{"t":"s","v":"dict"},{"t":"s","v":"redirect"},{"t":"s","v":"SX-Redirect"},{"t":"s","v":"refresh"},{"t":"s","v":"SX-Refresh"},{"t":"s","v":"trigger"},{"t":"s","v":"SX-Trigger"},{"t":"s","v":"retarget"},{"t":"s","v":"SX-Retarget"},{"t":"s","v":"reswap"},{"t":"s","v":"SX-Reswap"},{"t":"s","v":"location"},{"t":"s","v":"SX-Location"},{"t":"s","v":"replace-url"},{"t":"s","v":"SX-Replace-Url"},{"t":"s","v":"css-hash"},{"t":"s","v":"SX-Css-Hash"},{"t":"s","v":"trigger-swap"},{"t":"s","v":"SX-Trigger-After-Swap"},{"t":"s","v":"trigger-settle"},{"t":"s","v":"SX-Trigger-After-Settle"},{"t":"s","v":"content-type"},{"t":"s","v":"Content-Type"},{"t":"s","v":"cache-invalidate"},{"t":"s","v":"SX-Cache-Invalidate"},{"t":"s","v":"cache-update"},{"t":"s","v":"SX-Cache-Update"}],"arity":1}},{"t":"s","v":"parse-swap-spec"},{"t":"code","v":{"bytecode":[16,0,6,34,4,0,5,20,1,0,1,2,0,52,0,0,2,17,2,16,2,52,3,0,1,17,3,16,1,17,4,51,5,0,1,4,16,2,52,6,0,1,52,4,0,2,5,1,8,0,16,3,1,9,0,16,4,52,7,0,4,50],"constants":[{"t":"s","v":"split"},{"t":"s","v":"DEFAULT_SWAP"},{"t":"s","v":" "},{"t":"s","v":"first"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[16,0,1,1,0,52,0,0,2,33,6,0,3,19,0,32,19,0,16,0,1,2,0,52,0,0,2,33,6,0,4,19,0,32,1,0,2,50],"constants":[{"t":"s","v":"="},{"t":"s","v":"transition:true"},{"t":"s","v":"transition:false"}],"arity":1,"upvalue-count":1}},{"t":"s","v":"rest"},{"t":"s","v":"dict"},{"t":"s","v":"style"},{"t":"s","v":"transition"}],"arity":2}},{"t":"s","v":"parse-retry-spec"},{"t":"code","v":{"bytecode":[16,0,52,0,0,1,33,4,0,2,32,62,0,16,0,1,2,0,52,1,0,2,17,1,1,4,0,16,1,52,5,0,1,1,6,0,16,1,1,9,0,52,8,0,2,1,10,0,52,7,0,2,1,11,0,16,1,1,12,0,52,8,0,2,1,13,0,52,7,0,2,52,3,0,6,50],"constants":[{"t":"s","v":"nil?"},{"t":"s","v":"split"},{"t":"s","v":":"},{"t":"s","v":"dict"},{"t":"s","v":"strategy"},{"t":"s","v":"first"},{"t":"s","v":"start-ms"},{"t":"s","v":"parse-int"},{"t":"s","v":"nth"},{"t":"n","v":1},{"t":"n","v":1000},{"t":"s","v":"cap-ms"},{"t":"n","v":2},{"t":"n","v":30000}],"arity":1}},{"t":"s","v":"next-retry-ms"},{"t":"code","v":{"bytecode":[16,0,1,2,0,52,1,0,2,16,1,52,0,0,2,50],"constants":[{"t":"s","v":"min"},{"t":"s","v":"*"},{"t":"n","v":2}],"arity":2}},{"t":"s","v":"filter-params"},{"t":"code","v":{"bytecode":[16,0,52,0,0,1,33,5,0,16,1,32,116,0,16,0,1,2,0,52,1,0,2,33,7,0,52,3,0,0,32,97,0,16,0,1,4,0,52,1,0,2,33,5,0,16,1,32,80,0,16,0,1,6,0,52,5,0,2,33,39,0,20,8,0,16,0,1,11,0,52,10,0,2,1,12,0,52,9,0,2,52,7,0,2,17,2,51,14,0,1,2,16,1,52,13,0,2,32,29,0,20,8,0,16,0,1,12,0,52,9,0,2,52,7,0,2,17,2,51,15,0,1,2,16,1,52,13,0,2,50],"constants":[{"t":"s","v":"nil?"},{"t":"s","v":"="},{"t":"s","v":"none"},{"t":"s","v":"list"},{"t":"s","v":"*"},{"t":"s","v":"starts-with?"},{"t":"s","v":"not "},{"t":"s","v":"map"},{"t":"s","v":"trim"},{"t":"s","v":"split"},{"t":"s","v":"slice"},{"t":"n","v":4},{"t":"s","v":","},{"t":"s","v":"filter"},{"t":"code","v":{"bytecode":[18,0,16,0,52,2,0,1,52,1,0,2,52,0,0,1,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"contains?"},{"t":"s","v":"first"}],"arity":1,"upvalue-count":1}},{"t":"code","v":{"bytecode":[18,0,16,0,52,1,0,1,52,0,0,2,50],"constants":[{"t":"s","v":"contains?"},{"t":"s","v":"first"}],"arity":1,"upvalue-count":1}}],"arity":2}},{"t":"s","v":"resolve-target"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,16,1,52,2,0,1,6,34,10,0,5,16,1,1,4,0,52,3,0,2,33,5,0,16,0,32,29,0,16,1,1,5,0,52,3,0,2,33,10,0,20,6,0,16,0,49,1,32,7,0,20,7,0,16,1,49,1,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"sx-target"},{"t":"s","v":"nil?"},{"t":"s","v":"="},{"t":"s","v":"this"},{"t":"s","v":"closest"},{"t":"s","v":"dom-parent"},{"t":"s","v":"dom-query"}],"arity":1}},{"t":"s","v":"apply-optimistic"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,16,1,52,2,0,1,33,4,0,2,32,191,0,20,3,0,16,0,48,1,6,34,3,0,5,16,0,17,2,1,5,0,16,2,1,6,0,16,1,52,4,0,4,17,3,16,1,1,8,0,52,7,0,2,33,50,0,16,3,1,10,0,20,11,0,16,2,1,10,0,48,2,52,9,0,3,5,20,12,0,16,2,1,10,0,1,13,0,48,3,5,20,12,0,16,2,1,14,0,1,15,0,48,3,32,94,0,16,1,1,16,0,52,7,0,2,33,34,0,16,3,1,17,0,20,18,0,16,2,1,17,0,48,2,52,9,0,3,5,20,19,0,16,2,1,17,0,3,48,3,32,48,0,16,1,1,21,0,52,20,0,2,33,35,0,16,1,1,23,0,52,22,0,2,17,4,16,3,1,24,0,16,4,52,9,0,3,5,20,25,0,16,2,16,4,48,2,32,1,0,2,5,16,3,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"sx-optimistic"},{"t":"s","v":"nil?"},{"t":"s","v":"resolve-target"},{"t":"s","v":"dict"},{"t":"s","v":"target"},{"t":"s","v":"directive"},{"t":"s","v":"="},{"t":"s","v":"remove"},{"t":"s","v":"dict-set!"},{"t":"s","v":"opacity"},{"t":"s","v":"dom-get-style"},{"t":"s","v":"dom-set-style"},{"t":"s","v":"0"},{"t":"s","v":"pointer-events"},{"t":"s","v":"none"},{"t":"s","v":"disable"},{"t":"s","v":"disabled"},{"t":"s","v":"dom-get-prop"},{"t":"s","v":"dom-set-prop"},{"t":"s","v":"starts-with?"},{"t":"s","v":"add-class:"},{"t":"s","v":"slice"},{"t":"n","v":10},{"t":"s","v":"add-class"},{"t":"s","v":"dom-add-class"}],"arity":1}},{"t":"s","v":"revert-optimistic"},{"t":"code","v":{"bytecode":[16,0,33,153,0,16,0,1,1,0,52,0,0,2,17,1,16,0,1,2,0,52,0,0,2,17,2,16,2,1,4,0,52,3,0,2,33,44,0,20,5,0,16,1,1,6,0,16,0,1,6,0,52,0,0,2,6,34,4,0,5,1,7,0,48,3,5,20,5,0,16,1,1,8,0,1,7,0,49,3,32,72,0,16,2,1,9,0,52,3,0,2,33,28,0,20,10,0,16,1,1,11,0,16,0,1,11,0,52,0,0,2,6,34,2,0,5,4,49,3,32,32,0,16,0,1,12,0,52,0,0,2,33,19,0,20,13,0,16,1,16,0,1,12,0,52,0,0,2,49,2,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"get"},{"t":"s","v":"target"},{"t":"s","v":"directive"},{"t":"s","v":"="},{"t":"s","v":"remove"},{"t":"s","v":"dom-set-style"},{"t":"s","v":"opacity"},{"t":"s","v":""},{"t":"s","v":"pointer-events"},{"t":"s","v":"disable"},{"t":"s","v":"dom-set-prop"},{"t":"s","v":"disabled"},{"t":"s","v":"add-class"},{"t":"s","v":"dom-remove-class"}],"arity":1}},{"t":"s","v":"find-oob-swaps"},{"t":"code","v":{"bytecode":[52,0,0,0,17,1,51,2,0,1,0,1,1,1,3,0,1,4,0,52,0,0,2,52,1,0,2,5,16,1,50],"constants":[{"t":"s","v":"list"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,18,0,1,2,0,16,0,1,3,0,52,1,0,3,48,2,17,1,51,5,0,1,0,0,1,16,1,52,4,0,2,50],"constants":[{"t":"s","v":"dom-query-all"},{"t":"s","v":"str"},{"t":"s","v":"["},{"t":"s","v":"]"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,16,0,18,0,48,2,6,34,4,0,5,1,1,0,17,1,20,2,0,16,0,48,1,17,2,20,3,0,16,0,18,0,48,2,5,16,2,33,29,0,20,4,0,18,1,1,6,0,16,0,1,7,0,16,1,1,8,0,16,2,52,5,0,6,49,2,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"outerHTML"},{"t":"s","v":"dom-id"},{"t":"s","v":"dom-remove-attr"},{"t":"s","v":"append!"},{"t":"s","v":"dict"},{"t":"s","v":"element"},{"t":"s","v":"swap-type"},{"t":"s","v":"target-id"}],"arity":1,"upvalue-count":2}}],"arity":1,"upvalue-count":2}},{"t":"s","v":"sx-swap-oob"},{"t":"s","v":"hx-swap-oob"}],"arity":1}},{"t":"s","v":"morph-node"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,6,34,11,0,5,20,0,0,16,0,1,2,0,48,2,33,4,0,2,32,137,1,20,0,0,16,0,1,3,0,48,2,6,33,55,0,5,20,4,0,16,0,1,5,0,48,2,6,33,40,0,5,20,0,0,16,1,1,3,0,48,2,6,33,25,0,5,20,7,0,16,0,1,3,0,48,2,20,7,0,16,1,1,3,0,48,2,52,6,0,2,33,12,0,20,8,0,16,0,16,1,49,2,32,53,1,20,10,0,16,0,48,1,20,10,0,16,1,48,1,52,6,0,2,52,9,0,1,6,34,23,0,5,20,11,0,16,0,48,1,20,11,0,16,1,48,1,52,6,0,2,52,9,0,1,33,24,0,20,12,0,20,13,0,16,0,48,1,20,14,0,16,1,48,1,16,0,49,3,32,233,0,20,10,0,16,0,48,1,1,15,0,52,6,0,2,6,34,15,0,5,20,10,0,16,0,48,1,1,16,0,52,6,0,2,33,46,0,20,17,0,16,0,48,1,20,17,0,16,1,48,1,52,6,0,2,52,9,0,1,33,17,0,20,18,0,16,0,20,17,0,16,1,48,1,49,2,32,1,0,2,32,151,0,20,10,0,16,0,48,1,1,19,0,52,6,0,2,33,133,0,20,0,0,16,0,1,3,0,48,2,6,33,44,0,5,20,0,0,16,1,1,3,0,48,2,6,33,29,0,5,20,7,0,16,0,1,3,0,48,2,20,7,0,16,1,1,3,0,48,2,52,6,0,2,52,9,0,1,33,18,0,20,20,0,16,0,48,1,5,20,21,0,16,0,48,1,32,1,0,2,5,20,22,0,16,0,16,1,48,2,5,20,23,0,16,0,48,1,6,33,8,0,5,20,24,0,16,0,48,1,52,9,0,1,33,12,0,20,25,0,16,0,16,1,49,2,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"dom-has-attr?"},{"t":"s","v":"sx-preserve"},{"t":"s","v":"sx-ignore"},{"t":"s","v":"data-sx-island"},{"t":"s","v":"is-processed?"},{"t":"s","v":"island-hydrated"},{"t":"s","v":"="},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"morph-island-children"},{"t":"s","v":"not"},{"t":"s","v":"dom-node-type"},{"t":"s","v":"dom-node-name"},{"t":"s","v":"dom-replace-child"},{"t":"s","v":"dom-parent"},{"t":"s","v":"dom-clone"},{"t":"n","v":3},{"t":"n","v":8},{"t":"s","v":"dom-text-content"},{"t":"s","v":"dom-set-text-content"},{"t":"n","v":1},{"t":"s","v":"dispose-island"},{"t":"s","v":"dispose-islands-in"},{"t":"s","v":"sync-attrs"},{"t":"s","v":"dom-is-active-element?"},{"t":"s","v":"dom-is-input-element?"},{"t":"s","v":"morph-children"}],"arity":2}},{"t":"s","v":"sync-attrs"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,6,34,4,0,5,1,2,0,17,2,16,2,52,3,0,1,33,7,0,52,4,0,0,32,9,0,16,2,1,6,0,52,5,0,2,17,3,51,8,0,1,0,1,3,20,9,0,16,1,48,1,52,7,0,2,5,51,10,0,1,1,1,3,1,0,20,9,0,16,0,48,1,52,7,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx-reactive-attrs"},{"t":"s","v":""},{"t":"s","v":"empty?"},{"t":"s","v":"list"},{"t":"s","v":"split"},{"t":"s","v":","},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[16,0,52,0,0,1,17,1,16,0,1,2,0,52,1,0,2,17,2,20,5,0,18,0,16,1,48,2,16,2,52,4,0,2,52,3,0,1,6,33,13,0,5,18,1,16,1,52,6,0,2,52,3,0,1,33,14,0,20,7,0,18,0,16,1,16,2,49,3,32,1,0,2,50],"constants":[{"t":"s","v":"first"},{"t":"s","v":"nth"},{"t":"n","v":1},{"t":"s","v":"not"},{"t":"s","v":"="},{"t":"s","v":"dom-get-attr"},{"t":"s","v":"contains?"},{"t":"s","v":"dom-set-attr"}],"arity":1,"upvalue-count":2}},{"t":"s","v":"dom-attr-list"},{"t":"code","v":{"bytecode":[16,0,52,0,0,1,17,1,20,2,0,18,0,16,1,48,2,52,1,0,1,6,33,31,0,5,18,1,16,1,52,3,0,2,52,1,0,1,6,33,14,0,5,16,1,1,5,0,52,4,0,2,52,1,0,1,33,12,0,20,6,0,18,2,16,1,49,2,32,1,0,2,50],"constants":[{"t":"s","v":"first"},{"t":"s","v":"not"},{"t":"s","v":"dom-has-attr?"},{"t":"s","v":"contains?"},{"t":"s","v":"="},{"t":"s","v":"data-sx-reactive-attrs"},{"t":"s","v":"dom-remove-attr"}],"arity":1,"upvalue-count":3}}],"arity":2}},{"t":"s","v":"morph-children"},{"t":"code","v":{"bytecode":[20,0,0,16,0,48,1,17,2,20,0,0,16,1,48,1,17,3,51,2,0,52,3,0,0,16,2,52,1,0,3,17,4,1,4,0,17,5,51,6,0,1,4,1,5,1,2,1,0,16,3,52,5,0,2,5,51,7,0,1,5,1,2,1,0,16,5,16,2,52,9,0,1,52,8,0,2,52,5,0,2,50],"constants":[{"t":"s","v":"dom-child-list"},{"t":"s","v":"reduce"},{"t":"code","v":{"bytecode":[20,0,0,16,1,48,1,17,2,16,2,33,16,0,16,0,16,2,16,1,52,1,0,3,5,16,0,32,2,0,16,0,50],"constants":[{"t":"s","v":"dom-id"},{"t":"s","v":"dict-set!"}],"arity":2}},{"t":"s","v":"dict"},{"t":"n","v":0},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,16,0,48,1,17,1,16,1,33,11,0,18,0,16,1,52,1,0,2,32,1,0,2,17,2,16,2,6,33,11,0,5,16,2,52,3,0,1,52,2,0,1,33,100,0,18,1,18,2,52,5,0,1,52,4,0,2,6,33,19,0,5,16,2,18,2,18,1,52,7,0,2,52,6,0,2,52,2,0,1,33,39,0,20,8,0,18,3,16,2,18,1,18,2,52,5,0,1,52,4,0,2,33,11,0,18,2,18,1,52,7,0,2,32,1,0,2,48,3,32,1,0,2,5,20,9,0,16,2,16,0,48,2,5,18,1,52,10,0,1,19,1,32,100,0,18,1,18,2,52,5,0,1,52,4,0,2,33,71,0,18,2,18,1,52,7,0,2,17,3,20,0,0,16,3,48,1,6,33,7,0,5,16,1,52,2,0,1,33,19,0,20,8,0,18,3,20,11,0,16,0,48,1,16,3,49,3,32,18,0,20,9,0,16,3,16,0,48,2,5,18,1,52,10,0,1,19,1,32,14,0,20,12,0,18,3,20,11,0,16,0,48,1,49,2,50],"constants":[{"t":"s","v":"dom-id"},{"t":"s","v":"dict-get"},{"t":"s","v":"not"},{"t":"s","v":"nil?"},{"t":"s","v":"<"},{"t":"s","v":"len"},{"t":"s","v":"="},{"t":"s","v":"nth"},{"t":"s","v":"dom-insert-before"},{"t":"s","v":"morph-node"},{"t":"s","v":"inc"},{"t":"s","v":"dom-clone"},{"t":"s","v":"dom-append"}],"arity":1,"upvalue-count":4}},{"t":"code","v":{"bytecode":[16,0,18,0,52,0,0,2,33,76,0,18,1,16,0,52,1,0,2,17,1,20,2,0,16,1,18,2,48,2,6,33,34,0,5,20,4,0,16,1,1,5,0,48,2,52,3,0,1,6,33,15,0,5,20,4,0,16,1,1,6,0,48,2,52,3,0,1,33,12,0,20,7,0,18,2,16,1,49,2,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":">="},{"t":"s","v":"nth"},{"t":"s","v":"dom-is-child-of?"},{"t":"s","v":"not"},{"t":"s","v":"dom-has-attr?"},{"t":"s","v":"sx-preserve"},{"t":"s","v":"sx-ignore"},{"t":"s","v":"dom-remove-child"}],"arity":1,"upvalue-count":3}},{"t":"s","v":"range"},{"t":"s","v":"len"}],"arity":2}},{"t":"s","v":"morph-island-children"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,2,20,0,0,16,1,1,1,0,48,2,17,3,20,0,0,16,0,1,2,0,48,2,17,4,20,0,0,16,1,1,2,0,48,2,17,5,52,3,0,0,17,6,52,3,0,0,17,7,51,5,0,1,6,16,3,52,4,0,2,5,51,6,0,1,7,16,5,52,4,0,2,5,51,7,0,1,6,16,2,52,4,0,2,5,51,8,0,1,7,1,0,16,4,52,4,0,2,5,20,9,0,16,1,49,1,50],"constants":[{"t":"s","v":"dom-query-all"},{"t":"s","v":"[data-sx-lake]"},{"t":"s","v":"[data-sx-marsh]"},{"t":"s","v":"dict"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,16,1,33,13,0,18,0,16,1,16,0,52,2,0,3,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx-lake"},{"t":"s","v":"dict-set!"}],"arity":1,"upvalue-count":1}},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,16,1,33,13,0,18,0,16,1,16,0,52,2,0,3,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx-marsh"},{"t":"s","v":"dict-set!"}],"arity":1,"upvalue-count":1}},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,18,0,16,1,52,2,0,2,17,2,16,2,33,22,0,20,3,0,16,0,16,2,48,2,5,20,4,0,16,0,16,2,49,2,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx-lake"},{"t":"s","v":"dict-get"},{"t":"s","v":"sync-attrs"},{"t":"s","v":"morph-children"}],"arity":1,"upvalue-count":1}},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,18,0,16,1,52,2,0,2,17,2,16,2,33,14,0,20,3,0,16,0,16,2,18,1,49,3,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx-marsh"},{"t":"s","v":"dict-get"},{"t":"s","v":"morph-marsh"}],"arity":1,"upvalue-count":2}},{"t":"s","v":"process-signal-updates"}],"arity":2}},{"t":"s","v":"morph-marsh"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,3,20,0,0,16,0,1,2,0,48,2,17,4,20,3,0,16,1,48,1,17,5,16,4,6,33,18,0,5,16,5,6,33,11,0,5,16,5,52,5,0,1,52,4,0,1,33,61,0,20,6,0,16,5,48,1,17,6,16,3,33,16,0,20,7,0,16,3,16,6,52,8,0,1,48,2,32,2,0,16,6,17,7,20,9,0,16,0,48,1,5,20,10,0,16,0,51,11,0,1,7,1,4,1,0,49,2,32,19,0,20,12,0,16,0,16,1,48,2,5,20,13,0,16,0,16,1,49,2,50],"constants":[{"t":"s","v":"dom-get-data"},{"t":"s","v":"sx-marsh-transform"},{"t":"s","v":"sx-marsh-env"},{"t":"s","v":"dom-inner-html"},{"t":"s","v":"not"},{"t":"s","v":"empty?"},{"t":"s","v":"parse"},{"t":"s","v":"cek-call"},{"t":"s","v":"list"},{"t":"s","v":"dispose-marsh-scope"},{"t":"s","v":"with-marsh-scope"},{"t":"code","v":{"bytecode":[20,0,0,18,0,18,1,2,48,3,17,0,20,1,0,18,2,2,48,2,5,20,2,0,18,2,16,0,49,2,50],"constants":[{"t":"s","v":"render-to-dom"},{"t":"s","v":"dom-remove-children-after"},{"t":"s","v":"dom-append"}],"upvalue-count":3}},{"t":"s","v":"sync-attrs"},{"t":"s","v":"morph-children"}],"arity":3}},{"t":"s","v":"process-signal-updates"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,51,3,0,16,1,52,2,0,2,50],"constants":[{"t":"s","v":"dom-query-all"},{"t":"s","v":"[data-sx-signal]"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,16,1,33,94,0,16,1,1,3,0,52,2,0,2,17,2,16,2,1,5,0,52,4,0,2,33,67,0,16,1,1,5,0,16,2,52,6,0,3,17,3,16,1,16,2,1,8,0,52,7,0,2,52,6,0,2,17,4,20,9,0,16,4,48,1,17,5,20,10,0,20,11,0,16,3,48,1,16,5,48,2,5,20,12,0,16,0,1,1,0,49,2,32,1,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"data-sx-signal"},{"t":"s","v":"index-of"},{"t":"s","v":":"},{"t":"s","v":">"},{"t":"n","v":0},{"t":"s","v":"slice"},{"t":"s","v":"+"},{"t":"n","v":1},{"t":"s","v":"json-parse"},{"t":"s","v":"reset!"},{"t":"s","v":"use-store"},{"t":"s","v":"dom-remove-attr"}],"arity":1}}],"arity":1}},{"t":"s","v":"swap-dom-nodes"},{"t":"code","v":{"bytecode":[16,2,6,1,0,0,52,1,0,2,33,56,0,5,20,2,0,16,1,48,1,33,12,0,20,3,0,16,0,16,1,49,2,32,30,0,20,4,0,1,5,0,2,48,2,17,3,20,6,0,16,3,16,1,48,2,5,20,3,0,16,0,16,3,49,2,32,73,1,6,1,7,0,52,1,0,2,33,117,0,5,20,8,0,16,0,48,1,17,3,20,9,0,16,1,48,1,17,4,20,2,0,16,1,48,1,33,71,0,20,10,0,16,1,48,1,17,5,16,5,33,45,0,20,9,0,16,5,48,1,17,4,5,20,11,0,16,3,16,4,16,0,48,3,5,20,12,0,16,5,48,1,17,6,20,13,0,16,3,16,4,16,6,48,3,32,9,0,20,14,0,16,3,16,0,48,2,32,11,0,20,11,0,16,3,16,4,16,0,48,3,5,16,4,32,201,0,6,1,15,0,52,1,0,2,33,13,0,5,20,16,0,16,0,16,1,49,2,32,177,0,6,1,17,0,52,1,0,2,33,13,0,5,20,6,0,16,0,16,1,49,2,32,153,0,6,1,18,0,52,1,0,2,33,13,0,5,20,19,0,16,0,16,1,49,2,32,129,0,6,1,20,0,52,1,0,2,33,20,0,5,20,21,0,20,8,0,16,0,48,1,16,1,16,0,49,3,32,98,0,6,1,22,0,52,1,0,2,33,18,0,5,20,14,0,20,8,0,16,0,48,1,16,0,49,2,32,69,0,6,1,23,0,52,1,0,2,33,5,0,5,2,32,53,0,5,20,2,0,16,1,48,1,33,12,0,20,3,0,16,0,16,1,49,2,32,30,0,20,4,0,1,5,0,2,48,2,17,3,20,6,0,16,3,16,1,48,2,5,20,3,0,16,0,16,3,49,2,50],"constants":[{"t":"s","v":"innerHTML"},{"t":"s","v":"="},{"t":"s","v":"dom-is-fragment?"},{"t":"s","v":"morph-children"},{"t":"s","v":"dom-create-element"},{"t":"s","v":"div"},{"t":"s","v":"dom-append"},{"t":"s","v":"outerHTML"},{"t":"s","v":"dom-parent"},{"t":"s","v":"dom-clone"},{"t":"s","v":"dom-first-child"},{"t":"s","v":"dom-replace-child"},{"t":"s","v":"dom-next-sibling"},{"t":"s","v":"insert-remaining-siblings"},{"t":"s","v":"dom-remove-child"},{"t":"s","v":"afterend"},{"t":"s","v":"dom-insert-after"},{"t":"s","v":"beforeend"},{"t":"s","v":"afterbegin"},{"t":"s","v":"dom-prepend"},{"t":"s","v":"beforebegin"},{"t":"s","v":"dom-insert-before"},{"t":"s","v":"delete"},{"t":"s","v":"none"}],"arity":3}},{"t":"s","v":"insert-remaining-siblings"},{"t":"code","v":{"bytecode":[16,2,33,33,0,20,0,0,16,2,48,1,17,3,20,1,0,16,1,16,2,48,2,5,20,2,0,16,0,16,2,16,3,49,3,32,1,0,2,50],"constants":[{"t":"s","v":"dom-next-sibling"},{"t":"s","v":"dom-insert-after"},{"t":"s","v":"insert-remaining-siblings"}],"arity":3}},{"t":"s","v":"swap-html-string"},{"t":"code","v":{"bytecode":[16,2,6,1,0,0,52,1,0,2,33,13,0,5,20,2,0,16,0,16,1,49,2,32,212,0,6,1,3,0,52,1,0,2,33,38,0,5,20,4,0,16,0,48,1,17,3,20,5,0,16,0,1,6,0,16,1,48,3,5,20,7,0,16,3,16,0,48,2,5,16,3,32,163,0,6,1,6,0,52,1,0,2,33,16,0,5,20,5,0,16,0,1,6,0,16,1,49,3,32,136,0,6,1,8,0,52,1,0,2,33,16,0,5,20,5,0,16,0,1,8,0,16,1,49,3,32,109,0,6,1,9,0,52,1,0,2,33,16,0,5,20,5,0,16,0,1,9,0,16,1,49,3,32,82,0,6,1,10,0,52,1,0,2,33,16,0,5,20,5,0,16,0,1,10,0,16,1,49,3,32,55,0,6,1,11,0,52,1,0,2,33,18,0,5,20,7,0,20,4,0,16,0,48,1,16,0,49,2,32,26,0,6,1,12,0,52,1,0,2,33,5,0,5,2,32,10,0,5,20,2,0,16,0,16,1,49,2,50],"constants":[{"t":"s","v":"innerHTML"},{"t":"s","v":"="},{"t":"s","v":"dom-set-inner-html"},{"t":"s","v":"outerHTML"},{"t":"s","v":"dom-parent"},{"t":"s","v":"dom-insert-adjacent-html"},{"t":"s","v":"afterend"},{"t":"s","v":"dom-remove-child"},{"t":"s","v":"beforeend"},{"t":"s","v":"afterbegin"},{"t":"s","v":"beforebegin"},{"t":"s","v":"delete"},{"t":"s","v":"none"}],"arity":3}},{"t":"s","v":"handle-history"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,3,20,0,0,16,0,1,2,0,48,2,17,4,16,2,1,4,0,52,3,0,2,17,5,16,5,33,10,0,20,5,0,16,5,49,1,32,101,0,16,3,6,33,14,0,5,16,3,1,8,0,52,7,0,2,52,6,0,1,33,27,0,20,9,0,16,3,1,10,0,52,7,0,2,33,5,0,16,1,32,2,0,16,3,49,1,32,51,0,16,4,6,33,14,0,5,16,4,1,8,0,52,7,0,2,52,6,0,1,33,27,0,20,5,0,16,4,1,10,0,52,7,0,2,33,5,0,16,1,32,2,0,16,4,49,1,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"sx-push-url"},{"t":"s","v":"sx-replace-url"},{"t":"s","v":"get"},{"t":"s","v":"replace-url"},{"t":"s","v":"browser-replace-state"},{"t":"s","v":"not"},{"t":"s","v":"="},{"t":"s","v":"false"},{"t":"s","v":"browser-push-state"},{"t":"s","v":"true"}],"arity":3}},{"t":"s","v":"PRELOAD_TTL"},{"t":"n","v":30000},{"t":"s","v":"preload-cache-get"},{"t":"code","v":{"bytecode":[16,0,16,1,52,0,0,2,17,2,16,2,52,1,0,1,33,4,0,2,32,52,0,20,4,0,48,0,16,2,1,6,0,52,5,0,2,52,3,0,2,20,7,0,52,2,0,2,33,13,0,16,0,16,1,52,8,0,2,5,2,32,11,0,16,0,16,1,52,8,0,2,5,16,2,50],"constants":[{"t":"s","v":"dict-get"},{"t":"s","v":"nil?"},{"t":"s","v":">"},{"t":"s","v":"-"},{"t":"s","v":"now-ms"},{"t":"s","v":"get"},{"t":"s","v":"timestamp"},{"t":"s","v":"PRELOAD_TTL"},{"t":"s","v":"dict-delete!"}],"arity":2}},{"t":"s","v":"preload-cache-set"},{"t":"code","v":{"bytecode":[16,0,16,1,1,2,0,16,2,1,3,0,16,3,1,4,0,20,5,0,48,0,52,1,0,6,52,0,0,3,50],"constants":[{"t":"s","v":"dict-set!"},{"t":"s","v":"dict"},{"t":"s","v":"text"},{"t":"s","v":"content-type"},{"t":"s","v":"timestamp"},{"t":"s","v":"now-ms"}],"arity":4}},{"t":"s","v":"classify-trigger"},{"t":"code","v":{"bytecode":[16,0,1,1,0,52,0,0,2,17,1,16,1,1,3,0,52,2,0,2,33,6,0,1,4,0,32,57,0,16,1,1,5,0,52,2,0,2,33,6,0,1,5,0,32,39,0,16,1,1,6,0,52,2,0,2,33,6,0,1,6,0,32,21,0,16,1,1,7,0,52,2,0,2,33,6,0,1,7,0,32,3,0,1,1,0,50],"constants":[{"t":"s","v":"get"},{"t":"s","v":"event"},{"t":"s","v":"="},{"t":"s","v":"every"},{"t":"s","v":"poll"},{"t":"s","v":"intersect"},{"t":"s","v":"load"},{"t":"s","v":"revealed"}],"arity":1}},{"t":"s","v":"should-boost-link?"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,16,1,6,33,119,0,5,16,1,1,4,0,52,3,0,2,52,2,0,1,6,33,101,0,5,16,1,1,5,0,52,3,0,2,52,2,0,1,6,33,83,0,5,16,1,1,6,0,52,3,0,2,52,2,0,1,6,33,65,0,5,20,7,0,16,1,48,1,6,33,53,0,5,20,8,0,16,0,1,9,0,48,2,52,2,0,1,6,33,34,0,5,20,8,0,16,0,1,10,0,48,2,52,2,0,1,6,33,15,0,5,20,8,0,16,0,1,11,0,48,2,52,2,0,1,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"href"},{"t":"s","v":"not"},{"t":"s","v":"starts-with?"},{"t":"s","v":"#"},{"t":"s","v":"javascript:"},{"t":"s","v":"mailto:"},{"t":"s","v":"browser-same-origin?"},{"t":"s","v":"dom-has-attr?"},{"t":"s","v":"sx-get"},{"t":"s","v":"sx-post"},{"t":"s","v":"sx-disable"}],"arity":1}},{"t":"s","v":"should-boost-form?"},{"t":"code","v":{"bytecode":[20,1,0,16,0,1,2,0,48,2,52,0,0,1,6,33,34,0,5,20,1,0,16,0,1,3,0,48,2,52,0,0,1,6,33,15,0,5,20,1,0,16,0,1,4,0,48,2,52,0,0,1,50],"constants":[{"t":"s","v":"not"},{"t":"s","v":"dom-has-attr?"},{"t":"s","v":"sx-get"},{"t":"s","v":"sx-post"},{"t":"s","v":"sx-disable"}],"arity":1}},{"t":"s","v":"parse-sse-swap"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,6,34,4,0,5,1,2,0,50],"constants":[{"t":"s","v":"dom-get-attr"},{"t":"s","v":"sx-sse-swap"},{"t":"s","v":"message"}],"arity":1}}]}} \ No newline at end of file diff --git a/web/engine.sx b/web/engine.sx index d8cb0eb0..e66deaff 100644 --- a/web/engine.sx +++ b/web/engine.sx @@ -1,821 +1,770 @@ -;; ========================================================================== -;; 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 pure LOGIC of the engine in s-expressions: -;; parsing trigger specs, morph algorithm, swap dispatch, header building, -;; retry logic, target resolution, etc. -;; -;; 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) -;; render.sx — shared registries -;; ========================================================================== - - -;; -------------------------------------------------------------------------- -;; Constants -;; -------------------------------------------------------------------------- - (define ENGINE_VERBS (list "get" "post" "put" "delete" "patch")) + (define DEFAULT_SWAP "outerHTML") - -;; -------------------------------------------------------------------------- -;; Trigger parsing -;; -------------------------------------------------------------------------- -;; Parses the sx-trigger attribute value into a list of trigger descriptors. -;; Each descriptor is a dict with "event" and "modifiers" keys. - -(define parse-time :effects [] - (fn ((s :as string)) - ;; Parse time string: "2s" → 2000, "500ms" → 500 - ;; Uses nested if (not cond) because cond misclassifies 2-element - ;; function calls like (nil? s) as scheme-style ((test body)) clauses. - (if (nil? s) 0 - (if (ends-with? s "ms") (parse-int s 0) - (if (ends-with? s "s") (* (parse-int (replace s "s" "") 0) 1000) +(define + parse-time + :effects () + (fn + ((s :as string)) + (if + (nil? s) + 0 + (if + (ends-with? s "ms") + (parse-int s 0) + (if + (ends-with? s "s") + (* (parse-int (replace s "s" "") 0) 1000) (parse-int s 0)))))) - -(define parse-trigger-spec :effects [] - (fn ((spec :as string)) - ;; Parse "click delay:500ms once,change" → list of trigger descriptors - (if (nil? spec) +(define + parse-trigger-spec + :effects () + (fn + ((spec :as string)) + (if + (nil? spec) nil - (let ((raw-parts (split spec ","))) + (let + ((raw-parts (split spec ","))) (filter (fn (x) (not (nil? x))) (map - (fn ((part :as string)) - (let ((tokens (split (trim part) " "))) - (if (empty? tokens) + (fn + ((part :as string)) + (let + ((tokens (split (trim part) " "))) + (if + (empty? tokens) nil - (if (and (= (first tokens) "every") (>= (len tokens) 2)) - ;; Polling trigger + (if + (and (= (first tokens) "every") (>= (len tokens) 2)) (dict - "event" "every" - "modifiers" (dict "interval" (parse-time (nth tokens 1)))) - ;; Normal trigger with optional modifiers - (let ((mods (dict))) + "event" + "every" + "modifiers" + (dict "interval" (parse-time (nth tokens 1)))) + (let + ((mods (dict))) (for-each - (fn ((tok :as string)) + (fn + ((tok :as string)) (cond (= tok "once") - (dict-set! mods "once" true) + (dict-set! mods "once" true) (= tok "changed") - (dict-set! mods "changed" true) + (dict-set! mods "changed" true) (starts-with? tok "delay:") - (dict-set! mods "delay" - (parse-time (slice tok 6))) + (dict-set! + mods + "delay" + (parse-time (slice tok 6))) (starts-with? tok "from:") - (dict-set! mods "from" - (slice tok 5)))) + (dict-set! mods "from" (slice tok 5)))) (rest tokens)) (dict "event" (first tokens) "modifiers" mods)))))) raw-parts)))))) - -(define default-trigger :effects [] - (fn ((tag-name :as string)) - ;; Default trigger for element type +(define + default-trigger + :effects () + (fn + ((tag-name :as string)) (cond (= tag-name "FORM") - (list (dict "event" "submit" "modifiers" (dict))) - (or (= tag-name "INPUT") - (= tag-name "SELECT") - (= tag-name "TEXTAREA")) - (list (dict "event" "change" "modifiers" (dict))) - :else - (list (dict "event" "click" "modifiers" (dict)))))) + (list (dict "event" "submit" "modifiers" (dict))) + (or + (= tag-name "INPUT") + (= tag-name "SELECT") + (= tag-name "TEXTAREA")) + (list (dict "event" "change" "modifiers" (dict))) + :else (list (dict "event" "click" "modifiers" (dict)))))) +(define + get-verb-info + :effects (io) + (fn + (el) + (let + ((result nil)) + (for-each + (fn + (verb) + (when + (not result) + (let + ((url (dom-get-attr el (str "sx-" verb)))) + (when + url + (set! result (dict "method" (upper verb) "url" url)))))) + ENGINE_VERBS) + result))) -;; -------------------------------------------------------------------------- -;; Verb extraction -;; -------------------------------------------------------------------------- - -(define get-verb-info :effects [io] - (fn (el) - ;; Check element for sx-get, sx-post, etc. Returns (dict "method" "url") or nil. - (some - (fn ((verb :as string)) - (let ((url (dom-get-attr el (str "sx-" verb)))) - (if url - (dict "method" (upper verb) "url" url) - nil))) - ENGINE_VERBS))) - - -;; -------------------------------------------------------------------------- -;; Request header building -;; -------------------------------------------------------------------------- - -(define build-request-headers :effects [io] - (fn (el (loaded-components :as list) (css-hash :as string)) - ;; Build the SX request headers dict - (let ((headers (dict - "SX-Request" "true" - "SX-Current-URL" (browser-location-href)))) - ;; Target selector - (let ((target-sel (dom-get-attr el "sx-target"))) - (when target-sel - (dict-set! headers "SX-Target" target-sel))) - - ;; Send component hash instead of full name list to avoid 431 - ;; (Request Header Fields Too Large) with many loaded components. - ;; Server uses hash to decide whether to send component definitions. - (let ((comp-hash (dom-get-attr - (dom-query "script[data-components][data-hash]") - "data-hash"))) - (when comp-hash - (dict-set! headers "SX-Components-Hash" comp-hash))) - - ;; CSS class hash - (when css-hash - (dict-set! headers "SX-Css" css-hash)) - - ;; Extra headers from sx-headers attribute - (let ((extra-h (dom-get-attr el "sx-headers"))) - (when extra-h - (let ((parsed (parse-header-value extra-h))) - (when parsed +(define + build-request-headers + :effects (io) + (fn + (el (loaded-components :as list) (css-hash :as string)) + (let + ((headers (dict "SX-Request" "true" "SX-Current-URL" (browser-location-href)))) + (let + ((target-sel (dom-get-attr el "sx-target"))) + (when target-sel (dict-set! headers "SX-Target" target-sel))) + (let + ((comp-hash (dom-get-attr (dom-query "script[data-components][data-hash]") "data-hash"))) + (when comp-hash (dict-set! headers "SX-Components-Hash" comp-hash))) + (when css-hash (dict-set! headers "SX-Css" css-hash)) + (let + ((extra-h (dom-get-attr el "sx-headers"))) + (when + extra-h + (let + ((parsed (parse-header-value extra-h))) + (when + parsed (for-each - (fn ((key :as string)) (dict-set! headers key (str (get parsed key)))) + (fn + ((key :as string)) + (dict-set! headers key (str (get parsed key)))) (keys parsed)))))) - headers))) - -;; -------------------------------------------------------------------------- -;; Response header processing -;; -------------------------------------------------------------------------- - -(define process-response-headers :effects [] - (fn ((get-header :as lambda)) - ;; Extract all SX response header directives into a dict. - ;; get-header is (fn (name) → string or nil). +(define + process-response-headers + :effects () + (fn + ((get-header :as lambda)) (dict - "redirect" (get-header "SX-Redirect") - "refresh" (get-header "SX-Refresh") - "trigger" (get-header "SX-Trigger") - "retarget" (get-header "SX-Retarget") - "reswap" (get-header "SX-Reswap") - "location" (get-header "SX-Location") - "replace-url" (get-header "SX-Replace-Url") - "css-hash" (get-header "SX-Css-Hash") - "trigger-swap" (get-header "SX-Trigger-After-Swap") - "trigger-settle" (get-header "SX-Trigger-After-Settle") - "content-type" (get-header "Content-Type") - "cache-invalidate" (get-header "SX-Cache-Invalidate") - "cache-update" (get-header "SX-Cache-Update")))) + "redirect" + (get-header "SX-Redirect") + "refresh" + (get-header "SX-Refresh") + "trigger" + (get-header "SX-Trigger") + "retarget" + (get-header "SX-Retarget") + "reswap" + (get-header "SX-Reswap") + "location" + (get-header "SX-Location") + "replace-url" + (get-header "SX-Replace-Url") + "css-hash" + (get-header "SX-Css-Hash") + "trigger-swap" + (get-header "SX-Trigger-After-Swap") + "trigger-settle" + (get-header "SX-Trigger-After-Settle") + "content-type" + (get-header "Content-Type") + "cache-invalidate" + (get-header "SX-Cache-Invalidate") + "cache-update" + (get-header "SX-Cache-Update")))) - -;; -------------------------------------------------------------------------- -;; Swap specification parsing -;; -------------------------------------------------------------------------- - -(define parse-swap-spec :effects [] - (fn ((raw-swap :as string) (global-transitions? :as boolean)) - ;; Parse "innerHTML transition:true" → dict with style + transition flag - (let ((parts (split (or raw-swap DEFAULT_SWAP) " ")) - (style (first parts)) - (use-transition global-transitions?)) +(define + parse-swap-spec + :effects () + (fn + ((raw-swap :as string) (global-transitions? :as boolean)) + (let + ((parts (split (or raw-swap DEFAULT_SWAP) " ")) + (style (first parts)) + (use-transition global-transitions?)) (for-each - (fn ((p :as string)) + (fn + ((p :as string)) (cond - (= p "transition:true") (set! use-transition true) - (= p "transition:false") (set! use-transition false))) + (= p "transition:true") + (set! use-transition true) + (= p "transition:false") + (set! use-transition false))) (rest parts)) (dict "style" style "transition" use-transition)))) - -;; -------------------------------------------------------------------------- -;; Retry logic -;; -------------------------------------------------------------------------- - -(define parse-retry-spec :effects [] - (fn ((retry-attr :as string)) - ;; Parse "exponential:1000:30000" → spec dict or nil - (if (nil? retry-attr) +(define + parse-retry-spec + :effects () + (fn + ((retry-attr :as string)) + (if + (nil? retry-attr) nil - (let ((parts (split retry-attr ":"))) + (let + ((parts (split retry-attr ":"))) (dict - "strategy" (first parts) - "start-ms" (parse-int (nth parts 1) 1000) - "cap-ms" (parse-int (nth parts 2) 30000)))))) + "strategy" + (first parts) + "start-ms" + (parse-int (nth parts 1) 1000) + "cap-ms" + (parse-int (nth parts 2) 30000)))))) - -(define next-retry-ms :effects [] - (fn ((current-ms :as number) (cap-ms :as number)) - ;; Exponential backoff: double current, cap at max +(define + next-retry-ms + :effects () + (fn + ((current-ms :as number) (cap-ms :as number)) (min (* current-ms 2) cap-ms))) - -;; -------------------------------------------------------------------------- -;; Form parameter filtering -;; -------------------------------------------------------------------------- - -(define filter-params :effects [] - (fn ((params-spec :as string) (all-params :as list)) - ;; Filter form parameters by sx-params spec. - ;; all-params is a list of (key value) pairs. - ;; Returns filtered list of (key value) pairs. - ;; Uses nested if (not cond) — see parse-time comment. - (if (nil? params-spec) all-params - (if (= params-spec "none") (list) - (if (= params-spec "*") all-params - (if (starts-with? params-spec "not ") - (let ((excluded (map trim (split (slice params-spec 4) ",")))) +(define + filter-params + :effects () + (fn + ((params-spec :as string) (all-params :as list)) + (if + (nil? params-spec) + all-params + (if + (= params-spec "none") + (list) + (if + (= params-spec "*") + all-params + (if + (starts-with? params-spec "not ") + (let + ((excluded (map trim (split (slice params-spec 4) ",")))) (filter (fn ((p :as list)) (not (contains? excluded (first p)))) all-params)) - (let ((allowed (map trim (split params-spec ",")))) + (let + ((allowed (map trim (split params-spec ",")))) (filter (fn ((p :as list)) (contains? allowed (first p))) all-params)))))))) - -;; -------------------------------------------------------------------------- -;; Target resolution -;; -------------------------------------------------------------------------- - -(define resolve-target :effects [io] - (fn (el) - ;; Resolve the swap target for an element - (let ((sel (dom-get-attr el "sx-target"))) +(define + resolve-target + :effects (io) + (fn + (el) + (let + ((sel (dom-get-attr el "sx-target"))) (cond - (or (nil? sel) (= sel "this")) el - (= sel "closest") (dom-parent el) + (or (nil? sel) (= sel "this")) + el + (= sel "closest") + (dom-parent el) :else (dom-query sel))))) - -;; -------------------------------------------------------------------------- -;; Optimistic updates -;; -------------------------------------------------------------------------- - -(define apply-optimistic :effects [mutation io] - (fn (el) - ;; Apply optimistic update preview. Returns state for reverting, or nil. - (let ((directive (dom-get-attr el "sx-optimistic"))) - (if (nil? directive) +(define + apply-optimistic + :effects (mutation io) + (fn + (el) + (let + ((directive (dom-get-attr el "sx-optimistic"))) + (if + (nil? directive) nil - (let ((target (or (resolve-target el) el)) - (state (dict "target" target "directive" directive))) + (let + ((target (or (resolve-target el) el)) + (state (dict "target" target "directive" directive))) (cond (= directive "remove") - (do - (dict-set! state "opacity" (dom-get-style target "opacity")) - (dom-set-style target "opacity" "0") - (dom-set-style target "pointer-events" "none")) + (do + (dict-set! state "opacity" (dom-get-style target "opacity")) + (dom-set-style target "opacity" "0") + (dom-set-style target "pointer-events" "none")) (= directive "disable") - (do - (dict-set! state "disabled" (dom-get-prop target "disabled")) - (dom-set-prop target "disabled" true)) + (do + (dict-set! state "disabled" (dom-get-prop target "disabled")) + (dom-set-prop target "disabled" true)) (starts-with? directive "add-class:") - (let ((cls (slice directive 10))) - (dict-set! state "add-class" cls) - (dom-add-class target cls))) + (let + ((cls (slice directive 10))) + (dict-set! state "add-class" cls) + (dom-add-class target cls))) state))))) - -(define revert-optimistic :effects [mutation io] - (fn ((state :as dict)) - ;; Revert an optimistic update - (when state - (let ((target (get state "target")) - (directive (get state "directive"))) +(define + revert-optimistic + :effects (mutation io) + (fn + ((state :as dict)) + (when + state + (let + ((target (get state "target")) (directive (get state "directive"))) (cond (= directive "remove") - (do - (dom-set-style target "opacity" (or (get state "opacity") "")) - (dom-set-style target "pointer-events" "")) + (do + (dom-set-style target "opacity" (or (get state "opacity") "")) + (dom-set-style target "pointer-events" "")) (= directive "disable") - (dom-set-prop target "disabled" (or (get state "disabled") false)) + (dom-set-prop target "disabled" (or (get state "disabled") false)) (get state "add-class") - (dom-remove-class target (get state "add-class"))))))) + (dom-remove-class target (get state "add-class"))))))) - -;; -------------------------------------------------------------------------- -;; Out-of-band swap identification -;; -------------------------------------------------------------------------- - -(define find-oob-swaps :effects [mutation io] - (fn (container) - ;; Find elements marked for out-of-band swapping. - ;; Returns list of (dict "element" el "swap-type" type "target-id" id). - (let ((results (list))) +(define + find-oob-swaps + :effects (mutation io) + (fn + (container) + (let + ((results (list))) (for-each - (fn ((attr :as string)) - (let ((oob-els (dom-query-all container (str "[" attr "]")))) + (fn + ((attr :as string)) + (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))) + (fn + (oob) + (let + ((swap-type (or (dom-get-attr oob attr) "outerHTML")) + (target-id (dom-id oob))) (dom-remove-attr oob attr) - (when target-id - (append! results - (dict "element" oob - "swap-type" swap-type - "target-id" target-id))))) + (when + target-id + (append! + results + (dict + "element" + oob + "swap-type" + swap-type + "target-id" + target-id))))) oob-els))) (list "sx-swap-oob" "hx-swap-oob")) results))) - -;; -------------------------------------------------------------------------- -;; DOM morph algorithm -;; -------------------------------------------------------------------------- -;; Lightweight reconciler: patches oldNode to match newNode in-place, -;; preserving event listeners, focus, scroll position, and form state -;; on keyed (id) elements. - -(define morph-node :effects [mutation io] - (fn (old-node new-node) - ;; Morph old-node to match new-node, preserving listeners/state. +(define + morph-node + :effects (mutation io) + (fn + (old-node new-node) (cond - ;; sx-preserve / sx-ignore → skip - (or (dom-has-attr? old-node "sx-preserve") - (dom-has-attr? old-node "sx-ignore")) - nil - - ;; Hydrated island → preserve reactive state, morph lakes. - ;; If old and new are the same island (by name), keep the old DOM - ;; with its live signals, effects, and event listeners intact. - ;; But recurse into data-sx-lake slots so the server can update - ;; non-reactive content within the island. - (and (dom-has-attr? old-node "data-sx-island") - (is-processed? old-node "island-hydrated") - (dom-has-attr? new-node "data-sx-island") - (= (dom-get-attr old-node "data-sx-island") - (dom-get-attr new-node "data-sx-island"))) - (morph-island-children old-node new-node) - - ;; Different node type or tag → replace wholesale - (or (not (= (dom-node-type old-node) (dom-node-type new-node))) - (not (= (dom-node-name old-node) (dom-node-name new-node)))) - (dom-replace-child (dom-parent old-node) - (dom-clone new-node) old-node) - - ;; Text/comment nodes → update content + (or + (dom-has-attr? old-node "sx-preserve") + (dom-has-attr? old-node "sx-ignore")) + nil + (and + (dom-has-attr? old-node "data-sx-island") + (is-processed? old-node "island-hydrated") + (dom-has-attr? new-node "data-sx-island") + (= + (dom-get-attr old-node "data-sx-island") + (dom-get-attr new-node "data-sx-island"))) + (morph-island-children old-node new-node) + (or + (not (= (dom-node-type old-node) (dom-node-type new-node))) + (not (= (dom-node-name old-node) (dom-node-name new-node)))) + (dom-replace-child (dom-parent old-node) (dom-clone new-node) old-node) (or (= (dom-node-type old-node) 3) (= (dom-node-type old-node) 8)) - (when (not (= (dom-text-content old-node) (dom-text-content new-node))) - (dom-set-text-content old-node (dom-text-content new-node))) - - ;; Element nodes → sync attributes, then recurse children + (when + (not (= (dom-text-content old-node) (dom-text-content new-node))) + (dom-set-text-content old-node (dom-text-content new-node))) (= (dom-node-type old-node) 1) - (do - ;; If the island name changed, clear hydration flag so the new - ;; island gets re-hydrated after morph. The DOM element is reused - ;; (same tag) but the island content is completely different. - (when (and (dom-has-attr? old-node "data-sx-island") - (dom-has-attr? new-node "data-sx-island") - (not (= (dom-get-attr old-node "data-sx-island") - (dom-get-attr new-node "data-sx-island")))) - ;; Dispose the old island itself (not just sub-islands) - ;; to tear down reactive effects before morphing content - (dispose-island old-node) - (dispose-islands-in old-node)) - (sync-attrs old-node new-node) - ;; Skip morphing focused input to preserve user's in-progress edits - (when (not (and (dom-is-active-element? old-node) - (dom-is-input-element? old-node))) - (morph-children old-node new-node)))))) + (do + (when + (and + (dom-has-attr? old-node "data-sx-island") + (dom-has-attr? new-node "data-sx-island") + (not + (= + (dom-get-attr old-node "data-sx-island") + (dom-get-attr new-node "data-sx-island")))) + (dispose-island old-node) + (dispose-islands-in old-node)) + (sync-attrs old-node new-node) + (when + (not + (and + (dom-is-active-element? old-node) + (dom-is-input-element? old-node))) + (morph-children old-node new-node)))))) - -(define sync-attrs :effects [mutation io] - (fn (old-el new-el) - ;; Sync attributes from new to old, but skip reactively managed attrs. - ;; data-sx-reactive-attrs="style,class" means those attrs are owned by - ;; signal effects and must not be overwritten by the morph. - (let ((ra-str (or (dom-get-attr old-el "data-sx-reactive-attrs") "")) - (reactive-attrs (if (empty? ra-str) (list) (split ra-str ",")))) - ;; Add/update attributes from new, skip reactive ones +(define + sync-attrs + :effects (mutation io) + (fn + (old-el new-el) + (let + ((ra-str (or (dom-get-attr old-el "data-sx-reactive-attrs") "")) + (reactive-attrs (if (empty? ra-str) (list) (split ra-str ",")))) (for-each - (fn ((attr :as list)) - (let ((name (first attr)) - (val (nth attr 1))) - (when (and (not (= (dom-get-attr old-el name) val)) - (not (contains? reactive-attrs name))) + (fn + ((attr :as list)) + (let + ((name (first attr)) (val (nth attr 1))) + (when + (and + (not (= (dom-get-attr old-el name) val)) + (not (contains? reactive-attrs name))) (dom-set-attr old-el name val)))) (dom-attr-list new-el)) - ;; Remove attributes not in new, skip reactive + marker attrs (for-each - (fn ((attr :as list)) - (let ((aname (first attr))) - (when (and (not (dom-has-attr? new-el aname)) - (not (contains? reactive-attrs aname)) - (not (= aname "data-sx-reactive-attrs"))) + (fn + ((attr :as list)) + (let + ((aname (first attr))) + (when + (and + (not (dom-has-attr? new-el aname)) + (not (contains? reactive-attrs aname)) + (not (= aname "data-sx-reactive-attrs"))) (dom-remove-attr old-el aname)))) (dom-attr-list old-el))))) - -(define morph-children :effects [mutation io] - (fn (old-parent new-parent) - ;; Reconcile children of old-parent to match new-parent. - ;; Keyed elements (with id) are matched and moved in-place. - (let ((old-kids (dom-child-list old-parent)) - (new-kids (dom-child-list new-parent)) - ;; Build ID map of old children for keyed matching - (old-by-id (reduce - (fn ((acc :as dict) kid) - (let ((id (dom-id kid))) - (if id (do (dict-set! acc id kid) acc) acc))) - (dict) old-kids)) - (oi 0)) - - ;; Walk new children, morph/insert/append +(define + morph-children + :effects (mutation io) + (fn + (old-parent new-parent) + (let + ((old-kids (dom-child-list old-parent)) + (new-kids (dom-child-list new-parent)) + (old-by-id + (reduce + (fn + ((acc :as dict) kid) + (let + ((id (dom-id kid))) + (if id (do (dict-set! acc id kid) acc) acc))) + (dict) + old-kids)) + (oi 0)) (for-each - (fn (new-child) - (let ((match-id (dom-id new-child)) - (match-by-id (if match-id (dict-get old-by-id match-id) nil))) + (fn + (new-child) + (let + ((match-id (dom-id new-child)) + (match-by-id (if match-id (dict-get old-by-id match-id) nil))) (cond - ;; Keyed match — move into position if needed, then morph (and match-by-id (not (nil? match-by-id))) - (do - (when (and (< oi (len old-kids)) - (not (= match-by-id (nth old-kids oi)))) - (dom-insert-before old-parent match-by-id - (if (< oi (len old-kids)) (nth old-kids oi) nil))) - (morph-node match-by-id new-child) - (set! oi (inc oi))) - - ;; Positional match + (do + (when + (and + (< oi (len old-kids)) + (not (= match-by-id (nth old-kids oi)))) + (dom-insert-before + old-parent + match-by-id + (if (< oi (len old-kids)) (nth old-kids oi) nil))) + (morph-node match-by-id new-child) + (set! oi (inc oi))) (< oi (len old-kids)) - (let ((old-child (nth old-kids oi))) - (if (and (dom-id old-child) (not match-id)) - ;; Old has ID, new doesn't — insert new before old - (dom-insert-before old-parent - (dom-clone new-child) old-child) - ;; Normal positional morph - (do - (morph-node old-child new-child) - (set! oi (inc oi))))) - - ;; Extra new children — append - :else - (dom-append old-parent (dom-clone new-child))))) + (let + ((old-child (nth old-kids oi))) + (if + (and (dom-id old-child) (not match-id)) + (dom-insert-before + old-parent + (dom-clone new-child) + old-child) + (do (morph-node old-child new-child) (set! oi (inc oi))))) + :else (dom-append old-parent (dom-clone new-child))))) new-kids) - - ;; Remove leftover old children (for-each - (fn ((i :as number)) - (when (>= i oi) - (let ((leftover (nth old-kids i))) - (when (and (dom-is-child-of? leftover old-parent) - (not (dom-has-attr? leftover "sx-preserve")) - (not (dom-has-attr? leftover "sx-ignore"))) + (fn + ((i :as number)) + (when + (>= i oi) + (let + ((leftover (nth old-kids i))) + (when + (and + (dom-is-child-of? leftover old-parent) + (not (dom-has-attr? leftover "sx-preserve")) + (not (dom-has-attr? leftover "sx-ignore"))) (dom-remove-child old-parent leftover))))) (range oi (len old-kids)))))) - -;; -------------------------------------------------------------------------- -;; morph-island-children — deep morph into hydrated islands via lakes -;; -------------------------------------------------------------------------- -;; -;; Level 2-3 island morphing: the server can update non-reactive content -;; within hydrated islands by morphing data-sx-lake slots. -;; -;; The island's reactive DOM (signals, effects, event listeners) is preserved. -;; Only lake slots — explicitly marked server territory — receive new content. -;; -;; This is the Hegelian synthesis made concrete: -;; - Islands = client subjectivity (reactive state, preserved) -;; - Lakes = server substance (content, morphed) -;; - The morph = Aufhebung (cancellation/preservation/elevation of both) - -(define morph-island-children :effects [mutation io] - (fn (old-island new-island) - ;; Find all lake and marsh slots in both old and new islands - (let ((old-lakes (dom-query-all old-island "[data-sx-lake]")) - (new-lakes (dom-query-all new-island "[data-sx-lake]")) - (old-marshes (dom-query-all old-island "[data-sx-marsh]")) - (new-marshes (dom-query-all new-island "[data-sx-marsh]"))) - ;; Build ID→element maps for new lakes and marshes - (let ((new-lake-map (dict)) - (new-marsh-map (dict))) +(define + morph-island-children + :effects (mutation io) + (fn + (old-island new-island) + (let + ((old-lakes (dom-query-all old-island "[data-sx-lake]")) + (new-lakes (dom-query-all new-island "[data-sx-lake]")) + (old-marshes (dom-query-all old-island "[data-sx-marsh]")) + (new-marshes (dom-query-all new-island "[data-sx-marsh]"))) + (let + ((new-lake-map (dict)) (new-marsh-map (dict))) (for-each - (fn (lake) - (let ((id (dom-get-attr lake "data-sx-lake"))) + (fn + (lake) + (let + ((id (dom-get-attr lake "data-sx-lake"))) (when id (dict-set! new-lake-map id lake)))) new-lakes) (for-each - (fn (marsh) - (let ((id (dom-get-attr marsh "data-sx-marsh"))) + (fn + (marsh) + (let + ((id (dom-get-attr marsh "data-sx-marsh"))) (when id (dict-set! new-marsh-map id marsh)))) new-marshes) - ;; Morph each old lake from its new counterpart (for-each - (fn (old-lake) - (let ((id (dom-get-attr old-lake "data-sx-lake"))) - (let ((new-lake (dict-get new-lake-map id))) - (when new-lake + (fn + (old-lake) + (let + ((id (dom-get-attr old-lake "data-sx-lake"))) + (let + ((new-lake (dict-get new-lake-map id))) + (when + new-lake (sync-attrs old-lake new-lake) (morph-children old-lake new-lake))))) old-lakes) - ;; Morph each old marsh from its new counterpart (for-each - (fn (old-marsh) - (let ((id (dom-get-attr old-marsh "data-sx-marsh"))) - (let ((new-marsh (dict-get new-marsh-map id))) - (when new-marsh - (morph-marsh old-marsh new-marsh old-island))))) + (fn + (old-marsh) + (let + ((id (dom-get-attr old-marsh "data-sx-marsh"))) + (let + ((new-marsh (dict-get new-marsh-map id))) + (when new-marsh (morph-marsh old-marsh new-marsh old-island))))) old-marshes) - ;; Process data-sx-signal attributes — server writes to named stores (process-signal-updates new-island))))) - -;; -------------------------------------------------------------------------- -;; morph-marsh — re-evaluate server content in island's reactive scope -;; -------------------------------------------------------------------------- -;; -;; Marshes are zones inside islands where server content is re-evaluated by -;; the island's reactive evaluator. During morph, the new content is parsed -;; as SX and rendered in the island's signal context. If the marsh has a -;; :transform function, it reshapes the content before evaluation. - -(define morph-marsh :effects [mutation io] - (fn (old-marsh new-marsh island-el) - (let ((transform (dom-get-data old-marsh "sx-marsh-transform")) - (env (dom-get-data old-marsh "sx-marsh-env")) - (new-html (dom-inner-html new-marsh))) - (if (and env new-html (not (empty? new-html))) - ;; Parse new content as SX and re-evaluate in island scope - (let ((parsed (parse new-html))) - (let ((sx-content (if transform (cek-call transform (list parsed)) parsed))) - ;; Dispose old reactive bindings in this marsh +(define + morph-marsh + :effects (mutation io) + (fn + (old-marsh new-marsh island-el) + (let + ((transform (dom-get-data old-marsh "sx-marsh-transform")) + (env (dom-get-data old-marsh "sx-marsh-env")) + (new-html (dom-inner-html new-marsh))) + (if + (and env new-html (not (empty? new-html))) + (let + ((parsed (parse new-html))) + (let + ((sx-content (if transform (cek-call transform (list parsed)) parsed))) (dispose-marsh-scope old-marsh) - ;; Evaluate the SX in a new marsh scope — creates new reactive bindings - (with-marsh-scope old-marsh - (fn () - (let ((new-dom (render-to-dom sx-content env nil))) - ;; Replace marsh children + (with-marsh-scope + old-marsh + (fn + () + (let + ((new-dom (render-to-dom sx-content env nil))) (dom-remove-children-after old-marsh nil) (dom-append old-marsh new-dom)))))) - ;; Fallback: morph like a lake (do (sync-attrs old-marsh new-marsh) (morph-children old-marsh new-marsh)))))) - -;; -------------------------------------------------------------------------- -;; process-signal-updates — server responses write to named store signals -;; -------------------------------------------------------------------------- -;; -;; Elements with data-sx-signal="name:value" trigger signal writes. -;; After processing, the attribute is removed (consumed). -;; -;; Values are JSON-parsed: "7" → 7, "\"hello\"" → "hello", "true" → true. - -(define process-signal-updates :effects [mutation io] - (fn (root) - (let ((signal-els (dom-query-all root "[data-sx-signal]"))) +(define + process-signal-updates + :effects (mutation io) + (fn + (root) + (let + ((signal-els (dom-query-all root "[data-sx-signal]"))) (for-each - (fn (el) - (let ((spec (dom-get-attr el "data-sx-signal"))) - (when spec - (let ((colon-idx (index-of spec ":"))) - (when (> colon-idx 0) - (let ((store-name (slice spec 0 colon-idx)) - (raw-value (slice spec (+ colon-idx 1)))) - (let ((parsed (json-parse raw-value))) + (fn + (el) + (let + ((spec (dom-get-attr el "data-sx-signal"))) + (when + spec + (let + ((colon-idx (index-of spec ":"))) + (when + (> colon-idx 0) + (let + ((store-name (slice spec 0 colon-idx)) + (raw-value (slice spec (+ colon-idx 1)))) + (let + ((parsed (json-parse raw-value))) (reset! (use-store store-name) parsed)) (dom-remove-attr el "data-sx-signal"))))))) signal-els)))) - -;; -------------------------------------------------------------------------- -;; Swap dispatch -;; -------------------------------------------------------------------------- - -(define swap-dom-nodes :effects [mutation io] - (fn (target new-nodes (strategy :as string)) - ;; Execute a swap strategy on live DOM nodes. - ;; new-nodes is typically a DocumentFragment or Element. - (case strategy +(define + swap-dom-nodes + :effects (mutation io) + (fn + (target new-nodes (strategy :as string)) + (case + strategy "innerHTML" - (if (dom-is-fragment? new-nodes) - (morph-children target new-nodes) - (let ((wrapper (dom-create-element "div" nil))) - (dom-append wrapper new-nodes) - (morph-children target wrapper))) - + (if + (dom-is-fragment? new-nodes) + (morph-children target new-nodes) + (let + ((wrapper (dom-create-element "div" nil))) + (dom-append wrapper new-nodes) + (morph-children target wrapper))) "outerHTML" - (let ((parent (dom-parent target)) - (new-el (dom-clone new-nodes))) - (if (dom-is-fragment? new-nodes) - ;; Fragment — replace target with fragment children - (let ((fc (dom-first-child new-nodes))) - (if fc - (do - (set! new-el (dom-clone fc)) - (dom-replace-child parent new-el target) - (let ((sib (dom-next-sibling fc))) - (insert-remaining-siblings parent new-el sib))) - (dom-remove-child parent target))) - ;; Element — replace target with new element - (dom-replace-child parent new-el target)) - ;; Return the new element so post-swap can hydrate it - new-el) - + (let + ((parent (dom-parent target)) (new-el (dom-clone new-nodes))) + (if + (dom-is-fragment? new-nodes) + (let + ((fc (dom-first-child new-nodes))) + (if + fc + (do + (set! new-el (dom-clone fc)) + (dom-replace-child parent new-el target) + (let + ((sib (dom-next-sibling fc))) + (insert-remaining-siblings parent new-el sib))) + (dom-remove-child parent target))) + (dom-replace-child parent new-el target)) + new-el) "afterend" - (dom-insert-after target new-nodes) - + (dom-insert-after target new-nodes) "beforeend" - (dom-append target new-nodes) - + (dom-append target new-nodes) "afterbegin" - (dom-prepend target new-nodes) - + (dom-prepend target new-nodes) "beforebegin" - (dom-insert-before (dom-parent target) new-nodes target) - + (dom-insert-before (dom-parent target) new-nodes target) "delete" - (dom-remove-child (dom-parent target) target) - + (dom-remove-child (dom-parent target) target) "none" - nil + nil + :else (if + (dom-is-fragment? new-nodes) + (morph-children target new-nodes) + (let + ((wrapper (dom-create-element "div" nil))) + (dom-append wrapper new-nodes) + (morph-children target wrapper)))))) - ;; Default = innerHTML - :else - (if (dom-is-fragment? new-nodes) - (morph-children target new-nodes) - (let ((wrapper (dom-create-element "div" nil))) - (dom-append wrapper new-nodes) - (morph-children target wrapper)))))) - - -(define insert-remaining-siblings :effects [mutation io] - (fn (parent ref-node sib) - ;; Insert sibling chain after ref-node - (when sib - (let ((next (dom-next-sibling sib))) +(define + insert-remaining-siblings + :effects (mutation io) + (fn + (parent ref-node sib) + (when + sib + (let + ((next (dom-next-sibling sib))) (dom-insert-after ref-node sib) (insert-remaining-siblings parent sib next))))) - -;; -------------------------------------------------------------------------- -;; String-based swap (fallback for HTML responses) -;; -------------------------------------------------------------------------- - -(define swap-html-string :effects [mutation io] - (fn (target (html :as string) (strategy :as string)) - ;; Execute a swap strategy using an HTML string (DOMParser pipeline). - (case strategy +(define + swap-html-string + :effects (mutation io) + (fn + (target (html :as string) (strategy :as string)) + (case + strategy "innerHTML" - (dom-set-inner-html target html) + (dom-set-inner-html target html) "outerHTML" - (let ((parent (dom-parent target))) - (dom-insert-adjacent-html target "afterend" html) - (dom-remove-child parent target) - parent) - "afterend" + (let + ((parent (dom-parent target))) (dom-insert-adjacent-html target "afterend" html) + (dom-remove-child parent target) + parent) + "afterend" + (dom-insert-adjacent-html target "afterend" html) "beforeend" - (dom-insert-adjacent-html target "beforeend" html) + (dom-insert-adjacent-html target "beforeend" html) "afterbegin" - (dom-insert-adjacent-html target "afterbegin" html) + (dom-insert-adjacent-html target "afterbegin" html) "beforebegin" - (dom-insert-adjacent-html target "beforebegin" html) + (dom-insert-adjacent-html target "beforebegin" html) "delete" - (dom-remove-child (dom-parent target) target) + (dom-remove-child (dom-parent target) target) "none" - nil - :else - (dom-set-inner-html target html)))) + nil + :else (dom-set-inner-html target html)))) - -;; -------------------------------------------------------------------------- -;; History management -;; -------------------------------------------------------------------------- - -(define handle-history :effects [io] - (fn (el (url :as string) (resp-headers :as dict)) - ;; Process history push/replace based on element attrs and response headers - (let ((push-url (dom-get-attr el "sx-push-url")) - (replace-url (dom-get-attr el "sx-replace-url")) - (hdr-replace (get resp-headers "replace-url"))) +(define + handle-history + :effects (io) + (fn + (el (url :as string) (resp-headers :as dict)) + (let + ((push-url (dom-get-attr el "sx-push-url")) + (replace-url (dom-get-attr el "sx-replace-url")) + (hdr-replace (get resp-headers "replace-url"))) (cond - ;; Server override hdr-replace - (browser-replace-state hdr-replace) - ;; Client push + (browser-replace-state hdr-replace) (and push-url (not (= push-url "false"))) - (browser-push-state - (if (= push-url "true") url push-url)) - ;; Client replace + (browser-push-state (if (= push-url "true") url push-url)) (and replace-url (not (= replace-url "false"))) - (browser-replace-state - (if (= replace-url "true") url replace-url)))))) + (browser-replace-state (if (= replace-url "true") url replace-url)))))) +(define PRELOAD_TTL 30000) -;; -------------------------------------------------------------------------- -;; Preload cache -;; -------------------------------------------------------------------------- - -(define PRELOAD_TTL 30000) ;; 30 seconds - -(define preload-cache-get :effects [mutation] - (fn ((cache :as dict) (url :as string)) - ;; Get and consume a cached preload response. - ;; Returns (dict "text" ... "content-type" ...) or nil. - (let ((entry (dict-get cache url))) - (if (nil? entry) +(define + preload-cache-get + :effects (mutation) + (fn + ((cache :as dict) (url :as string)) + (let + ((entry (dict-get cache url))) + (if + (nil? entry) nil - (if (> (- (now-ms) (get entry "timestamp")) PRELOAD_TTL) + (if + (> (- (now-ms) (get entry "timestamp")) PRELOAD_TTL) (do (dict-delete! cache url) nil) (do (dict-delete! cache url) entry)))))) - -(define preload-cache-set :effects [mutation] - (fn ((cache :as dict) (url :as string) (text :as string) (content-type :as string)) - ;; Store a preloaded response - (dict-set! cache url +(define + preload-cache-set + :effects (mutation) + (fn + ((cache :as dict) + (url :as string) + (text :as string) + (content-type :as string)) + (dict-set! + cache + url (dict "text" text "content-type" content-type "timestamp" (now-ms))))) - -;; -------------------------------------------------------------------------- -;; Trigger dispatch table -;; -------------------------------------------------------------------------- -;; Maps trigger event names to binding strategies. -;; This is the logic; actual browser event binding is platform interface. - -(define classify-trigger :effects [] - (fn ((trigger :as dict)) - ;; Classify a parsed trigger descriptor for binding. - ;; Returns one of: "poll", "intersect", "load", "revealed", "event" - (let ((event (get trigger "event"))) +(define + classify-trigger + :effects () + (fn + ((trigger :as dict)) + (let + ((event (get trigger "event"))) (cond - (= event "every") "poll" - (= event "intersect") "intersect" - (= event "load") "load" - (= event "revealed") "revealed" - :else "event")))) + (= event "every") + "poll" + (= event "intersect") + "intersect" + (= event "load") + "load" + (= event "revealed") + "revealed" + :else "event")))) +(define + should-boost-link? + :effects (io) + (fn + (link) + (let + ((href (dom-get-attr link "href"))) + (and + href + (not (starts-with? href "#")) + (not (starts-with? href "javascript:")) + (not (starts-with? href "mailto:")) + (browser-same-origin? href) + (not (dom-has-attr? link "sx-get")) + (not (dom-has-attr? link "sx-post")) + (not (dom-has-attr? link "sx-disable")))))) -;; -------------------------------------------------------------------------- -;; Boost logic -;; -------------------------------------------------------------------------- +(define + should-boost-form? + :effects (io) + (fn + (form) + (and + (not (dom-has-attr? form "sx-get")) + (not (dom-has-attr? form "sx-post")) + (not (dom-has-attr? form "sx-disable"))))) -(define should-boost-link? :effects [io] - (fn (link) - ;; Whether a link inside an sx-boost container should be boosted - (let ((href (dom-get-attr link "href"))) - (and href - (not (starts-with? href "#")) - (not (starts-with? href "javascript:")) - (not (starts-with? href "mailto:")) - (browser-same-origin? href) - (not (dom-has-attr? link "sx-get")) - (not (dom-has-attr? link "sx-post")) - (not (dom-has-attr? link "sx-disable")))))) - - -(define should-boost-form? :effects [io] - (fn (form) - ;; Whether a form inside an sx-boost container should be boosted - (and (not (dom-has-attr? form "sx-get")) - (not (dom-has-attr? form "sx-post")) - (not (dom-has-attr? form "sx-disable"))))) - - -;; -------------------------------------------------------------------------- -;; SSE event classification -;; -------------------------------------------------------------------------- - -(define parse-sse-swap :effects [io] - (fn (el) - ;; Parse sx-sse-swap attribute - ;; Returns event name to listen for (default "message") - (or (dom-get-attr el "sx-sse-swap") "message"))) - - -;; -------------------------------------------------------------------------- -;; Platform interface — Engine (pure logic) -;; -------------------------------------------------------------------------- -;; -;; From adapter-dom.sx: -;; dom-get-attr, dom-set-attr, dom-remove-attr, dom-has-attr?, dom-attr-list -;; dom-query, dom-query-all, dom-id, dom-parent, dom-first-child, -;; dom-next-sibling, dom-child-list, dom-node-type, dom-node-name, -;; dom-text-content, dom-set-text-content, dom-is-fragment?, -;; dom-is-child-of?, dom-is-active-element?, dom-is-input-element?, -;; dom-create-element, dom-append, dom-prepend, dom-insert-before, -;; dom-insert-after, dom-remove-child, dom-replace-child, dom-clone, -;; dom-get-style, dom-set-style, dom-get-prop, dom-set-prop, -;; dom-add-class, dom-remove-class, dom-set-inner-html, -;; dom-insert-adjacent-html -;; -;; Browser/Network: -;; (browser-location-href) → current URL string -;; (browser-same-origin? url) → boolean -;; (browser-push-state url) → void (history.pushState) -;; (browser-replace-state url) → void (history.replaceState) -;; -;; Parsing: -;; (parse-header-value s) → parsed dict from header string -;; (now-ms) → current timestamp in milliseconds -;; -------------------------------------------------------------------------- +(define + parse-sse-swap + :effects (io) + (fn (el) (or (dom-get-attr el "sx-sse-swap") "message")))