diff --git a/sx/sx/plans.sx b/sx/sx/plans.sx index f122fa7..2840c7b 100644 --- a/sx/sx/plans.sx +++ b/sx/sx/plans.sx @@ -1455,7 +1455,7 @@ (p (code "handle-popstate") " also tries client routing before server fetch on back/forward.")))) (~doc-subsection :title "What becomes client-routable" - (p "Pages WITHOUT " (code ":data") " that have pure content expressions — most of this docs app:") + (p "All pages with content expressions — most of this docs app. Pure pages render instantly; :data pages fetch data then render client-side (Phase 4):") (ul :class "list-disc pl-5 text-stone-700 space-y-1" (li (code "/") ", " (code "/docs/") ", " (code "/docs/") " (most slugs), " (code "/protocols/") ", " (code "/protocols/")) (li (code "/examples/") ", " (code "/examples/") ", " (code "/essays/") ", " (code "/essays/")) @@ -1465,7 +1465,8 @@ (li (code "/docs/primitives") " and " (code "/docs/special-forms") " (call " (code "primitives-data") " / " (code "special-forms-data") " helpers)") (li (code "/reference/") " (has " (code ":data (reference-data slug)") ")") (li (code "/bootstrappers/") " (has " (code ":data (bootstrapper-data slug)") ")") - (li (code "/isomorphism/bundle-analyzer") " (has " (code ":data (bundle-analyzer-data)") ")"))) + (li (code "/isomorphism/bundle-analyzer") " (has " (code ":data (bundle-analyzer-data)") ")") + (li (code "/isomorphism/data-test") " (has " (code ":data (data-test-data)") " — " (a :href "/isomorphism/data-test" :class "text-violet-700 underline" "Phase 4 demo") ")"))) (~doc-subsection :title "Try-first/fallback design" (p "Client routing uses a try-first approach: attempt local evaluation in a try/catch, fall back to server fetch on any failure. This avoids needing perfect static analysis of content expressions — if a content expression calls a page helper the client doesn't have, the eval throws, and the server handles it transparently.") @@ -1494,19 +1495,71 @@ (~doc-section :title "Phase 4: Client Async & IO Bridge" :id "phase-4" - (div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4" - (p :class "text-violet-900 font-medium" "What it enables") - (p :class "text-violet-800" "Client evaluates IO primitives by mapping them to server REST calls. Same SX code, different transport. (query \"market\" \"products\" :ids \"1,2,3\") on server → DB; on client → fetch(\"/internal/data/products?ids=1,2,3\").")) + (div :class "rounded border border-green-300 bg-green-50 p-4 mb-4" + (div :class "flex items-center gap-2 mb-2" + (span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete") + (a :href "/isomorphism/data-test" :class "text-green-700 underline text-sm font-medium" "Live data test page")) + (p :class "text-green-900 font-medium" "What it enables") + (p :class "text-green-800" "Client fetches server-evaluated data and renders :data pages locally. Data cached with TTL to avoid redundant fetches on back/forward navigation. All IO stays server-side — no continuations needed.")) - (~doc-subsection :title "Approach" + (~doc-subsection :title "Architecture" + (p "Separates IO from rendering. Server evaluates :data expression (async, with DB/service access), serializes result as SX wire format. Client fetches pre-evaluated data, parses it, merges into env, renders pure :content client-side.") (div :class "space-y-4" (div - (h4 :class "font-semibold text-stone-700" "1. Async client evaluator") - (p "Two possible mechanisms:") + (h4 :class "font-semibold text-stone-700" "1. Abstract resolve-page-data") + (p "Spec-level primitive in orchestration.sx. The spec says \"I need data for this page\" — platform provides transport:") + (~doc-code :code (highlight "(resolve-page-data page-name params\n (fn (data)\n ;; data is a dict — merge into env and render\n (let ((env (merge closure params data))\n (rendered (try-eval-content content-src env)))\n (swap-rendered-content target rendered pathname))))" "lisp")) + (p "Browser platform: HTTP fetch to " (code "/sx/data/") ". Future platforms could use IPC, cache, WebSocket, etc.")) + + (div + (h4 :class "font-semibold text-stone-700" "2. Server data endpoint") + (p (code "evaluate_page_data()") " evaluates the :data expression, kebab-cases dict keys (Python " (code "total_count") " → SX " (code "total-count") "), serializes as SX wire format.") + (p "Response content type: " (code "text/sx; charset=utf-8") ". Per-page auth enforcement via " (code "_check_page_auth()") ".")) + + (div + (h4 :class "font-semibold text-stone-700" "3. Client data cache") + (p "In-memory cache in orchestration.sx, keyed by " (code "page-name:param=value") ". 30-second TTL prevents redundant fetches on back/forward navigation:") (ul :class "list-disc pl-5 text-stone-700 space-y-1" - (li (strong "Promise-based: ") "evalExpr returns value or Promise; rendering awaits") - (li (strong "Continuation-based: ") "use existing shift/reset to suspend on IO, resume when data arrives (architecturally cleaner, leverages existing spec)"))) + (li "Cache miss: " (code "sx:route client+data /path") " — fetches from server, caches, renders") + (li "Cache hit: " (code "sx:route client+cache /path") " — instant render from cached data") + (li "After TTL: stale entry evicted, fresh fetch on next visit")) + (p "Try it: navigate to the " (a :href "/isomorphism/data-test" :class "text-violet-700 underline" "data test page") ", go back, return within 30s — the server-time stays the same (cached). Wait 30s+ and return — new time (fresh fetch).")))) + + (~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 — resolve-page-data spec, data cache") + (li "shared/sx/ref/bootstrap_js.py — platform resolvePageData (HTTP fetch)") + (li "shared/sx/pages.py — evaluate_page_data(), auto_mount_page_data()") + (li "shared/sx/helpers.py — deps for :data pages in page registry") + (li "sx/sx/data-test.sx — test component") + (li "shared/sx/tests/test_page_data.py — 30 unit tests"))) + + (~doc-subsection :title "Verification" + (ul :class "list-disc pl-5 text-stone-700 space-y-1" + (li "30 unit tests: serialize roundtrip, kebab-case, deps, full pipeline simulation, cache TTL") + (li "Console: " (code "sx:route client+data") " on first visit, " (code "sx:route client+cache") " on return within 30s") + (li (a :href "/isomorphism/data-test" :class "text-violet-700 underline" "Live data test page") " exercises the full pipeline with server time + pipeline steps") + (li "append! and dict-set! registered as proper primitives in spec + both hosts")))) + + ;; ----------------------------------------------------------------------- + ;; Phase 5 + ;; ----------------------------------------------------------------------- + + (~doc-section :title "Phase 5: Async Continuations & Inline IO" :id "phase-5" + + (div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4" + (p :class "text-violet-900 font-medium" "What it enables") + (p :class "text-violet-800" "Components call IO primitives directly in their body. The evaluator suspends mid-evaluation via async-aware continuations, fetches data, resumes. Same component source works on both server (Python async/await) and client (continuation-based suspension).")) + + (~doc-subsection :title "The Problem" + (p "The existing shift/reset continuations extension is synchronous (throw/catch). Client-side IO via fetch() returns a Promise — you can't throw-catch across an async boundary. The evaluator needs Promise-aware continuations or a CPS transform.")) + + (~doc-subsection :title "Approach" + (div :class "space-y-4" + (div + (h4 :class "font-semibold text-stone-700" "1. Async-aware shift/reset") + (p "Extend the continuations extension: sfShift captures the continuation and returns a Promise, sfReset awaits Promise results in the trampoline. Continuation resume feeds the fetched value back into evaluation.")) (div (h4 :class "font-semibold text-stone-700" "2. IO primitive bridge") @@ -1518,27 +1571,17 @@ (li "current-user → cached from initial page load"))) (div - (h4 :class "font-semibold text-stone-700" "3. Client data cache") - (p "Keyed by (service, query, params-hash), configurable TTL, server can invalidate via SX-Invalidate header.")) - - (div - (h4 :class "font-semibold text-stone-700" "4. Optimistic updates") - (p "Extend existing apply-optimistic/revert-optimistic in engine.sx from DOM-level to data-level.")))) + (h4 :class "font-semibold text-stone-700" "3. CPS transform option") + (p "Alternative: transform the evaluator to continuation-passing style. Every eval step takes a continuation argument. IO primitives call the continuation after fetch resolves. Architecturally cleaner but requires deeper changes.")))) (div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2" - (p :class "text-amber-800 text-sm" (strong "Depends on: ") "Phase 2 (IO affinity), Phase 3 (routing for when to trigger IO).")) - - (~doc-subsection :title "Verification" - (ul :class "list-disc pl-5 text-stone-700 space-y-1" - (li "Client (query ...) returns identical data to server-side") - (li "Data cache prevents redundant fetches") - (li "Same component source → identical output on either side")))) + (p :class "text-amber-800 text-sm" (strong "Depends on: ") "Phase 4 (data endpoint infrastructure)."))) ;; ----------------------------------------------------------------------- - ;; Phase 5 + ;; Phase 6 ;; ----------------------------------------------------------------------- - (~doc-section :title "Phase 5: Streaming & Suspense" :id "phase-5" + (~doc-section :title "Phase 6: Streaming & Suspense" :id "phase-6" (div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4" (p :class "text-violet-900 font-medium" "What it enables") @@ -1568,13 +1611,13 @@ (p "Above-fold content resolves first. All IO starts concurrently (asyncio.create_task), results flushed in priority order.")))) (div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2" - (p :class "text-amber-800 text-sm" (strong "Depends on: ") "Phase 4 (client async for filling suspended subtrees), Phase 2 (IO analysis for priority)."))) + (p :class "text-amber-800 text-sm" (strong "Depends on: ") "Phase 5 (async continuations for filling suspended subtrees), Phase 2 (IO analysis for priority)."))) ;; ----------------------------------------------------------------------- - ;; Phase 6 + ;; Phase 7 ;; ----------------------------------------------------------------------- - (~doc-section :title "Phase 6: Full Isomorphism" :id "phase-6" + (~doc-section :title "Phase 7: Full Isomorphism" :id "phase-7" (div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4" (p :class "text-violet-900 font-medium" "What it enables") @@ -1593,15 +1636,19 @@ (p "Default: auto (runtime decides from IO analysis).")) (div - (h4 :class "font-semibold text-stone-700" "3. Offline data layer") + (h4 :class "font-semibold text-stone-700" "3. Optimistic data updates") + (p "Extend existing apply-optimistic/revert-optimistic in engine.sx from DOM-level to data-level. Client updates cached data optimistically, sends mutation to server, reverts on rejection.")) + + (div + (h4 :class "font-semibold text-stone-700" "4. Offline data layer") (p "Service Worker intercepts /internal/data/ requests, serves from IndexedDB when offline, syncs when back online.")) (div - (h4 :class "font-semibold text-stone-700" "4. Isomorphic testing") + (h4 :class "font-semibold text-stone-700" "5. Isomorphic testing") (p "Evaluate same component on Python and JS, compare output. Extends existing test_sx_ref.py cross-evaluator comparison.")) (div - (h4 :class "font-semibold text-stone-700" "5. Universal page descriptor") + (h4 :class "font-semibold text-stone-700" "6. Universal page descriptor") (p "defpage is portable: server executes via execute_page(), client executes via route match → fetch data → eval content → render DOM. Same descriptor, different execution environment.")))) (div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2" @@ -1618,7 +1665,7 @@ (li "Phase 1: \"Unknown component\" includes which page expected it and what bundle was sent") (li "Phase 2: Server logs which components expanded server-side vs sent to client") (li "Phase 3: Client route failures include unmatched path and available routes") - (li "Phase 4: Client IO errors include query name, params, server response") + (li "Phase 4: Client data errors include page name, params, server response status") (li "Source location tracking in parser → propagate through eval → include in error messages"))) (~doc-subsection :title "Backward Compatibility"