Skip to content

test(app-router): cover consumed prefetch cache handoff - #2758

Merged
james-elicx merged 5 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/fix-client-cache-prefetch-flake
Aug 3, 2026
Merged

test(app-router): cover consumed prefetch cache handoff#2758
james-elicx merged 5 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/fix-client-cache-prefetch-flake

Conversation

@NathanDrake2406

@NathanDrake2406 NathanDrake2406 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a deterministic App Router browser regression for the consumed-prefetch to committed-cache handoff
  • delay the redundant cache tee after a full prefetch is consumed and assert that remounting the destination Link does not issue a duplicate RSC request
  • retain the existing upstream Next.js five-minute parallel-route reuse coverage

Why

The original production fix on this branch was superseded by the broader cache-publication work merged in #2760. After merging current main, the conflict was resolved in favor of that newer implementation, leaving this PR as focused regression coverage for the same race.

The test remains useful because it deterministically delays the cache branch that previously created the false-miss window. Current main passes by publishing the already-buffered consumed-prefetch snapshot without waiting for that redundant branch to drain.

Upstream parity reference:

Closes #2756.

Validation

  • vp check tests/e2e/app-router/nextjs-compat/client-cache.spec.ts
  • targeted browser regression: 1/1 passed
  • targeted browser regression repeated 10 times in parallel: 10/10 passed

Keep the buffered snapshot discoverable while navigation publishes the committed visited response. This prevents a remounted prefetch-enabled Link from observing a false cache miss and starting a duplicate RSC request.
@NathanDrake2406

Copy link
Copy Markdown
Contributor Author

@codex review

@pkg-pr-new

pkg-pr-new Bot commented Jul 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2758
npm i https://pkg.pr.new/create-vinext-app@2758
npm i https://pkg.pr.new/@vinext/types@2758
npm i https://pkg.pr.new/vinext@2758

commit: f7c1526

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: e5e80479aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@NathanDrake2406
NathanDrake2406 marked this pull request as ready for review July 30, 2026 08:32
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared f7c1526 against base dc05459 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 134.5 KB 134.5 KB ⚫ -0.0%
Client entry size (gzip) vinext 122.1 KB 122.1 KB ⚫ 0.0%
Dev server cold start vinext 3.06 s 3.03 s ⚫ -1.2%
Production build time vinext 3.28 s 3.31 s ⚫ +0.9%
RSC entry closure size (gzip) vinext 112.3 KB 112.3 KB ⚫ +0.0%
Server bundle size (gzip) vinext 189.7 KB 189.7 KB ⚫ +0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

# Conflicts:
#	packages/vinext/src/server/app-browser-entry.ts
@james-elicx james-elicx changed the title fix(app-router): preserve consumed prefetch during cache handoff test(app-router): cover consumed prefetch cache handoff Jul 31, 2026
@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the test-only change (+59 lines). This is well-constructed, deterministic regression coverage for the consumed-prefetch → committed-cache handoff race.

Verification done:

  • Confirmed the final diff is test-only; the source fix (e5e8047) was superseded by #2760, as the PR body states.
  • Confirmed the mechanism under test still exists in current main: consumedPrefetchSnapshot at app-browser-entry.ts:1974/2007/2285 is exactly what lets navigation publish the buffered snapshot without awaiting the delayed cacheBufferPromise (line 2207/2286). The test deliberately stalls that redundant cache tee via releaseOldNavigationTail, so a regression that reintroduced the await cacheBufferPromise dependency would hang the poll at line 335 or surface a duplicate request at line 358 — i.e. the test would fail loudly. Good.
  • The self-restoring global ReadableStream.prototype.tee override (line 308-329) is safe here: no browser-side code runs between install (page.evaluate at 298) and targetLink.click() (333), so the first tee() after install is guaranteed to be the navigation's navBody.tee() at app-browser-entry.ts:2200. This matches the existing prototype-patch pattern already used at lines 502-514.
  • vp check passes (format, lint, types).

Only minor, non-blocking observations below. These mirror patterns already present in this file, so they are consistency notes rather than defects. No changes required.

const cacheReader = cacheBranch.getReader();
const delayedCacheBranch = new ReadableStream({
async start(controller) {
while (true) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: the while (true) drain loop has no error propagation — if cacheReader.read() rejects, start() rejects, releaseOldNavigationTail is never set, and the poll at line 335 times out rather than failing with a clear message. This is acceptable for a test (it fails loudly either way) and is identical to the existing pattern at lines 462-478 / 598-614, so I'd keep it consistent. Just flagging that a rejected read surfaces as a timeout, not a targeted assertion failure.

"tee",
) as typeof ReadableStream.prototype.tee;
ReadableStream.prototype.tee = function (this: ReadableStream<unknown>) {
ReadableStream.prototype.tee = originalTee;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: the override self-restores on the first tee() call, so exactly one tee() gets delayed. That's the intended contract here (only the navigation's cache tee at app-browser-entry.ts:2200 should be stalled), and the timing guarantees it fires first. Worth a one-line comment noting the "delay exactly the first post-install tee" intent, since the self-restore is load-bearing and easy to accidentally remove in a future refactor.

@ask-bonk

ask-bonk Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully to PR #2758.

Summary

I reviewed this test-only PR (+59 lines to client-cache.spec.ts) and posted a COMMENT review with 2 non-blocking observations.

Verdict: LGTM. This is well-constructed, deterministic regression coverage. Key findings:

  • Confirmed test-only diff — the source fix (e5e8047) was superseded by fix(app-router): preserve page result render ordering #2760, as the PR body states. Final diff touches only the spec.
  • The test is meaningful against current mainconsumedPrefetchSnapshot still exists in app-browser-entry.ts (lines 1974/2007/2285). The test stalls the redundant cache tee via releaseOldNavigationTail, so a regression that reintroduced an await cacheBufferPromise dependency would hang the poll (line 335) or surface a duplicate RSC request (line 358) — it fails loudly.
  • The global self-restoring ReadableStream.prototype.tee override is safe — no browser-side code runs between install and the navigation click, so the first tee() is guaranteed to be the navigation's. This matches the existing prototype-patch pattern already in the file (lines 502-514).
  • vp check passes (format, lint, types).

Non-blocking notes posted:

  1. The while (true) drain loop surfaces a rejected read as a poll timeout rather than a targeted assertion — acceptable and consistent with existing patterns in the file.
  2. Suggested a one-line comment documenting that the self-restore is load-bearing ("delay exactly the first post-install tee"), since it's easy to accidentally remove in a refactor.

No changes required.

github run

@james-elicx
james-elicx merged commit 87220d0 into cloudflare:main Aug 3, 2026
57 checks passed
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.

test(app-router): client-cache prefetch reuse E2E is flaky

2 participants