Repository navigation
fix(router): shell capture no longer waits for loader pushes its record leaves out (#1035) - #1059
Merged
Merged
Conversation
… leaves out (#1035) The capture waited for every deferred handle push before writing the record, including promises pushed by ssr: false loaders, which the record drops and the prelude has no slot for. A push slower than ppr.captureTimeout (or one that never settles) refused the capture. It now waits on the recorded cut only (recordedHandleData). Handler pushes are in the cut and still bound the capture.
commit: |
…ndered-push-wait # Conflicts: # packages/rangojs-router/CHANGELOG.md
This was referenced Oct 4, 2026
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.
Problem
Since #1054 a shell keeps only the handle pushes its record keeps; a promise pushed by an
ssr: falseloader is live, has no slot in the prelude, and arrives after hydration. The capture still waited for that promise before writing the record. If it settled afterppr.captureTimeout, or never (it depends on a masked live loader), the capture was refused and the route lost its shell for a value the shell does not contain.Before / after
Semantics
ctx.use(H)(value)ctx.use(H)(promise)ppr.captureTimeout; a miss still refuses the capture (unchanged)ssr: falseloader, settled valuessr: falseloader, promise, fast or slow or neverCode change
settleCaptureRecord(src/rsc/shell-capture.ts) resolvedfreshHandleStore.getData()whole. It now resolvesrecordedHandleData(freshHandleStore, data)instead.recordedHandleData(src/handles/deferred-resolution.ts) is the helperresolvedHandleStream(..., recordedOnly)already used, exported; it appliesHandleStore.getDataForSegment(id, true), the same tagged-push rule the record writer uses. No third copy of the rule; about 20 lines of non-test source; no stored format change.Checks
runsbit is still set for a loader whose push was not waited for: the HIT unit test shows the body runs at g2 and deliversslow-deferred@g2after hydration. The pin is written by the loader funnel, which the change does not touch./deferred-plaintests (Promise.resolvepush, "is not in the shell") stay green.a handler's deferred push slower than ppr.captureTimeout still refuses the capture, green before and after.Test plan
serve-shell-request-push-ownership.rsc-test.tsx, group "the capture does not wait for a push its record leaves out": slow loader push does not refuse the capture and the shell/hydration have the settled row only; the HIT's run delivers the deferred row late; handler push control.origin/main(3 of the 4 new tests; the handler control passes):expect(hit.shellStatus).toBe("HIT")received"MISS".expectSlowDeferredPushDoesNotBlockCapture(shared body,tests/shared-e2e/src/index.ts), dev and(production)describes in both apps:packages/rangojs-router/e2e/shell-push-ownership.test.ts(route/shell-push/slow) andtests/cloudflare-basic/e2e/ppr-push-ownership.test.ts(route/ppr-push/slow). Green in all four. Red onorigin/main(router test-app, dev):warmShellToHitnever seesx-rango-shell: HIT(expect(res.headers()["x-rango-shell"]).toBe("HIT"),tests/shared-e2e/src/index.ts:2051), 20 s timeout.--no-deps): router test-appshell ppr semantic-matrixdev 131 passed (1 flaky pass on retry:ppr-header-guardhit a pnpm deps-status wrapper error, environment), production 133 passed; cloudflare-basicpprproduction 85 passed, dev 86 passed with 1 failure (ppr-shell dev readiness: cold first request (#719)cold virgin route), which passes alone on the same tree. Not run onorigin/main.test:unit:all, lint, format,check:e2e-bucketing,check:e2e-parity --strict,check:docs-api,check:bundle-guards.Notes
Regenerated
router.named-routes.gen.tsin both apps that gained a route (test-app, cloudflare-basic); no other app changed.A route whose loader push never settles still hangs a document MISS (the MISS hydration data awaits it); the unit test therefore uses a slow push, not a never-settling one. Only the capture stopped waiting.
Follows fix(router): a document served from a PPR shell hydrates with the handle data its shell was rendered from (#1035) #1054.
Review: read by the reviewer directly given the size (about 20 lines of source:
settleCaptureRecordwaits onrecordedHandleData, the filter the record writer and the prelude already share, now exported; no new copy of the rule). Second run on the final tree, after mergingmain: gate green (router 7566 unit, 516 RSC with 2 expected failures);--no-depse2e: router appshell-push-ownershipandshell-cache70 passed in dev and 70 in production; cloudflare-basicppr-push-ownershipandppr-capture-timeout10 and 10.Not changed, found while testing: a loader promise push that NEVER settles hangs a document MISS, because a normal document awaits every top-level pushed promise for its HTML (
resolvedHandleStream, documented as "a hung handle promise blocks the full render like any unresolved await"). The capture no longer waits for it, but the MISS that triggers the capture still does.