[backport] Fix Nav Inspector request loop on repeat captures (#97326)
Backports #97050 ("Fix Nav Inspector request loop on repeat captures")
to `next-16-3`.
With the Nav Inspector enabled, clicking a `<Link prefetch={true}>`,
closing the inspector, navigating home, re-enabling it, and clicking the
same link left the app hung in a pending "Compiling…" state while it
fired 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. Because the segment cache resolves
lookups by most-specific-match, 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 — so every scheduler pass
discarded and refetched forever.
Rather than patch the filter, the fix replaces the `ownedEntries`
mechanism with a per-scope segment `CacheMap`. Each lock scope owns a
private map that starts empty and is discarded at release, so a captured
navigation structurally observes only data fetched under the lock. The
map is bound to a unit of work when it is created: a prefetch task
captures its map when scheduled (`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, history-traversal restores, 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, so the non-testing paths are unchanged.
### Verification on this branch
Cherry-picked from `5dc3ae1fe7`. Three files conflicted; each was
resolved by keeping the release branch's existing code shape and
applying only the fix's actual change — threading the segment `CacheMap`
through as an explicit parameter.
- **`refresh-reducer.ts`** and **`restore-reducer.ts`** — the conflicts
were confined to the import block. Canary imports
`convertServerPatchToFullTree` from
`./segment-cache/decode-server-response`, a module that doesn't exist on
this branch; `next-16-3` still imports it from
`./segment-cache/navigation`. I kept the release branch's import source
and added the new `segmentCacheMap` import alongside it. The
function-body changes (passing `segmentCacheMap` as the shared map)
applied cleanly, and the `startPPRNavigation`/`spawnDynamicRequests`
argument order lines up with this branch's signatures.
- **`cache.ts`** — the substantive one. `next-16-3` predates the
post-16.3.0 segment-cache refactors, so three areas differ from canary
and had to be adapted rather than taken verbatim from the patch: (1)
`RouteTree` is still non-generic here (`RouteTree`, not
`RouteTree<RSCSegmentData | null>` — this branch has no `RSCSegmentData`
and the tree doesn't carry render data), so the three signatures the
patch retyped were kept as plain `RouteTree`; (2)
`writeSegmentBundleResponse` still has the older `StaticShell`/`else`
re-key split guarded by `__NEXT_VARY_PARAMS`, versus canary's single
`upsertSegmentEntry` — I kept the release-branch logic and threaded
`map` into both of its `upsertSegmentEntry` calls; (3)
`writeDynamicRenderResponseIntoCache` and `writeSeedDataIntoCache` still
use the `flightDatas` + `CacheNodeSeedData` write model rather than
canary's route-tree-carries-data model — I kept the release-branch
traversal and threaded `map` into each `writeSeedDataIntoCache` /
`fulfillEntrySpawnedByRuntimePrefetch` call. Everything else in
`cache.ts` — the `ownedEntries` removal, the `segmentCacheMap` export,
`insertEmptySegmentCacheEntry`, and the `map` parameter added to every
read/write helper — applied cleanly and is identical to canary. I
confirmed no stale references to the removed `ownedEntries` /
`readSegmentCacheEntry` / `navigationLockPrefetch`-param APIs remain,
and that every call site of the retyped helpers passes a map.
The remaining files applied without conflict. `server-patch-reducer.ts`,
`navigation-testing-lock.ts`, `navigation-testing-lock.disabled.ts`, and
the `instant-navs-devtools` test are byte-identical to canary.
`create-initial-router-state.ts`, `ppr-navigations.ts`,
`server-action-reducer.ts`, `navigation.ts`, and `scheduler.ts` differ
from canary only in pre-existing `next-16-3` divergence (the older
transport-data / route-tree models); the fix's own hunks (the
`segmentCacheMap` threading) applied to them cleanly. No local typecheck
was run — this fresh worktree has no `node_modules` and installing it
carries the known corepack/SWC-binary gotchas — so behavioral and type
verification is left to CI on this branch, which carries the
`instant-navs-devtools` regression test from #96692.
**Reviewer, focus here:** the `writeSegmentBundleResponse` resolution in
`cache.ts` (area 2 above) is the hunk I'd most like a second pair of
eyes on. I preserved this branch's `StaticShell`/`else` re-key branch
structure and only inserted the `map` argument into its two
`upsertSegmentEntry` calls; please confirm that routing both branches'
writes through the task's bound map (rather than the module-level
`segmentCacheMap`) is the correct behavior for the older re-key logic.
<!-- NEXT_JS_LLM -->
Co-authored-by: Sam Selikoff <sam.selikoff@gmail.com>