From a4a1c092625ab4fdc47698bd7add73e61f10ad7e Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 25 Feb 2026 12:55:39 -0500 Subject: [PATCH] Remove revalidation settling period from router-act MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 500ms settling period in router-act's act() polled for 500ms after the request queue emptied before exiting. This was added to accommodate the 300ms revalidation cooldown in the client router — after a server action calls revalidatePath/revalidateTag, the client delays re-prefetching for 300ms. The settling period ensured act() waited long enough to catch the resulting re-prefetch. The settling period applied to every act() call, not just ones involving revalidation, adding ~500ms per call across 28 test files. Remove it entirely and instead use Playwright's fake clock (page.clock.install / page.clock.fastForward) in the revalidation tests to deterministically control the cooldown. This is both faster and architecturally cleaner — the test utility no longer embeds knowledge of a specific client-side timer. --- .../segment-cache-revalidation.test.ts | 87 +++++++++++++------ test/lib/router-act.ts | 26 +----- 2 files changed, 62 insertions(+), 51 deletions(-) diff --git a/test/e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts b/test/e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts index 81ea5841724ef1..100a46dbd58b77 100644 --- a/test/e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts +++ b/test/e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts @@ -1,5 +1,5 @@ -import { isNextDev, isNextDeploy, createNext } from 'e2e-utils' import type * as Playwright from 'playwright' +import { isNextDev, isNextDeploy, createNext } from 'e2e-utils' import { createRouterAct } from 'router-act' import { createTestDataServer } from 'test-data-service/writer' import { createTestLog } from 'test-log' @@ -52,9 +52,11 @@ describe('segment cache (revalidation)', () => { it('evict client cache when Server Action calls revalidatePath', async () => { let act: ReturnType + let page: Playwright.Page const browser = await next.browser('/', { - beforePageLoad(page) { - act = createRouterAct(page) + beforePageLoad(p: Playwright.Page) { + page = p + act = createRouterAct(p) }, }) @@ -72,13 +74,21 @@ describe('segment cache (revalidation)', () => { } ) + // Install fake timers — freezes the 300ms cooldown setTimeout + await page.clock.install() + // Perform an action that calls revalidatePath. This should cause the // corresponding entry to be evicted from the client cache, and a new // prefetch to be requested. + await act(async () => { + const revalidateByPath = await browser.elementById('revalidate-by-path') + await revalidateByPath.click() + }) + + // Advance past cooldown inside act() to intercept the re-prefetch await act( async () => { - const revalidateByPath = await browser.elementById('revalidate-by-path') - await revalidateByPath.click() + await page.clock.fastForward(300) }, { includes: 'random-greeting [1]', @@ -103,9 +113,11 @@ describe('segment cache (revalidation)', () => { // bother to test every feature using both Link and Form; this test should // be sufficient. let act: ReturnType + let page: Playwright.Page const browser = await next.browser('/', { - beforePageLoad(page) { - act = createRouterAct(page) + beforePageLoad(p: Playwright.Page) { + page = p + act = createRouterAct(p) }, }) @@ -123,13 +135,21 @@ describe('segment cache (revalidation)', () => { } ) + // Install fake timers — freezes the 300ms cooldown setTimeout + await page.clock.install() + // Perform an action that calls revalidatePath. This should cause the // corresponding entry to be evicted from the client cache, and a new // prefetch to be requested. + await act(async () => { + const revalidateByPath = await browser.elementById('revalidate-by-path') + await revalidateByPath.click() + }) + + // Advance past cooldown inside act() to intercept the re-prefetch await act( async () => { - const revalidateByPath = await browser.elementById('revalidate-by-path') - await revalidateByPath.click() + await page.clock.fastForward(300) }, { includes: 'random-greeting [1]', @@ -156,9 +176,11 @@ describe('segment cache (revalidation)', () => { // possible to simulate the revalidating behavior of Link using the manual // prefetch API. let act: ReturnType + let page: Playwright.Page const browser = await next.browser('/', { - beforePageLoad(page) { - act = createRouterAct(page) + beforePageLoad(p: Playwright.Page) { + page = p + act = createRouterAct(p) }, }) @@ -176,13 +198,21 @@ describe('segment cache (revalidation)', () => { } ) + // Install fake timers — freezes the 300ms cooldown setTimeout + await page.clock.install() + // Perform an action that calls revalidatePath. This should cause the // corresponding entry to be evicted from the client cache, and a new // prefetch to be requested. + await act(async () => { + const revalidateByPath = await browser.elementById('revalidate-by-path') + await revalidateByPath.click() + }) + + // Advance past cooldown inside act() to intercept the re-prefetch await act( async () => { - const revalidateByPath = await browser.elementById('revalidate-by-path') - await revalidateByPath.click() + await page.clock.fastForward(300) }, { includes: 'random-greeting [1]', @@ -203,9 +233,11 @@ describe('segment cache (revalidation)', () => { it('evict client cache when Server Action calls revalidateTag', async () => { let act: ReturnType + let page: Playwright.Page const browser = await next.browser('/', { - beforePageLoad(page) { - act = createRouterAct(page) + beforePageLoad(p: Playwright.Page) { + page = p + act = createRouterAct(p) }, }) @@ -223,13 +255,21 @@ describe('segment cache (revalidation)', () => { } ) + // Install fake timers — freezes the 300ms cooldown setTimeout + await page.clock.install() + // Perform an action that calls revalidateTag. This should cause the // corresponding entry to be evicted from the client cache, and a new // prefetch to be requested. + await act(async () => { + const revalidateByTag = await browser.elementById('revalidate-by-tag') + await revalidateByTag.click() + }) + + // Advance past cooldown inside act() to intercept the re-prefetch await act( async () => { - const revalidateByTag = await browser.elementById('revalidate-by-tag') - await revalidateByTag.click() + await page.clock.fastForward(300) }, { includes: 'random-greeting [1]', @@ -354,15 +394,10 @@ describe('segment cache (revalidation)', () => { // Perform an action that calls revalidatePath. This triggers a 300ms // cooldown before any new prefetch requests can be made. - // Don't use act() here — act()'s settling loop calls - // waitForIdleCallback({ timeout: 100 }) repeatedly, which advances - // the fake clock and would cause the cooldown to fire prematurely. - const revalidateByPath = await browser.elementById('revalidate-by-path') - const actionResponse = page.waitForResponse( - (response) => response.request().headers()['next-action'] !== undefined - ) - await revalidateByPath.click() - await actionResponse + await act(async () => { + const revalidateByPath = await browser.elementById('revalidate-by-path') + await revalidateByPath.click() + }) // The cooldown timer is frozen, so no prefetch should have occurred. TestLog.assert([]) diff --git a/test/lib/router-act.ts b/test/lib/router-act.ts index 1e468afd9565ac..c2bbf0931b627b 100644 --- a/test/lib/router-act.ts +++ b/test/lib/router-act.ts @@ -341,31 +341,7 @@ export function createRouterAct( let claimedExpectations = new Set() - // Track when the queue was last empty to implement a settling period - let queueEmptyStartTime: number | null = null - const SETTLING_PERIOD_MS = 500 // Wait 500ms after queue empties - - while ( - batch.pendingRequests.size > 0 || - queueEmptyStartTime === null || - Date.now() - queueEmptyStartTime < SETTLING_PERIOD_MS - ) { - if (batch.pendingRequests.size > 0) { - // Queue has requests, reset settling timer - queueEmptyStartTime = null - } else if (queueEmptyStartTime === null) { - // Queue just became empty, start settling timer - queueEmptyStartTime = Date.now() - } - - if (batch.pendingRequests.size === 0) { - // Queue is empty during settling period, wait a bit and check again - await new Promise((resolve) => setTimeout(resolve, 50)) - await waitForIdleCallback() - await waitForPendingRequestChecks() - continue - } - + while (batch.pendingRequests.size > 0) { const pending = batch.pendingRequests batch.pendingRequests = new Set() for (const item of pending) {