Repository navigation
refactor(router): deriveRenderPass, one declared field table for derived renders (S5) - #1245
Merged
Merged
Conversation
A PPR shell capture, a PPR HIT tail and a background re-render of a stale route record each render on Object.create of the request's context. Each site kept its own hand-built reset list. server/render-pass.ts now holds RENDER_PASS_FIELDS, one row per purpose listing every field the pass owns and why, and deriveRenderPass(base, purpose, init), the only place a request context is derived. Each row reproduces the own-field set its site had, including the background pass inheriting the request's render barrier (kept; open question rango-t135-q1). The rows write fields with named stores: an Object.assign of the capture's 30-odd fields left it in V8 dictionary mode, and context-fast-properties.test.ts now pins fast properties for every purpose. render-pass.bench.tsx times the derivations and a shell MISS and HIT through serveShellRequest, using only seams that predate render-pass.ts so it also runs against an earlier checkout. check-internals: request-context-object-create lowered by 3, no raise.
Cut the comments from 167 of 300 lines to 62 of 195: one line per field where its fresh-or-shared reason is not obvious, with the ticket or issue reference kept (#684, #700, #1035, #1109). The background row now points at rango-t160, and the #1109 note names the trigger: an Object.assign that takes the object to 20 own properties. Comment-only: both versions print identically through the TypeScript printer with comments removed, and their token streams match.
49 of 50 tasks
ivogt
added a commit
that referenced
this pull request
Oct 23, 2026
Brings in S16 (#1248, wire-params.ts WIRE), S5 (#1245, deriveRenderPass), S15 (#1243, discovery phases), #1246 and #1247. Conflicts, each resolved by keeping S14's structure and applying S16's intent to it: - rsc/handler.ts imports: kept the request-stages.js import; dropped the classifyRequest, RequestPlan and INTERNAL_RANGO_DEBUG imports S14 had already moved out; added S16's WIRE import, still read by the code that stayed in the handler (warmup, loader refuse, redirect tracking mode, isPartialAction). - rsc/handler.ts response tail: kept the finalizeRequestResponse call; the inline partial/action interception S16 rewrote lives in request-stages.ts and now reads WIRE.partial and WIRE.action. - rsc/handler.ts terminal plans: kept the terminalPlanResponse call; the action-rewrite check, partial redirect and version-mismatch log S16 rewrote live in request-stages.ts and now read WIRE.action, WIRE.partial and WIRE.version. - testing/dispatch.ts imports: added WIRE; dropped applyStreamIdleTimeout, which the stages now call. - testing/dispatch.ts isPartial: kept S14's shape (the reverse and isAction moved into the stages) with S16's WIRE.partial. - tools/internals-baseline.json: took the stack's version; check-internals --update leaves it unchanged (rsc-param-literal 4, all in use-loader.tsx). request-stages.ts has no `_rsc_*` literal left. The handler and stages together make the same searchParams calls as S16's handler.ts: has() on action x3, partial x3, loader and warmup; get() on loader and version.
16 of 17 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
devcorner: rango-t135 · session rango-s275
Problem
A second render over a request (a PPR shell capture, a PPR HIT tail, a background re-render of a stale route record) runs on
Object.createof the request's context, so every field it does not own reads the visitor request's value through the prototype. Each of the three sites built its derived context by hand, with its own reset list:deriveShellCaptureContext(anObject.assignplus about 18 own-property writes),createTailContextinserveShellHit(barrier wiring, then three seed writes after it) andrerenderAndCacheRoute(anObject.assignof 13 fields plusbindRes). A field missing from one list leaks foreground state into the derived render without any error: #684 was a capture that inherited the foreground's already-resolved render barrier and baked foreground handle data into the shared shell. With three lists in three files, nothing shows side by side what each pass owns.This is S5 of the internals reorganisation (plan rango-d195, target architecture rango-d194 section 3.1). No consumer-visible behaviour changes: the public
ctx.*surface, theRequestContextfield declarations andrebaseRequestContextare untouched.Before / after (internal shape)
Before, three sites:
After, one table and one constructor in
server/render-pass.ts:Each row reproduces exactly the own-field set its site had. That includes the background pass inheriting the request's render barrier, which is kept as it is and recorded as question rango-t135-q1.
Code changes
packages/rangojs-router/src/server/render-pass.ts(new):RenderPassInit(what each caller builds),RenderPurpose(its keys),RENDER_PASS_FIELDS(one row per purpose; the reset-rationale comments from the three sites moved onto the rows) andderiveRenderPass(base, purpose, init). The constructor runsObject.create(base), thenwireRenderBarrierwhen the row owns its barrier, then the row.Object.assignof a literal. A first cut that usedObject.assignleft the capture context in V8 dictionary mode: on Node 24 anObject.assignof 20 properties normalizes the object, and the capture owns over 30. The hand-built derivation kept fast properties (perf/hardening: request context, middleware context and handle store sit in V8 dictionary mode; request-derived keys on plain objects #1109). New cases incontext-fast-properties.test.tspin fast properties for each purpose and fail on that first cut.rsc/shell-capture.tsderiveShellCaptureContext: builds the recording store and implicit doc scope (when the request has a cache store) and callsderiveRenderPass. The handle-store push funnel and_resolvedCacheKeys ??= new Map()on the request stay at the site. TheattemptCaptureJSDoc's per-field bullets moved onto the table row.rsc/rsc-rendering.tsserveShellHit:createTailContextis gone. The tail is derived after its loader seed is built, with the seed and thedocTailimplicit scope passed ininit. Nothing reads the derived context in between, and no derivation step has a side effect on the request.router/match-middleware/background-revalidation.tsrerenderAndCacheRoute: callsderiveRenderPasswith its handle store and the throwaway sink. The secondcreateRequestContextused as the sink stays until S7.server/request-context.ts: thewireRenderBarrierand_handleStoreJSDoc namederiveRenderPass.tools/internals-baseline.json:request-context-object-createis lowered by 3 withnode tools/check-internals.mjs --update.background-revalidation.ts,rsc-rendering.tsandshell-capture.tsgo from 1 to 0, and nothing is raised. In non-test source,Object.createof aRequestContextnow appears only inserver/render-pass.ts.server/__tests__/render-pass.bench.tsx(new, vitest bench,vitest.rsc.config.ts) times:serveShellRequest, over a fixed route set.The bench reaches the passes only through
deriveShellCaptureContext,rerenderAndCacheRouteandserveShellRequest, so the same file runs on the base commit.Docs:
docs/internal/feature-map.md: Request Context section.docs/internal/feature-file-map.md: Request context row.docs/internal/rendered-barrier.md: who wires the capture's barrier.Diff: 11 files, +614 / -229.
render-pass.tsis 300 lines: 167 are comments (126 moved off the three sites, the rest the module, row and type docs) and 124 are code.Test plan
server/__tests__/derived-context-shape.rsc-test.tsx(S3 characterisation, unchanged): 7/7 green. A mutated table turns 6 of its 6 derivation cases red: the capture barrier inherited, and_tracing, the tail's_shellFragmentPayloadand the background_setKeepCacheDirectivedropped.server/__tests__/context-fast-properties.test.ts, newderived render passcases: each purpose keeps V8 fast properties.Object.assigncut (capture case:expected false to be true). Green on the named-store rows.router/__tests__/shell-capture-barrier-isolation.test.ts,rendered-barrier.test.ts,background-revalidation.test.ts,rsc/__tests__/,prerender/__tests__/build-shell-capture.test.ts,server/__tests__/context-fast-properties.test.tsandcache/__tests__/.serveShellRequestrsc suites (testing/__tests__/serve-shell-request*.rsc-test.tsx) plus the S3 test: 240 passed, 3 expected fail.pnpm run test:unit:all(every package) on the final commit:packages/rangojs-routerwith--grep "shell-cache|prerender-ppr|semantic-matrix". That coversshell-cache,shell-cache-action,prerender-pprandsemantic-matrix;--listfirst showed 125 dev and 126 production tests in 4 files.beforeAllhooks at load average 22-27. An edit torender-pass.tsduring that run broke the dev server's full reload ("./server.ts" is not exported). The rerun, with no edits, passed 125/125.tests/cloudflare-basic,e2e/ppr-shell.test.ts e2e/cache.test.ts:ppr-shell dev readiness: injected boot-race (#719) > injected reopt NOT-READY: the FIRST request re-polls to HIT(expected HIT, got MISS). Rerun alone on this branch, it passed 3/3. It was not run on the base commit, so it is not called pre-existing.bindRes(pass, () => sink.res)into its row, with the same call in the same order. The other changes are type-only edits, comments, the test move and the bench. Unit, rsc and typecheck were rerun on the amended commit.pnpm run typecheck: 16 tasks;oxlint --deny-warnings: 0 warnings;oxfmt --check .;node --test tools/*.test.mjs: 51 passed;check-e2e-bucketing,check-e2e-parity --strict,check-e2e-console-baseline,check-suspense-contract,check-no-external-store,check-doc-links,check-docs-api,check-skills,generate-skills-catalog --check,generate-route-types --checkandcheck-internals: all pass.*.gen.tschange.Practices
oxlint --deny-warningsandoxfmt --check .are clean on the change. Nothing was silenced.createTailContext; thewireRenderBarrierandbindResimports the three sites no longer use; a one-rowresknob on the table policy, now abindRescall in the background row; and a separate fast-properties test file, folded intocontext-fast-properties.test.ts.RENDER_PASS_FIELDS,RenderPurposeandRenderPassInitare exported with no importer outsiderender-pass.ts. The plan names the table as the module's export, and the two types arederiveRenderPass's signature.derived-context-shape.rsc-test.tsxpins the behaviour and was not changed.context-fast-properties.test.ts);makeRequestContext, which is on base too;RenderPurposenow derives fromRenderPassInit's keys;PassUnderConstructionnarrowed to the one readonly field the rows write;PassFieldPolicyis no longer exported;resknob noted above.init.handleStoreis the request's own store (handler.ts:1099) and is kept exactly as the site passed it. Tying it tobase._handleStoreis a later cleanup.renderBarrierfield stays in the table, so the Follow-ups: caching/PPR/render-pipeline audit (DRY consolidations, deferred coverage, held/investigate items) #684 field is stated per row._handleStore,_metricsStore,_renderErrors,_recoveredHandlerErrors,_onResponseCallbacks) stay written out in each row. A shared helper would hide each row's set, and S6 and S7 shrink both rows.cache/runtime imports out ofserver/.createTailContextname: that test must stay unchanged.docs/internal/feature-map.md,docs/internal/feature-file-map.mdanddocs/internal/rendered-barrier.md, and therequest-context.tsJSDoc.deriveRenderPassas the landed seam.Notes
The S3 characterisation test
server/__tests__/derived-context-shape.rsc-test.tsxis unchanged. Its PPR tail describe still sayscreateTailContext, the closure this PR removes. The test is left as it is because the proof bar for this step is "unchanged and green".Background pass barrier: kept inherited, as today, and recorded as question rango-t135-q1. The answer there is to keep it in S5 and look at it separately, with a userland proof first: does a refresh that reads the visitor's barrier ever wait on or release the wrong render?
Size: after the comment trim,
render-pass.tsis 195 lines (62 of them comments). The step is a move; its net source line change is small, and most of the new file is the table and its types.Timing, measured on a quiet machine (load average under 4, no other test runs),
render-pass.bench.tsx, 6 rounds alternating base (7b38ae4) and head. Each cell is the median of the per-round medians, with the range of round medians in brackets:No regression.
After review rango-t135-c1
render-pass.tscomments from 167 of 300 lines to 62 of 195. It changes comments only: with comments stripped, both versions print the same through the TypeScript printer, their 767-token streams match, andgit diff -wshows no non-comment line. The background row now points at rango-t160, and the perf/hardening: request context, middleware context and handle store sit in V8 dictionary mode; request-derived keys on plain objects #1109 note names the trigger, anObject.assignthat takes the object to 20 own properties.