diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index f042abf0..3ef016bd 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-10T14:25:47Z"; + var SX_VERSION = "2026-03-10T15:52:32Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -1781,6 +1781,7 @@ return result; }, args); var container = domCreateElement("span", NIL); var disposers = []; domSetAttr(container, "data-sx-island", islandName); + markProcessed(container, "island-hydrated"); return (function() { var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(island), local, ns); }); domAppend(container, bodyDom); diff --git a/shared/sx/ref/adapter-dom.sx b/shared/sx/ref/adapter-dom.sx index 5f4cb746..4b660718 100644 --- a/shared/sx/ref/adapter-dom.sx +++ b/shared/sx/ref/adapter-dom.sx @@ -637,8 +637,9 @@ (let ((container (dom-create-element "span" nil)) (disposers (list))) - ;; Mark as island + ;; Mark as island + already hydrated (so boot.sx skips it) (dom-set-attr container "data-sx-island" island-name) + (mark-processed! container "island-hydrated") ;; Render island body inside a scope that tracks disposers (let ((body-dom diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 4b6f542f..c0254b25 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -213,6 +213,8 @@ :summary "Page-level signal containers via def-store/use-store — persist across island destruction/recreation.") (dict :label "Plan" :href "/reactive-islands/plan" :summary "The full design document — rendering boundary, state flow, signal primitives, island lifecycle.") + (dict :label "Marshes" :href "/reactive-islands/marshes" + :summary "Where reactivity and hypermedia interpenetrate — server writes to signals, reactive transforms reshape server content, client state modifies how hypermedia is interpreted.") (dict :label "Phase 2" :href "/reactive-islands/phase2" :summary "Input binding, keyed lists, reactive class/style, refs, portals, error boundaries, suspense, transitions."))) diff --git a/sx/sx/reactive-islands/marshes.sx b/sx/sx/reactive-islands/marshes.sx new file mode 100644 index 00000000..367405b4 --- /dev/null +++ b/sx/sx/reactive-islands/marshes.sx @@ -0,0 +1,275 @@ +;; --------------------------------------------------------------------------- +;; Marshes — where reactivity and hypermedia interpenetrate +;; --------------------------------------------------------------------------- + +(defcomp ~reactive-islands-marshes-content () + (~doc-page :title "Marshes" + (p :class "text-stone-500 text-sm italic mb-8" + "Islands are dry land. Lakes are open water. Marshes are the saturated ground between — where you can't tell whether you're standing on reactivity or wading through hypermedia.") + + ;; ===================================================================== + ;; I. The problem + ;; ===================================================================== + + (~doc-section :title "The boundary dissolves" :id "problem" + (p "Islands and lakes establish a clear territorial agreement. Islands own reactive state — signals, computed, effects. Lakes own server content — morphed during navigation, updated by " (code "sx-get") "/" (code "sx-post") ". The morph algorithm enforces the border: it enters islands, finds lakes, updates them, and leaves everything else untouched.") + (p "But real applications need more than peaceful coexistence. They need " (em "interpenetration") ":") + (ul :class "list-disc pl-5 space-y-2 text-stone-600" + (li "A server response that writes to a signal — the lake feeds the island.") + (li "A reactive transform that reshapes server content before it enters the DOM — the island filters the water.") + (li "A signal that controls which server endpoint to call, what swap target to use, which OOB slots to request — the island directs the current.") + (li "Client state that changes " (em "how") " server content is rendered — not just what to fetch, but how to interpret what arrives.")) + (p "These aren't edge cases. They're what every non-trivial interactive application does. The island/lake model handles the 90% case; marshes handle the other 10% where the boundary needs to be soft.")) + + ;; ===================================================================== + ;; II. Three marsh patterns + ;; ===================================================================== + + (~doc-section :title "Three marsh patterns" :id "patterns" + (p "Marshes manifest in three directions, each reversing the flow between the reactive and hypermedia worlds.") + + ;; ----------------------------------------------------------------- + ;; Pattern 1: Server → Signal + ;; ----------------------------------------------------------------- + + (~doc-subsection :title "Pattern 1: Hypermedia writes to reactive state" + (p "The server response carries data that should update a signal. The lake doesn't just display content — it " (em "feeds") " the island's reactive graph.") + + (h4 :class "font-semibold mt-4 mb-2" "Mechanism: data-sx-signal") + (p "A server-rendered element carries a " (code "data-sx-signal") " attribute naming a store signal and its new value. When the morph processes this element, it writes to the signal instead of (or in addition to) updating the DOM.") + (~doc-code :code (highlight ";; Server response includes:\n(div :data-sx-signal \"cart-count:7\"\n (span \"7 items\"))\n\n;; The morph sees data-sx-signal, parses it:\n;; store name = \"cart-count\"\n;; value = 7\n;; Then: (reset! (use-store \"cart-count\") 7)\n;;\n;; Any island anywhere on the page that reads cart-count\n;; updates immediately — fine-grained, no re-render." "lisp")) + + (h4 :class "font-semibold mt-4 mb-2" "Mechanism: sx-on-settle") + (p "An " (code "sx-on-settle") " attribute on a hypermedia trigger element. After the swap completes and the DOM settles, the SX expression is evaluated. This gives the response a chance to run arbitrary reactive logic.") + (~doc-code :code (highlight ";; A search form that updates a signal after results arrive:\n(form :sx-post \"/search\" :sx-target \"#results\"\n :sx-on-settle (reset! (use-store \"result-count\") result-count)\n (input :name \"q\" :placeholder \"Search...\"))" "lisp")) + + (h4 :class "font-semibold mt-4 mb-2" "Mechanism: event bridge (already exists)") + (p "The event bridge (" (code "data-sx-emit") ") already provides server → island communication via custom DOM events. Marshes generalise this: " (code "data-sx-signal") " is a declarative shorthand for the common case of \"server says update this value.\"") + + (div :class "rounded border border-violet-200 bg-violet-50 p-4 mt-4" + (p :class "text-violet-900 font-medium" "Why not just use the event bridge for everything?") + (p :class "text-violet-800 text-sm" "You can. " (code "data-sx-emit") " dispatches a custom event; an effect in an island listens and calls " (code "reset!") ". " (code "data-sx-signal") " cuts out the boilerplate for the most common case. The event bridge remains the right tool for complex multi-step reactions."))) + + ;; ----------------------------------------------------------------- + ;; Pattern 2: Server modifies reactive structure + ;; ----------------------------------------------------------------- + + (~doc-subsection :title "Pattern 2: Hypermedia modifies reactive components" + (p "Lake morphing lets the server update " (em "content") " inside an island. Marsh morphing goes further: the server can send new SX that the island evaluates reactively.") + + (h4 :class "font-semibold mt-4 mb-2" "Mechanism: marsh tag") + (p "A " (code "marsh") " is a zone inside an island where server content is " (em "re-evaluated") " by the island's reactive evaluator, not just inserted as static DOM. When the morph updates a marsh, the new content is parsed as SX and rendered in the island's signal context.") + (~doc-code :code (highlight ";; Inside an island — a marsh re-evaluates on morph:\n(defisland ~product-card (&key product-id)\n (let ((quantity (signal 1))\n (variant (signal nil)))\n (div :class \"card\"\n ;; Lake: server content, inserted as static HTML\n (lake :id \"description\"\n (p \"Loading...\"))\n ;; Marsh: server content, evaluated with access to island signals\n (marsh :id \"controls\"\n ;; Initial content from server — has signal references:\n (div\n (select :bind variant\n (option :value \"red\" \"Red\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"number\" :bind quantity))))))" "lisp")) + (p "When the server sends updated marsh content (e.g., new variant options fetched from a database), the island re-evaluates it in its signal scope. The new " (code "select") " options bind to the existing " (code "variant") " signal. The reactive graph reconnects seamlessly.") + + (h4 :class "font-semibold mt-4 mb-2" "Lake vs. Marsh") + (div :class "overflow-x-auto rounded border border-stone-200 mb-4" + (table :class "w-full text-left text-sm" + (thead (tr :class "border-b border-stone-200 bg-stone-100" + (th :class "px-3 py-2 font-medium text-stone-600" "") + (th :class "px-3 py-2 font-medium text-stone-600" "Lake") + (th :class "px-3 py-2 font-medium text-stone-600" "Marsh"))) + (tbody + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 font-medium text-stone-700" "Content") + (td :class "px-3 py-2 text-stone-600" "Static HTML") + (td :class "px-3 py-2 text-stone-600" "SX expressions")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 font-medium text-stone-700" "Morph action") + (td :class "px-3 py-2 text-stone-600" "Replace DOM children") + (td :class "px-3 py-2 text-stone-600" "Parse SX, evaluate in island scope, replace DOM")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 font-medium text-stone-700" "Signal access") + (td :class "px-3 py-2 text-stone-600" "None (static)") + (td :class "px-3 py-2 text-stone-600" "Full (binds to island signals)")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 font-medium text-stone-700" "Use case") + (td :class "px-3 py-2 text-stone-600" "Display text, labels, metadata") + (td :class "px-3 py-2 text-stone-600" "Forms, controls, interactive fragments")) + (tr + (td :class "px-3 py-2 font-medium text-stone-700" "Overhead") + (td :class "px-3 py-2 text-stone-600" "Minimal (DOM swap)") + (td :class "px-3 py-2 text-stone-600" "Parse + eval (small — SX parser is fast)")))))) + + ;; ----------------------------------------------------------------- + ;; Pattern 3: Reactive state modifies hypermedia + ;; ----------------------------------------------------------------- + + (~doc-subsection :title "Pattern 3: Reactive state directs and transforms hypermedia" + (p "The deepest marsh pattern. Client signals don't just maintain local UI state — they control the hypermedia system itself: what to fetch, where to put it, and " (strong "how to interpret it") ".") + + (h4 :class "font-semibold mt-4 mb-2" "3a: Signal-bound hypermedia attributes") + (p "Hypermedia trigger attributes (" (code "sx-get") ", " (code "sx-post") ", " (code "sx-target") ", " (code "sx-swap") ") can reference signals. The URL, target, and swap strategy become reactive.") + (~doc-code :code (highlight ";; A search input whose endpoint depends on reactive state:\n(defisland ~smart-search ()\n (let ((mode (signal \"products\"))\n (query (signal \"\")))\n (div\n ;; Mode selector — changes what we're searching\n (select :bind mode\n (option :value \"products\" \"Products\")\n (option :value \"events\" \"Events\")\n (option :value \"posts\" \"Posts\"))\n ;; Search input — endpoint changes reactively\n (input :type \"text\" :bind query\n :sx-get (computed (fn () (str \"/search/\" (deref mode) \"?q=\" (deref query))))\n :sx-trigger \"input changed delay:300ms\"\n :sx-target \"#results\")\n (div :id \"results\"))))" "lisp")) + (p "The " (code "sx-get") " URL isn't a static string — it's a computed signal. When the mode changes, the next search hits a different endpoint. The hypermedia trigger system reads the signal's current value at trigger time.") + + (h4 :class "font-semibold mt-4 mb-2" "3b: Reactive swap transforms") + (p "A " (code "marsh-transform") " function that processes server content " (em "before") " it enters the DOM. The transform has access to island signals, so it can reshape the same server response differently based on client state.") + (~doc-code :code (highlight ";; View mode transforms how server results are displayed:\n(defisland ~result-list ()\n (let ((view (signal \"list\"))\n (sort-key (signal \"date\")))\n (div\n (div :class \"flex gap-2\"\n (button :on-click (fn (e) (reset! view \"list\")) \"List\")\n (button :on-click (fn (e) (reset! view \"grid\")) \"Grid\")\n (button :on-click (fn (e) (reset! view \"compact\")) \"Compact\"))\n ;; Marsh: server sends a result list; client transforms its rendering\n (marsh :id \"results\"\n :transform (fn (sx-content)\n (case (deref view)\n \"grid\" (wrap-grid sx-content)\n \"compact\" (compact-view sx-content)\n :else sx-content))\n ;; Initial server content\n (div :class \"space-y-2\" \"Loading...\")))))" "lisp")) + (p "The server sends the same canonical result list every time. The " (code ":transform") " function — a reactive closure over the " (code "view") " signal — reshapes it into a grid, compact list, or default list. Change the view signal → existing content is re-transformed without a server round-trip. Fetch new results → they arrive pre-sorted, then the transform applies the current view.") + + (h4 :class "font-semibold mt-4 mb-2" "3c: Reactive interpretation") + (p "The most intimate marsh pattern. Reactive state modifies " (em "how the hypermedia system itself behaves") " — not just what's fetched or how it's displayed, but the rules of interpretation.") + (ul :class "list-disc pl-5 space-y-2 text-stone-600" + (li (strong "Reactive swap strategy: ") "A signal controls whether the swap is " (code "innerHTML") ", " (code "outerHTML") ", " (code "morph") ", or " (code "append") ". The same server response, the same target — but the merge strategy depends on client state. A chat app might " (code "append") " new messages normally but " (code "morph") " when the user scrolls to a different thread.") + (li (strong "Reactive content filter: ") "A signal-driven predicate that filters server content before insertion. The server sends everything; the client's reactive state determines what's visible. A notification feed where the read/unread signal controls whether dismissed items re-appear after a morph.") + (li (strong "Reactive content rewriting: ") "Signals that rewrite attributes or classes on incoming server HTML. A dark-mode signal that rewrites " (code "bg-white") " to " (code "bg-stone-900") " on every server fragment before insertion. An accessibility signal that increases font sizes. The server doesn't need to know about these preferences — the marsh transform applies them at the edge.") + (li (strong "Reactive routing: ") "A signal that changes how hypermedia URLs are resolved. A " (code "locale") " signal that prepends " (code "/fr/") " to every " (code "sx-get") " URL. A " (code "preview-mode") " signal that reroutes to draft endpoints. The server sees clean, canonical URLs — the client's reactive state performs the translation.")))) + + ;; ===================================================================== + ;; III. Spec primitives + ;; ===================================================================== + + (~doc-section :title "Spec primitives" :id "primitives" + (p "Five new constructs, all specced in " (code ".sx") " files, bootstrapped to every host.") + + (h4 :class "font-semibold mt-4 mb-2" "1. marsh tag") + (~doc-code :code (highlight ";; In adapter-dom.sx / adapter-html.sx / adapter-sx.sx:\n;;\n;; (marsh :id \"controls\" :transform transform-fn children...)\n;;\n;; Server: renders as
children HTML
\n;; Client: wraps children in reactive evaluation scope\n;; Morph: re-parses incoming SX, evaluates in island scope, replaces DOM\n;;\n;; The :transform is optional. If present, it's called on the parsed SX\n;; before evaluation. The transform has full signal access." "lisp")) + + (h4 :class "font-semibold mt-4 mb-2" "2. data-sx-signal (morph integration)") + (~doc-code :code (highlight ";; In engine.sx, morph-children:\n;;\n;; When processing a new element with data-sx-signal=\"name:value\":\n;; 1. Parse the attribute: store-name, signal-value\n;; 2. Look up (use-store store-name) — finds or creates the signal\n;; 3. (reset! signal parsed-value)\n;; 4. Remove the data-sx-signal attribute from DOM (consumed)\n;;\n;; Values are JSON-parsed: \"7\" → 7, '\"hello\"' → \"hello\",\n;; 'true' → true, '{...}' → dict" "lisp")) + + (h4 :class "font-semibold mt-4 mb-2" "3. Signal-bound hypermedia attributes") + (~doc-code :code (highlight ";; In orchestration.sx, resolve-trigger-attrs:\n;;\n;; Before issuing a fetch, read sx-get/sx-post/sx-target/sx-swap.\n;; If the value is a signal or computed, deref it at trigger time.\n;;\n;; (define resolve-trigger-url\n;; (fn (el attr)\n;; (let ((val (dom-get-attr el attr)))\n;; (if (signal? val) (deref val) val))))\n;;\n;; This means the URL is evaluated lazily — it reflects the current\n;; signal state at the moment the user acts, not when the DOM was built." "lisp")) + + (h4 :class "font-semibold mt-4 mb-2" "4. marsh-transform (swap pipeline)") + (~doc-code :code (highlight ";; In orchestration.sx, process-swap:\n;;\n;; After receiving server HTML and before inserting into target:\n;; 1. Find the target element\n;; 2. If target has data-sx-marsh, find its transform function\n;; 3. Parse server content as SX\n;; 4. Call transform(sx-content) — transform is a reactive closure\n;; 5. Evaluate the transformed SX in the island's signal scope\n;; 6. Replace the marsh's DOM children\n;;\n;; The transform runs inside the island's tracking context,\n;; so computed/effect dependencies are captured automatically.\n;; When a signal the transform reads changes, the marsh re-transforms." "lisp")) + + (h4 :class "font-semibold mt-4 mb-2" "5. sx-on-settle (post-swap hook)") + (~doc-code :code (highlight ";; In orchestration.sx, after swap completes:\n;;\n;; (define process-settle-hooks\n;; (fn (trigger-el)\n;; (let ((hook (dom-get-attr trigger-el \"sx-on-settle\")))\n;; (when hook\n;; (eval-expr (parse hook) (island-env trigger-el))))))\n;;\n;; The expression is evaluated in the nearest island's environment,\n;; giving it access to signals, stores, and island-local functions." "lisp"))) + + ;; ===================================================================== + ;; IV. The morph enters the marsh + ;; ===================================================================== + + (~doc-section :title "The morph enters the marsh" :id "morph" + (p "The morph algorithm already handles three zones: static DOM (full reconciliation), islands (preserve reactive nodes), and lakes (update static content within islands). Marshes add a fourth:") + + (div :class "overflow-x-auto rounded border border-stone-200 mb-4" + (table :class "w-full text-left text-sm" + (thead (tr :class "border-b border-stone-200 bg-stone-100" + (th :class "px-3 py-2 font-medium text-stone-600" "Zone") + (th :class "px-3 py-2 font-medium text-stone-600" "Marker") + (th :class "px-3 py-2 font-medium text-stone-600" "Morph behaviour"))) + (tbody + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700" "Static DOM") + (td :class "px-3 py-2 font-mono text-sm text-stone-600" "(none)") + (td :class "px-3 py-2 text-stone-600" "Full morph — attrs, children, text")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700" "Island") + (td :class "px-3 py-2 font-mono text-sm text-stone-600" "data-sx-island") + (td :class "px-3 py-2 text-stone-600" "Enter, find lakes/marshes, update them, skip everything else")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700" "Lake") + (td :class "px-3 py-2 font-mono text-sm text-stone-600" "data-sx-lake") + (td :class "px-3 py-2 text-stone-600" "Replace static HTML children")) + (tr + (td :class "px-3 py-2 text-stone-700" "Marsh") + (td :class "px-3 py-2 font-mono text-sm text-stone-600" "data-sx-marsh") + (td :class "px-3 py-2 text-stone-600" "Parse new content as SX, apply transform, evaluate in island scope, replace DOM"))))) + + (~doc-code :code (highlight ";; Updated morph-island-children in engine.sx:\n\n(define morph-island-children\n (fn (old-island new-island)\n (let ((old-lakes (dom-query-all old-island \"[data-sx-lake]\"))\n (new-lakes (dom-query-all new-island \"[data-sx-lake]\"))\n (old-marshes (dom-query-all old-island \"[data-sx-marsh]\"))\n (new-marshes (dom-query-all new-island \"[data-sx-marsh]\")))\n ;; Build lookup maps\n (let ((new-lake-map (index-by-attr new-lakes \"data-sx-lake\"))\n (new-marsh-map (index-by-attr new-marshes \"data-sx-marsh\")))\n ;; Lakes: static DOM swap\n (for-each\n (fn (old-lake)\n (let ((id (dom-get-attr old-lake \"data-sx-lake\"))\n (new-lake (dict-get new-lake-map id)))\n (when new-lake\n (sync-attrs old-lake new-lake)\n (morph-children old-lake new-lake))))\n old-lakes)\n ;; Marshes: parse + evaluate + replace\n (for-each\n (fn (old-marsh)\n (let ((id (dom-get-attr old-marsh \"data-sx-marsh\"))\n (new-marsh (dict-get new-marsh-map id)))\n (when new-marsh\n (morph-marsh old-marsh new-marsh old-island))))\n old-marshes)\n ;; Signal updates from data-sx-signal\n (process-signal-updates new-island)))))" "lisp")) + + (~doc-code :code (highlight ";; morph-marsh: re-evaluate server content in island scope\n\n(define morph-marsh\n (fn (old-marsh new-marsh island-el)\n (let ((transform (get-marsh-transform old-marsh))\n (new-sx (dom-inner-sx new-marsh))\n (island-env (get-island-env island-el)))\n ;; Apply transform if present\n (let ((transformed (if transform (transform new-sx) new-sx)))\n ;; Dispose old reactive bindings in this marsh\n (dispose-marsh-scope old-marsh)\n ;; Evaluate the SX in island scope — creates new reactive bindings\n (with-marsh-scope old-marsh\n (let ((new-dom (render-to-dom transformed island-env)))\n (dom-replace-children old-marsh new-dom)))))))" "lisp"))) + + ;; ===================================================================== + ;; V. Signal lifecycle in marshes + ;; ===================================================================== + + (~doc-section :title "Signal lifecycle" :id "lifecycle" + (p "Marshes introduce a sub-scope within the island's reactive context. When a marsh is re-evaluated (morph or transform change), its old effects and computeds must be disposed without disturbing the island's own reactive graph.") + + (~doc-subsection :title "Scoping" + (~doc-code :code (highlight ";; In signals.sx:\n\n(define with-marsh-scope\n (fn (marsh-el body-fn)\n ;; Create a child scope under the current island scope\n ;; All effects/computeds created during body-fn register here\n (let ((parent-scope (current-island-scope))\n (marsh-scope (create-child-scope parent-scope (dom-get-attr marsh-el \"data-sx-marsh\"))))\n (with-scope marsh-scope\n (body-fn)))))\n\n(define dispose-marsh-scope\n (fn (marsh-el)\n ;; Dispose all effects/computeds registered in this marsh's scope\n ;; Parent island scope and sibling marshes are unaffected\n (let ((scope (get-marsh-scope marsh-el)))\n (when scope (dispose-scope scope)))))" "lisp")) + (p "The scoping hierarchy: " (strong "island") " → " (strong "marsh") " → " (strong "effects/computeds") ". Disposing a marsh disposes its subscope. Disposing an island disposes all its marshes. The signal graph is a tree, not a flat list.")) + + (~doc-subsection :title "Reactive transforms" + (p "When a marsh has a " (code ":transform") " function, the transform itself is an effect. It reads signals (via " (code "deref") " inside the transform body) and produces transformed SX. When those signals change, the transform re-runs, the marsh re-evaluates, and the DOM updates — all without a server round-trip.") + (p "The transform effect belongs to the marsh scope, so it's automatically disposed when the marsh is morphed with new content."))) + + ;; ===================================================================== + ;; VI. Reactive interpretation — the deep end + ;; ===================================================================== + + (~doc-section :title "Reactive interpretation" :id "interpretation" + (p "The deepest marsh pattern isn't about transforming content — it's about transforming the " (em "rules") ". Reactive state modifies how the hypermedia system itself operates.") + + (~doc-subsection :title "Swap strategy as signal" + (p "The same server response inserted differently based on client state:") + (~doc-code :code (highlight ";; Chat app: append messages normally, morph when switching threads\n(defisland ~chat ()\n (let ((mode (signal \"live\")))\n (div\n (div :sx-get \"/messages/latest\"\n :sx-trigger \"every 2s\"\n :sx-target \"#messages\"\n :sx-swap (computed (fn ()\n (if (= (deref mode) \"live\") \"beforeend\" \"innerHTML\")))\n (div :id \"messages\")))))" "lisp")) + (p "In " (code "\"live\"") " mode, new messages append. Switch to thread view — the same polling endpoint now replaces the whole list. The server doesn't change. The client's reactive state changes the " (em "semantics") " of the swap.")) + + (~doc-subsection :title "URL rewriting as signal" + (p "Reactive state transparently modifies request URLs:") + (~doc-code :code (highlight ";; Locale prefix — the server sees /fr/products, /en/products, etc.\n;; The author writes /products — the marsh layer prepends the locale.\n(def-store \"locale\" \"en\")\n\n;; In orchestration.sx, resolve-trigger-url:\n(define resolve-trigger-url\n (fn (el attr)\n (let ((raw (dom-get-attr el attr))\n (locale (deref (use-store \"locale\"))))\n (if (and locale (not (starts-with? raw (str \"/\" locale))))\n (str \"/\" locale raw)\n raw))))" "lisp")) + (p "Every " (code "sx-get") " and " (code "sx-post") " URL passes through the resolver. A locale signal, a preview-mode signal, an A/B-test signal — any reactive state can transparently rewrite the request the server sees.")) + + (~doc-subsection :title "Content rewriting as signal" + (p "Incoming server HTML passes through a reactive filter before insertion:") + (~doc-code :code (highlight ";; Dark mode — rewrites server classes before insertion\n(def-store \"theme\" \"light\")\n\n;; In orchestration.sx, after receiving server HTML:\n(define apply-theme-transform\n (fn (html-str)\n (if (= (deref (use-store \"theme\")) \"dark\")\n (-> html-str\n (replace-all \"bg-white\" \"bg-stone-900\")\n (replace-all \"text-stone-800\" \"text-stone-100\")\n (replace-all \"border-stone-200\" \"border-stone-700\"))\n html-str)))" "lisp")) + (p "The server renders canonical light-mode HTML. The client's theme signal rewrites it at the edge. No server-side theme support needed. No separate dark-mode templates. The same document, different interpretation.") + (p "This is the Hegelian deepening: the reactive state isn't just " (em "alongside") " the hypermedia content. It " (em "constitutes the lens through which the content is perceived") ". The marsh isn't a zone in the DOM — it's a layer in the interpretation pipeline."))) + + ;; ===================================================================== + ;; VII. Implementation order + ;; ===================================================================== + + (~doc-section :title "Implementation order" :id "implementation" + (p "Spec-first, bootstrap-second, like everything else.") + + (div :class "overflow-x-auto rounded border border-stone-200 mb-4" + (table :class "w-full text-left text-sm" + (thead (tr :class "border-b border-stone-200 bg-stone-100" + (th :class "px-3 py-2 font-medium text-stone-600" "Phase") + (th :class "px-3 py-2 font-medium text-stone-600" "Spec files") + (th :class "px-3 py-2 font-medium text-stone-600" "What it enables"))) + (tbody + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700 font-medium" "1. marsh tag") + (td :class "px-3 py-2 text-stone-600" "adapter-dom.sx, adapter-html.sx, adapter-sx.sx") + (td :class "px-3 py-2 text-stone-600" "Server-morphable zones with reactive re-evaluation inside islands")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700 font-medium" "2. data-sx-signal") + (td :class "px-3 py-2 text-stone-600" "engine.sx (morph-island-children)") + (td :class "px-3 py-2 text-stone-600" "Server responses write to named store signals")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700 font-medium" "3. Signal-bound triggers") + (td :class "px-3 py-2 text-stone-600" "orchestration.sx (resolve-trigger-url)") + (td :class "px-3 py-2 text-stone-600" "sx-get/sx-post URLs computed from signals")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700 font-medium" "4. marsh-transform") + (td :class "px-3 py-2 text-stone-600" "engine.sx, signals.sx (marsh scopes)") + (td :class "px-3 py-2 text-stone-600" "Reactive closures reshape server content before insertion")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700 font-medium" "5. sx-on-settle") + (td :class "px-3 py-2 text-stone-600" "orchestration.sx (process-settle-hooks)") + (td :class "px-3 py-2 text-stone-600" "Post-swap SX evaluation with island scope access")) + (tr + (td :class "px-3 py-2 text-stone-700 font-medium" "6. Reactive interpretation") + (td :class "px-3 py-2 text-stone-600" "orchestration.sx (swap pipeline)") + (td :class "px-3 py-2 text-stone-600" "Signal-driven swap strategy, URL rewriting, content transforms"))))) + + (p "Each phase is independently deployable. Phase 1-2 are the foundation. Phase 3-5 enable the marsh patterns. Phase 6 is the deep end — reactive interpretation of the hypermedia system itself.")) + + ;; ===================================================================== + ;; VIII. Design principles + ;; ===================================================================== + + (~doc-section :title "Design principles" :id "principles" + (ol :class "space-y-3 text-stone-600 list-decimal list-inside" + (li (strong "Marshes are opt-in per zone.") " " (code "lake") " remains the default for server content inside islands. " (code "marsh") " is for the zones that need reactive re-evaluation. Don't use a marsh where a lake suffices.") + (li (strong "The server doesn't need to know.") " Marsh transforms, signal-bound URLs, reactive interpretation — these are client-side concerns. The server sends canonical content. The client's reactive state shapes how it arrives. The server remains simple.") + (li (strong "The signal graph is a tree.") " Island → marsh → effects. Dispose a marsh, its subscope dies. Dispose an island, all its marshes die. No orphan effects. No memory leaks. No cleanup boilerplate.") + (li (strong "Transforms are pure where possible.") " A " (code ":transform") " function should be a pure function of (content, signal-values). Side effects belong in " (code "effect") " blocks, not transforms. This makes transforms testable, composable, and predictable.") + (li (strong "Spec-first.") " Every marsh primitive lives in " (code ".sx") " spec files. Bootstrapped to JS and Python. The same marshes will work on future hosts."))) + + ;; ===================================================================== + ;; IX. The dialectic continues + ;; ===================================================================== + + (~doc-section :title "The dialectic continues" :id "dialectic" + (p "Islands separated client state from server content. Lakes let server content flow through islands. Marshes dissolve the boundary entirely — the same zone is simultaneously server-authored and reactively interpreted.") + (p "This is the next turn of the Hegelian spiral. The thesis (pure hypermedia) posited the server as sole authority. The antithesis (reactive islands) gave the client its own inner life. The first synthesis (islands + lakes) maintained the boundary between them. The second synthesis (marshes) " (em "sublates the boundary itself") ".") + (p "In a marsh, you can't point to a piece of DOM and say \"this is server territory\" or \"this is client territory.\" It's both. The server sent it. The client transformed it. The server can update it. The client will re-transform it. The signal reads the server data. The server data feeds the signal. Subject and substance are one.") + (p "The practical consequence: an SX application can handle " (em "any") " interaction pattern without breaking its architecture. Pure content → hypermedia. Micro-interactions → L1 DOM ops. Reactive UI → islands. Server slots → lakes. And now, for the places where reactivity and hypermedia must truly merge — marshes.")))) diff --git a/sx/sxc/pages/docs.sx b/sx/sxc/pages/docs.sx index ac8dd3db..9bf9b207 100644 --- a/sx/sxc/pages/docs.sx +++ b/sx/sxc/pages/docs.sx @@ -541,6 +541,7 @@ "event-bridge" (~reactive-islands-event-bridge-content) "named-stores" (~reactive-islands-named-stores-content) "plan" (~reactive-islands-plan-content) + "marshes" (~reactive-islands-marshes-content) "phase2" (~reactive-islands-phase2-content) :else (~reactive-islands-index-content))))