Add :effects annotations to all spec files and update bootstrappers

Bootstrappers (bootstrap_py.py, js.sx) now skip :effects keyword in
define forms, enabling effect annotations throughout the spec without
changing generated output.

Annotated 180+ functions across 14 spec files:
- signals.sx: signal/deref [] pure, reset!/swap!/effect/batch [mutation]
- engine.sx: parse-* [] pure, morph-*/swap-* [mutation io]
- orchestration.sx: all [mutation io] (browser event binding)
- adapter-html.sx: render-* [render]
- adapter-dom.sx: render-* [render], reactive-* [render mutation]
- adapter-sx.sx: aser-* [render]
- adapter-async.sx: async-render-*/async-aser-* [render io]
- parser.sx: all [] pure
- render.sx: predicates [] pure, process-bindings [mutation]
- boot.sx: all [mutation io] (browser init)
- deps.sx: scan-*/transitive-* [] pure, compute-all-* [mutation]
- router.sx: all [] pure (URL matching)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 23:22:34 +00:00
parent 0f9b449315
commit 2f42e8826c
16 changed files with 274 additions and 259 deletions

View File

@@ -33,7 +33,7 @@
;; Event dispatch helpers
;; --------------------------------------------------------------------------
(define dispatch-trigger-events
(define dispatch-trigger-events :effects [mutation io]
(fn (el (header-val :as string))
;; Dispatch events from SX-Trigger / SX-Trigger-After-Swap headers.
;; Value can be JSON object (name → detail) or comma-separated names.
@@ -58,7 +58,7 @@
;; CSS tracking
;; --------------------------------------------------------------------------
(define init-css-tracking
(define init-css-tracking :effects [mutation io]
(fn ()
;; Read initial CSS hash from meta tag
(let ((meta (dom-query "meta[name=\"sx-css-classes\"]")))
@@ -72,7 +72,7 @@
;; Request execution
;; --------------------------------------------------------------------------
(define execute-request
(define execute-request :effects [mutation io]
(fn (el (verbInfo :as dict) (extraParams :as dict))
;; Gate checks then delegate to do-fetch.
;; verbInfo: dict with "method" and "url" (or nil to read from element).
@@ -105,7 +105,7 @@
extraParams))))))))))))
(define do-fetch
(define do-fetch :effects [mutation io]
(fn (el (verb :as string) (method :as string) (url :as string) (extraParams :as dict))
;; Execute the actual fetch. Manages abort, headers, body, loading state.
(let ((sync (dom-get-attr el "sx-sync")))
@@ -201,7 +201,7 @@
(dict "error" err))))))))))))
(define handle-fetch-success
(define handle-fetch-success :effects [mutation io]
(fn (el (url :as string) (verb :as string) (extraParams :as dict) get-header (text :as string))
;; Route a successful response through the appropriate handler.
(let ((resp-headers (process-response-headers get-header)))
@@ -269,7 +269,7 @@
(dict "target" target-el "swap" swap-style)))))))
(define handle-sx-response
(define handle-sx-response :effects [mutation io]
(fn (el target (text :as string) (swap-style :as string) (use-transition :as boolean))
;; Handle SX-format response: strip components, extract CSS, render, swap.
(let ((cleaned (strip-component-scripts text)))
@@ -300,7 +300,7 @@
(post-swap target)))))))))))
(define handle-html-response
(define handle-html-response :effects [mutation io]
(fn (el target (text :as string) (swap-style :as string) (use-transition :as boolean))
;; Handle HTML-format response: parse, OOB, select, swap.
(let ((doc (dom-parse-html-document text)))
@@ -337,7 +337,7 @@
;; Retry
;; --------------------------------------------------------------------------
(define handle-retry
(define handle-retry :effects [mutation io]
(fn (el (verb :as string) (method :as string) (url :as string) (extraParams :as dict))
;; Handle retry on failure if sx-retry is configured
(let ((retry-attr (dom-get-attr el "sx-retry"))
@@ -357,7 +357,7 @@
;; Trigger binding
;; --------------------------------------------------------------------------
(define bind-triggers
(define bind-triggers :effects [mutation io]
(fn (el (verbInfo :as dict))
;; Bind triggers from sx-trigger attribute (or defaults)
(let ((triggers (or (parse-trigger-spec (dom-get-attr el "sx-trigger"))
@@ -392,7 +392,7 @@
triggers))))
(define bind-event
(define bind-event :effects [mutation io]
(fn (el (event-name :as string) (mods :as dict) (verbInfo :as dict))
;; Bind a standard DOM event trigger.
;; Handles delay, once, changed, optimistic, preventDefault.
@@ -453,7 +453,7 @@
;; Post-swap lifecycle
;; --------------------------------------------------------------------------
(define post-swap
(define post-swap :effects [mutation io]
(fn (root)
;; Run lifecycle after swap: activate scripts, process SX, hydrate, process
(activate-scripts root)
@@ -474,7 +474,7 @@
;;
;; Example: (button :sx-get "/search" :sx-on-settle "(reset! (use-store \"count\") 0)")
(define process-settle-hooks
(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)))
@@ -484,7 +484,7 @@
exprs))))))
(define activate-scripts
(define activate-scripts :effects [mutation io]
(fn (root)
;; Re-activate scripts in swapped content.
;; Scripts inserted via innerHTML are inert — clone to make them execute.
@@ -505,7 +505,7 @@
;; OOB swap processing
;; --------------------------------------------------------------------------
(define process-oob-swaps
(define process-oob-swaps :effects [mutation io]
(fn (container (swap-fn :as lambda))
;; Find and process out-of-band swaps in container.
;; swap-fn is (fn (target oob-element swap-type) ...).
@@ -529,7 +529,7 @@
;; Head element hoisting
;; --------------------------------------------------------------------------
(define hoist-head-elements
(define hoist-head-elements :effects [mutation io]
(fn (container)
;; Move style[data-sx-css] and link[rel=stylesheet] to <head>
;; so they take effect globally.
@@ -551,7 +551,7 @@
;; Boost processing
;; --------------------------------------------------------------------------
(define process-boosted
(define process-boosted :effects [mutation io]
(fn (root)
;; Find [sx-boost] containers and boost their descendants
(for-each
@@ -560,7 +560,7 @@
(dom-query-all (or root (dom-body)) "[sx-boost]"))))
(define boost-descendants
(define boost-descendants :effects [mutation io]
(fn (container)
;; Boost links and forms within a container.
;; The sx-boost attribute value is the default target selector
@@ -609,7 +609,7 @@
(define _page-data-cache (dict))
(define _page-data-cache-ttl 30000) ;; 30 seconds in ms
(define page-data-cache-key
(define page-data-cache-key :effects []
(fn ((page-name :as string) (params :as dict))
;; Build a cache key from page name + params.
;; Params are from route matching so order is deterministic.
@@ -623,7 +623,7 @@
(keys params))
(str base ":" (join "&" parts)))))))
(define page-data-cache-get
(define page-data-cache-get :effects [mutation io]
(fn ((cache-key :as string))
;; Return cached data if fresh, else nil.
(let ((entry (get _page-data-cache cache-key)))
@@ -635,7 +635,7 @@
nil)
(get entry "data"))))))
(define page-data-cache-set
(define page-data-cache-set :effects [mutation io]
(fn ((cache-key :as string) data)
;; Store data with current timestamp.
(dict-set! _page-data-cache cache-key
@@ -646,7 +646,7 @@
;; Client-side routing — cache management
;; --------------------------------------------------------------------------
(define invalidate-page-cache
(define invalidate-page-cache :effects [mutation io]
(fn ((page-name :as string))
;; Clear cached data for a page. Removes all cache entries whose key
;; matches page-name (exact) or starts with "page-name:" (with params).
@@ -659,14 +659,14 @@
(sw-post-message {"type" "invalidate" "page" page-name})
(log-info (str "sx:cache invalidate " page-name))))
(define invalidate-all-page-cache
(define invalidate-all-page-cache :effects [mutation io]
(fn ()
;; Clear all cached page data and notify service worker.
(set! _page-data-cache (dict))
(sw-post-message {"type" "invalidate" "page" "*"})
(log-info "sx:cache invalidate *")))
(define update-page-cache
(define update-page-cache :effects [mutation io]
(fn ((page-name :as string) data)
;; Replace cached data for a page with server-provided data.
;; Uses a bare page-name key (no params) — the server knows the
@@ -675,7 +675,7 @@
(page-data-cache-set cache-key data)
(log-info (str "sx:cache update " page-name)))))
(define process-cache-directives
(define process-cache-directives :effects [mutation io]
(fn (el (resp-headers :as dict) (response-text :as string))
;; Process cache invalidation and update directives from both
;; element attributes and response headers.
@@ -721,7 +721,7 @@
(define _optimistic-snapshots (dict))
(define optimistic-cache-update
(define optimistic-cache-update :effects [mutation]
(fn ((cache-key :as string) (mutator :as lambda))
;; Apply predicted mutation to cached data. Saves snapshot for rollback.
;; Returns predicted data or nil if no cached data exists.
@@ -734,7 +734,7 @@
(page-data-cache-set cache-key predicted)
predicted)))))
(define optimistic-cache-revert
(define optimistic-cache-revert :effects [mutation]
(fn ((cache-key :as string))
;; Revert to pre-mutation snapshot. Returns restored data or nil.
(let ((snapshot (get _optimistic-snapshots cache-key)))
@@ -743,12 +743,12 @@
(dict-delete! _optimistic-snapshots cache-key)
snapshot))))
(define optimistic-cache-confirm
(define optimistic-cache-confirm :effects [mutation]
(fn ((cache-key :as string))
;; Server accepted — discard the rollback snapshot.
(dict-delete! _optimistic-snapshots cache-key)))
(define submit-mutation
(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))
;; Optimistic mutation: predict locally, send to server, confirm or revert.
;; on-complete is called with "confirmed" or "reverted" status.
@@ -787,14 +787,14 @@
(define _is-online true)
(define _offline-queue (list))
(define offline-is-online?
(define offline-is-online? :effects [io]
(fn () _is-online))
(define offline-set-online!
(define offline-set-online! :effects [mutation]
(fn ((val :as boolean))
(set! _is-online val)))
(define offline-queue-mutation
(define offline-queue-mutation :effects [mutation io]
(fn ((action-name :as string) payload (page-name :as string) (params :as dict) (mutator-fn :as lambda))
;; Queue a mutation for later sync. Apply optimistic update locally.
(let ((cache-key (page-data-cache-key page-name params))
@@ -813,7 +813,7 @@
(log-info (str "sx:offline queued " action-name " (" (len _offline-queue) " pending)"))
entry)))
(define offline-sync
(define offline-sync :effects [mutation io]
(fn ()
;; Replay all pending mutations. Called on reconnect.
(let ((pending (filter (fn ((e :as dict)) (= (get e "status") "pending")) _offline-queue)))
@@ -830,11 +830,11 @@
(log-warn (str "sx:offline sync failed " (get entry "action") ": " error)))))
pending)))))
(define offline-pending-count
(define offline-pending-count :effects [io]
(fn ()
(len (filter (fn ((e :as dict)) (= (get e "status") "pending")) _offline-queue))))
(define offline-aware-mutation
(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))
;; Top-level mutation function. Routes to submit-mutation when online,
;; offline-queue-mutation when offline.
@@ -849,7 +849,7 @@
;; Client-side routing
;; --------------------------------------------------------------------------
(define current-page-layout
(define current-page-layout :effects [io]
(fn ()
;; Find the layout name of the currently displayed page by matching
;; the browser URL against the page route table.
@@ -859,7 +859,7 @@
(or (get match "layout") "")))))
(define swap-rendered-content
(define swap-rendered-content :effects [mutation io]
(fn (target rendered (pathname :as string))
;; Swap rendered DOM content into target and run post-processing.
;; Shared by pure and data page client routes.
@@ -875,7 +875,7 @@
(log-info (str "sx:route client " pathname)))))
(define resolve-route-target
(define resolve-route-target :effects [io]
(fn ((target-sel :as string))
;; Resolve a target selector to a DOM element, or nil.
(if (and target-sel (not (= target-sel "true")))
@@ -883,7 +883,7 @@
nil)))
(define deps-satisfied?
(define deps-satisfied? :effects [io]
(fn ((match :as dict))
;; Check if all component deps for a page are loaded client-side.
(let ((deps (get match "deps"))
@@ -893,7 +893,7 @@
(every? (fn ((dep :as string)) (contains? loaded dep)) deps)))))
(define try-client-route
(define try-client-route :effects [mutation io]
(fn ((pathname :as string) (target-sel :as string))
;; Try to render a page client-side. Returns true if successful, false otherwise.
;; target-sel is the CSS selector for the swap target (from sx-boost value).
@@ -1011,7 +1011,7 @@
true))))))))))))))))))
(define bind-client-route-link
(define bind-client-route-link :effects [mutation io]
(fn (link (href :as string))
;; Bind a boost link with client-side routing. If the route can be
;; rendered client-side (pure page, no :data), do so. Otherwise
@@ -1026,7 +1026,7 @@
;; SSE processing
;; --------------------------------------------------------------------------
(define process-sse
(define process-sse :effects [mutation io]
(fn (root)
;; Find and bind SSE elements
(for-each
@@ -1037,7 +1037,7 @@
(dom-query-all (or root (dom-body)) "[sx-sse]"))))
(define bind-sse
(define bind-sse :effects [mutation io]
(fn (el)
;; Connect to SSE endpoint and bind swap handler
(let ((url (dom-get-attr el "sx-sse")))
@@ -1049,7 +1049,7 @@
(bind-sse-swap el data))))))))
(define bind-sse-swap
(define bind-sse-swap :effects [mutation io]
(fn (el (data :as string))
;; Handle an SSE event: swap data into element
(let ((target (resolve-target el))
@@ -1081,7 +1081,7 @@
;; Inline event handlers
;; --------------------------------------------------------------------------
(define bind-inline-handlers
(define bind-inline-handlers :effects [mutation io]
(fn (root)
;; Find elements with sx-on:* attributes and bind SX event handlers.
;; Handler bodies are SX expressions evaluated with `event` and `this`
@@ -1115,7 +1115,7 @@
;; Preload
;; --------------------------------------------------------------------------
(define bind-preload-for
(define bind-preload-for :effects [mutation io]
(fn (el)
;; Bind preload event listeners based on sx-preload attribute
(let ((preload-attr (dom-get-attr el "sx-preload")))
@@ -1134,7 +1134,7 @@
(loaded-component-names) _css-hash)))))))))))
(define do-preload
(define do-preload :effects [mutation io]
(fn ((url :as string) (headers :as dict))
;; Execute a preload fetch into the cache
(when (nil? (preload-cache-get _preload-cache url))
@@ -1148,7 +1148,7 @@
(define VERB_SELECTOR
(str "[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]"))
(define process-elements
(define process-elements :effects [mutation io]
(fn (root)
;; Find all elements with sx-* verb attributes and process them.
(let ((els (dom-query-all (or root (dom-body)) VERB_SELECTOR)))
@@ -1165,7 +1165,7 @@
(process-emit-elements root)))
(define process-one
(define process-one :effects [mutation io]
(fn (el)
;; Process a single element with an sx-* verb attribute
(let ((verb-info (get-verb-info el)))
@@ -1193,7 +1193,7 @@
;; On click → dispatches CustomEvent "cart:add" with detail {id:42, name:"Widget"}
;; The event bubbles up to the island container where bridge-event catches it.
(define process-emit-elements
(define process-emit-elements :effects [mutation io]
(fn (root)
(let ((els (dom-query-all (or root (dom-body)) "[data-sx-emit]")))
(for-each
@@ -1214,7 +1214,7 @@
;; History: popstate handler
;; --------------------------------------------------------------------------
(define handle-popstate
(define handle-popstate :effects [mutation io]
(fn ((scrollY :as number))
;; Handle browser back/forward navigation.
;; Derive target from [sx-boost] container or fall back to #main-panel.
@@ -1241,7 +1241,7 @@
;; Initialization
;; --------------------------------------------------------------------------
(define engine-init
(define engine-init :effects [mutation io]
(fn ()
;; Initialize: CSS tracking, scripts, hydrate, process.
(do