Phase 7b: page render plans — per-page boundary optimizer
Add page-render-plan to deps.sx: given page source + env + IO names, computes a dict mapping each needed component to "server" or "client", with server/client lists and IO dep collection. 5 new spec tests. Integration: - PageDef.render_plan field caches the plan at registration - compute_page_render_plans() called from auto_mount_pages() - Client page registry includes :render-plan per page - Affinity demo page shows per-page render plans Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -549,6 +549,7 @@ class JSEmitter:
|
||||
"compute-all-io-refs": "computeAllIoRefs",
|
||||
"component-pure?": "componentPure_p",
|
||||
"render-target": "renderTarget",
|
||||
"page-render-plan": "pageRenderPlan",
|
||||
# router.sx
|
||||
"split-path-segments": "splitPathSegments",
|
||||
"make-route-segment": "makeRouteSegment",
|
||||
|
||||
@@ -278,6 +278,7 @@ class PyEmitter:
|
||||
"compute-all-io-refs": "compute_all_io_refs",
|
||||
"component-pure?": "component_pure_p",
|
||||
"render-target": "render_target",
|
||||
"page-render-plan": "page_render_plan",
|
||||
# router.sx
|
||||
"split-path-segments": "split_path_segments",
|
||||
"make-route-segment": "make_route_segment",
|
||||
|
||||
@@ -341,6 +341,50 @@
|
||||
:else "client")))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 6. Page render plan — pre-computed boundary decisions for a page
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Given page source + env + IO names, returns a render plan dict:
|
||||
;;
|
||||
;; {:components {~name "server"|"client" ...}
|
||||
;; :server (list of ~names that render server-side)
|
||||
;; :client (list of ~names that render client-side)
|
||||
;; :io-deps (list of IO primitives needed by server components)}
|
||||
;;
|
||||
;; This is computed once at page registration and cached on the page def.
|
||||
;; The async evaluator and client router both use it to make decisions
|
||||
;; without recomputing at every request.
|
||||
|
||||
(define page-render-plan
|
||||
(fn (page-source env io-names)
|
||||
(let ((needed (components-needed page-source env))
|
||||
(comp-targets (dict))
|
||||
(server-list (list))
|
||||
(client-list (list))
|
||||
(io-deps (list)))
|
||||
|
||||
(for-each
|
||||
(fn (name)
|
||||
(let ((target (render-target name env io-names)))
|
||||
(dict-set! comp-targets name target)
|
||||
(if (= target "server")
|
||||
(do
|
||||
(append! server-list name)
|
||||
;; Collect IO deps from server components
|
||||
(for-each
|
||||
(fn (io-ref)
|
||||
(when (not (contains? io-deps io-ref))
|
||||
(append! io-deps io-ref)))
|
||||
(transitive-io-refs name env io-names)))
|
||||
(append! client-list name))))
|
||||
needed)
|
||||
|
||||
{:components comp-targets
|
||||
:server server-list
|
||||
:client client-list
|
||||
:io-deps io-deps})))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Host obligation: selective expansion in async partial evaluation
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -1301,6 +1301,9 @@ component_pure_p = lambda name, env, io_names: empty_p(transitive_io_refs(name,
|
||||
# render-target
|
||||
render_target = lambda name, env, io_names: (lambda key: (lambda val: ('server' if sx_truthy((not sx_truthy((type_of(val) == 'component')))) else (lambda affinity: ('server' if sx_truthy((affinity == 'server')) else ('client' if sx_truthy((affinity == 'client')) else ('server' if sx_truthy((not sx_truthy(component_pure_p(name, env, io_names)))) else 'client'))))(component_affinity(val))))(env_get(env, key)))((name if sx_truthy(starts_with_p(name, '~')) else sx_str('~', name)))
|
||||
|
||||
# page-render-plan
|
||||
page_render_plan = lambda page_source, env, io_names: (lambda needed: (lambda comp_targets: (lambda server_list: (lambda client_list: (lambda io_deps: _sx_begin(for_each(lambda name: (lambda target: _sx_begin(_sx_dict_set(comp_targets, name, target), (_sx_begin(_sx_append(server_list, name), for_each(lambda io_ref: (_sx_append(io_deps, io_ref) if sx_truthy((not sx_truthy(contains_p(io_deps, io_ref)))) else NIL), transitive_io_refs(name, env, io_names))) if sx_truthy((target == 'server')) else _sx_append(client_list, name))))(render_target(name, env, io_names)), needed), {'components': comp_targets, 'server': server_list, 'client': client_list, 'io-deps': io_deps}))([]))([]))([]))({}))(components_needed(page_source, env))
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Fixups -- wire up render adapter dispatch
|
||||
|
||||
@@ -263,3 +263,43 @@
|
||||
|
||||
(deftest "unknown name targets server"
|
||||
(assert-equal "server" (render-target "~nonexistent" (test-env) (list "fetch-data")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 7. page-render-plan — per-page boundary plan
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
;; A page component that uses both pure and IO components
|
||||
(defcomp ~plan-page (&key data)
|
||||
(div
|
||||
(~dep-auto-pure :x "hello")
|
||||
(~dep-auto-io :x data)
|
||||
(~dep-force-client :x "interactive")))
|
||||
|
||||
(defsuite "page-render-plan"
|
||||
|
||||
(deftest "plan classifies components correctly"
|
||||
(let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
|
||||
;; ~plan-page has transitive IO deps (via ~dep-auto-io) so targets server
|
||||
(assert-equal "server" (dict-get (get plan :components) "~plan-page"))
|
||||
(assert-equal "client" (dict-get (get plan :components) "~dep-auto-pure"))
|
||||
(assert-equal "server" (dict-get (get plan :components) "~dep-auto-io"))
|
||||
(assert-equal "client" (dict-get (get plan :components) "~dep-force-client"))))
|
||||
|
||||
(deftest "plan server list contains IO components"
|
||||
(let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
|
||||
(assert-true (contains? (get plan :server) "~dep-auto-io"))))
|
||||
|
||||
(deftest "plan client list contains pure components"
|
||||
(let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
|
||||
(assert-true (contains? (get plan :client) "~dep-auto-pure"))
|
||||
(assert-true (contains? (get plan :client) "~dep-force-client"))))
|
||||
|
||||
(deftest "plan collects IO deps from server components"
|
||||
(let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
|
||||
(assert-true (contains? (get plan :io-deps) "fetch-data"))))
|
||||
|
||||
(deftest "pure-only page has empty server list"
|
||||
(let ((plan (page-render-plan "(~dep-auto-pure :x 1)" (test-env) (list "fetch-data"))))
|
||||
(assert-equal 0 (len (get plan :server)))
|
||||
(assert-true (> (len (get plan :client)) 0)))))
|
||||
|
||||
Reference in New Issue
Block a user