Fix 4 pre-existing test failures in deps and orchestration
component-pure?: was trusting empty component-io-refs as "definitely pure",
bypassing transitive dependency scan. Now only short-circuits on non-empty
direct IO refs; empty/nil falls through to transitive-io-refs-walk.
render-target: env-get threw on unknown component names. Now guards with
env-has? and returns "server" for missing components.
offline-aware-mutation test: execute-action was a no-op stub that never
called the success callback. Added mock that invokes success-fn so
submit-mutation's on-complete("confirmed") fires.
page-render-plan: was downstream of component-pure? bug, now passes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
638
web/deps.sx
638
web/deps.sx
@@ -1,459 +1,337 @@
|
||||
;; ==========================================================================
|
||||
;; deps.sx — Component dependency analysis specification
|
||||
;;
|
||||
;; Pure functions for analyzing component dependency graphs.
|
||||
;; Used by the bundling system to compute per-page component bundles
|
||||
;; instead of sending every definition to every page.
|
||||
;;
|
||||
;; All functions are pure — no IO, no platform-specific operations.
|
||||
;; Each host bootstraps this to native code alongside eval.sx/render.sx.
|
||||
;;
|
||||
;; From eval.sx platform (already provided by every host):
|
||||
;; (type-of x) → type string
|
||||
;; (symbol-name s) → string name of symbol
|
||||
;; (component-body c) → unevaluated AST of component body
|
||||
;; (component-name c) → string name (without ~)
|
||||
;; (macro-body m) → macro body AST
|
||||
;; (env-get env k) → value or nil
|
||||
;;
|
||||
;; New platform functions for deps (each host implements):
|
||||
;; (component-deps c) → cached deps list (may be empty)
|
||||
;; (component-set-deps! c d)→ cache deps on component
|
||||
;; (component-css-classes c)→ pre-scanned CSS class list
|
||||
;; (regex-find-all pat src) → list of capture group 1 matches
|
||||
;; (scan-css-classes src) → list of CSS class strings from source
|
||||
;; ==========================================================================
|
||||
(define
|
||||
scan-refs
|
||||
:effects ()
|
||||
(fn (node) (let ((refs (list))) (scan-refs-walk node refs) refs)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 1. AST scanning — collect ~component references from an AST node
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Walks all branches of control flow (if/when/cond/case) to find
|
||||
;; every component that *could* be rendered.
|
||||
|
||||
(define scan-refs :effects []
|
||||
(fn (node)
|
||||
(let ((refs (list)))
|
||||
(scan-refs-walk node refs)
|
||||
refs)))
|
||||
|
||||
|
||||
(define scan-refs-walk :effects []
|
||||
(fn (node (refs :as list))
|
||||
(define
|
||||
scan-refs-walk
|
||||
:effects ()
|
||||
(fn
|
||||
(node (refs :as list))
|
||||
(cond
|
||||
;; Symbol starting with ~ → component reference
|
||||
(= (type-of node) "symbol")
|
||||
(let ((name (symbol-name node)))
|
||||
(when (starts-with? name "~")
|
||||
(when (not (contains? refs name))
|
||||
(append! refs name))))
|
||||
|
||||
;; List → recurse into all elements (covers all control flow branches)
|
||||
(let
|
||||
((name (symbol-name node)))
|
||||
(when
|
||||
(starts-with? name "~")
|
||||
(when (not (contains? refs name)) (append! refs name))))
|
||||
(= (type-of node) "list")
|
||||
(for-each (fn (item) (scan-refs-walk item refs)) node)
|
||||
|
||||
;; Dict → recurse into values
|
||||
(for-each (fn (item) (scan-refs-walk item refs)) node)
|
||||
(= (type-of node) "dict")
|
||||
(for-each (fn (key) (scan-refs-walk (dict-get node key) refs))
|
||||
(keys node))
|
||||
|
||||
;; Literals (number, string, boolean, nil, keyword) → no refs
|
||||
(for-each
|
||||
(fn (key) (scan-refs-walk (dict-get node key) refs))
|
||||
(keys node))
|
||||
:else nil)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 2. Transitive dependency closure
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Given a component name and an environment, compute all components
|
||||
;; that it can transitively render. Handles cycles via seen-set.
|
||||
|
||||
(define transitive-deps-walk :effects []
|
||||
(fn ((n :as string) (seen :as list) (env :as dict))
|
||||
(when (not (contains? seen n))
|
||||
(define
|
||||
transitive-deps-walk
|
||||
:effects ()
|
||||
(fn
|
||||
((n :as string) (seen :as list) (env :as dict))
|
||||
(when
|
||||
(not (contains? seen n))
|
||||
(append! seen n)
|
||||
(let ((val (env-get env n)))
|
||||
(let
|
||||
((val (env-get env n)))
|
||||
(cond
|
||||
(or (= (type-of val) "component") (= (type-of val) "island"))
|
||||
(for-each (fn ((ref :as string)) (transitive-deps-walk ref seen env))
|
||||
(scan-refs (component-body val)))
|
||||
(for-each
|
||||
(fn ((ref :as string)) (transitive-deps-walk ref seen env))
|
||||
(scan-refs (component-body val)))
|
||||
(= (type-of val) "macro")
|
||||
(for-each (fn ((ref :as string)) (transitive-deps-walk ref seen env))
|
||||
(scan-refs (macro-body val)))
|
||||
(for-each
|
||||
(fn ((ref :as string)) (transitive-deps-walk ref seen env))
|
||||
(scan-refs (macro-body val)))
|
||||
:else nil)))))
|
||||
|
||||
|
||||
(define transitive-deps :effects []
|
||||
(fn ((name :as string) (env :as dict))
|
||||
(let ((seen (list))
|
||||
(key (if (starts-with? name "~") name (str "~" name))))
|
||||
(define
|
||||
transitive-deps
|
||||
:effects ()
|
||||
(fn
|
||||
((name :as string) (env :as dict))
|
||||
(let
|
||||
((seen (list))
|
||||
(key (if (starts-with? name "~") name (str "~" name))))
|
||||
(transitive-deps-walk key seen env)
|
||||
(filter (fn ((x :as string)) (not (= x key))) seen))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3. Compute deps for all components in an environment
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Iterates env, calls transitive-deps for each component, and
|
||||
;; stores the result via the platform's component-set-deps! function.
|
||||
;;
|
||||
;; Platform interface:
|
||||
;; (env-components env) → list of component names in env
|
||||
;; (component-set-deps! comp deps) → store deps on component
|
||||
|
||||
(define compute-all-deps :effects [mutation]
|
||||
(fn ((env :as dict))
|
||||
(define
|
||||
compute-all-deps
|
||||
:effects (mutation)
|
||||
(fn
|
||||
((env :as dict))
|
||||
(for-each
|
||||
(fn ((name :as string))
|
||||
(let ((val (env-get env name)))
|
||||
(when (or (= (type-of val) "component") (= (type-of val) "island"))
|
||||
(fn
|
||||
((name :as string))
|
||||
(let
|
||||
((val (env-get env name)))
|
||||
(when
|
||||
(or (= (type-of val) "component") (= (type-of val) "island"))
|
||||
(component-set-deps! val (transitive-deps name env)))))
|
||||
(env-components env))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 4. Scan serialized SX source for component references
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Regex-based extraction of (~name patterns from SX wire format.
|
||||
;; Returns list of names WITH ~ prefix.
|
||||
;;
|
||||
;; Platform interface:
|
||||
;; (regex-find-all pattern source) → list of matched group strings
|
||||
|
||||
(define scan-components-from-source :effects []
|
||||
(fn ((source :as string))
|
||||
(let ((matches (regex-find-all "\\(~([a-zA-Z_][a-zA-Z0-9_\\-:/]*)" source)))
|
||||
(define
|
||||
scan-components-from-source
|
||||
:effects ()
|
||||
(fn
|
||||
((source :as string))
|
||||
(let
|
||||
((matches (regex-find-all "\\(~([a-zA-Z_][a-zA-Z0-9_\\-:/]*)" source)))
|
||||
(map (fn ((m :as string)) (str "~" m)) matches))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 5. Components needed for a page
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Scans page source for direct component references, then computes
|
||||
;; the transitive closure. Returns list of ~names.
|
||||
|
||||
(define components-needed :effects []
|
||||
(fn ((page-source :as string) (env :as dict))
|
||||
(let ((direct (scan-components-from-source page-source))
|
||||
(all-needed (list)))
|
||||
|
||||
;; Add each direct ref + its transitive deps
|
||||
(define
|
||||
components-needed
|
||||
:effects ()
|
||||
(fn
|
||||
((page-source :as string) (env :as dict))
|
||||
(let
|
||||
((direct (scan-components-from-source page-source))
|
||||
(all-needed (list)))
|
||||
(for-each
|
||||
(fn ((name :as string))
|
||||
(when (not (contains? all-needed name))
|
||||
(append! all-needed name))
|
||||
(let ((val (env-get env name)))
|
||||
(let ((deps (if (and (= (type-of val) "component")
|
||||
(not (empty? (component-deps val))))
|
||||
(component-deps val)
|
||||
(transitive-deps name env))))
|
||||
(fn
|
||||
((name :as string))
|
||||
(when (not (contains? all-needed name)) (append! all-needed name))
|
||||
(let
|
||||
((val (env-get env name)))
|
||||
(let
|
||||
((deps (if (and (= (type-of val) "component") (not (empty? (component-deps val)))) (component-deps val) (transitive-deps name env))))
|
||||
(for-each
|
||||
(fn ((dep :as string))
|
||||
(when (not (contains? all-needed dep))
|
||||
(fn
|
||||
((dep :as string))
|
||||
(when
|
||||
(not (contains? all-needed dep))
|
||||
(append! all-needed dep)))
|
||||
deps))))
|
||||
direct)
|
||||
|
||||
all-needed)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 6. Build per-page component bundle
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Given page source and env, returns list of component names needed.
|
||||
;; The host uses this list to serialize only the needed definitions
|
||||
;; and compute a page-specific hash.
|
||||
;;
|
||||
;; This replaces the "send everything" approach with per-page bundles.
|
||||
|
||||
(define page-component-bundle :effects []
|
||||
(fn ((page-source :as string) (env :as dict))
|
||||
(define
|
||||
page-component-bundle
|
||||
:effects ()
|
||||
(fn
|
||||
((page-source :as string) (env :as dict))
|
||||
(components-needed page-source env)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 7. CSS classes for a page
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Returns the union of CSS classes from components this page uses,
|
||||
;; plus classes from the page source itself.
|
||||
;;
|
||||
;; Platform interface:
|
||||
;; (component-css-classes c) → set/list of class strings
|
||||
;; (scan-css-classes source) → set/list of class strings from source
|
||||
|
||||
(define page-css-classes :effects []
|
||||
(fn ((page-source :as string) (env :as dict))
|
||||
(let ((needed (components-needed page-source env))
|
||||
(classes (list)))
|
||||
|
||||
;; Collect classes from needed components
|
||||
(define
|
||||
page-css-classes
|
||||
:effects ()
|
||||
(fn
|
||||
((page-source :as string) (env :as dict))
|
||||
(let
|
||||
((needed (components-needed page-source env)) (classes (list)))
|
||||
(for-each
|
||||
(fn ((name :as string))
|
||||
(let ((val (env-get env name)))
|
||||
(when (= (type-of val) "component")
|
||||
(fn
|
||||
((name :as string))
|
||||
(let
|
||||
((val (env-get env name)))
|
||||
(when
|
||||
(= (type-of val) "component")
|
||||
(for-each
|
||||
(fn ((cls :as string))
|
||||
(when (not (contains? classes cls))
|
||||
(append! classes cls)))
|
||||
(fn
|
||||
((cls :as string))
|
||||
(when (not (contains? classes cls)) (append! classes cls)))
|
||||
(component-css-classes val)))))
|
||||
needed)
|
||||
|
||||
;; Add classes from page source
|
||||
(for-each
|
||||
(fn ((cls :as string))
|
||||
(when (not (contains? classes cls))
|
||||
(append! classes cls)))
|
||||
(fn
|
||||
((cls :as string))
|
||||
(when (not (contains? classes cls)) (append! classes cls)))
|
||||
(scan-css-classes page-source))
|
||||
|
||||
classes)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 8. IO detection — scan component ASTs for IO primitive references
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Extends the dependency walker to detect references to IO primitives.
|
||||
;; IO names are provided by the host (from boundary.sx declarations).
|
||||
;; A component is "pure" if it (transitively) references no IO primitives.
|
||||
;;
|
||||
;; Platform interface additions:
|
||||
;; (component-io-refs c) → cached IO ref list (may be empty)
|
||||
;; (component-set-io-refs! c r) → cache IO refs on component
|
||||
|
||||
(define scan-io-refs-walk :effects []
|
||||
(fn (node (io-names :as list) (refs :as list))
|
||||
(define
|
||||
scan-io-refs-walk
|
||||
:effects ()
|
||||
(fn
|
||||
(node (io-names :as list) (refs :as list))
|
||||
(cond
|
||||
;; Symbol → check if name is in the IO set
|
||||
(= (type-of node) "symbol")
|
||||
(let ((name (symbol-name node)))
|
||||
(when (contains? io-names name)
|
||||
(when (not (contains? refs name))
|
||||
(append! refs name))))
|
||||
|
||||
;; List → recurse into all elements
|
||||
(let
|
||||
((name (symbol-name node)))
|
||||
(when
|
||||
(contains? io-names name)
|
||||
(when (not (contains? refs name)) (append! refs name))))
|
||||
(= (type-of node) "list")
|
||||
(for-each (fn (item) (scan-io-refs-walk item io-names refs)) node)
|
||||
|
||||
;; Dict → recurse into values
|
||||
(for-each (fn (item) (scan-io-refs-walk item io-names refs)) node)
|
||||
(= (type-of node) "dict")
|
||||
(for-each (fn (key) (scan-io-refs-walk (dict-get node key) io-names refs))
|
||||
(keys node))
|
||||
|
||||
;; Literals → no IO refs
|
||||
(for-each
|
||||
(fn (key) (scan-io-refs-walk (dict-get node key) io-names refs))
|
||||
(keys node))
|
||||
:else nil)))
|
||||
|
||||
(define
|
||||
scan-io-refs
|
||||
:effects ()
|
||||
(fn
|
||||
(node (io-names :as list))
|
||||
(let ((refs (list))) (scan-io-refs-walk node io-names refs) refs)))
|
||||
|
||||
(define scan-io-refs :effects []
|
||||
(fn (node (io-names :as list))
|
||||
(let ((refs (list)))
|
||||
(scan-io-refs-walk node io-names refs)
|
||||
refs)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 9. Transitive IO refs — follow component deps and union IO refs
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define transitive-io-refs-walk :effects []
|
||||
(fn ((n :as string) (seen :as list) (all-refs :as list) (env :as dict) (io-names :as list))
|
||||
(when (not (contains? seen n))
|
||||
(define
|
||||
transitive-io-refs-walk
|
||||
:effects ()
|
||||
(fn
|
||||
((n :as string)
|
||||
(seen :as list)
|
||||
(all-refs :as list)
|
||||
(env :as dict)
|
||||
(io-names :as list))
|
||||
(when
|
||||
(not (contains? seen n))
|
||||
(append! seen n)
|
||||
(let ((val (env-get env n)))
|
||||
(let
|
||||
((val (env-get env n)))
|
||||
(cond
|
||||
(= (type-of val) "component")
|
||||
(do
|
||||
;; Scan this component's body for IO refs
|
||||
(for-each
|
||||
(fn ((ref :as string))
|
||||
(when (not (contains? all-refs ref))
|
||||
(append! all-refs ref)))
|
||||
(scan-io-refs (component-body val) io-names))
|
||||
;; Recurse into component deps
|
||||
(for-each
|
||||
(fn ((dep :as string)) (transitive-io-refs-walk dep seen all-refs env io-names))
|
||||
(scan-refs (component-body val))))
|
||||
|
||||
(do
|
||||
(for-each
|
||||
(fn
|
||||
((ref :as string))
|
||||
(when (not (contains? all-refs ref)) (append! all-refs ref)))
|
||||
(scan-io-refs (component-body val) io-names))
|
||||
(for-each
|
||||
(fn
|
||||
((dep :as string))
|
||||
(transitive-io-refs-walk dep seen all-refs env io-names))
|
||||
(scan-refs (component-body val))))
|
||||
(= (type-of val) "macro")
|
||||
(do
|
||||
(for-each
|
||||
(fn ((ref :as string))
|
||||
(when (not (contains? all-refs ref))
|
||||
(append! all-refs ref)))
|
||||
(scan-io-refs (macro-body val) io-names))
|
||||
(for-each
|
||||
(fn ((dep :as string)) (transitive-io-refs-walk dep seen all-refs env io-names))
|
||||
(scan-refs (macro-body val))))
|
||||
|
||||
(do
|
||||
(for-each
|
||||
(fn
|
||||
((ref :as string))
|
||||
(when (not (contains? all-refs ref)) (append! all-refs ref)))
|
||||
(scan-io-refs (macro-body val) io-names))
|
||||
(for-each
|
||||
(fn
|
||||
((dep :as string))
|
||||
(transitive-io-refs-walk dep seen all-refs env io-names))
|
||||
(scan-refs (macro-body val))))
|
||||
:else nil)))))
|
||||
|
||||
|
||||
(define transitive-io-refs :effects []
|
||||
(fn ((name :as string) (env :as dict) (io-names :as list))
|
||||
(let ((all-refs (list))
|
||||
(seen (list))
|
||||
(key (if (starts-with? name "~") name (str "~" name))))
|
||||
(define
|
||||
transitive-io-refs
|
||||
:effects ()
|
||||
(fn
|
||||
((name :as string) (env :as dict) (io-names :as list))
|
||||
(let
|
||||
((all-refs (list))
|
||||
(seen (list))
|
||||
(key (if (starts-with? name "~") name (str "~" name))))
|
||||
(transitive-io-refs-walk key seen all-refs env io-names)
|
||||
all-refs)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 10. Compute IO refs for all components in an environment
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define compute-all-io-refs :effects [mutation]
|
||||
(fn ((env :as dict) (io-names :as list))
|
||||
(define
|
||||
compute-all-io-refs
|
||||
:effects (mutation)
|
||||
(fn
|
||||
((env :as dict) (io-names :as list))
|
||||
(for-each
|
||||
(fn ((name :as string))
|
||||
(let ((val (env-get env name)))
|
||||
(when (= (type-of val) "component")
|
||||
(component-set-io-refs! val (transitive-io-refs name env io-names)))))
|
||||
(fn
|
||||
((name :as string))
|
||||
(let
|
||||
((val (env-get env name)))
|
||||
(when
|
||||
(= (type-of val) "component")
|
||||
(component-set-io-refs!
|
||||
val
|
||||
(transitive-io-refs name env io-names)))))
|
||||
(env-components env))))
|
||||
|
||||
|
||||
(define component-io-refs-cached :effects []
|
||||
(fn ((name :as string) (env :as dict) (io-names :as list))
|
||||
(let ((key (if (starts-with? name "~") name (str "~" name))))
|
||||
(let ((val (env-get env key)))
|
||||
(if (and (= (type-of val) "component")
|
||||
(not (nil? (component-io-refs val)))
|
||||
(not (empty? (component-io-refs val))))
|
||||
(define
|
||||
component-io-refs-cached
|
||||
:effects ()
|
||||
(fn
|
||||
((name :as string) (env :as dict) (io-names :as list))
|
||||
(let
|
||||
((key (if (starts-with? name "~") name (str "~" name))))
|
||||
(let
|
||||
((val (env-get env key)))
|
||||
(if
|
||||
(and
|
||||
(= (type-of val) "component")
|
||||
(not (nil? (component-io-refs val)))
|
||||
(not (empty? (component-io-refs val))))
|
||||
(component-io-refs val)
|
||||
;; Fallback: not yet cached (shouldn't happen after compute-all-io-refs)
|
||||
(transitive-io-refs name env io-names))))))
|
||||
|
||||
(define component-pure? :effects []
|
||||
(fn ((name :as string) (env :as dict) (io-names :as list))
|
||||
(let ((key (if (starts-with? name "~") name (str "~" name))))
|
||||
(let ((val (env-get env key)))
|
||||
(if (and (= (type-of val) "component")
|
||||
(not (nil? (component-io-refs val))))
|
||||
;; Use cached io-refs (empty list = pure)
|
||||
(empty? (component-io-refs val))
|
||||
;; Fallback
|
||||
(define
|
||||
component-pure?
|
||||
:effects ()
|
||||
(fn
|
||||
(name (env :as dict) (io-names :as list))
|
||||
(let
|
||||
((key (if (starts-with? name "~") name (str "~" name))))
|
||||
(let
|
||||
((val (if (env-has? env key) (env-get env key) nil)))
|
||||
(if
|
||||
(and
|
||||
(= (type-of val) "component")
|
||||
(not (nil? (component-io-refs val)))
|
||||
(not (empty? (component-io-refs val))))
|
||||
false
|
||||
(empty? (transitive-io-refs name env io-names)))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 5. Render target — boundary decision per component
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Combines IO analysis with affinity annotations to decide where a
|
||||
;; component should render:
|
||||
;;
|
||||
;; :affinity :server → always "server" (auth-sensitive, secrets)
|
||||
;; :affinity :client → "client" even if IO-dependent (IO proxy)
|
||||
;; :affinity :auto → "server" if IO-dependent, "client" if pure
|
||||
;;
|
||||
;; Returns: "server" | "client"
|
||||
|
||||
(define render-target :effects []
|
||||
(fn ((name :as string) (env :as dict) (io-names :as list))
|
||||
(let ((key (if (starts-with? name "~") name (str "~" name))))
|
||||
(let ((val (env-get env key)))
|
||||
(if (not (= (type-of val) "component"))
|
||||
(define
|
||||
render-target
|
||||
:effects ()
|
||||
(fn
|
||||
(name (env :as dict) (io-names :as list))
|
||||
(let
|
||||
((key (if (starts-with? name "~") name (str "~" name))))
|
||||
(let
|
||||
((val (if (env-has? env key) (env-get env key) nil)))
|
||||
(if
|
||||
(not (= (type-of val) "component"))
|
||||
"server"
|
||||
(let ((affinity (component-affinity val)))
|
||||
(let
|
||||
((affinity (component-affinity val)))
|
||||
(cond
|
||||
(= affinity "server") "server"
|
||||
(= affinity "client") "client"
|
||||
;; auto: decide from IO analysis
|
||||
(not (component-pure? name env io-names)) "server"
|
||||
(= affinity "server")
|
||||
"server"
|
||||
(= affinity "client")
|
||||
"client"
|
||||
(not (component-pure? name env io-names))
|
||||
"server"
|
||||
:else "client")))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 6. Page render plan — pre-computed boundary decisions for a page
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Given page source + env + IO names, returns a render plan dict:
|
||||
;;
|
||||
;; {:components {~name "server"|"client" ...}
|
||||
;; :server (list of ~names that render server-side)
|
||||
;; :client (list of ~names that render client-side)
|
||||
;; :io-deps (list of IO primitives needed by server components)}
|
||||
;;
|
||||
;; This is computed once at page registration and cached on the page def.
|
||||
;; The async evaluator and client router both use it to make decisions
|
||||
;; without recomputing at every request.
|
||||
|
||||
(define page-render-plan :effects []
|
||||
(fn ((page-source :as string) (env :as dict) (io-names :as list))
|
||||
(let ((needed (components-needed page-source env))
|
||||
(comp-targets (dict))
|
||||
(server-list (list))
|
||||
(client-list (list))
|
||||
(io-deps (list)))
|
||||
|
||||
(define
|
||||
page-render-plan
|
||||
:effects ()
|
||||
(fn
|
||||
((page-source :as string) (env :as dict) (io-names :as list))
|
||||
(let
|
||||
((needed (components-needed page-source env))
|
||||
(comp-targets (dict))
|
||||
(server-list (list))
|
||||
(client-list (list))
|
||||
(io-deps (list)))
|
||||
(for-each
|
||||
(fn ((name :as string))
|
||||
(let ((target (render-target name env io-names)))
|
||||
(fn
|
||||
((name :as string))
|
||||
(let
|
||||
((target (render-target name env io-names)))
|
||||
(dict-set! comp-targets name target)
|
||||
(if (= target "server")
|
||||
(if
|
||||
(= target "server")
|
||||
(do
|
||||
(append! server-list name)
|
||||
;; Collect IO deps from server components (use cache)
|
||||
(for-each
|
||||
(fn ((io-ref :as string))
|
||||
(when (not (contains? io-deps io-ref))
|
||||
(fn
|
||||
((io-ref :as string))
|
||||
(when
|
||||
(not (contains? io-deps io-ref))
|
||||
(append! io-deps io-ref)))
|
||||
(component-io-refs-cached name env io-names)))
|
||||
(append! client-list name))))
|
||||
needed)
|
||||
{:io-deps io-deps :server server-list :components comp-targets :client client-list})))
|
||||
|
||||
{:components comp-targets
|
||||
:server server-list
|
||||
:client client-list
|
||||
:io-deps io-deps})))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Host obligation: selective expansion in async partial evaluation
|
||||
;; --------------------------------------------------------------------------
|
||||
;; The spec classifies components as pure or IO-dependent and provides
|
||||
;; per-component render-target decisions. Each host's async partial
|
||||
;; evaluator (the server-side rendering path that bridges sync evaluation
|
||||
;; with async IO) must use this classification:
|
||||
;;
|
||||
;; render-target "server" → expand server-side (IO must resolve)
|
||||
;; render-target "client" → serialize for client (can render anywhere)
|
||||
;; Layout slot context → expand all (server needs full HTML)
|
||||
;;
|
||||
;; The spec provides: component-io-refs, component-pure?, render-target,
|
||||
;; component-affinity. The host provides the async runtime that acts on it.
|
||||
;; This is not SX semantics — it is host infrastructure. Every host
|
||||
;; with a server-side async evaluator implements the same rule.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface summary
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; From eval.sx (already provided):
|
||||
;; (type-of x) → type string
|
||||
;; (symbol-name s) → string name of symbol
|
||||
;; (env-get env k) → value or nil
|
||||
;;
|
||||
;; New for deps.sx (each host implements):
|
||||
;; (component-body c) → AST body of component
|
||||
;; (component-name c) → name string
|
||||
;; (component-deps c) → cached deps list (may be empty)
|
||||
;; (component-set-deps! c d)→ cache deps on component
|
||||
;; (component-css-classes c)→ pre-scanned CSS class list
|
||||
;; (component-io-refs c) → cached IO ref list (may be empty)
|
||||
;; (component-set-io-refs! c r)→ cache IO refs on component
|
||||
;; (component-affinity c) → "auto" | "client" | "server"
|
||||
;; (macro-body m) → AST body of macro
|
||||
;; (regex-find-all pat src) → list of capture group matches
|
||||
;; (scan-css-classes src) → list of CSS class strings from source
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; env-components — list component/macro names in an environment
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Moved from platform to spec: pure logic using type predicates.
|
||||
|
||||
(define env-components :effects []
|
||||
(fn ((env :as dict))
|
||||
(define
|
||||
env-components
|
||||
:effects ()
|
||||
(fn
|
||||
((env :as dict))
|
||||
(filter
|
||||
(fn ((k :as string))
|
||||
(let ((v (env-get env k)))
|
||||
(or (component? v) (macro? v))))
|
||||
(fn
|
||||
((k :as string))
|
||||
(let ((v (env-get env k))) (or (component? v) (macro? v))))
|
||||
(keys env))))
|
||||
|
||||
@@ -1,170 +1,142 @@
|
||||
;; ==========================================================================
|
||||
;; test-orchestration.sx — Tests for orchestration.sx Phase 7c + 7d
|
||||
;;
|
||||
;; Requires: test-framework.sx loaded first.
|
||||
;; Platform functions mocked by test runner:
|
||||
;; now-ms, log-info, log-warn, execute-action, try-rerender-page
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 1. page-data-cache — basic cache operations
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "page-data-cache"
|
||||
|
||||
(deftest "cache-key bare page name"
|
||||
(defsuite
|
||||
"page-data-cache"
|
||||
(deftest
|
||||
"cache-key bare page name"
|
||||
(assert-equal "my-page" (page-data-cache-key "my-page" nil)))
|
||||
|
||||
(deftest "cache-key with params"
|
||||
(let ((key (page-data-cache-key "my-page" {"id" "42"})))
|
||||
(deftest
|
||||
"cache-key with params"
|
||||
(let
|
||||
((key (page-data-cache-key "my-page" {:id "42"})))
|
||||
(assert-equal "my-page:id=42" key)))
|
||||
|
||||
(deftest "cache-set then get"
|
||||
(let ((key "test-cache-1"))
|
||||
(page-data-cache-set key {"items" (list 1 2 3)})
|
||||
(let ((result (page-data-cache-get key)))
|
||||
(deftest
|
||||
"cache-set then get"
|
||||
(let
|
||||
((key "test-cache-1"))
|
||||
(page-data-cache-set key {:items (list 1 2 3)})
|
||||
(let
|
||||
((result (page-data-cache-get key)))
|
||||
(assert-equal (list 1 2 3) (get result "items")))))
|
||||
|
||||
(deftest "cache miss returns nil"
|
||||
(deftest
|
||||
"cache miss returns nil"
|
||||
(assert-nil (page-data-cache-get "nonexistent-key"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 2. optimistic-cache-update — predicted mutation with snapshot
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "optimistic-cache-update"
|
||||
|
||||
(deftest "applies mutator to cached data"
|
||||
(let ((key "opt-test-1"))
|
||||
;; Seed the cache
|
||||
(page-data-cache-set key {"count" 10})
|
||||
;; Apply optimistic mutation
|
||||
(let ((predicted (optimistic-cache-update key
|
||||
(fn (data) (merge data {"count" 11})))))
|
||||
(defsuite
|
||||
"optimistic-cache-update"
|
||||
(deftest
|
||||
"applies mutator to cached data"
|
||||
(let
|
||||
((key "opt-test-1"))
|
||||
(page-data-cache-set key {:count 10})
|
||||
(let
|
||||
((predicted (optimistic-cache-update key (fn (data) (merge data {:count 11})))))
|
||||
(assert-equal 11 (get predicted "count")))))
|
||||
|
||||
(deftest "updates cache with prediction"
|
||||
(let ((key "opt-test-2"))
|
||||
(page-data-cache-set key {"count" 5})
|
||||
(optimistic-cache-update key (fn (data) (merge data {"count" 6})))
|
||||
;; Cache now has predicted value
|
||||
(let ((cached (page-data-cache-get key)))
|
||||
(deftest
|
||||
"updates cache with prediction"
|
||||
(let
|
||||
((key "opt-test-2"))
|
||||
(page-data-cache-set key {:count 5})
|
||||
(optimistic-cache-update key (fn (data) (merge data {:count 6})))
|
||||
(let
|
||||
((cached (page-data-cache-get key)))
|
||||
(assert-equal 6 (get cached "count")))))
|
||||
|
||||
(deftest "returns nil when no cached data"
|
||||
(let ((result (optimistic-cache-update "no-such-key"
|
||||
(fn (data) (merge data {"x" 1})))))
|
||||
(deftest
|
||||
"returns nil when no cached data"
|
||||
(let
|
||||
((result (optimistic-cache-update "no-such-key" (fn (data) (merge data {:x 1})))))
|
||||
(assert-nil result))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3. optimistic-cache-revert — restore from snapshot
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "optimistic-cache-revert"
|
||||
|
||||
(deftest "reverts to original data"
|
||||
(let ((key "revert-test-1"))
|
||||
(page-data-cache-set key {"count" 10})
|
||||
(optimistic-cache-update key (fn (data) (merge data {"count" 99})))
|
||||
;; Cache now has 99
|
||||
(defsuite
|
||||
"optimistic-cache-revert"
|
||||
(deftest
|
||||
"reverts to original data"
|
||||
(let
|
||||
((key "revert-test-1"))
|
||||
(page-data-cache-set key {:count 10})
|
||||
(optimistic-cache-update key (fn (data) (merge data {:count 99})))
|
||||
(assert-equal 99 (get (page-data-cache-get key) "count"))
|
||||
;; Revert
|
||||
(let ((restored (optimistic-cache-revert key)))
|
||||
(let
|
||||
((restored (optimistic-cache-revert key)))
|
||||
(assert-equal 10 (get restored "count"))
|
||||
;; Cache is back to original
|
||||
(assert-equal 10 (get (page-data-cache-get key) "count")))))
|
||||
|
||||
(deftest "returns nil when no snapshot"
|
||||
(deftest
|
||||
"returns nil when no snapshot"
|
||||
(assert-nil (optimistic-cache-revert "never-mutated"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 4. optimistic-cache-confirm — discard snapshot
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "optimistic-cache-confirm"
|
||||
|
||||
(deftest "confirm clears snapshot"
|
||||
(let ((key "confirm-test-1"))
|
||||
(page-data-cache-set key {"val" "a"})
|
||||
(optimistic-cache-update key (fn (data) (merge data {"val" "b"})))
|
||||
;; Confirm — accepts the optimistic value
|
||||
(defsuite
|
||||
"optimistic-cache-confirm"
|
||||
(deftest
|
||||
"confirm clears snapshot"
|
||||
(let
|
||||
((key "confirm-test-1"))
|
||||
(page-data-cache-set key {:val "a"})
|
||||
(optimistic-cache-update key (fn (data) (merge data {:val "b"})))
|
||||
(optimistic-cache-confirm key)
|
||||
;; Revert should now return nil (no snapshot)
|
||||
(assert-nil (optimistic-cache-revert key))
|
||||
;; Cache still has optimistic value
|
||||
(assert-equal "b" (get (page-data-cache-get key) "val")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 5. offline-is-online? / offline-set-online! — connectivity tracking
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "offline-connectivity"
|
||||
|
||||
(deftest "initially online"
|
||||
(assert-true (offline-is-online?)))
|
||||
|
||||
(deftest "set offline"
|
||||
(defsuite
|
||||
"offline-connectivity"
|
||||
(deftest "initially online" (assert-true (offline-is-online?)))
|
||||
(deftest
|
||||
"set offline"
|
||||
(offline-set-online! false)
|
||||
(assert-false (offline-is-online?)))
|
||||
|
||||
(deftest "set back online"
|
||||
(deftest
|
||||
"set back online"
|
||||
(offline-set-online! true)
|
||||
(assert-true (offline-is-online?))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 6. offline-queue-mutation — queue entries when offline
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "offline-queue-mutation"
|
||||
|
||||
(deftest "queues an entry"
|
||||
;; Seed cache so optimistic update works
|
||||
(let ((key (page-data-cache-key "notes" nil)))
|
||||
(page-data-cache-set key {"items" (list "a" "b")})
|
||||
(let ((entry (offline-queue-mutation "add-note"
|
||||
{"text" "c"}
|
||||
"notes" nil
|
||||
(fn (data) (merge data {"items" (list "a" "b" "c")})))))
|
||||
(defsuite
|
||||
"offline-queue-mutation"
|
||||
(deftest
|
||||
"queues an entry"
|
||||
(let
|
||||
((key (page-data-cache-key "notes" nil)))
|
||||
(page-data-cache-set key {:items (list "a" "b")})
|
||||
(let
|
||||
((entry (offline-queue-mutation "add-note" {:text "c"} "notes" nil (fn (data) (merge data {:items (list "a" "b" "c")})))))
|
||||
(assert-equal "add-note" (get entry "action"))
|
||||
(assert-equal "pending" (get entry "status")))))
|
||||
|
||||
(deftest "pending count increases"
|
||||
;; Previous test queued 1 entry; count should be >= 1
|
||||
(deftest
|
||||
"pending count increases"
|
||||
(assert-true (> (offline-pending-count) 0))))
|
||||
|
||||
(define
|
||||
execute-action
|
||||
(fn (action-name payload success-fn error-fn) (success-fn nil)))
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 7. offline-aware-mutation — routes by connectivity
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "offline-aware-mutation"
|
||||
|
||||
(deftest "when online calls submit-mutation path"
|
||||
(defsuite
|
||||
"offline-aware-mutation"
|
||||
(deftest
|
||||
"when online calls submit-mutation path"
|
||||
(offline-set-online! true)
|
||||
(let ((key (page-data-cache-key "test-page" nil)))
|
||||
(page-data-cache-set key {"v" 1})
|
||||
;; This will trigger execute-action (mocked) which calls success cb
|
||||
(let ((status nil))
|
||||
(offline-aware-mutation "test-page" nil "do-thing" {"x" 1}
|
||||
(fn (data) (merge data {"v" 2}))
|
||||
(let
|
||||
((key (page-data-cache-key "test-page" nil)))
|
||||
(page-data-cache-set key {:v 1})
|
||||
(let
|
||||
((status nil))
|
||||
(offline-aware-mutation
|
||||
"test-page"
|
||||
nil
|
||||
"do-thing"
|
||||
{:x 1}
|
||||
(fn (data) (merge data {:v 2}))
|
||||
(fn (s) (set! status s)))
|
||||
;; Mock execute-action calls success immediately
|
||||
(assert-equal "confirmed" status))))
|
||||
|
||||
(deftest "when offline queues mutation"
|
||||
(deftest
|
||||
"when offline queues mutation"
|
||||
(offline-set-online! false)
|
||||
(let ((key (page-data-cache-key "test-page-2" nil)))
|
||||
(page-data-cache-set key {"v" 1})
|
||||
(let ((status nil))
|
||||
(offline-aware-mutation "test-page-2" nil "do-thing" {"x" 1}
|
||||
(fn (data) (merge data {"v" 2}))
|
||||
(let
|
||||
((key (page-data-cache-key "test-page-2" nil)))
|
||||
(page-data-cache-set key {:v 1})
|
||||
(let
|
||||
((status nil))
|
||||
(offline-aware-mutation
|
||||
"test-page-2"
|
||||
nil
|
||||
"do-thing"
|
||||
{:x 1}
|
||||
(fn (data) (merge data {:v 2}))
|
||||
(fn (s) (set! status s)))
|
||||
(assert-equal "queued" status)))
|
||||
;; Clean up: go back online
|
||||
(offline-set-online! true)))
|
||||
|
||||
Reference in New Issue
Block a user