

(import (web boot-helpers))
(import (sx dom))
(import (sx browser))
(import (web adapter-dom))
(import (web engine))

(define-library
  (web orchestration)
  (export
    _preload-cache
    dispatch-trigger-events
    execute-request
    do-fetch
    handle-fetch-success
    flush-collected-styles
    handle-sx-response
    handle-html-response
    handle-retry
    bind-triggers
    bind-event
    post-swap
    process-settle-hooks
    activate-scripts
    process-oob-swaps
    hoist-head-elements
    process-boosted
    boost-descendants
    _page-data-cache
    _page-data-cache-ttl
    page-data-cache-key
    page-data-cache-get
    page-data-cache-set
    invalidate-page-cache
    invalidate-all-page-cache
    update-page-cache
    process-cache-directives
    _optimistic-snapshots
    optimistic-cache-update
    optimistic-cache-revert
    optimistic-cache-confirm
    submit-mutation
    _is-online
    _offline-queue
    offline-is-online?
    offline-set-online!
    offline-queue-mutation
    offline-sync
    offline-pending-count
    offline-aware-mutation
    current-page-layout
    swap-rendered-content
    resolve-route-target
    deps-satisfied?
    try-client-route
    bind-client-route-link
    process-sse
    bind-sse
    bind-sse-swap
    bind-inline-handlers
    bind-preload-for
    do-preload
    VERB_SELECTOR
    process-elements
    process-one
    process-emit-elements
    save-scroll-position
    handle-popstate
    engine-init)
  (begin
    (define _preload-cache (dict))
    (define
      dispatch-trigger-events
      :effects (mutation io)
      (fn
        (el (header-val :as string))
        (when
          header-val
          (let
            ((parsed (try-parse-json header-val)))
            (if
              parsed
              (for-each
                (fn
                  ((key :as string))
                  (dom-dispatch el key (get parsed key)))
                (keys parsed))
              (for-each
                (fn
                  ((name :as string))
                  (let
                    ((trimmed (trim name)))
                    (when
                      (not (empty? trimmed))
                      (dom-dispatch el trimmed (dict)))))
                (split header-val ",")))))))
    (define
      execute-request
      :effects (mutation io)
      (fn
        (el (verbInfo :as dict) (extraParams :as dict))
        (let
          ((info (or (get-verb-info el) verbInfo)))
          (if
            (nil? info)
            (promise-resolve nil)
            (let
              ((verb (get info "method")) (url (get info "url")))
              (if
                (let
                  ((media (dom-get-attr el "sx-media")))
                  (and media (not (browser-media-matches? media))))
                (promise-resolve nil)
                (if
                  (let
                    ((confirm-msg (dom-get-attr el "sx-confirm")))
                    (and confirm-msg (not (browser-confirm confirm-msg))))
                  (promise-resolve nil)
                  (let
                    ((prompt-msg (dom-get-attr el "sx-prompt"))
                      (prompt-val
                        (if prompt-msg (browser-prompt prompt-msg) nil)))
                    (if
                      (and prompt-msg (nil? prompt-val))
                      (promise-resolve nil)
                      (if
                        (or
                          (nil? verb)
                          (nil? url)
                          (not (validate-for-request el)))
                        (promise-resolve nil)
                        (do-fetch
                          el
                          verb
                          verb
                          url
                          (if
                            prompt-val
                            (assoc
                              (or extraParams (dict))
                              "SX-Prompt"
                              prompt-val)
                            extraParams))))))))))))
    (define
      do-fetch
      :effects (mutation io)
      (fn
        (el
          (verb :as string)
          (method :as string)
          (url :as string)
          (extraParams :as dict))
        (let
          ((sync (dom-get-attr el "sx-sync")))
          (when (= sync "replace") (abort-previous el))
          (let
            ((target-el (resolve-target el)))
            (when
              (and target-el (not (identical? el target-el)))
              (abort-previous-target target-el)))
          (let
            ((ctrl (new-abort-controller)))
            (track-controller el ctrl)
            (let
              ((target-el (resolve-target el)))
              (when target-el (track-controller-target target-el ctrl)))
            (let
              ((body-info (build-request-body el method url))
                (final-url (get body-info "url"))
                (body (get body-info "body"))
                (ct (get body-info "content-type"))
                (headers (build-request-headers el (loaded-component-names)))
                (csrf (csrf-token)))
              (when
                extraParams
                (for-each
                  (fn
                    ((k :as string))
                    (dict-set! headers k (get extraParams k)))
                  (keys extraParams)))
              (when ct (dict-set! headers "Content-Type" ct))
              (when csrf (dict-set! headers "X-CSRFToken" csrf))
              (let
                ((cached (preload-cache-get _preload-cache final-url))
                  (optimistic-state (apply-optimistic el))
                  (indicator (show-indicator el))
                  (disabled-elts (disable-elements el)))
                (dom-add-class el "sx-request")
                (dom-set-attr el "aria-busy" "true")
                (dom-dispatch
                  el
                  "sx:beforeRequest"
                  (dict "url" final-url "method" method))
                (fetch-request
                  (dict
                    "url"
                    final-url
                    "method"
                    method
                    "headers"
                    headers
                    "body"
                    body
                    "signal"
                    (controller-signal ctrl)
                    "cross-origin"
                    (cross-origin? final-url)
                    "preloaded"
                    cached)
                  (fn
                    ((resp-ok :as boolean)
                      (status :as number)
                      get-header
                      (text :as string))
                    (do
                      (clear-loading-state el indicator disabled-elts)
                      (revert-optimistic optimistic-state)
                      (if
                        (not resp-ok)
                        (do
                          (dom-dispatch
                            el
                            "sx:responseError"
                            (dict "status" status "text" text))
                          (if
                            (and text (> (len text) 0))
                            (handle-fetch-success
                              el
                              final-url
                              verb
                              extraParams
                              get-header
                              text)
                            (handle-retry el verb method final-url extraParams)))
                        (do
                          (dom-dispatch
                            el
                            "sx:afterRequest"
                            (dict "status" status))
                          (handle-fetch-success
                            el
                            final-url
                            verb
                            extraParams
                            get-header
                            text)))))
                  (fn
                    (err)
                    (do
                      (clear-loading-state el indicator disabled-elts)
                      (revert-optimistic optimistic-state)
                      (when
                        (not (abort-error? err))
                        (log-warn
                          (str
                            "sx:fetch error "
                            method
                            " "
                            final-url
                            " — "
                            err))
                        (dom-dispatch el "sx:requestError" (dict "error" err))))))))))))
    (define
      handle-fetch-success
      :effects (mutation io)
      (fn
        (el
          (url :as string)
          (verb :as string)
          (extraParams :as dict)
          get-header
          (text :as string))
        (let
          ((resp-headers (process-response-headers get-header)))
          (dispatch-trigger-events el (get resp-headers "trigger"))
          (process-cache-directives el resp-headers text)
          (cond
            (get resp-headers "redirect")
            (browser-navigate (get resp-headers "redirect"))
            (get resp-headers "refresh")
            (browser-reload)
            (get resp-headers "location")
            (fetch-location (get resp-headers "location"))
            :else (let
              ((target-el (if (get resp-headers "retarget") (dom-query (get resp-headers "retarget")) (resolve-target el)))
                (swap-spec
                  (parse-swap-spec
                    (or
                      (get resp-headers "reswap")
                      (dom-get-attr el "sx-swap"))
                    (dom-has-class? (dom-body) "sx-transitions")))
                (swap-style (get swap-spec "style"))
                (use-transition (get swap-spec "transition"))
                (ct (or (get resp-headers "content-type") "")))
              (if
                (contains? ct "text/sx")
                (handle-sx-response
                  el
                  target-el
                  text
                  swap-style
                  use-transition)
                (handle-html-response
                  el
                  target-el
                  text
                  swap-style
                  use-transition))
              (dispatch-trigger-events el (get resp-headers "trigger-swap"))
              (handle-history el url resp-headers)
              (set-timeout
                (fn
                  ()
                  (when
                    (get resp-headers "trigger-settle")
                    (dispatch-trigger-events
                      el
                      (get resp-headers "trigger-settle")))
                  (process-settle-hooks el))
                20)
              (dom-dispatch
                el
                "sx:afterSwap"
                (dict "target" target-el "swap" swap-style)))))))
    (define
      flush-collected-styles
      :effects (mutation io)
      (fn
        ()
        (let
          ((rules (collected "cssx")))
          (when
            (not (empty? rules))
            (clear-collected! "cssx")
            (let
              ((el (dom-create-element "style" nil)))
              (dom-set-attr el "data-sx-css" "true")
              (dom-set-prop el "textContent" (join "" rules))
              (dom-append-to-head el))))))
    (define
      handle-sx-response
      :effects (mutation io)
      (fn
        (el
          target
          (text :as string)
          (swap-style :as string)
          (use-transition :as boolean))
        (let
          ((cleaned (strip-component-scripts text)))
          (let
            ((final (extract-response-css cleaned)))
            (let
              ((trimmed (trim final)))
              (when
                (not (empty? trimmed))
                (let
                  ((rendered (sx-render trimmed))
                    (container (dom-create-element "div" nil)))
                  (dom-append container rendered)
                  (process-oob-swaps
                    container
                    (fn
                      (t oob (s :as string))
                      (dispose-islands-in t)
                      (swap-dom-nodes
                        t
                        (if (= s "innerHTML") (children-to-fragment oob) oob)
                        s)
                      (post-swap t)))
                  (hoist-head-elements container)
                  (let
                    ((select-sel (dom-get-attr el "sx-select")))
                    (let
                      ((content (if select-sel (select-from-container container select-sel) (children-to-fragment container))))
                      (dispose-islands-in target)
                      (with-transition
                        use-transition
                        (fn
                          ()
                          (let
                            ((swap-result (swap-dom-nodes target content swap-style)))
                            (post-swap
                              (if
                                (= swap-style "outerHTML")
                                (dom-parent (or swap-result target))
                                (or swap-result target)))))))))))))))
    (define
      handle-html-response
      :effects (mutation io)
      (fn
        (el
          target
          (text :as string)
          (swap-style :as string)
          (use-transition :as boolean))
        (let
          ((doc (dom-parse-html-document text)))
          (when
            doc
            (let
              ((select-sel (dom-get-attr el "sx-select")))
              (dispose-islands-in target)
              (if
                select-sel
                (let
                  ((container (dom-create-element "div" nil)))
                  (dom-set-inner-html container (dom-body-inner-html doc))
                  (process-oob-swaps
                    container
                    (fn
                      (t oob (s :as string))
                      (dispose-islands-in t)
                      (swap-dom-nodes t oob s)
                      (post-swap t)))
                  (hoist-head-elements container)
                  (let
                    ((manifest-el (dom-query-in container "script[data-sx-manifest]")))
                    (when
                      manifest-el
                      (host-call
                        (host-global "Sx")
                        "mergeManifest"
                        manifest-el)))
                  (let
                    ((html (select-from-container container select-sel)))
                    (with-transition
                      use-transition
                      (fn
                        ()
                        (let
                          ((swap-root (swap-dom-nodes target html swap-style)))
                          (log-info
                            (str
                              "swap-root: "
                              (if swap-root (dom-tag-name swap-root) "nil")
                              " target: "
                              (dom-tag-name target)))
                          (post-swap (or swap-root target)))))))
                (let
                  ((container (dom-create-element "div" nil)))
                  (dom-set-inner-html container (dom-body-inner-html doc))
                  (process-oob-swaps
                    container
                    (fn
                      (t oob (s :as string))
                      (dispose-islands-in t)
                      (swap-dom-nodes t oob s)
                      (post-swap t)))
                  (hoist-head-elements container)
                  (with-transition
                    use-transition
                    (fn
                      ()
                      (swap-dom-nodes
                        target
                        (children-to-fragment container)
                        swap-style)
                      (post-swap target))))))))))
    (define
      handle-retry
      :effects (mutation io)
      (fn
        (el
          (verb :as string)
          (method :as string)
          (url :as string)
          (extraParams :as dict))
        (let
          ((retry-attr (dom-get-attr el "sx-retry"))
            (spec (parse-retry-spec retry-attr)))
          (when
            spec
            (let
              ((current-ms (or (dom-get-attr el "data-sx-retry-ms") (get spec "start-ms"))))
              (let
                ((ms (parse-int current-ms (get spec "start-ms"))))
                (dom-set-attr
                  el
                  "data-sx-retry-ms"
                  (str (next-retry-ms ms (get spec "cap-ms"))))
                (set-timeout
                  (fn () (do-fetch el verb method url extraParams))
                  ms)))))))
    (define
      bind-triggers
      :effects (mutation io)
      (fn
        (el (verbInfo :as dict))
        (let
          ((triggers (or (parse-trigger-spec (dom-get-attr el "sx-trigger")) (default-trigger (dom-tag-name el)))))
          (for-each
            (fn
              ((trigger :as dict))
              (let
                ((kind (classify-trigger trigger))
                  (mods (get trigger "modifiers")))
                (cond
                  (= kind "poll")
                  (let
                    ((interval-id nil))
                    (set!
                      interval-id
                      (set-interval
                        (fn
                          ()
                          (if
                            (host-get el "isConnected")
                            (execute-request el nil nil)
                            (do
                              (clear-interval interval-id)
                              (log-info "poll stopped: element removed"))))
                        (get mods "interval"))))
                  (= kind "intersect")
                  (observe-intersection
                    el
                    (fn () (execute-request el nil nil))
                    true
                    (get mods "delay"))
                  (= kind "load")
                  (set-timeout
                    (fn () (execute-request el nil nil))
                    (or (get mods "delay") 0))
                  (= kind "revealed")
                  (observe-intersection
                    el
                    (fn () (execute-request el nil nil))
                    false
                    (get mods "delay"))
                  (= kind "event")
                  (bind-event el (get trigger "event") mods verbInfo))))
            triggers))))
    (define
      bind-event
      :effects (mutation io)
      (fn
        (el (event-name :as string) (mods :as dict) (verbInfo :as dict))
        (let
          ((timer nil)
            (last-val nil)
            (listen-target
              (let
                ((from-sel (get mods "from")))
                (cond
                  (nil? from-sel)
                  el
                  (= from-sel "body")
                  (dom-body)
                  (= from-sel "document")
                  (dom-document)
                  (= from-sel "window")
                  (dom-window)
                  :else (dom-query from-sel)))))
          (when
            listen-target
            (dom-add-listener
              listen-target
              event-name
              (fn
                (e)
                (let
                  ((should-fire (if (get mods "filter") (let ((f (get mods "filter"))) (let ((key-match (index-of f "key=='"))) (if (>= key-match 0) (let ((key-char (slice f (+ key-match 5) (+ key-match 6)))) (and (= (host-get e "key") key-char) (not (dom-matches? (host-get e "target") "input,textarea,select")))) true))) true)))
                  (when
                    (get mods "changed")
                    (let
                      ((val element-value))
                      (if
                        (= val last-val)
                        (set! should-fire false)
                        (set! last-val val))))
                  (when
                    (and
                      should-fire
                      (not
                        (and (= event-name "click") (event-modifier-key? e))))
                    (when
                      (or
                        (= event-name "submit")
                        (and
                          (= event-name "click")
                          (dom-has-attr? el "href")))
                      (prevent-default e))
                    (let
                      ((live-info (get-verb-info el))
                        (is-get-link
                          (and
                            (= event-name "click")
                            (= (get live-info "method") "GET")
                            (dom-has-attr? el "href")
                            (not (get mods "delay"))))
                        (client-routed false))
                      (when
                        is-get-link
                        (set!
                          client-routed
                          (try-client-route
                            (url-pathname (get live-info "url"))
                            (dom-get-attr el "sx-target"))))
                      (if
                        client-routed
                        (do
                          (save-scroll-position)
                          (browser-push-state (get live-info "url"))
                          (browser-scroll-to 0 0))
                        (do
                          (when
                            is-get-link
                            (log-info
                              (str
                                "sx:route server fetch "
                                (get live-info "url"))))
                          (if
                            (get mods "delay")
                            (do
                              (clear-timeout timer)
                              (set!
                                timer
                                (set-timeout
                                  (fn () (execute-request el nil nil))
                                  (get mods "delay"))))
                            (execute-request el nil nil))))))))
              (if (get mods "once") (dict "once" true) nil))))))
    (define
      post-swap
      :effects (mutation io)
      (fn
        (root)
        (log-info
          (str "post-swap: root=" (if root (dom-tag-name root) "nil")))
        (activate-scripts root)
        (sx-process-scripts root)
        (sx-hydrate root)
        (sx-hydrate-islands root)
        (run-post-render-hooks)
        (flush-collected-styles)
        (process-elements root)))
    (define
      process-settle-hooks
      :effects (mutation io)
      (fn
        (el)
        (let
          ((settle-expr (dom-get-attr el "sx-on-settle")))
          (when
            (and settle-expr (not (empty? settle-expr)))
            (let
              ((exprs (sx-parse settle-expr)))
              (for-each (fn (expr) (cek-eval expr)) exprs))))))
    (define
      activate-scripts
      :effects (mutation io)
      (fn
        (root)
        (when
          root
          (let
            ((scripts (dom-query-all root "script")))
            (for-each
              (fn
                (dead)
                (when
                  (and
                    (not (dom-has-attr? dead "data-components"))
                    (not (dom-has-attr? dead "data-sx-activated")))
                  (let
                    ((live (create-script-clone dead)))
                    (dom-set-attr live "data-sx-activated" "true")
                    (dom-replace-child (dom-parent dead) live dead))))
              scripts)))))
    (define
      process-oob-swaps
      :effects (mutation io)
      (fn
        (container (swap-fn :as lambda))
        (let
          ((oobs (find-oob-swaps container)))
          (for-each
            (fn
              ((oob :as dict))
              (let
                ((target-id (get oob "target-id"))
                  (target (dom-query-by-id target-id))
                  (oob-el (get oob "element"))
                  (swap-type (get oob "swap-type")))
                (when
                  (dom-parent oob-el)
                  (dom-remove-child (dom-parent oob-el) oob-el))
                (when target (swap-fn target oob-el swap-type))))
            oobs))))
    (define
      hoist-head-elements
      :effects (mutation io)
      (fn
        (container)
        (for-each
          (fn
            (style)
            (when
              (dom-parent style)
              (dom-remove-child (dom-parent style) style))
            (dom-append-to-head style))
          (dom-query-all container "style[data-sx-css]"))
        (for-each
          (fn
            (link)
            (when
              (dom-parent link)
              (dom-remove-child (dom-parent link) link))
            (dom-append-to-head link))
          (dom-query-all container "link[rel=\"stylesheet\"]"))))
    (define
      process-boosted
      :effects (mutation io)
      (fn
        (root)
        (for-each
          (fn (container) (boost-descendants container))
          (dom-query-all (or root (dom-body)) "[sx-boost]"))))
    (define
      boost-descendants
      :effects (mutation io)
      (fn
        (container)
        (let
          ((boost-target (dom-get-attr container "sx-boost")))
          (for-each
            (fn
              (link)
              (when
                (and
                  (not (is-processed? link "boost"))
                  (should-boost-link? link))
                (mark-processed! link "boost")
                (when
                  (and
                    (not (dom-has-attr? link "sx-target"))
                    boost-target
                    (not (= boost-target "true")))
                  (dom-set-attr link "sx-target" boost-target))
                (when
                  (not (dom-has-attr? link "sx-swap"))
                  (dom-set-attr link "sx-swap" "innerHTML"))
                (when
                  (not (dom-has-attr? link "sx-push-url"))
                  (dom-set-attr link "sx-push-url" "true"))
                (bind-client-route-link link (dom-get-attr link "href"))))
            (dom-query-all container "a[href]"))
          (for-each
            (fn
              (form)
              (when
                (and
                  (not (is-processed? form "boost"))
                  (should-boost-form? form))
                (mark-processed! form "boost")
                (let
                  ((method (upper (or (dom-get-attr form "method") "GET")))
                    (action
                      (or
                        (dom-get-attr form "action")
                        (browser-location-href))))
                  (when
                    (and
                      (not (dom-has-attr? form "sx-target"))
                      boost-target
                      (not (= boost-target "true")))
                    (dom-set-attr form "sx-target" boost-target))
                  (when
                    (not (dom-has-attr? form "sx-swap"))
                    (dom-set-attr form "sx-swap" "innerHTML"))
                  (bind-boost-form form method action))))
            (dom-query-all container "form")))))
    (define _page-data-cache (dict))
    (define _page-data-cache-ttl 30000)
    (define
      page-data-cache-key
      :effects ()
      (fn
        ((page-name :as string) (params :as dict))
        (let
          ((base page-name))
          (if
            (or (nil? params) (empty? (keys params)))
            base
            (let
              ((parts (list)))
              (for-each
                (fn
                  ((k :as string))
                  (append! parts (str k "=" (get params k))))
                (keys params))
              (str base ":" (join "&" parts)))))))
    (define
      page-data-cache-get
      :effects (mutation io)
      (fn
        ((cache-key :as string))
        (let
          ((entry (get _page-data-cache cache-key)))
          (if
            (nil? entry)
            nil
            (if
              (> (- (now-ms) (get entry "ts")) _page-data-cache-ttl)
              (do (dict-set! _page-data-cache cache-key nil) nil)
              (get entry "data"))))))
    (define
      page-data-cache-set
      :effects (mutation io)
      (fn
        ((cache-key :as string) data)
        (dict-set! _page-data-cache cache-key {:data data :ts (now-ms)})))
    (define
      invalidate-page-cache
      :effects (mutation io)
      (fn
        ((page-name :as string))
        (for-each
          (fn
            ((k :as string))
            (when
              (or (= k page-name) (starts-with? k (str page-name ":")))
              (dict-set! _page-data-cache k nil)))
          (keys _page-data-cache))
        (sw-post-message {:type "invalidate" :page page-name})
        (log-info (str "sx:cache invalidate " page-name))))
    (define
      invalidate-all-page-cache
      :effects (mutation io)
      (fn
        ()
        (set! _page-data-cache (dict))
        (sw-post-message {:type "invalidate" :page "*"})
        (log-info "sx:cache invalidate *")))
    (define
      update-page-cache
      :effects (mutation io)
      (fn
        ((page-name :as string) data)
        (let
          ((cache-key (page-data-cache-key page-name (dict))))
          (page-data-cache-set cache-key data)
          (log-info (str "sx:cache update " page-name)))))
    (define
      process-cache-directives
      :effects (mutation io)
      (fn
        (el (resp-headers :as dict) (response-text :as string))
        (let
          ((el-invalidate (dom-get-attr el "sx-cache-invalidate")))
          (when
            el-invalidate
            (if
              (= el-invalidate "*")
              (invalidate-all-page-cache)
              (invalidate-page-cache el-invalidate))))
        (let
          ((hdr-invalidate (get resp-headers "cache-invalidate")))
          (when
            hdr-invalidate
            (if
              (= hdr-invalidate "*")
              (invalidate-all-page-cache)
              (invalidate-page-cache hdr-invalidate))))
        (let
          ((hdr-update (get resp-headers "cache-update")))
          (when
            hdr-update
            (let
              ((data (parse-sx-data response-text)))
              (when data (update-page-cache hdr-update data)))))))
    (define _optimistic-snapshots (dict))
    (define
      optimistic-cache-update
      :effects (mutation)
      (fn
        ((cache-key :as string) (mutator :as lambda))
        (let
          ((cached (page-data-cache-get cache-key)))
          (when
            cached
            (let
              ((predicted (mutator cached)))
              (dict-set! _optimistic-snapshots cache-key cached)
              (page-data-cache-set cache-key predicted)
              predicted)))))
    (define
      optimistic-cache-revert
      :effects (mutation)
      (fn
        ((cache-key :as string))
        (let
          ((snapshot (get _optimistic-snapshots cache-key)))
          (when
            snapshot
            (page-data-cache-set cache-key snapshot)
            (dict-delete! _optimistic-snapshots cache-key)
            snapshot))))
    (define
      optimistic-cache-confirm
      :effects (mutation)
      (fn
        ((cache-key :as string))
        (dict-delete! _optimistic-snapshots cache-key)))
    (define
      submit-mutation
      :effects (mutation io)
      (fn
        ((page-name :as string)
          (params :as dict)
          (action-name :as string)
          payload
          (mutator-fn :as lambda)
          (on-complete :as lambda))
        (let
          ((cache-key (page-data-cache-key page-name params))
            (predicted (optimistic-cache-update cache-key mutator-fn)))
          (when predicted (try-rerender-page page-name params predicted))
          (execute-action
            action-name
            payload
            (fn
              (result)
              (when result (page-data-cache-set cache-key result))
              (optimistic-cache-confirm cache-key)
              (when result (try-rerender-page page-name params result))
              (log-info (str "sx:optimistic confirmed " page-name))
              (when on-complete (on-complete "confirmed")))
            (fn
              ((error :as string))
              (let
                ((reverted (optimistic-cache-revert cache-key)))
                (when reverted (try-rerender-page page-name params reverted))
                (log-warn
                  (str "sx:optimistic reverted " page-name ": " error))
                (when on-complete (on-complete "reverted"))))))))
    (define _is-online true)
    (define _offline-queue (list))
    (define offline-is-online? :effects (io) (fn () _is-online))
    (define
      offline-set-online!
      :effects (mutation)
      (fn ((val :as boolean)) (set! _is-online val)))
    (define
      offline-queue-mutation
      :effects (mutation io)
      (fn
        ((action-name :as string)
          payload
          (page-name :as string)
          (params :as dict)
          (mutator-fn :as lambda))
        (let
          ((cache-key (page-data-cache-key page-name params))
            (entry
              (dict
                "action"
                action-name
                "payload"
                payload
                "page"
                page-name
                "params"
                params
                "timestamp"
                (now-ms)
                "status"
                "pending")))
          (append! _offline-queue entry)
          (let
            ((predicted (optimistic-cache-update cache-key mutator-fn)))
            (when predicted (try-rerender-page page-name params predicted)))
          (log-info
            (str
              "sx:offline queued "
              action-name
              " ("
              (len _offline-queue)
              " pending)"))
          entry)))
    (define
      offline-sync
      :effects (mutation io)
      (fn
        ()
        (let
          ((pending (filter (fn ((e :as dict)) (= (get e "status") "pending")) _offline-queue)))
          (when
            (not (empty? pending))
            (log-info (str "sx:offline syncing " (len pending) " mutations"))
            (for-each
              (fn
                ((entry :as dict))
                (execute-action
                  (get entry "action")
                  (get entry "payload")
                  (fn
                    (result)
                    (dict-set! entry "status" "synced")
                    (log-info
                      (str "sx:offline synced " (get entry "action"))))
                  (fn
                    ((error :as string))
                    (dict-set! entry "status" "failed")
                    (log-warn
                      (str
                        "sx:offline sync failed "
                        (get entry "action")
                        ": "
                        error)))))
              pending)))))
    (define
      offline-pending-count
      :effects (io)
      (fn
        ()
        (len
          (filter
            (fn ((e :as dict)) (= (get e "status") "pending"))
            _offline-queue))))
    (define
      offline-aware-mutation
      :effects (mutation io)
      (fn
        ((page-name :as string)
          (params :as dict)
          (action-name :as string)
          payload
          (mutator-fn :as lambda)
          (on-complete :as lambda))
        (if
          _is-online
          (submit-mutation
            page-name
            params
            action-name
            payload
            mutator-fn
            on-complete)
          (do
            (offline-queue-mutation
              action-name
              payload
              page-name
              params
              mutator-fn)
            (when on-complete (on-complete "queued"))))))
    (define
      current-page-layout
      :effects (io)
      (fn
        ()
        (let
          ((pathname (url-pathname (browser-location-href)))
            (match (find-matching-route pathname _page-routes)))
          (if (nil? match) "" (or (get match "layout") "")))))
    (define
      swap-rendered-content
      :effects (mutation io)
      (fn
        (target rendered (pathname :as string))
        (let
          ((container (dom-create-element "div" nil)))
          (dom-append container rendered)
          (process-oob-swaps
            container
            (fn
              (t oob (s :as string))
              (dispose-islands-in t)
              (swap-dom-nodes
                t
                (if (= s "innerHTML") (children-to-fragment oob) oob)
                s)
              (post-swap t)))
          (let
            ((target-id (dom-get-attr target "id")))
            (let
              ((inner (if target-id (dom-query container (str "#" target-id)) nil)))
              (let
                ((content (if inner (children-to-fragment inner) (children-to-fragment container))))
                (dispose-islands-in target)
                (dom-set-text-content target "")
                (dom-append target content)
                (hoist-head-elements-full target)
                (process-elements target)
                (sx-hydrate-elements target)
                (sx-hydrate-islands target)
                (run-post-render-hooks)
                (dom-dispatch
                  target
                  "sx:clientRoute"
                  (dict "pathname" pathname))
                (log-info (str "sx:route client " pathname))))))))
    (define
      resolve-route-target
      :effects (io)
      (fn
        ((target-sel :as string))
        (if
          (and target-sel (not (= target-sel "true")))
          (dom-query target-sel)
          nil)))
    (define
      deps-satisfied?
      :effects (io)
      (fn
        ((match :as dict))
        (let
          ((deps (get match "deps")) (loaded (loaded-component-names)))
          (if
            (nil? deps)
            false
            (if
              (empty? deps)
              true
              (every? (fn ((dep :as string)) (contains? loaded dep)) deps))))))
    (define
      try-client-route
      :effects (mutation io)
      (fn
        ((pathname :as string) (target-sel :as string))
        (let
          ((match (find-matching-route pathname _page-routes)))
          (if
            (nil? match)
            (do
              (log-info
                (str
                  "sx:route no match ("
                  (len _page-routes)
                  " routes) "
                  pathname))
              false)
            (let
              ((target-layout (or (get match "layout") ""))
                (cur-layout (current-page-layout)))
              (if
                (not (= target-layout cur-layout))
                (do
                  (log-info
                    (str
                      "sx:route server (layout: "
                      cur-layout
                      " -> "
                      target-layout
                      ") "
                      pathname))
                  false)
                (let
                  ((content-src (get match "content"))
                    (closure (or (get match "closure") {}))
                    (params (get match "params"))
                    (page-name (get match "name")))
                  (if
                    (or (nil? content-src) (empty? content-src))
                    (do
                      (log-warn (str "sx:route no content for " pathname))
                      false)
                    (let
                      ((target (resolve-route-target target-sel)))
                      (if
                        (nil? target)
                        (do
                          (log-warn
                            (str "sx:route target not found: " target-sel))
                          false)
                        (if
                          (not (deps-satisfied? match))
                          (do
                            (log-info
                              (str "sx:route deps miss for " page-name))
                            false)
                          (let
                            ((io-deps (get match "io-deps"))
                              (has-io (and io-deps (not (empty? io-deps))))
                              (render-plan (get match "render-plan")))
                            (when
                              render-plan
                              (let
                                ((srv (or (get render-plan "server") (list)))
                                  (cli
                                    (or (get render-plan "client") (list))))
                                (log-info
                                  (str
                                    "sx:route plan "
                                    page-name
                                    " — "
                                    (len srv)
                                    " server, "
                                    (len cli)
                                    " client"))))
                            (when has-io (register-io-deps io-deps))
                            (if
                              (get match "stream")
                              (do
                                (log-info
                                  (str "sx:route streaming " pathname))
                                (fetch-streaming
                                  target
                                  pathname
                                  (build-request-headers
                                    target
                                    (loaded-component-names)))
                                true)
                              (if
                                (get match "has-data")
                                (let
                                  ((cache-key (page-data-cache-key page-name params))
                                    (cached (page-data-cache-get cache-key)))
                                  (if
                                    cached
                                    (let
                                      ((env (merge closure params cached)))
                                      (if
                                        has-io
                                        (do
                                          (log-info
                                            (str
                                              "sx:route client+cache+async "
                                              pathname))
                                          (try-async-eval-content
                                            content-src
                                            env
                                            (fn
                                              (rendered)
                                              (if
                                                (nil? rendered)
                                                (do
                                                  (log-warn
                                                    (str
                                                      "sx:route cache+async eval failed for "
                                                      pathname
                                                      " — server fallback"))
                                                  (fetch-and-restore
                                                    target
                                                    pathname
                                                    (build-request-headers
                                                      target
                                                      (loaded-component-names))
                                                    0))
                                                (swap-rendered-content
                                                  target
                                                  rendered
                                                  pathname))))
                                          true)
                                        (let
                                          ((rendered (try-eval-content content-src env)))
                                          (if
                                            (nil? rendered)
                                            (do
                                              (log-warn
                                                (str
                                                  "sx:route cached eval failed for "
                                                  pathname))
                                              false)
                                            (do
                                              (log-info
                                                (str
                                                  "sx:route client+cache "
                                                  pathname))
                                              (swap-rendered-content
                                                target
                                                rendered
                                                pathname)
                                              true)))))
                                    (do
                                      (log-info
                                        (str "sx:route client+data " pathname))
                                      (resolve-page-data
                                        page-name
                                        params
                                        (fn
                                          ((data :as dict))
                                          (page-data-cache-set cache-key data)
                                          (let
                                            ((env (merge closure params data)))
                                            (if
                                              has-io
                                              (try-async-eval-content
                                                content-src
                                                env
                                                (fn
                                                  (rendered)
                                                  (if
                                                    (nil? rendered)
                                                    (do
                                                      (log-warn
                                                        (str
                                                          "sx:route data+async eval failed for "
                                                          pathname
                                                          " — server fallback"))
                                                      (fetch-and-restore
                                                        target
                                                        pathname
                                                        (build-request-headers
                                                          target
                                                          (loaded-component-names))
                                                        0))
                                                    (swap-rendered-content
                                                      target
                                                      rendered
                                                      pathname))))
                                              (let
                                                ((rendered (try-eval-content content-src env)))
                                                (if
                                                  (nil? rendered)
                                                  (do
                                                    (log-warn
                                                      (str
                                                        "sx:route data eval failed for "
                                                        pathname
                                                        " — server fallback"))
                                                    (fetch-and-restore
                                                      target
                                                      pathname
                                                      (build-request-headers
                                                        target
                                                        (loaded-component-names))
                                                      0))
                                                  (swap-rendered-content
                                                    target
                                                    rendered
                                                    pathname)))))))
                                      true)))
                                (if
                                  has-io
                                  (do
                                    (log-info
                                      (str "sx:route client+async " pathname))
                                    (try-async-eval-content
                                      content-src
                                      (merge closure params)
                                      (fn
                                        (rendered)
                                        (if
                                          (nil? rendered)
                                          (do
                                            (log-warn
                                              (str
                                                "sx:route async eval failed for "
                                                pathname
                                                " — server fallback"))
                                            (fetch-and-restore
                                              target
                                              pathname
                                              (build-request-headers
                                                target
                                                (loaded-component-names))
                                              0))
                                          (swap-rendered-content
                                            target
                                            rendered
                                            pathname))))
                                    true)
                                  (let
                                    ((env (merge closure params))
                                      (rendered
                                        (try-eval-content content-src env)))
                                    (if
                                      (nil? rendered)
                                      (do
                                        (log-info
                                          (str
                                            "sx:route server (eval failed) "
                                            pathname))
                                        false)
                                      (do
                                        (swap-rendered-content
                                          target
                                          rendered
                                          pathname)
                                        true))))))))))))))))))
    (define
      bind-client-route-link
      :effects (mutation io)
      (fn
        (link (href :as string))
        (bind-client-route-click
          link
          href
          (fn () (bind-boost-link link href)))))
    (define
      process-sse
      :effects (mutation io)
      (fn
        (root)
        (for-each
          (fn
            (el)
            (when
              (not (is-processed? el "sse"))
              (mark-processed! el "sse")
              (bind-sse el)))
          (dom-query-all (or root (dom-body)) "[sx-sse]"))))
    (define
      bind-sse
      :effects (mutation io)
      (fn
        (el)
        (let
          ((url (dom-get-attr el "sx-sse")))
          (when
            url
            (let
              ((source (event-source-connect url el))
                (event-name (parse-sse-swap el)))
              (event-source-listen
                source
                event-name
                (fn ((data :as string)) (bind-sse-swap el data))))))))
    (define
      bind-sse-swap
      :effects (mutation io)
      (fn
        (el (data :as string))
        (let
          ((target (resolve-target el))
            (swap-spec
              (parse-swap-spec
                (dom-get-attr el "sx-swap")
                (dom-has-class? (dom-body) "sx-transitions")))
            (swap-style (get swap-spec "style"))
            (use-transition (get swap-spec "transition"))
            (trimmed (trim data)))
          (when
            (not (empty? trimmed))
            (dispose-islands-in target)
            (if
              (starts-with? trimmed "(")
              (let
                ((rendered (sx-render trimmed))
                  (container (dom-create-element "div" nil)))
                (dom-append container rendered)
                (with-transition
                  use-transition
                  (fn
                    ()
                    (swap-dom-nodes
                      target
                      (children-to-fragment container)
                      swap-style)
                    (post-swap target))))
              (with-transition
                use-transition
                (fn
                  ()
                  (swap-html-string target trimmed swap-style)
                  (post-swap target))))))))
    (define
      bind-inline-handlers
      :effects (mutation io)
      (fn
        (root)
        (for-each
          (fn
            (el)
            (for-each
              (fn
                ((attr :as list))
                (let
                  ((name (first attr)) (body (nth attr 1)))
                  (when
                    (and (string? name) (starts-with? name "sx-on:"))
                    (let
                      ((event-name (let ((raw (slice name 6))) (if (or (starts-with? raw "after") (starts-with? raw "before")) (str "sx:" raw) raw))))
                      (when
                        (not (is-processed? el (str "on:" event-name)))
                        (mark-processed! el (str "on:" event-name))
                        (dom-on
                          el
                          event-name
                          (if
                            (contains? body ".")
                            (fn
                              (e)
                              (host-call
                                (host-call
                                  (dom-window)
                                  "Function"
                                  "event"
                                  body)
                                "call"
                                el
                                e))
                            (let
                              ((exprs (parse body)))
                              (fn
                                (e)
                                (let
                                  ((handler-env (make-env)))
                                  (env-bind! handler-env "event" e)
                                  (env-bind! handler-env "this" el)
                                  (env-bind!
                                    handler-env
                                    "detail"
                                    (event-detail e))
                                  (for-each
                                    (fn (expr) (eval-expr expr handler-env))
                                    exprs)))))))))))
              (dom-attr-list el)))
          (dom-query-all (or root (dom-body)) "[sx-on\\:]"))))
    (define
      bind-preload-for
      :effects (mutation io)
      (fn
        (el)
        (let
          ((preload-attr (dom-get-attr el "sx-preload")))
          (when
            preload-attr
            (let
              ((events (if (= preload-attr "mousedown") (list "mousedown" "touchstart") (list "mouseover")))
                (debounce-ms (if (= preload-attr "mousedown") 0 100)))
              (bind-preload
                el
                events
                debounce-ms
                (fn
                  ()
                  (let
                    ((info (get-verb-info el)))
                    (when
                      info
                      (do-preload
                        (get info "url")
                        (build-request-headers el (loaded-component-names))))))))))))
    (define
      do-preload
      :effects (mutation io)
      (fn
        ((url :as string) (headers :as dict))
        (when
          (nil? (preload-cache-get _preload-cache url))
          (fetch-preload url headers _preload-cache))))
    (define
      VERB_SELECTOR
      (str "[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]"))
    (define
      process-elements
      :effects (mutation io)
      (fn
        (root)
        (let
          ((els (dom-query-all (or root (dom-body)) VERB_SELECTOR)))
          (for-each
            (fn
              (el)
              (when
                (not (is-processed? el "verb"))
                (process-one el)
                (mark-processed! el "verb")))
            els))
        (process-boosted root)
        (process-sse root)
        (bind-inline-handlers root)
        (process-emit-elements root)
        (hs-boot-subtree! (or root (host-global "document")))
        (htmx-boot-subtree! (or root (host-global "document")))))
    (define
      process-one
      :effects (mutation io)
      (fn
        (el)
        (let
          ((verb-info (get-verb-info el)))
          (when
            verb-info
            (when
              (not (dom-has-attr? el "sx-disable"))
              (do (bind-triggers el verb-info) (bind-preload-for el))
              (bind-preload-for el))))))
    (define
      process-emit-elements
      :effects (mutation io)
      (fn
        (root)
        (let
          ((els (dom-query-all (or root (dom-body)) "[data-sx-emit]")))
          (for-each
            (fn
              (el)
              (when
                (not (is-processed? el "emit"))
                (mark-processed! el "emit")
                (let
                  ((event-name (dom-get-attr el "data-sx-emit")))
                  (when
                    event-name
                    (dom-on
                      el
                      "click"
                      (fn
                        (e)
                        (let
                          ((detail-json (dom-get-attr el "data-sx-emit-detail"))
                            (detail
                              (if
                                detail-json
                                (json-parse detail-json)
                                (dict))))
                          (dom-dispatch el event-name detail))))))))
            els))))
    (define
      save-scroll-position
      :effects (io)
      (fn
        ()
        (let
          ((scrollY (host-get (dom-window) "scrollY")))
          (browser-replace-state
            (dict "scrollY" scrollY)
            ""
            (browser-location-href)))))
    (define
      handle-popstate
      :effects (mutation io)
      (fn
        (scrollY)
        (let
          ((url (browser-location-href))
            (boost-el (dom-query "[sx-boost]"))
            (target-sel
              (if
                boost-el
                (let
                  ((attr (dom-get-attr boost-el "sx-boost")))
                  (if (and attr (not (= attr "true"))) attr "#sx-content"))
                "#sx-content"))
            (target (dom-query target-sel))
            (pathname (url-pathname url)))
          (when
            target
            (when
              (not (try-client-route pathname target-sel))
              (let
                ((headers (build-request-headers target (loaded-component-names))))
                (fetch-and-restore target url headers scrollY)))))))
    (define
      engine-init
      :effects (mutation io)
      (fn
        ()
        (do
          (sx-process-scripts nil)
          (sx-hydrate nil)
          (process-elements nil)))))) ;; end define-library

;; Re-export to global namespace for backward compatibility
(import (web orchestration))
