Phase 4 complete: client data cache + plan update

- Add page data cache in orchestration.sx (30s TTL, keyed by page-name+params)
- Cache hit path: sx:route client+cache (instant render, no fetch)
- Cache miss path: sx:route client+data (fetch, cache, render)
- Fix HTMX response dep computation to include :data pages
- Update isomorphic-sx-plan.md: Phases 1-4 marked done with details,
  reorder remaining phases (continuations→Phase 5, suspense→Phase 6,
  optimistic updates→Phase 7)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 00:06:22 +00:00
parent 732923a7ef
commit d3617ab7f3
4 changed files with 241 additions and 139 deletions

View File

@@ -545,6 +545,50 @@
(dom-query-all container "form")))))
;; --------------------------------------------------------------------------
;; Client-side routing — data cache
;; --------------------------------------------------------------------------
;; Cache for page data resolved via resolve-page-data.
;; Keyed by "page-name:param1=val1&param2=val2", value is {data, ts}.
;; Default TTL: 30s. Prevents redundant fetches on back/forward navigation.
(define _page-data-cache (dict))
(define _page-data-cache-ttl 30000) ;; 30 seconds in ms
(define page-data-cache-key
(fn (page-name params)
;; Build a cache key from page name + params.
;; Params are from route matching so order is deterministic.
(let ((base page-name))
(if (or (nil? params) (empty? (keys params)))
base
(let ((parts (list)))
(for-each
(fn (k)
(append! parts (str k "=" (get params k))))
(keys params))
(str base ":" (join "&" parts)))))))
(define page-data-cache-get
(fn (cache-key)
;; Return cached data if fresh, else nil.
(let ((entry (get _page-data-cache cache-key)))
(if (nil? entry)
nil
(if (> (- (now-ms) (get entry "ts")) _page-data-cache-ttl)
(do
(dict-set! _page-data-cache cache-key nil)
nil)
(get entry "data"))))))
(define page-data-cache-set
(fn (cache-key data)
;; Store data with current timestamp.
(dict-set! _page-data-cache cache-key
{"data" data "ts" (now-ms)})))
;; --------------------------------------------------------------------------
;; Client-side routing
;; --------------------------------------------------------------------------
@@ -593,18 +637,31 @@
(if (nil? target)
(do (log-warn (str "sx:route target not found: " target-sel)) false)
(if (get match "has-data")
;; Data page: resolve data asynchronously, then render client-side
(do
(log-info (str "sx:route client+data " pathname))
(resolve-page-data page-name params
(fn (data)
;; data is a dict — merge into env and render
(let ((env (merge closure params data))
(rendered (try-eval-content content-src env)))
(if (nil? rendered)
(log-warn (str "sx:route data eval failed for " pathname))
(swap-rendered-content target rendered pathname)))))
true)
;; Data page: check cache, else resolve asynchronously
(let ((cache-key (page-data-cache-key page-name params))
(cached (page-data-cache-get cache-key)))
(if cached
;; Cache hit — render immediately
(let ((env (merge closure params cached))
(rendered (try-eval-content content-src env)))
(if (nil? rendered)
(do (log-warn (str "sx:route cached eval failed for " pathname)) false)
(do
(log-info (str "sx:route client+cache " pathname))
(swap-rendered-content target rendered pathname)
true)))
;; Cache miss — fetch, cache, render
(do
(log-info (str "sx:route client+data " pathname))
(resolve-page-data page-name params
(fn (data)
(page-data-cache-set cache-key data)
(let ((env (merge closure params data))
(rendered (try-eval-content content-src env)))
(if (nil? rendered)
(log-warn (str "sx:route data eval failed for " pathname))
(swap-rendered-content target rendered pathname)))))
true)))
;; Pure page: render immediately
(let ((env (merge closure params))
(rendered (try-eval-content content-src env)))