Repository navigation
refactor(router): matrix-critical de-duplications — middleware merge, loader-cache interceptor, cache-hit loaders, parallel-slot (#568) - #576
Merged
Conversation
…#568) Replace 3 near-identical merge-and-clone Response sites in executeMiddleware (end-of-chain next() and short-circuit) and executeInterceptMiddleware with a private mergeResponse(base, stub, {stubOverridesNonCookie, mergeReqCtx}) helper. The two flags encode the only real differences: stubOverridesNonCookie (chain ran vs explicit short-circuit) and mergeReqCtx (false ONLY for the intercept path -- the one deliberate divergence, pinned by intercept-middleware-headers). WebSocket-upgrade early returns + the hasStubHeaders gate stay at the call sites. Behavior-preserving; semantic matrix + middleware e2e green.
) yieldFromStore (prerender-store path) and withCacheLookup (runtime cache-hit path) duplicated the fresh-loader-on-cache-hit block verbatim (loaders are never cached): the isFullMatch branch, resolveLoadersOnly[WithRevalidation] calls, state.matchedIds update, and the loader-resolve + cache-hit metrics. Extract resolveFreshLoadersAndYield(). The router resolve fns are passed in (captured early by each caller) rather than re-read from the ALS-backed getRouterContext() inside the helper, preserving workerd ALS-disruption robustness. The render barrier + cacheSource assignment stay at each call site (they differ). Behavior- preserving; semantic matrix + cache e2e (dev+prod) green.
…terceptor (#568) resolveLoaderData rebound ctx.use once per cached loader, chaining one wrapper per loader (O(N) dispatch) and relying on an undocumented synchronous capture-before-overwrite invariant. Install ONE stable interceptor on the first cached loader that consults a per-ctx override table (_loaderCacheOverrides); each subsequent cached loader just primes the table (O(1) dispatch). The cache-miss execute runs through the captured pre-interceptor ctx.use (_loaderCacheOriginalUse), so a loader never awaits its own in-flight promise. Stays agnostic to which setup mode installed ctx.use (loader/silent/build), like the old rebind. Adds a multi-cached-loader accumulation test. Behavior- preserving; matrix + loader-cache unit + cache e2e green.
resolveParallelSegmentsWithRevalidation and the orphan-inlined loop in
resolveOrphanLayoutWithRevalidation duplicated the parallel-slot
component-resolution block (~45 lines): tryStaticSlot, then run the slot handler
with the _currentSegmentId slot-id pin, handlerRan flag, and streamed-handler
wrap. Extract resolveParallelSlotComponent() returning {component, handlerRan}.
The block is byte-identical between the two and SEPARABLE from their known
divergences: the defaultOverride/shouldResolve seed is computed by the caller
before the call, and loader-resolution ordering stays at the call site (orphan
before push, main after) -- neither is touched. Behavior-preserving; semantic
matrix + parallel-revalidate-fns + parallel-reval e2e green.
commit: |
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.
Summary
The matrix-critical internal refactors deferred from #575 (issue #568) — four behavior-preserving de-duplications on the segment-resolution / middleware / cache hot paths. No consumer-observable change: no public API, response shape, generated type, or DX delta. The point is maintainability (kill copy-paste drift) without disturbing the execution contract, so the bar here is provable behavior-preservation, verified by the semantic matrix (61/61 dev+prod, no row changes), targeted dev+prod e2e, and a 4-way adversarial refutation pass.
A consumer writes nothing differently and sees nothing differently. What changes is that four pairs of near-identical blocks now share one implementation, so the next edit can't silently drift one copy from the other.
What was de-duplicated
1. Middleware merge-and-clone Response builder (
middleware.ts)Three sites cloned a downstream
Responsewith merged stub/reqCtx headers:executeMiddleware's end-of-chainnext(), its short-circuitreturn, andexecuteInterceptMiddleware's early response. ExtractedmergeResponse(base, stub, { stubOverridesNonCookie, mergeReqCtx }). The two flags encode the only real differences:stubOverridesNonCookiemergeReqCtxnext()end-of-chain (chain completed)truetruereturn Response)falsetruefalsefalseThe intercept path's
mergeReqCtx: falseis the one deliberate divergence (it does not merge RequestContext stub headers) — pinned byintercept-middleware-headers.test.ts. WebSocket-upgrade early returns and thehasStubHeadersgate stay at the call sites. (Thenext()site's WS check moved before the merge; proven unobservable —mergeStubHeaders/mergeReqCtxStub/_getRequestContexthave no side effects beyond the local mergedHeaders, which the WS branch discards.)2. Cache-hit fresh-loader resolution (
match-middleware/cache-lookup.ts)Loaders are never cached, so on every cache hit the router re-resolves them fresh.
yieldFromStore(prerender-store path) andwithCacheLookup(runtime cache-hit path) carried a verbatim copy of that block — theisFullMatchbranch, theresolveLoadersOnly/resolveLoadersOnlyWithRevalidationcalls, thematchedIdsupdate, and theloader-resolve+cache-hitmetrics. ExtractedresolveFreshLoadersAndYield().The router resolve fns are passed in (captured early by each caller) rather than re-read from
getRouterContext()inside the helper —getRouterContextisAsyncLocalStorage-backed and the callers run awaits before this point, so re-reading could hit ALS disruption on workerd. The render barrier andcacheSourceassignment differ between the two callers and stay at each call site.3. loader-cache
ctx.useinterceptor (loader-cache.ts)resolveLoaderDatareboundctx.useonce per cached loader — chaining one wrapper per loader (O(N) dispatch) and relying on an undocumented synchronous capture-before-overwrite invariant.The cache-miss execute runs through the captured pre-interceptor
ctx.use, so a loader never awaits its own in-flight promise. The mechanism stays agnostic to which setup mode installedctx.use(setupLoaderAccess/setupLoaderAccessSilent/setupBuildUse), exactly like the old rebind. Two new internalInternalHandlerContextfields (_loaderCacheOverrides,_loaderCacheOriginalUse) back the table; both are read only insideresolveLoaderData.4. Parallel-slot component resolution (
segment-resolution/revalidation.ts)resolveParallelSegmentsWithRevalidationand the orphan-inlined loop inresolveOrphanLayoutWithRevalidationcarried a byte-identical ~45-line component-resolution block (static-slot lookup → run the slot handler with the_currentSegmentIdslot-id pin,handlerRanflag, and streamed-handler wrap). ExtractedresolveParallelSlotComponent()→{ component, handlerRan }.Critically, this block is separable from the two known divergences between those functions: the
defaultOverride/shouldResolveseed is computed by the caller before the call, and loader-resolution ordering stays at the call site (orphan before push, main after). The extraction touches neither.Deferred (with a forensic rationale) — issue #568 sub-items 1 & 2
The bigger unifications — folding the orphan-inlined parallel loop into the shared helper via policy args (sub-item 2), and unifying the
fresh.tsvsrevalidation.tstraversal trees (sub-item 1) — are deliberately not in this PR. A forensic investigation of the two divergences found:defaultOverride: { value: true }vs mainbelongsToRoute || isNewParentbelongsToRoute=false; avoids the #482 blank-slot staleness bug)$$id)The blocker for doing them now: the existing pinning test (
parallel-revalidate-fns-invocation.test.ts) does not actually lock either divergence — every orphan case it runs usesbelongsToRoute=true, where the policies coincide, and the revalidate fns override the seed. So it stays green under any unification and cannot catch a regression in (A). Per the stability bar, sub-items 1 & 2 get their own focused PR that first adds a pinning test for the divergent (A) case (orphan,belongsToRoute=false, slot ∉ client, parent ∈ client, zero revalidate fns → slot renders non-null + inmatchedIds), then carries (A) as a policy arg and normalizes (B).Test plan
pnpm run typecheck— 12/12 packagespnpm run test:unit:all(repo root) — router + all consumer suites, 0 failurespnpm run lint/pnpm run format— cleansemantic-matrix— 61/61 dev + production, no row changes (behavior-preserving across middleware scope, handler-first ordering, parallel-slot visibility, PE/JS parity, cache)cache.test.ts) dev + production — 145/145 (covers fresh-loader-on-cache-hit + loader-cache round-trips)parallel-reval-after-action,parallel-loader-reval) dev + productionapp-middleware,include-middleware) dev + production — 77/77refuted: false, high confidenceNotes
feature-map.md/feature-file-map.mdupdate needed.debugPerformance: Loading states 11 28 #3 is a strict O(N)→O(1) dispatch reduction; Router idea 2 revisit #1/feat: add error boundaries and notFound boundaries with default fallbacks #2/Transaction improvements #4 are allocation-neutral extractions — no hot-path regression expected.