Skip to content

refactor(router): handler stages; dispatch built from them (S14) - #1242

Merged
ivogt merged 5 commits into
reorg/stackfrom
reorg/s14-handler-stages
Oct 23, 2026
Merged

ivogt merged 5 commits into
reorg/stackfrom
reorg/s14-handler-stages

Conversation

@ivogt

@ivogt ivogt commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

devcorner: rango-t143 · session rango-s295

Problem

testing/dispatch.ts re-implemented, by hand, the orchestration inside createRSCHandler (rsc/handler.ts) for non-Flight routes: the global middleware passes around the 308/404, the response route under its renderStartMs deadline, the timeout report, the partial/action redirect interception, the onResponse drain, the rewrite header, the open-redirect guard and the stream-idle watchdog. Its comments said it worked "exactly as handleResponseRoute does"; it drifted once (#1128). With two copies, a fix in the handler could leave dispatch from @rangojs/router/testing answering differently from production, and no test would fail.

This is S14 of the internals reorganisation (plan rango-d195 section 3, architecture rango-d194 section 3.7). The stages move out of the handler closure into one module, and both callers run it.

Before / after (internal shape)

Before:

createRSCHandler (rsc/handler.ts, closure)
  callOnError, handleTimeoutResponse                  closure functions
  handler():  runRewritePasses + matchMiddleware/executeMiddleware inline;
              response span: intercept / finalize / Server-Timing / rewrite header / guard / stream idle inline
  coreRequestHandler(): classifyRequest + 404 fallback, action-rewrite check,
              terminal plans, origin guard inline
  executeRequest(): response route = withTimeout(handleResponseRoute) + timeout + Vary inline

dispatch (testing/dispatch.ts)
  its own copy of the pass loop + global chain, the response-route handler
  (serialize, rewrap, errors, Vary, cache leaf), the timeout report, and the
  exit interception, finalize, rewrite header, guard and stream idle

After:

rsc/request-stages.ts                      no @vitejs/plugin-rsc import
  createErrorReporter, reportBackgroundErrors
  classify
  terminalPlanResponse                     synchronous: action-rewrite check, partial redirect, reloads
  originRejectedResponse                   onError + telemetry for a rejected origin
  partialRedirectResponse
  runRequestPasses                         rewrite passes + global middleware chain
  serveResponseRoute, timeoutResponse
  finalizeRequestResponse                  interception, onResponse drain, rewrite header,
                                           open-redirect guard, stream idle

createRSCHandler = the stages + its await of checkRequestOrigin + the Flight executor
dispatch         = router.previewMatch matching + its throw on an RSC route
                   + runRequestPasses / partialRedirectResponse / serveResponseRoute / finalizeRequestResponse

Code-change summary

  • src/rsc/request-stages.ts (new, 730 lines). Most of it is code moved verbatim from handler.ts, comments included.
  • src/rsc/handler.ts goes from 1,630 to 1,253 lines.
    • coreRequestHandler is now classify, then terminalPlanResponse, then base's origin guard shape: checkRequestOrigin awaited only for the modes ORIGIN_CHECK_PHASE_BY_MODE gates, with originRejectedResponse reporting a rejection, then executeRequest.
    • The response branch calls serveResponseRoute.
    • The pass loop calls runRequestPasses. The response tail calls finalizeRequestResponse; the handler's Server-Timing runs in its beforeGuard hook and the span attributes run after it.
    • Not touched: the Flight executor (the other executeRequest branches), executeRenderWithMiddleware, createRedirectFlightResponse, and request setup.
  • src/testing/dispatch.ts goes from 1,023 to 571 lines.
    • Kept: matchDispatchPass (findMatch + previewMatch, with the RSC-route throw before middleware), its 308/404, its telemetry lifecycle, and its public options and errors.
    • Everything else is now a stage call.
    • Its documented boundaries stay. It has no Flight, so its createRedirectFlightResponse is the no-state 204 + X-RSC-Redirect. It does not call onError for a response-route handler's own throw; that is now explicit through reportResponseRouteError: () => {}.
    • It loads the stages with one held lazy import(), which replaces its two lazy imports of cache-scope and response-cache-serve.
    • The eager testing-barrel graph: on base 210 modules, on head 108, nothing added. The first dispatch now loads 11 modules instead of 4: request-stages, request-classification and response-route-handler among them, none of them plugin-rsc. It was 12 at 4b4634c; the guard split dropped origin-guard.ts from the stages' imports. Measured with a Vite module runner importing src/testing/index.ts, then dispatching once.
  • src/rsc/handler-context.ts: HandlerContext extends the stages' RequestStageContext, so the handler passes its existing context bundle to the stages.
  • src/rsc/response-route-handler.ts: handleResponseRoute now takes only the members it uses. It reports a handler's throw through reportResponseRouteError when that is set; the handler does not set it, so production still uses callOnError(error, "handler", …).
  • Comment fixes where dispatch's old lazy cache path was described: cache/cache-scope.ts, cache/handle-snapshot.ts, rsc/json-route-result.ts, rsc/response-cache-serve.ts.
  • tools/internals-baseline.json: no change against reorg/stack. Since the merge onto S16, the code that moved reads WIRE.*, so request-stages.ts has no _rsc_* literal and rsc-param-literal stays at 4 (see "Updated onto reorg/stack after S16").
  • Non-test source: +1,062 / −1,167 lines, net −105.
  • src/rsc/__tests__/request-stages.bench.tsx (new, 135 lines): the hot-path timing bench (see Hot path timing). It runs only under vitest bench with vitest.rsc.config.ts; vitest.config.ts gains a benchmark.exclude for it, so a bare vitest bench under the main config does not load it without the react-server condition.

Production behaviour does not change: each stage is the handler's code, moved, run in the same order. These differences cannot be observed from outside:

  • Microtask hops. None crosses a macrotask boundary or moves a visible effect.
    • classify: +1 on every request.
    • runRequestPasses: +1 on every request.
    • serveResponseRoute: +1 on a response route.
    • The guard adds none. terminalPlanResponse is synchronous, and the handler awaits checkRequestOrigin only for the modes ORIGIN_CHECK_PHASE_BY_MODE gates, as base did. 4b4634c always awaited guardRequest, which added one more hop on every request. An async wrapper around the origin check would still add one on action, loader and PE-form requests, so the handler awaits checkRequestOrigin itself and originRejectedResponse is synchronous.
  • The Server-Timing header is set after the rewrite header instead of before. Headers iterates sorted by name, so the order does not show.

Microtask turns from handler(request) to its resolved Response, counted by a microtask that re-queues itself. The counts are deterministic. Measured with createRSCHandler and a real router in the rsc Vitest project, warm, without a global middleware on the path:

Request kind base 4b4634c head head − base
Document (GET /page/1) 70 73 72 +2
Flight partial (?_rsc_partial) 79 82 81 +2
Response route (path.json) 19 23 22 +3
Loader fetch (?_rsc_loader, origin passes; unknown loader, 404) 15 18 17 +2
Action whose origin is rejected (403) 12 15 14 +2

With a global middleware on the path, every count is 7 higher on all three revisions; the differences are the same.

Test plan

  • pnpm run typecheck: 16/16 tasks.
  • pnpm run test:unit:all:
    • router unit: 561 files, 9,235 passed, 5 skipped, 6 todo.
    • router rsc: 82 files, 1,056 passed, 3 expected fail.
    • cloudflare-basic: 90 unit, 39 rsc.
    • vite-rsc-demo: 27 unit, 1 rsc.
    • e2e/mini: 25 unit, 3 rsc.
    • shared-e2e: 20.
    • On the review-fix commits, two earlier full runs on a loaded machine each failed one test on its 5,000 ms timeout: dev-reload-handoff.test.ts, "hands its request to the router the reload re-created". It passes alone, takes the same time on head as on base when run alone and alternated, and the third full run passed. It times out on base under the same load.
    • The src/testing/__tests__ dispatch suites (dispatch.test.ts, dispatch-*.test.ts, telemetry-emission, rewrite-userland, async-include-dispatch) and every consumer dogfood suite are unchanged.
  • New test, src/rsc/__tests__/request-microtasks.rsc-test.tsx. It pins the microtask hops on the request path. It does not test private internals: it drives the public createRSCHandler (@rangojs/router/rsc) with a real router and observes from route code. It counts only router code.
    • A document is counted up to its route middleware, and from its global middleware's next() to the response, so React's render in between is left out. It pins 7 and 9.
    • A response route's whole path is router code. It pins 8 turns to the handler and 22 to the response.
    • Shown red before green. On 4b4634c's handler.ts and request-stages.ts it fails with expected { toRouteMiddleware: 8, … } to deeply equal { toRouteMiddleware: 7, … } and expected { toHandler: 9, toResponse: 23 } to deeply equal { toHandler: 8, toResponse: 22 }. On base it reads 6 and 8 for the document, and 7 and 19 for the response route.
    • Any await added to or removed from the path fails it. That is intended: update the count and name the hop in the PR.
  • New test, src/testing/__tests__/testing-module-graph.test.ts. It starts a fresh Vite module runner and imports the ./testing export's entry, checking that the export maps to ./src/testing/index.ts. It then dispatches a cached path.json route behind global middleware, plus a partial request to a trailing-slash redirect. It asserts that runner.evaluatedModules holds no @vitejs/plugin-rsc module, both after the import and after the dispatches. Shown red before green: with a static import "@vitejs/plugin-rsc/rsc/server" added to request-stages.ts it fails with dispatch /api/data failed; plugin-rsc modules evaluated: …/@vitejs/plugin-rsc/dist/rsc/server.js. With that import added to the barrel it also fails. It passes without either.
  • E2e, router test-app, run on 4b4634c from packages/rangojs-router with --no-deps and one grep: response-handler|content-negotiation|redirect guard|global middleware Response short-circuit|throw rewrite|Semantic matrix. Checked with --list first: it selects only those six files.
    • --project=dev: 121 passed.
    • --project=production: 117 passed.
  • E2e, cloudflare-basic response-cache, from tests/cloudflare-basic, on 4b4634c:
    • dev: 8 passed.
    • production: 5 passed.
  • E2e on the review-fix commits: not run locally. They reorder awaits inside the guard, which the unit tests above cover; CI runs the e2e when the PR is marked ready.
  • New bench, src/rsc/__tests__/request-stages.bench.tsx, ran once with vitest bench --run --config vitest.rsc.config.ts: all 10 cases ran, each route answering its expected status (200, 308 or 404; checked before timing). vitest list picks it up in neither the main nor the rsc config, so test:unit:all does not run it.
  • oxlint --deny-warnings, oxfmt --check ..
  • CI lint scripts all OK: check-e2e-bucketing, check-e2e-parity --strict, check-e2e-console-baseline, check-suspense-contract, check-no-external-store, node --test tools/*.test.mjs (51 pass), check-doc-links, check-docs-api, check-skills, generate-skills-catalog --check, generate-route-types --check, and check-internals. No routes changed and no *.gen.ts diff.

Hot path timing

Quiet machine (load average under 4), base 7b38ae4 against head 3901965. 6 iterations, each running 2,000 timed requests per route on each side, alternating which side goes first. Each cell is the median of the run medians, in µs. The last column is a rerun with 10 iterations on a busier machine.

Route base head Δ Δ, rerun
handler json, global mw (GET /api/data) 30 31
handler text, no mw (GET /ping) 20 20
handler document (GET /page/1) 2773 2771 −0.1% +1.3%
handler Flight partial (?_rsc_partial) 2726 2884 +5.8% (inside the run spread) +0.4%
handler 308 (GET /old) 18 22 −14.6%
handler 404 document (GET /missing) 2747 2839 +3.3% +1.2%
dispatch json, global mw (/api/data) 25 24 −10.5%
dispatch text, no mw (/ping) 18 15 −18.3%
dispatch 308 (/old) 9 10
dispatch 404 (/missing) 11 11

No regression on the document and Flight paths beyond run-to-run spread; the two extra microtask turns cost no measurable time; dispatch is faster.

The routes and the per-request measurement are committed as packages/rangojs-router/src/rsc/__tests__/request-stages.bench.tsx: one request per sample, timed from the call to the end of its body read, 200 warmup and 2,000 timed requests per route. Run it from packages/rangojs-router with ./node_modules/.bin/vitest bench --run --config vitest.rsc.config.ts src/rsc/__tests__/request-stages.bench.tsx. To compare two revisions, run it in both checkouts, alternating, several times each.

Updated onto reorg/stack after S16

After this PR was approved, reorg/stack gained S16 (#1248), S5 (#1245), S15 (#1243), #1246 and #1247. This branch merges it in one merge commit, not a rebase, so the reviewed commits stay. S16 replaced every _rsc_* string literal with WIRE.* from src/wire-params.ts, including in code this PR moved out of handler.ts and dispatch.ts, so three files conflicted. Each conflict keeps this PR's structure and applies S16's change to it:

Conflict Resolution
rsc/handler.ts imports Kept the request-stages.js import. Dropped classifyRequest, RequestPlan and INTERNAL_RANGO_DEBUG, which moved to the stages. Kept S16's WIRE import for the readers that stayed in the handler.
rsc/handler.ts response tail Kept the finalizeRequestResponse call. Its partial/action interception, in request-stages.ts, reads WIRE.partial and WIRE.action.
rsc/handler.ts terminal plans Kept the terminalPlanResponse call. Its action-rewrite check, partial redirect and version-mismatch log read WIRE.action, WIRE.partial and WIRE.version.
testing/dispatch.ts imports Added WIRE. Dropped applyStreamIdleTimeout, which the stages call.
testing/dispatch.ts isPartial Kept this PR's shape (the reverse function and isAction live in the stages) with WIRE.partial.
tools/internals-baseline.json Took the stack's version. check-internals --update leaves it unchanged.

request-stages.ts has no _rsc_* literal left. The names still spelled in request-stages.ts (one) and handler.ts (two) are in comments, which the rule does not count and S16 also left. The handler and the stages together make the same searchParams calls as S16's handler.ts: has() on WIRE.action three times, WIRE.partial three times, WIRE.loader and WIRE.warmup, and get() on WIRE.loader and WIRE.version. Against the pre-merge head, the merge changes this PR's files only by those literal-to-WIRE replacements and three WIRE imports.

Checks run on the merge:

  • pnpm run typecheck: 16/16 tasks.
  • pnpm run test:unit:all:
    • router unit: 565 files, 9,348 passed, 5 skipped, 6 todo.
    • router rsc: 84 files, 1,067 passed, 3 expected fail.
    • cloudflare-basic: 90 unit, 39 rsc.
    • vite-rsc-demo: 27 unit, 1 rsc.
    • e2e/mini: 25 unit, 3 rsc.
    • shared-e2e: 20.
  • check-internals: OK, rsc-param-literal 4 (all in use-loader.tsx), request-context-object-create 18, internal-vi-mock 327, browser-import-into-server 1.
  • S16's src/browser/__tests__/wire-urls.test.ts, testing-module-graph.test.ts, and the dispatch suites (dispatch.test.ts, dispatch-*.test.ts, async-include-dispatch.test.ts): 9 files, 115 passed.
  • request-microtasks.rsc-test.tsx: 2 passed, with its counts unchanged (7 and 9 for a document, 8 and 22 for a response route).
  • oxlint --deny-warnings, oxfmt --check ., and the CI lint scripts: check-e2e-bucketing, check-e2e-parity --strict, check-e2e-console-baseline, check-suspense-contract, check-no-external-store, node --test tools/*.test.mjs (53 pass), check-doc-links, check-docs-api, check-skills, generate-skills-catalog --check, generate-route-types --check. No *.gen.ts diff.
  • E2e, router test-app, from packages/rangojs-router, --project=dev --project=production --no-deps with one grep: response-handler|content-negotiation|redirect-guard|global-mw-response|rewrite|semantic-matrix. --list first: it selects those six files plus one url-hooks test whose title contains "rewrite".
    • dev: 122 passed.
    • production: 118 passed.

Practices for the merge:

  • Lint: oxlint and oxfmt --check are clean; nothing suppressed.
  • Dead code: the stack-side imports the moved code no longer needs (classifyRequest, RequestPlan, INTERNAL_RANGO_DEBUG in handler.ts; applyStreamIdleTimeout in dispatch.ts) are not carried over. WIRE is imported only where it is read.
  • Tests: the runs above; no test was added, skipped or edited.
  • Code review: reviewed the resolution by hand against both parents. The merge's diff against the pre-merge head is only the WIRE replacements and imports; nothing else moved.
  • Docs: no change. feature-map.md and feature-file-map.md merged without conflict, and S16's wire-protocol row names the module and its writer, not its readers, so it needs no change.

Practices

  • Lint: ran oxlint --deny-warnings and oxfmt --check . over the repo. Both are clean, and nothing is suppressed.
  • Dead code:
    • Removed: everything in dispatch.ts and handler.ts that the stages replace, the closure callOnError and handleTimeoutResponse, and the imports they left unused. In the review fixes, guardRequest is gone, and so is ClassifyContext.router.originCheck, which no stage reads once the handler awaits the origin check.
    • Searched src, tests, e2e, tools and docs for the removed names. The only references left were doc and comment pointers, and those are updated (execution-model.md, feature-map.md, security-checklist.md, router/instrument.ts).
    • Kept on purpose: the createCacheScope injection in response-cache-serve.ts, because rsc/__tests__/response-cache-serve.test.ts injects a stub through it. Also rsc/json-route-result.ts, which still has its caller response-route-handler.ts.
  • Tests:
    • Ran the full test:unit:all (see the timeout note in the test plan), the targeted e2e listed above in dev and production on 4b4634c, and the CI lint scripts.
    • Added the module-graph test and the microtask-hop test, each shown red before green.
    • Added the request-stages bench and ran it once; it asserts only each route's status before timing.
    • No test was skipped, loosened or edited.
  • Code review: reviewed the diff against base for any change to createRSCHandler and to dispatch, and reviewed the review-fix commits again; that pass found nothing. Reviewed the bench commit by hand: its header named only the document and Flight requests as React-rendered; the 404 document is too, now named.
    • Fixed: dispatch's handler-error filter was a phase check, phase === "handler" without timeout metadata. It is now the explicit reportResponseRouteError seam.
    • Fixed: serveResponseRoute took eight positional parameters; it now takes an options object.
    • Fixed: partialRedirectResponse was typed any; it is now generic.
    • Fixed: dispatch ran a dynamic import() per call; it now keeps the loaded module.
    • Fixed (review rango-t143-c2): the always-awaited guardRequest hop, the PR body's module-graph claim, the missing table rows, and the consumer note.
    • Dismissed: "an onResponse callback is dropped on dispatch's 308/404 when only an earlier pass ran middleware". The 308 and 404 are built by createResponseWithMergedHeaders, which drains the callbacks.
    • Dismissed: "background-error dedup now goes through ALS". Cache writes run inside the request's ALS scope, and this is production's reporter.
    • Left: matchDispatchPass builds ResponseRouteMatch from previewMatch's fields next to responseRouteMatch(plan). The two take different input shapes.
    • Left: the Server-Timing header is set after the rewrite header (review nit). It cannot be observed, and moving beforeGuard up is not needed for this step.
  • Docs: updated docs/internal/feature-file-map.md, feature-map.md, security-checklist.md and execution-model.md, and the router/instrument.ts comment. Added a ## Unreleased entry to packages/rangojs-router/CHANGELOG.md, and made one line of skills/testing/response-routes.md precise (see Notes). The bench needs no doc change: the internal maps do not list benches, and its run command is in its header and under Hot path timing.

Notes

Where dispatch now answers differently, it answers as production does, because the stage is production's code. No test pins any of these cases; the dispatch suites pass unchanged. The ## Unreleased entry in packages/rangojs-router/CHANGELOG.md lists them for consumers.

Case Before After (as production)
?_rsc_action request (not partial), no global middleware on the final pass, response is a 3xx: a trailing-slash redirect, or a response route or its route middleware returning a redirect 204 + X-RSC-Redirect the 3xx, through the open-redirect guard
?_rsc_partial request to a trailing-slash path, with a global middleware that reads the response after next() the middleware sees the 308 the middleware sees the 204 + X-RSC-Redirect; the response is unchanged
A response-route renderStartMs timeout with no global middleware onResponse callbacks the route registered before the deadline drained onto the 504 / onTimeout response not drained (production drains at the exit only after a global chain)
Negotiated response route whose route middleware short-circuits no Vary: Accept Vary: Accept
Response route returning a non-Response value for a type with no MIME (image/stream/any) dev 500 body "dispatch(): response route handler for …" production's "Response route handler for … must return a Response object, got …"
onTimeout throwing logs [dispatch] onTimeout callback error: logs [RSC] onTimeout callback error:
Response-route timeout on a rewritten pass onError, onTimeout and request.timeout carried the visitor's request and URL the pass's request and URL, as in production; without a request-id header, request.timeout then carries a different requestId from dispatch's own request.start/request.end
Stream-idle onError / request.timeout no routeKey routeKey
Timeout, stream-idle and rewrite-error reports not deduplicated deduplicated per request (production's _reportedErrors)

skills/testing/response-routes.md said that every 3xx on an action request becomes a 204. It now says that when a global middleware matches the path, any 3xx leaving the chain on a partial or action request becomes a 204. Without one, an action request's 3xx goes out raw, as in production.

Recorded on the ticket and not changed here (rango-t143, comment c1):

  • dispatch keeps router.previewMatch, because the dispatch suites drive stub routers that supply only findMatch and previewMatch (dispatch.test.ts, "Vary: Accept on negotiated response routes"). As a result, dispatch still lacks production's _rsc_v and _rsc_rid reloads and the action-rewrite check: it runs neither classify nor terminalPlanResponse. Review rango-t143-c2 proposes a follow-up step (S14b) that moves dispatch onto them, with the stub-router tests rewritten onto real routers, sequenced before the S22 pilot (rango-t150).
  • dispatch sets ctx.params and routeName before the global chain; production sets them after it.
  • The "no onError for a response-route handler throw" boundary is now one seam (reportResponseRouteError), which a later step could drop.

Lift the stages around rendering out of the createRSCHandler closure
into rsc/request-stages.ts, which imports no @vitejs/plugin-rsc:
classify, guardRequest, partialRedirectResponse, runRequestPasses (the
rewrite passes and the global middleware chain), serveResponseRoute and
timeoutResponse, finalizeRequestResponse (redirect interception,
onResponse drain, rewrite header, open-redirect guard, stream idle),
createErrorReporter and reportBackgroundErrors.

createRSCHandler runs them around its Flight executor; HandlerContext
extends the stage context. dispatch drops its hand-kept copy of the same
orchestration and runs the passes, the response route, the partial
redirect and the finalization, keeping its previewMatch matching, its
throw on an RSC route, its telemetry lifecycle and its documented
boundaries. It loads the stages lazily, so the testing barrel's eager
graph is unchanged; a module-graph test pins both graphs plugin-rsc-free.
ivogt added 4 commits October 23, 2026 17:57
guardRequest was always awaited, which put a microtask between
classification and execution on every request, including the full,
partial and response plans it answers synchronously. Split it:
terminalPlanResponse (the action-rewrite check, the partial redirect and
the reloads) is synchronous, and coreRequestHandler awaits
checkRequestOrigin itself, only for the modes ORIGIN_CHECK_PHASE_BY_MODE
gates, as before the stages moved; originRejectedResponse reports a
rejection through onError and telemetry. The guard adds no microtask on
any request kind.

request-microtasks.rsc-test.tsx pins the hops through createRSCHandler,
observed from route code: a document to its route middleware (7) and
from its global chain to the response (9), a response route to its
handler (8) and to the response (22).
dispatch() now runs the handler's request stages, and in a few edge
cases its answer moved to production's. List them in an Unreleased
CHANGELOG entry, and make the testing skill's redirect line precise:
when a global middleware matches the path, any 3xx leaving the chain on
a partial or action request becomes a 204 + X-RSC-Redirect; without one,
an action request's 3xx goes out raw, as production sends it.
request-stages.bench.tsx times one request, call to body read, per route:
createRSCHandler for a json route behind global middleware, a text route,
a document, a Flight partial, a 308 and a 404 document; dispatch for json,
text, 308 and 404. 200 warmup and 2,000 timed requests per route. It runs
under vitest.rsc.config.ts; the main config's benchmark exclude keeps a
bare `vitest bench` from loading it without the react-server condition.
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.
@ivogt
ivogt merged commit 4a77cd3 into reorg/stack Oct 23, 2026
19 checks passed
@ivogt
ivogt deleted the reorg/s14-handler-stages branch October 23, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant