next
Version:
The React Framework
173 lines (172 loc) • 5.82 kB
JavaScript
/**
* 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).
*/ import { NEXT_INSTANT_TEST_COOKIE } from '../app-router-headers';
import { refreshOnInstantNavigationUnlock } from '../use-action-queue';
function parseCookieValue(raw) {
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed) && parsed.length >= 3) {
const rawState = parsed[2];
return rawState === null ? 'mpa' : 'spa';
}
} catch {}
return 'pending';
}
function writeCookieValue(value) {
if (typeof cookieStore === 'undefined') {
return;
}
// Read the existing cookie to preserve its attributes (domain, path),
// then write back with the new value. This updates the same cookie
// entry that the external actor created, regardless of how it was
// scoped.
cookieStore.get(NEXT_INSTANT_TEST_COOKIE).then((existing)=>{
if (existing) {
const options = {
name: NEXT_INSTANT_TEST_COOKIE,
value: JSON.stringify(value),
path: existing.path ?? '/'
};
if (existing.domain) {
options.domain = existing.domain;
}
cookieStore.set(options);
}
});
}
let lockState = null;
function acquireLock() {
if (lockState !== null) {
return;
}
let resolve;
const promise = new Promise((r)=>{
resolve = r;
});
lockState = {
promise,
resolve: resolve
};
}
function releaseLock() {
if (lockState !== null) {
lockState.resolve();
lockState = null;
}
}
/**
* 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 function startListeningForInstantNavigationCookie() {
if (process.env.__NEXT_EXPOSE_TESTING_API) {
// If the server served a static shell, this is an MPA page load
// while the lock is held. Transition to captured-MPA and acquire.
if (self.__next_instant_test) {
if (typeof cookieStore !== 'undefined') {
// If the cookie was already cleared during the MPA page
// transition, reload to get the full dynamic page.
cookieStore.get(NEXT_INSTANT_TEST_COOKIE).then((cookie)=>{
if (!cookie) {
window.location.reload();
}
});
}
writeCookieValue([
1,
`c${Math.random()}`,
null
]);
acquireLock();
}
if (typeof cookieStore === 'undefined') {
return;
}
cookieStore.addEventListener('change', (event)=>{
for (const cookie of event.changed){
if (cookie.name === NEXT_INSTANT_TEST_COOKIE) {
const state = parseCookieValue(cookie.value ?? '');
if (state !== 'pending') {
// Captured value — our own transition. Ignore.
return;
}
// Pending value — external actor starting a new lock scope.
if (lockState !== null) {
releaseLock();
}
acquireLock();
return;
}
}
for (const cookie of event.deleted){
if (cookie.name === NEXT_INSTANT_TEST_COOKIE) {
releaseLock();
refreshOnInstantNavigationUnlock();
return;
}
}
});
}
}
/**
* Transitions the cookie from pending to captured-SPA. Called when a
* client-side navigation is captured by the lock.
*
* @param fromTree - The flight router state of the from-route
* @param toTree - The flight router state of the to-route (null if not yet known)
*/ export function transitionToCapturedSPA(fromTree, toTree) {
if (process.env.__NEXT_EXPOSE_TESTING_API) {
writeCookieValue([
1,
`c${Math.random()}`,
{
from: fromTree,
to: toTree
}
]);
}
}
/**
* Updates the captured-SPA cookie with the resolved route trees.
* Called after the prefetch resolves and the target route tree is known.
*/ export function updateCapturedSPAToTree(fromTree, toTree) {
if (process.env.__NEXT_EXPOSE_TESTING_API) {
writeCookieValue([
1,
`c${Math.random()}`,
{
from: fromTree,
to: toTree
}
]);
}
}
/**
* Returns true if the navigation lock is currently active.
*/ export function isNavigationLocked() {
if (process.env.__NEXT_EXPOSE_TESTING_API) {
return lockState !== null;
}
return false;
}
/**
* Waits for the navigation lock to be released, if it's currently held.
* No-op if the lock is not acquired.
*/ export async function waitForNavigationLockIfActive() {
if (process.env.__NEXT_EXPOSE_TESTING_API) {
if (lockState !== null) {
await lockState.promise;
}
}
}
//# sourceMappingURL=navigation-testing-lock.js.map