next
Version:
The React Framework
164 lines (163 loc) • 8.64 kB
TypeScript
/**
* Navigation lock for the Instant Navigation Testing API.
*
* Manages the in-memory lock (a promise) that gates dynamic data writes
* during instant navigation captures, and owns all cookie state
* transitions (pending → captured-MPA, pending → captured-SPA).
*
* External actors (Playwright, devtools) set [0] to start a lock scope
* and delete the cookie to end one. Next.js writes captured values.
* The CookieStore handler distinguishes them by value: pending = external,
* captured = self-write (ignored).
*
* This module assumes the Instant Navigation Testing API is enabled. When it
* is disabled, the bundler resolves this module to
* `./navigation-testing-lock.disabled` instead (see
* `create-compiler-aliases.ts` for webpack and
* `crates/next-core/src/next_import_map.rs` for Turbopack), so none of this
* code ships in the browser bundle.
*/
import { type FlightRouterState } from '../../../shared/lib/app-router-types';
import { type PendingSegmentCacheEntry, type SegmentCacheEntry } from './cache';
import type { FetchStrategy } from './types';
/**
* The "wait for the locked navigation's prefetch to fulfill" state for a single
* locked navigation. `promise` resolves once that prefetch has spawned every
* request and all of them have fulfilled, so the navigation reads present data
* rather than a still-in-flight entry. Owned by the prefetch task (one per
* navigation, so successive navigations in a scope resolve independently) and
* also tracked in `NavigationLockState.activePrefetches` so the lock can
* force-resolve any that are still pending when it's released.
*
* `pendingCount` holds one reference for the scheduler while it is still
* spawning, plus one per in-flight entry; `promise` resolves when it drains to
* 0. `trackedEntries` dedupes entry registration.
*/
export type NavigationLockPrefetch = {
promise: Promise<void>;
resolve: () => void;
pendingCount: number;
trackedEntries: Set<PendingSegmentCacheEntry>;
};
export type NavigationLockState = {
released: Promise<void>;
resolveReleased: () => void;
fetch: typeof fetch;
activePrefetches: Set<NavigationLockPrefetch>;
ownedEntries: Set<SegmentCacheEntry>;
currentNavigation: Promise<void>;
resolveCurrentNavigation: () => void;
};
export declare function getPreLockFetch(): typeof fetch | null;
/**
* Creates the "wait for prefetch to fulfill" state for one locked navigation,
* registers it on the current lock, and returns it (the caller stores it on the
* prefetch task and awaits `.promise`). Returns null if no lock is held.
*
* `pendingCount` starts at 1, representing the scheduler itself while it is
* still spawning requests; that reference is released by
* `finishNavigationLockPrefetchSpawning`. Each spawned pending entry adds
* another (see `trackNavigationLockPrefetchEntry`). `promise` resolves when the
* count drains to 0 — i.e. spawning finished and every entry fulfilled.
*/
export declare function beginNavigationLockPrefetch(): NavigationLockPrefetch | null;
/**
* Records a freshly-created segment entry as owned by the current lock scope, so
* navigation reads will match it — and only entries created within the scope
* (see `NavigationLockState.ownedEntries`). Called from
* `createDetachedSegmentCacheEntry`, the single factory every creation path
* funnels through, so re-keyed entries created during response processing (e.g.
* a runtime prefetch resolving a concrete param) are owned too. No-op when no
* lock is held.
*/
export declare function recordNavigationLockOwnedEntry(entry: SegmentCacheEntry): void;
/**
* Called by `upgradeToPendingSegment` whenever the locked-navigation prefetch
* spawns a pending segment entry. Adds the entry to the prefetch's ref count and
* decrements when it fulfills (or rejects — `waitForSegmentCacheEntry` resolves
* to null). Deduped so the same entry never double-counts.
*/
export declare function trackNavigationLockPrefetchEntry(prefetch: NavigationLockPrefetch, entry: PendingSegmentCacheEntry): void;
/**
* Called once the scheduler has finished spawning every request for the
* locked-navigation prefetch, releasing the scheduler's reference from the ref
* count. The prefetch resolves here if every spawned entry already fulfilled.
*/
export declare function finishNavigationLockPrefetchSpawning(prefetch: NavigationLockPrefetch): void;
/**
* Called when a new locked navigation begins (from `navigate` while the lock is
* held). Rolls over the lock's withheld-data gate: it resolves the current
* `currentNavigation` promise — so the *previous* locked navigation's withheld
* dynamic write proceeds and the cache nodes it produced stop holding pending
* deferred `rsc` promises that a reused shared segment in this navigation would
* otherwise suspend on — then installs a fresh promise for this navigation.
* Only the most recent navigation's data stays withheld; a new navigation
* always releases the previous one. Returns this navigation's gate — the
* immutable promise its dynamic write awaits — or null when no lock is held.
*
* This is the testing-lock behavior for repeated navigations while paused. It
* is not a principled fix for the underlying `useDeferredValue`/reuse-suspend
* behavior; it just ensures that, under the lock, a reused segment never
* carries a still-pending deferred `rsc` from an earlier navigation.
*/
export declare function beginLockedNavigation(): Promise<void> | null;
/**
* Called when the router applies a history traversal (Back/Forward restore) while
* the testing lock is active. A traversal is not a capture — the mental model is
* that history entries are already cached — so it must not participate in the
* current capture. Instead it resets the lock to a fresh pending scope:
*
* - `releaseLock` flushes every still-withheld write from prior forward
* navigations, so the pages you navigated away from finish streaming.
* - `acquireLock` immediately re-arms a fresh pending scope (no gap where the
* lock or fetch blocker is down).
* - the cookie flips from the captured state back to pending.
*
* The traversal's own dynamic requests are spawned ungated by the caller (see
* `restore-reducer`), so they render from cache or fetch normally rather than
* being withheld.
*/
export declare function resetNavigationLockToPending(): void;
/**
* Sets up the cookie-based lock. Handles the initial page load state and
* registers a CookieStore listener for runtime changes.
*
* Called once during page initialization from app-globals.ts.
*/
export declare function startListeningForInstantNavigationCookie(): void;
/**
* Transitions the cookie from pending to captured-SPA once the prefetch resolves
* and the navigation is known to be an SPA.
*/
export declare function updateCapturedSPAToTree(fromTree: FlightRouterState, toTree: FlightRouterState): void;
/**
* Returns true if the navigation lock is currently active.
*/
export declare function isNavigationLocked(): boolean;
export declare function getCurrentNavigationLock(): NavigationLockState | null;
/**
* Returns the current locked navigation's withheld-data gate — the same
* immutable promise `beginLockedNavigation` handed that navigation — or null
* when no lock is held. For router work that spawns a dynamic write without
* beginning a navigation of its own (refreshes, server actions, server
* patches): it gates behind the navigation that is current when it spawns, so
* the next locked navigation (or unlock) releases it.
*/
export declare function getCurrentNavigationGate(): Promise<void> | null;
/**
* Decides whether segment reads during a navigation should be restricted to
* shell entries (every param substituted with Fallback) rather than matching
* entries that vary on concrete route params.
*
* The testing tools (Navigation Inspector, instant()) simulate what a user
* would see with a warm cache. When the lock is held, partial prefetching is
* enabled for the target route, and no whole-route ("speculative") prefetch
* would have been made, only the shell is prefetched — so that's all a
* navigation should be allowed to match. A speculative prefetch happens for a
* `<Link prefetch={true}>` or an eagerly-prefetched subtree, in which case the
* concrete-param entry is genuinely warm and may be matched.
*
* Always returns false outside the testing API, via the aliased
* `navigation-testing-lock.disabled` module.
*/
export declare function shouldRestrictNavigationToShell(rootPrefetchHints: number, linkFetchStrategy: FetchStrategy): boolean;