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:
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||
var SX_VERSION = "2026-03-06T23:52:56Z";
|
||||
var SX_VERSION = "2026-03-07T00:04:07Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -1930,6 +1930,31 @@ return domAppendToHead(link); }, domQueryAll(container, "link[rel=\"stylesheet\"
|
||||
})()) : NIL); }, domQueryAll(container, "form"));
|
||||
})(); };
|
||||
|
||||
// _page-data-cache
|
||||
var _pageDataCache = {};
|
||||
|
||||
// _page-data-cache-ttl
|
||||
var _pageDataCacheTtl = 30000;
|
||||
|
||||
// page-data-cache-key
|
||||
var pageDataCacheKey = function(pageName, params) { return (function() {
|
||||
var base = pageName;
|
||||
return (isSxTruthy(sxOr(isNil(params), isEmpty(keys(params)))) ? base : (function() {
|
||||
var parts = [];
|
||||
{ var _c = keys(params); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; parts.push((String(k) + String("=") + String(get(params, k)))); } }
|
||||
return (String(base) + String(":") + String(join("&", parts)));
|
||||
})());
|
||||
})(); };
|
||||
|
||||
// page-data-cache-get
|
||||
var pageDataCacheGet = function(cacheKey) { return (function() {
|
||||
var entry = get(_pageDataCache, cacheKey);
|
||||
return (isSxTruthy(isNil(entry)) ? NIL : (isSxTruthy(((nowMs() - get(entry, "ts")) > _pageDataCacheTtl)) ? (dictSet(_pageDataCache, cacheKey, NIL), NIL) : get(entry, "data")));
|
||||
})(); };
|
||||
|
||||
// page-data-cache-set
|
||||
var pageDataCacheSet = function(cacheKey, data) { return dictSet(_pageDataCache, cacheKey, {"data": data, "ts": nowMs()}); };
|
||||
|
||||
// swap-rendered-content
|
||||
var swapRenderedContent = function(target, rendered, pathname) { return (domSetTextContent(target, ""), domAppend(target, rendered), hoistHeadElementsFull(target), processElements(target), sxHydrateElements(target), domDispatch(target, "sx:clientRoute", {["pathname"]: pathname}), logInfo((String("sx:route client ") + String(pathname)))); };
|
||||
|
||||
@@ -1946,11 +1971,20 @@ return domAppendToHead(link); }, domQueryAll(container, "link[rel=\"stylesheet\"
|
||||
var pageName = get(match, "name");
|
||||
return (isSxTruthy(sxOr(isNil(contentSrc), isEmpty(contentSrc))) ? (logWarn((String("sx:route no content for ") + String(pathname))), false) : (function() {
|
||||
var target = resolveRouteTarget(targetSel);
|
||||
return (isSxTruthy(isNil(target)) ? (logWarn((String("sx:route target not found: ") + String(targetSel))), false) : (isSxTruthy(get(match, "has-data")) ? (logInfo((String("sx:route client+data ") + String(pathname))), resolvePageData(pageName, params, function(data) { return (function() {
|
||||
return (isSxTruthy(isNil(target)) ? (logWarn((String("sx:route target not found: ") + String(targetSel))), false) : (isSxTruthy(get(match, "has-data")) ? (function() {
|
||||
var cacheKey = pageDataCacheKey(pageName, params);
|
||||
var cached = pageDataCacheGet(cacheKey);
|
||||
return (isSxTruthy(cached) ? (function() {
|
||||
var env = merge(closure, params, cached);
|
||||
var rendered = tryEvalContent(contentSrc, env);
|
||||
return (isSxTruthy(isNil(rendered)) ? (logWarn((String("sx:route cached eval failed for ") + String(pathname))), false) : (logInfo((String("sx:route client+cache ") + String(pathname))), swapRenderedContent(target, rendered, pathname), true));
|
||||
})() : (logInfo((String("sx:route client+data ") + String(pathname))), resolvePageData(pageName, params, function(data) { pageDataCacheSet(cacheKey, data);
|
||||
return (function() {
|
||||
var env = merge(closure, params, data);
|
||||
var rendered = tryEvalContent(contentSrc, env);
|
||||
return (isSxTruthy(isNil(rendered)) ? logWarn((String("sx:route data eval failed for ") + String(pathname))) : swapRenderedContent(target, rendered, pathname));
|
||||
})(); }), true) : (function() {
|
||||
})(); }), true));
|
||||
})() : (function() {
|
||||
var env = merge(closure, params);
|
||||
var rendered = tryEvalContent(contentSrc, env);
|
||||
return (isSxTruthy(isNil(rendered)) ? (logInfo((String("sx:route server (eval failed) ") + String(pathname))), false) : (swapRenderedContent(target, rendered, pathname), true));
|
||||
|
||||
@@ -282,7 +282,7 @@ async def execute_page(
|
||||
# Compute content expression deps so the server sends component
|
||||
# definitions the client needs for future client-side routing
|
||||
extra_deps: set[str] | None = None
|
||||
if page_def.content_expr is not None and page_def.data_expr is None:
|
||||
if page_def.content_expr is not None:
|
||||
from .deps import components_needed
|
||||
from .parser import serialize
|
||||
try:
|
||||
|
||||
@@ -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¶m2=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)))
|
||||
|
||||
Reference in New Issue
Block a user