Add predictive component prefetching plan to sx-docs

4-phase design: server endpoint for on-demand component defs,
SX-specced client prefetch logic (hover/viewport triggers),
boundary declarations, and bootstrap integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 22:30:26 +00:00
parent 093050059d
commit daf76c3e5b
3 changed files with 172 additions and 1 deletions

View File

@@ -112,7 +112,9 @@
(dict :label "Reader Macros" :href "/plans/reader-macros"
:summary "Extensible parse-time transformations via # dispatch — datum comments, raw strings, and quote shorthand.")
(dict :label "SX-Activity" :href "/plans/sx-activity"
:summary "A new web built on SX — executable content, shared components, parsers, and logic on IPFS, provenance on Bitcoin, all running within your own security context.")))
:summary "A new web built on SX — executable content, shared components, parsers, and logic on IPFS, provenance on Bitcoin, all running within your own security context.")
(dict :label "Predictive Prefetching" :href "/plans/predictive-prefetch"
:summary "Prefetch missing component definitions before the user clicks — hover a link, fetch its deps, navigate client-side.")))
(define bootstrappers-nav-items (list
(dict :label "Overview" :href "/bootstrappers/")

View File

@@ -594,6 +594,174 @@
(td :class "px-3 py-2 text-stone-700" "Content addressing — shared with component CIDs")
(td :class "px-3 py-2 text-stone-600" "2, 3"))))))))
;; ---------------------------------------------------------------------------
;; Predictive Component Prefetching
;; ---------------------------------------------------------------------------
(defcomp ~plan-predictive-prefetch-content ()
(~doc-page :title "Predictive Component Prefetching"
(~doc-section :title "Context" :id "context"
(p "Phase 3 of the isomorphic roadmap added client-side routing with component dependency checking. When a user clicks a link, " (code "try-client-route") " checks " (code "has-all-deps?") " — if the target page needs components not yet loaded, the client falls back to a server fetch. This works correctly but misses an opportunity: " (strong "we can prefetch those missing components before the click happens."))
(p "The page registry already carries " (code ":deps") " metadata for every page. The client already knows which components are loaded via " (code "loaded-component-names") ". The gap is a mechanism to " (em "proactively") " resolve the difference — fetching missing component definitions so that by the time the user clicks, client-side routing succeeds."))
(~doc-section :title "Current State" :id "current-state"
(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" "Layer")
(th :class "px-3 py-2 font-medium text-stone-600" "What exists")
(th :class "px-3 py-2 font-medium text-stone-600" "Where")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Page registry")
(td :class "px-3 py-2 text-stone-700" "Each page carries " (code ":deps (\"~card\" \"~essay-foo\" ...)"))
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "helpers.py → <script type=\"text/sx-pages\">"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Dep check")
(td :class "px-3 py-2 text-stone-700" (code "has-all-deps?") " gates client routing")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "orchestration.sx:546-559"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Component bundle")
(td :class "px-3 py-2 text-stone-700" "Per-page inline " (code "<script type=\"text/sx\" data-components>"))
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "helpers.py:715, jinja_bridge.py"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Incremental defs")
(td :class "px-3 py-2 text-stone-700" (code "components_for_request()") " sends only missing defs in SX responses")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "helpers.py:459-509"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Preload cache")
(td :class "px-3 py-2 text-stone-700" (code "sx-preload") " prefetches full responses on hover/mousedown")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "orchestration.sx:686-708"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Route matching")
(td :class "px-3 py-2 text-stone-700" (code "find-matching-route") " matches pathname to page entry")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "router.sx"))))))
;; -----------------------------------------------------------------------
;; Design
;; -----------------------------------------------------------------------
(~doc-section :title "Design" :id "design"
(p "Per the SX host architecture principle: all SX-specific logic goes in " (code ".sx") " spec files and gets bootstrapped. The prefetch logic — scanning links, computing missing deps, managing the component cache — must be specced in " (code ".sx") ", not written directly in JS or Python.")
(~doc-subsection :title "Phase 1: Component Fetch Endpoint (Python)"
(p "A new " (strong "public") " endpoint (not " (code "/internal/") " — the client's browser calls it) that returns component definitions by name.")
(~doc-code :code (highlight "GET /<service-prefix>/sx/components?names=~card,~essay-foo\n\nResponse (text/sx):\n(defcomp ~card (&key title &rest children)\n (div :class \"border rounded p-4\" (h2 title) children))\n(defcomp ~essay-foo (&key id)\n (div (~card :title id)))" "http"))
(p "The server resolves transitive deps via " (code "deps.py") ", subtracts anything listed in the " (code "SX-Components") " request header (already loaded), serializes and returns. This is essentially " (code "components_for_request()") " driven by an explicit " (code "?names=") " param.")
(p "Cache-friendly: the response is a pure function of component hash + requested names. " (code "Cache-Control: public, max-age=3600") " with the component hash as ETag."))
(~doc-subsection :title "Phase 2: Client Prefetch Logic (SX spec)"
(p "New functions in " (code "orchestration.sx") " (or a new " (code "prefetch.sx") " if scope warrants):")
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. compute-missing-deps")
(p "Given a pathname, find the page, return dep names not in " (code "loaded-component-names") ". Returns nil if page not found or has data (can't client-route anyway).")
(~doc-code :code (highlight "(define compute-missing-deps\n (fn (pathname)\n (let ((match (find-matching-route pathname _page-routes)))\n (when (and match (not (get match \"has-data\")))\n (let ((deps (or (get match \"deps\") (list)))\n (loaded (loaded-component-names)))\n (filter (fn (d) (not (contains? loaded d))) deps))))))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. prefetch-components")
(p "Fetch component definitions from the server for a list of names. Deduplicates in-flight requests. On success, parses and registers the returned definitions into the component env.")
(~doc-code :code (highlight "(define _prefetch-pending (dict))\n\n(define prefetch-components\n (fn (names)\n (let ((key (join \",\" (sort names))))\n (when (not (get _prefetch-pending key))\n (set! _prefetch-pending\n (merge _prefetch-pending {key true}))\n (fetch-components-from-server names\n (fn (sx-text)\n (sx-process-component-text sx-text)\n (dict-remove! _prefetch-pending key)))))))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "3. prefetch-route-deps")
(p "High-level composition: compute missing deps for a route, fetch if any.")
(~doc-code :code (highlight "(define prefetch-route-deps\n (fn (pathname)\n (let ((missing (compute-missing-deps pathname)))\n (when (and missing (not (empty? missing)))\n (log-info (str \"sx:prefetch \"\n (len missing) \" components for \" pathname))\n (prefetch-components missing)))))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "4. Trigger: link hover")
(p "On mouseover of a boosted link, prefetch its route's missing components. Debounced 150ms to avoid fetching on quick mouse-throughs.")
(~doc-code :code (highlight "(define bind-prefetch-on-hover\n (fn (link)\n (let ((timer nil))\n (dom-add-listener link \"mouseover\"\n (fn (e)\n (clear-timeout timer)\n (set! timer (set-timeout\n (fn () (prefetch-route-deps\n (url-pathname (dom-get-attr link \"href\"))))\n 150))))\n (dom-add-listener link \"mouseout\"\n (fn (e) (clear-timeout timer))))))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "5. Trigger: viewport intersection (opt-in)")
(p "More aggressive strategy: when a link scrolls into view, prefetch its route's deps. Opt-in via " (code "sx-prefetch=\"visible\"") " attribute.")
(~doc-code :code (highlight "(define bind-prefetch-on-visible\n (fn (link)\n (observe-intersection link\n (fn () (prefetch-route-deps\n (url-pathname (dom-get-attr link \"href\"))))\n true 0)))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "6. Integration into process-elements")
(p "During the existing hydration pass, for each boosted link:")
(~doc-code :code (highlight ";; In process-elements, after binding boost behavior:\n(when (and (should-boost-link? link)\n (dom-get-attr link \"href\"))\n (bind-prefetch-on-hover link))\n\n;; Explicit viewport prefetch:\n(when (dom-has-attr? link \"sx-prefetch\")\n (bind-prefetch-on-visible link))" "lisp")))))
(~doc-subsection :title "Phase 3: Boundary Declaration"
(p "Two new IO primitives in " (code "boundary.sx") " (browser-only):")
(~doc-code :code (highlight ";; IO primitives (browser-only)\n(io fetch-components-from-server (names callback) -> void)\n(io sx-process-component-text (sx-text) -> void)" "lisp"))
(p "These are thin wrappers around " (code "fetch()") " + the existing component script processing logic already in the boundary adapter."))
(~doc-subsection :title "Phase 4: Bootstrap"
(p (code "bootstrap_js.py") " picks up the new functions from the spec and emits them into " (code "sx-browser.js") ". The two new boundary IO functions get implemented in the JS boundary adapter — the hand-written glue code that the bootstrapper doesn't generate.")
(~doc-code :code (highlight "// fetch-components-from-server: calls the endpoint\nfunction fetchComponentsFromServer(names, callback) {\n const url = `${routePrefix}/sx/components?names=${names.join(\",\")}`;\n const headers = {\n \"SX-Components\": loadedComponentNames().join(\",\")\n };\n fetch(url, { headers })\n .then(r => r.ok ? r.text() : \"\")\n .then(text => callback(text))\n .catch(() => {}); // silent fail — prefetch is best-effort\n}\n\n// sx-process-component-text: parse defcomp/defmacro into env\nfunction sxProcessComponentText(sxText) {\n if (!sxText) return;\n const frag = document.createElement(\"div\");\n frag.innerHTML =\n `<script type=\"text/sx\" data-components>${sxText}<\\/script>`;\n Sx.processScripts(frag);\n}" "javascript"))))
;; -----------------------------------------------------------------------
;; Request flow
;; -----------------------------------------------------------------------
(~doc-section :title "Request Flow" :id "request-flow"
(p "End-to-end example: user hovers a link, components prefetch, click goes client-side.")
(~doc-code :code (highlight "User hovers link \"/docs/sx-manifesto\"\n |\n +-- bind-prefetch-on-hover fires (150ms debounce)\n |\n +-- compute-missing-deps(\"/docs/sx-manifesto\")\n | +-- find-matching-route -> page with deps:\n | | [\"~essay-sx-manifesto\", \"~doc-code\"]\n | +-- loaded-component-names -> [\"~nav\", \"~footer\", \"~doc-code\"]\n | +-- missing: [\"~essay-sx-manifesto\"]\n |\n +-- prefetch-components([\"~essay-sx-manifesto\"])\n | +-- GET /sx/components?names=~essay-sx-manifesto\n | | Headers: SX-Components: ~nav,~footer,~doc-code\n | +-- Server resolves transitive deps\n | | (also needs ~rich-text, subtracts already-loaded)\n | +-- Response:\n | (defcomp ~essay-sx-manifesto ...) \n | (defcomp ~rich-text ...)\n |\n +-- sx-process-component-text registers defcomps in env\n |\n +-- User clicks link\n +-- try-client-route(\"/docs/sx-manifesto\")\n +-- has-all-deps? -> true (prefetched!)\n +-- eval content -> DOM\n +-- Client-side render, no server roundtrip" "text")))
;; -----------------------------------------------------------------------
;; File changes
;; -----------------------------------------------------------------------
(~doc-section :title "File Changes" :id "file-changes"
(div :class "overflow-x-auto rounded border border-stone-200"
(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" "File")
(th :class "px-3 py-2 font-medium text-stone-600" "Change")
(th :class "px-3 py-2 font-medium text-stone-600" "Phase")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/helpers.py")
(td :class "px-3 py-2 text-stone-700" "New " (code "sx_components_endpoint()") " route handler")
(td :class "px-3 py-2 text-stone-600" "1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/infrastructure/factory.py")
(td :class "px-3 py-2 text-stone-700" "Register " (code "/sx/components") " route on all SX apps")
(td :class "px-3 py-2 text-stone-600" "1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/orchestration.sx")
(td :class "px-3 py-2 text-stone-700" "Prefetch functions: compute-missing-deps, prefetch-components, prefetch-route-deps, bind-prefetch-on-hover, bind-prefetch-on-visible")
(td :class "px-3 py-2 text-stone-600" "2"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/boundary.sx")
(td :class "px-3 py-2 text-stone-700" "Declare " (code "fetch-components-from-server") ", " (code "sx-process-component-text"))
(td :class "px-3 py-2 text-stone-600" "3"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/ref/bootstrap_js.py")
(td :class "px-3 py-2 text-stone-700" "Emit new spec functions, boundary adapter stubs")
(td :class "px-3 py-2 text-stone-600" "4"))))))
;; -----------------------------------------------------------------------
;; Non-goals & rollout
;; -----------------------------------------------------------------------
(~doc-section :title "Non-Goals (This Phase)" :id "non-goals"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Predictive ML/heuristic prefetch") " — no analytics-driven \"likely next page\" predictions. Just hover + optional viewport.")
(li (strong "Cross-service prefetch") " — components are per-service. A link to a different service domain is always a server navigation.")
(li (strong "Service worker caching") " — could layer on later, but basic fetch + in-memory registration is sufficient.")
(li (strong "Prefetching page data") " — pages with " (code ":data_expr") " still go to the server. Prefetching their data is a separate, larger problem (Phase 4 of isomorphic roadmap).")))
(~doc-section :title "Rollout" :id "rollout"
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (strong "Implement endpoint") " — lowest risk, purely additive. Refactor " (code "components_for_request()") " to accept explicit names param.")
(li (strong "Implement SX spec functions") " — no wiring yet, testable in isolation.")
(li (strong "Wire hover prefetch into process-elements") " — all boosted links get it automatically.")
(li (strong "Observe") " — console logs (" (code "sx:prefetch N components for /path") ") show what's happening. Check network tab for redundant fetches, timing.")
(li (strong "Add viewport prefetch") " — opt-in via " (code "sx-prefetch=\"visible\"") " attribute on nav containers.")))
(~doc-section :title "Relationship to Isomorphic Roadmap" :id "relationship"
(p "This plan sits between Phase 3 (client-side routing) and Phase 4 (client async & IO bridge) of the "
(a :href "/plans/isomorphic-architecture" :class "text-violet-700 underline" "isomorphic architecture roadmap")
". It extends Phase 3 by making more navigations go client-side without needing any IO bridge — purely by ensuring component definitions are available before they're needed.")
(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 3 (client-side routing with deps checking). No dependency on Phase 4.")))))
;; ---------------------------------------------------------------------------
;; Isomorphic Architecture Roadmap
;; ---------------------------------------------------------------------------

View File

@@ -477,4 +477,5 @@
:content (case slug
"reader-macros" (~plan-reader-macros-content)
"sx-activity" (~plan-sx-activity-content)
"predictive-prefetch" (~plan-predictive-prefetch-content)
:else (~plans-index-content)))