Fix Nav Inspector request loop on repeat captures - #97050
Merged
Conversation
Repro: enable the Nav Inspector, click a <Link prefetch={true}>, close
the inspector, navigate home, re-enable it, and click the same link.
The app hung in a pending state while firing prefetch requests in an
infinite loop.
The Instant Navigation Testing lock restricted navigation reads to
entries created within the current lock scope (ownedEntries), enforced
as a post-hoc filter after the cache lookup. But the segment cache
resolves lookups by most-specific-match, so a previous scope's
runtime-prefetch entries at concrete param keypaths kept winning the
lookup, the filter kept rejecting them, and the locked prefetch created
its replacement at a more generic keypath that could never win — every
scheduler pass discarded and refetched forever.
Rather than patch the filter, this replaces the ownedEntries mechanism:
- Each lock scope owns a private segment CacheMap that starts empty and
is discarded at release, so a captured navigation structurally
observes only data fetched under the lock.
- The map is an explicit capability bound when work is created: a
prefetch task captures its map when scheduled (the single place that
consults lock state), a locked navigation inherits its driving task's
map, and everything else — unlocked navigations, hydration, refreshes,
traversals, server actions and patches — binds to the shared map.
Reads and response writes receive the map explicitly, so a request
that straddles a scope boundary still writes into the map its entries
live in.
Includes the regression test from the original repro, hardened to use
the retry-based panel-reopen helper the sibling tests use.
Contributor
Tests PassedCommit: e73b699 |
Contributor
Stats from current PR🔴 1 regression
📊 All Metrics📖 Metrics GlossaryDev Server Metrics:
Build Metrics:
Change Thresholds:
⚡ Dev Server
📦 Dev Server (Webpack) (Legacy)📦 Dev Server (Webpack)
⚡ Production Builds
📦 Production Builds (Webpack) (Legacy)📦 Production Builds (Webpack)
📦 Bundle SizesBundle Sizes⚡ TurbopackClient Main Bundles
Server Middleware
Build DetailsBuild Manifests
Build Cache
📦 WebpackClient Main Bundles
Polyfills
Pages
Server Edge SSR
Middleware
Build DetailsBuild Manifests
Build Cache
🔄 Shared (bundler-independent)Runtimes
📝 Changed Files (12 files)Files with changes:
View diffsapp-page-exp..ntime.dev.jsfailed to diffapp-page-exp..time.prod.jsfailed to diffapp-page-tur..ntime.dev.jsfailed to diffapp-page-tur..time.prod.jsfailed to diffapp-page-tur..ntime.dev.jsfailed to diffapp-page-tur..time.prod.jsDiff too large to display app-page.runtime.dev.jsfailed to diffapp-page.runtime.prod.jsDiff too large to display pages-api.runtime.dev.jsDiff too large to display pages-api.ru..time.prod.jsDiff too large to display pages.runtime.dev.jsDiff too large to display pages.runtime.prod.jsDiff too large to display 📎 Tarball URLCommit: e73b699 |
acdlite
marked this pull request as ready for review
August 10, 2026 18:47
samselikoff
approved these changes
Aug 10, 2026
The Instant Navigation Testing lock's per-entry ref-counting (pendingCount / trackNavigationLockPrefetchEntry) is redundant now that a prefetch task only completes after a full pass observes every segment response it registered on via blockTaskOnPendingResponse. Replace it with a single resolve (resolveNavigationLockPrefetch) fired when the locked navigation's driving task completes, and delete the track-on-reuse fork of readOrCreateSegmentCacheEntry along with the navigationLockPrefetch threading through the scheduler and cache. Also close a gap in the completion invariant this now relies on: an InlinedIntoChild entry read into a bundle chain was never blocked on if the chain was dropped before being pinged, so a task could complete while that entry's response was still in flight. Block on Pending entries at read time in accumulateSegmentBundle.
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
Repro (from #96692): enable the Nav Inspector, click a
<Link prefetch={true}>, close the inspector, navigate home, re-enable it, and click the same link. The app hung in a pending "Compiling..." state while firing prefetch requests in an infinite loop (~30/sec).The Instant Navigation Testing lock restricted navigation reads to entries created within the current lock scope (
ownedEntries), enforced as a post-hoc filter after the cache lookup. But the segment cache resolves lookups by most-specific-match, so a previous scope's runtime-prefetch entries at concrete param keypaths kept winning the lookup, the filter kept rejecting them, and the locked prefetch created its replacement at a more generic keypath that could never win — every scheduler pass discarded and refetched forever.Rather than patch the filter, this replaces the
ownedEntriesmechanism:CacheMapthat starts empty and is discarded at release, so a captured navigation structurally observes only data fetched under the lock — cross-scope shadowing becomes impossible by construction.PrefetchTask.segmentCacheMap— the single place that consults lock state), a locked navigation inherits its driving task's map, and everything else — unlocked navigations, hydration, refreshes, traversals, server actions and patches — binds to the shared map. Reads and response writes receive the map explicitly, so a request that straddles a scope boundary still writes into the map its entries live in.In production builds without the testing API this compiles down to the previous single-map behavior.
Includes the failing test from #96692 (thanks @samselikoff), hardened to use the retry-based panel-reopen helper the sibling tests use.
Verification
pnpm test-dev-turbo test/development/app-dir/instant-navs-devtools/instant-navs-devtools.test.ts(32/32, includes the new regression test)pnpm test-dev-webpack test/development/app-dir/instant-navs-devtools/instant-navs-devtools.test.ts -t "repeat clicks"pnpm test-dev-turbo test/e2e/app-dir/instant-navigation-testing-api/instant-navigation-testing-api.test.tspnpm test-start-turbo test/e2e/app-dir/instant-navigation-testing-api/instant-navigation-testing-api.test.tspnpm test-start-turbo test/e2e/app-dir/segment-cache/basic/segment-cache-basic.test.ts(production paths unregressed)