Merge worktree-zero-tooling-essay into macros

Resolves conflicts by keeping both:
- HEAD: cache invalidation, service worker, sw-post-message
- Worktree: optimistic mutations, offline queue, action endpoint
Plans.sx unified with combined 7c/7d documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 01:33:49 +00:00
8 changed files with 465 additions and 32 deletions

View File

@@ -135,7 +135,9 @@
(dict :label "Data Test" :href "/isomorphism/data-test")
(dict :label "Async IO" :href "/isomorphism/async-io")
(dict :label "Streaming" :href "/isomorphism/streaming")
(dict :label "Affinity" :href "/isomorphism/affinity")))
(dict :label "Affinity" :href "/isomorphism/affinity")
(dict :label "Optimistic" :href "/isomorphism/optimistic")
(dict :label "Offline" :href "/isomorphism/offline")))
(define plans-nav-items (list
(dict :label "Status" :href "/plans/status"

76
sx/sx/offline-demo.sx Normal file
View File

@@ -0,0 +1,76 @@
;; Offline data layer demo — exercises Phase 7d offline mutation queue.
;;
;; Shows connectivity status, note list, and offline mutation queue.
;; When offline, mutations are queued locally. On reconnect, they sync.
;;
;; Open browser console and look for:
;; "sx:offline queued" — mutation added to queue while offline
;; "sx:offline syncing" — reconnected, replaying queued mutations
;; "sx:offline synced" — individual mutation confirmed by server
(defcomp ~offline-demo-content (&key notes server-time)
(div :class "space-y-8"
(div :class "border-b border-stone-200 pb-6"
(h1 :class "text-2xl font-bold text-stone-900" "Offline Data Layer")
(p :class "mt-2 text-stone-600"
"This page tests Phase 7d offline capabilities. Mutations made while "
"offline are queued locally and replayed when connectivity returns."))
;; Connectivity indicator
(div :class "rounded-lg border border-stone-200 bg-white p-6 space-y-3"
(h2 :class "text-lg font-semibold text-stone-800" "Status")
(dl :class "grid grid-cols-2 gap-2 text-sm"
(dt :class "font-medium text-stone-600" "Server time")
(dd :class "font-mono text-stone-900" server-time)
(dt :class "font-medium text-stone-600" "Notes count")
(dd :class "text-stone-900" (str (len notes)))
(dt :class "font-medium text-stone-600" "Connectivity")
(dd :class "text-stone-900"
(span :id "offline-status" :class "inline-flex items-center gap-1.5"
(span :class "w-2 h-2 rounded-full bg-green-500")
"Online"))))
;; Note list
(div :class "space-y-3"
(h2 :class "text-lg font-semibold text-stone-800" "Notes")
(div :id "offline-notes" :class "space-y-2"
(map (fn (note)
(div :class "flex items-center justify-between rounded border border-stone-100 bg-white p-3"
(div :class "flex items-center gap-3"
(span :class "flex-none rounded-full bg-blue-100 text-blue-700 w-6 h-6 flex items-center justify-center text-xs font-bold"
(str (get note "id")))
(span :class "text-stone-900" (get note "text")))
(span :class "text-xs text-stone-400 font-mono"
(get note "created"))))
notes)))
;; Architecture
(div :class "space-y-4"
(h2 :class "text-lg font-semibold text-stone-800" "How it works")
(div :class "space-y-2"
(map-indexed
(fn (i step)
(div :class "flex items-start gap-3 rounded border border-stone-100 bg-white p-3"
(span :class "flex-none rounded-full bg-stone-100 text-stone-700 w-6 h-6 flex items-center justify-center text-xs font-bold"
(str (+ i 1)))
(div
(div :class "font-medium text-stone-900" (get step "label"))
(div :class "text-sm text-stone-500" (get step "detail")))))
(list
(dict :label "Online mutation" :detail "Routes through submit-mutation (Phase 7c) — optimistic predict, server confirm/revert")
(dict :label "Offline detection" :detail "Browser 'offline' event sets _is-online to false. offline-aware-mutation routes to queue")
(dict :label "Queue mutation" :detail "Mutation stored in _offline-queue with 'pending' status. Optimistic update applied locally")
(dict :label "Reconnect" :detail "Browser 'online' event triggers offline-sync — replays queue in order via execute-action")
(dict :label "Sync result" :detail "Each mutation marked 'synced' or 'failed'. Failed mutations stay for manual retry")))))
;; How to test
(div :class "rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm space-y-2"
(p :class "font-semibold text-amber-800" "How to test offline behavior")
(ol :class "list-decimal list-inside text-amber-700 space-y-1"
(li "Open the browser console and Network tab")
(li "Navigate to this page via client-side routing")
(li "In DevTools, set Network to \"Offline\" mode")
(li "The connectivity indicator should change to red/Offline")
(li "Watch console for " (code :class "bg-amber-100 px-1 rounded" "sx:offline queued"))
(li "Re-enable network — watch for " (code :class "bg-amber-100 px-1 rounded" "sx:offline syncing"))
(li "Queued mutations replay and confirm or fail")))))

81
sx/sx/optimistic-demo.sx Normal file
View File

@@ -0,0 +1,81 @@
;; Optimistic update demo — exercises Phase 7c client-side predicted mutations.
;;
;; This page shows a todo list with optimistic add/remove.
;; Mutations are predicted client-side, sent to server, and confirmed/reverted.
;;
;; Open browser console and look for:
;; "sx:optimistic confirmed" — server accepted the mutation
;; "sx:optimistic reverted" — server rejected, data rolled back
(defcomp ~optimistic-demo-content (&key items server-time)
(div :class "space-y-8"
(div :class "border-b border-stone-200 pb-6"
(h1 :class "text-2xl font-bold text-stone-900" "Optimistic Updates")
(p :class "mt-2 text-stone-600"
"This page tests Phase 7c optimistic data mutations. Items are updated "
"instantly on the client, then confirmed or reverted when the server responds."))
;; Server metadata
(div :class "rounded-lg border border-stone-200 bg-white p-6 space-y-3"
(h2 :class "text-lg font-semibold text-stone-800" "Current state")
(dl :class "grid grid-cols-2 gap-2 text-sm"
(dt :class "font-medium text-stone-600" "Server time")
(dd :class "font-mono text-stone-900" server-time)
(dt :class "font-medium text-stone-600" "Item count")
(dd :class "text-stone-900" (str (len items)))))
;; Item list
(div :class "space-y-3"
(h2 :class "text-lg font-semibold text-stone-800" "Items")
(div :id "optimistic-items" :class "space-y-2"
(map (fn (item)
(div :class "flex items-center justify-between rounded border border-stone-100 bg-white p-3"
(div :class "flex items-center gap-3"
(span :class "flex-none rounded-full bg-violet-100 text-violet-700 w-6 h-6 flex items-center justify-center text-xs font-bold"
(str (get item "id")))
(span :class "text-stone-900" (get item "label")))
(span :class "text-xs px-2 py-0.5 rounded-full"
:class (case (get item "status")
"confirmed" "bg-green-100 text-green-700"
"pending" "bg-amber-100 text-amber-700"
"reverted" "bg-red-100 text-red-700"
:else "bg-stone-100 text-stone-500")
(get item "status"))))
items))
;; Add button — triggers optimistic mutation
(div :class "pt-2"
(button :class "px-4 py-2 bg-violet-500 text-white rounded hover:bg-violet-600 text-sm"
:sx-post "/sx/action/add-demo-item"
:sx-target "#main-panel"
:sx-vals "{\"label\": \"New item\"}"
"Add item (optimistic)")))
;; How it works
(div :class "space-y-4"
(h2 :class "text-lg font-semibold text-stone-800" "How it works")
(div :class "space-y-2"
(map-indexed
(fn (i step)
(div :class "flex items-start gap-3 rounded border border-stone-100 bg-white p-3"
(span :class "flex-none rounded-full bg-stone-100 text-stone-700 w-6 h-6 flex items-center justify-center text-xs font-bold"
(str (+ i 1)))
(div
(div :class "font-medium text-stone-900" (get step "label"))
(div :class "text-sm text-stone-500" (get step "detail")))))
(list
(dict :label "Predict" :detail "Client applies mutator function to cached data immediately")
(dict :label "Snapshot" :detail "Pre-mutation data saved in _optimistic-snapshots for rollback")
(dict :label "Re-render" :detail "Page content re-evaluated and swapped with predicted data")
(dict :label "Submit" :detail "Mutation sent to server via POST /sx/action/<name>")
(dict :label "Confirm or revert" :detail "Server responds — cache updated with truth, or reverted to snapshot")))))
;; How to verify
(div :class "rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm space-y-2"
(p :class "font-semibold text-amber-800" "How to verify")
(ol :class "list-decimal list-inside text-amber-700 space-y-1"
(li "Open the browser console (F12)")
(li "Navigate to this page from another isomorphism page")
(li "Click \"Add item\" — item appears instantly with \"pending\" status")
(li "Watch console for " (code :class "bg-amber-100 px-1 rounded" "sx:optimistic confirmed"))
(li "Item status changes to \"confirmed\" when server responds")))))

View File

@@ -1393,12 +1393,12 @@
(p :class "text-sm text-stone-600" "Server streams partially-evaluated SX as IO resolves. ~suspense component renders fallbacks, inline resolution scripts fill in content. Concurrent IO via asyncio, chunked transfer encoding.")
(p :class "text-sm text-stone-500 mt-1" "Demo: " (a :href "/isomorphism/streaming" "/isomorphism/streaming")))
(div :class "rounded border border-stone-200 bg-stone-50 p-4"
(div :class "rounded border border-green-300 bg-green-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-stone-500 text-white uppercase" "Not Started")
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/isomorphism/" :class "font-semibold text-stone-800 underline" "Isomorphic Phase 7: Full Isomorphism"))
(p :class "text-sm text-stone-600" "Runtime boundary optimizer, affinity annotations, offline data layer via Service Worker + IndexedDB, isomorphic testing harness.")
(p :class "text-sm text-stone-500 mt-1" "Depends on: all previous phases."))))))
(p :class "text-sm text-stone-600" "Affinity annotations, render plans, optimistic data updates, offline mutation queue, isomorphic testing harness, universal page descriptor.")
(p :class "text-sm text-stone-500 mt-1" "All 6 sub-phases (7a7f) complete."))))))
;; ---------------------------------------------------------------------------
;; Fragment Protocol
@@ -2039,55 +2039,56 @@
(li "Render plans visible on " (a :href "/isomorphism/affinity" "affinity demo page"))
(li "Client page registry includes :render-plan for each page"))))
(~doc-subsection :title "7c. Cache Invalidation & Data Updates"
(~doc-subsection :title "7c. Cache Invalidation & Optimistic Data Updates"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
(p :class "text-green-800 text-sm" "Client data cache management: invalidation on mutation, server-driven cache updates, and programmatic cache control from SX code."))
(p :class "text-green-800 text-sm" "Client data cache management, optimistic predicted mutations with snapshot rollback, and server-driven cache updates."))
(p "The client-side page data cache (30-second TTL) now supports cache invalidation and server-driven updates, extending the existing DOM-level " (code "apply-optimistic") "/" (code "revert-optimistic") " to data-level cache management.")
(p "The client-side page data cache (30-second TTL) now supports cache invalidation, server-driven updates, and optimistic mutations. The client predicts the result of a mutation, immediately re-renders with the predicted data, and confirms or reverts when the server responds.")
(~doc-subsection :title "Element Attributes"
(~doc-subsection :title "Cache Invalidation"
(p "Component authors can declare cache invalidation on elements that trigger mutations:")
(~doc-code :code (highlight ";; Clear specific page's cache after successful action\n(form :sx-post \"/cart/remove\"\n :sx-cache-invalidate \"cart-page\"\n ...)\n\n;; Clear ALL page caches after action\n(button :sx-post \"/admin/reset\"\n :sx-cache-invalidate \"*\")" "lisp"))
(p "When the request succeeds, the named page's data cache is cleared. The next client-side navigation to that page will re-fetch fresh data from the server."))
(~doc-subsection :title "Response Headers"
(p "The server can also control client cache via response headers:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "SX-Cache-Invalidate: page-name") " — clear cache for a page")
(li (code "SX-Cache-Invalidate: *") " — clear all page caches")
(li (code "SX-Cache-Update: page-name") " — replace cache with the response body (SX-format data)"))
(p (code "SX-Cache-Update") " is the strongest form: the server pushes authoritative data directly into the client cache, so the user sees fresh data immediately on next navigation — no re-fetch needed."))
(li (code "SX-Cache-Update: page-name") " — replace cache with the response body (SX-format data)")))
(~doc-subsection :title "Programmatic API"
(p "Three functions available from SX orchestration code:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "(invalidate-page-cache page-name)")
(li "(invalidate-all-page-cache)")
(li "(update-page-cache page-name data)")))
(~doc-subsection :title "Optimistic Mutations"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "optimistic-cache-update") " — applies a mutator function to cached data, saves a snapshot for rollback")
(li (strong "optimistic-cache-revert") " — restores the pre-mutation snapshot if the server rejects")
(li (strong "optimistic-cache-confirm") " — discards the snapshot after server confirmation")
(li (strong "submit-mutation") " — orchestration function: predict, submit, confirm/revert")
(li (strong "/sx/action/<name>") " — server endpoint for processing mutations (POST, returns SX wire format)")))
(~doc-subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/ref/orchestration.sx — cache management + optimistic cache functions + submit-mutation spec")
(li "shared/sx/ref/engine.sx — SX-Cache-Invalidate, SX-Cache-Update response headers")
(li "shared/sx/ref/orchestration.sx — cache management functions, process-cache-directives")
(li "shared/sx/ref/bootstrap_js.py — parseSxData platform function"))))
(li "shared/sx/pages.py — mount_action_endpoint for /sx/action/<name>")
(li "sx/sx/optimistic-demo.sx — live demo component")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Live demo at " (a :href "/isomorphism/optimistic" :class "text-violet-600 hover:underline" "/isomorphism/optimistic"))
(li "Console log: " (code "sx:optimistic confirmed") " / " (code "sx:optimistic reverted")))))
(~doc-subsection :title "7d. Offline Data Layer"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
(p :class "text-green-800 text-sm" "Service Worker with IndexedDB caching for offline-capable page data and IO responses. Static assets cached via Cache API."))
(p :class "text-green-800 text-sm" "Service Worker with IndexedDB caching, connectivity tracking, and offline mutation queue with replay on reconnect."))
(p "A Service Worker registered at " (code "/sx-sw.js") " provides three-tier caching:")
(p "A Service Worker registered at " (code "/sx-sw.js") " provides three-tier caching, plus an offline mutation queue that builds on Phase 7c's optimistic updates:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "/sx/data/* ") "— network-first with IndexedDB fallback. Page data is cached on successful fetch and served from IndexedDB when offline.")
(li (strong "/sx/data/* ") "— network-first with IndexedDB fallback. Page data cached on fetch, served from IndexedDB when offline.")
(li (strong "/sx/io/* ") "— network-first with IndexedDB fallback. IO proxy responses cached the same way.")
(li (strong "/static/* ") "— stale-while-revalidate via Cache API. Serves cached CSS/JS/images immediately, updates in background."))
(p "Cache invalidation flows through to the Service Worker: when " (code "invalidate-page-cache") " clears the in-memory cache, it also sends a " (code "postMessage") " to the SW which removes matching entries from IndexedDB.")
(li (strong "/static/* ") "— stale-while-revalidate via Cache API. Serves cached assets immediately, updates in background.")
(li (strong "Offline mutations") " — " (code "offline-aware-mutation") " routes to " (code "submit-mutation") " when online, " (code "offline-queue-mutation") " when offline. " (code "offline-sync") " replays the queue on reconnect."))
(~doc-subsection :title "How It Works"
(ol :class "list-decimal list-inside text-stone-700 space-y-2"
@@ -2095,14 +2096,21 @@
(li "SW intercepts fetch events and routes by URL pattern")
(li "For data/IO: try network first, on failure serve from IndexedDB")
(li "For static assets: serve from Cache API, revalidate in background")
(li "Cache invalidation propagates: element attr / response header → in-memory cache → SW message → IndexedDB")))
(li "Cache invalidation propagates: element attr / response header → in-memory cache → SW message → IndexedDB")
(li "Offline mutations queue locally, replay on reconnect via " (code "offline-sync"))))
(~doc-subsection :title "Files"
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/static/scripts/sx-sw.js — Service Worker (network-first + stale-while-revalidate)")
(li "shared/sx/ref/orchestration.sx — offline queue, sync, connectivity tracking, sw-post-message")
(li "shared/sx/pages.py — mount_service_worker() serves SW at /sx-sw.js")
(li "shared/sx/ref/bootstrap_js.py — SW registration in boot init")
(li "shared/sx/ref/orchestration.sx — sw-post-message for cache invalidation"))))
(li "sx/sx/offline-demo.sx — live demo component")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Live demo at " (a :href "/isomorphism/offline" :class "text-violet-600 hover:underline" "/isomorphism/offline"))
(li "Test with DevTools Network → Offline mode")
(li "Console log: " (code "sx:offline queued") ", " (code "sx:offline syncing") ", " (code "sx:offline synced")))))
(~doc-subsection :title "7e. Isomorphic Testing"

View File

@@ -525,6 +525,30 @@
:data (affinity-demo-data)
:content (~affinity-demo-content :components components :page-plans page-plans))
(defpage optimistic-demo
:path "/isomorphism/optimistic"
:auth :public
:layout (:sx-section
:section "Isomorphism"
:sub-label "Isomorphism"
:sub-href "/isomorphism/"
:sub-nav (~section-nav :items isomorphism-nav-items :current "Optimistic")
:selected "Optimistic")
:data (optimistic-demo-data)
:content (~optimistic-demo-content :items items :server-time server-time))
(defpage offline-demo
:path "/isomorphism/offline"
:auth :public
:layout (:sx-section
:section "Isomorphism"
:sub-label "Isomorphism"
:sub-href "/isomorphism/"
:sub-nav (~section-nav :items isomorphism-nav-items :current "Offline")
:selected "Offline")
:data (offline-demo-data)
:content (~offline-demo-content :notes notes :server-time server-time))
;; Wildcard must come AFTER specific routes (first-match routing)
(defpage isomorphism-page
:path "/isomorphism/<slug>"

View File

@@ -28,6 +28,9 @@ def _register_sx_helpers() -> None:
"run-modular-tests": _run_modular_tests,
"streaming-demo-data": _streaming_demo_data,
"affinity-demo-data": _affinity_demo_data,
"optimistic-demo-data": _optimistic_demo_data,
"action:add-demo-item": _add_demo_item,
"offline-demo-data": _offline_demo_data,
})
@@ -922,3 +925,45 @@ def _affinity_demo_data() -> dict:
})
return {"components": components, "page-plans": page_plans}
def _optimistic_demo_data() -> dict:
"""Return demo data for the optimistic update test page."""
from datetime import datetime, timezone
return {
"items": [
{"id": 1, "label": "First item", "status": "confirmed"},
{"id": 2, "label": "Second item", "status": "confirmed"},
{"id": 3, "label": "Third item", "status": "confirmed"},
],
"server-time": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}
def _add_demo_item(**kwargs) -> dict:
"""Action: add a demo item. Returns confirmation with new item."""
from datetime import datetime, timezone
import random
label = kwargs.get("label", "Untitled")
return {
"id": random.randint(100, 9999),
"label": label,
"status": "confirmed",
"added-at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}
def _offline_demo_data() -> dict:
"""Return demo data for the offline data layer test page."""
from datetime import datetime, timezone
return {
"notes": [
{"id": 1, "text": "First note", "created": "2026-03-08T10:00:00Z"},
{"id": 2, "text": "Second note", "created": "2026-03-08T11:30:00Z"},
{"id": 3, "text": "Third note", "created": "2026-03-08T14:15:00Z"},
],
"server-time": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}