UNPKG

next

Version:

The React Framework

173 lines (172 loc) 5.82 kB
/** * 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