From 5d280abc230b081fb6b7e00ee5cb736368a468c5 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Thu, 28 Mar 2024 13:17:30 -0700 Subject: [PATCH] switch to a global time configuration --- .../webpack/plugins/define-env-plugin.ts | 12 +- .../router-reducer/prefetch-cache-utils.ts | 25 +- .../router-reducer/router-reducer-types.ts | 8 - packages/next/src/server/config-schema.ts | 10 +- packages/next/src/server/config-shared.ts | 20 +- .../client-cache.experimental.test.ts | 98 ++- .../app-client-cache/client-cache.test.ts | 744 +++++++++--------- 7 files changed, 500 insertions(+), 417 deletions(-) diff --git a/packages/next/src/build/webpack/plugins/define-env-plugin.ts b/packages/next/src/build/webpack/plugins/define-env-plugin.ts index 2a3e7fb1f14865..aea7e34537f2db 100644 --- a/packages/next/src/build/webpack/plugins/define-env-plugin.ts +++ b/packages/next/src/build/webpack/plugins/define-env-plugin.ts @@ -171,8 +171,16 @@ export function getDefineEnv({ 'process.env.__NEXT_MIDDLEWARE_MATCHERS': middlewareMatchers ?? [], 'process.env.__NEXT_MANUAL_CLIENT_BASE_PATH': config.experimental.manualClientBasePath ?? false, - 'process.env.__NEXT_CLIENT_ROUTER_CACHE_MODE': - config.experimental.clientRouterCacheMode ?? 'default', + 'process.env.__NEXT_CLIENT_ROUTER_DYNAMIC_STALETIME': JSON.stringify( + isNaN(Number(config.experimental.staleTimes?.dynamic)) + ? 30 // 30 seconds + : config.experimental.staleTimes?.dynamic + ), + 'process.env.__NEXT_CLIENT_ROUTER_STATIC_STALETIME': JSON.stringify( + isNaN(Number(config.experimental.staleTimes?.static)) + ? 5 * 60 // 5 minutes + : config.experimental.staleTimes?.static + ), 'process.env.__NEXT_CLIENT_ROUTER_FILTER_ENABLED': config.experimental.clientRouterFilter ?? true, 'process.env.__NEXT_CLIENT_ROUTER_S_FILTER': diff --git a/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts b/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts index d9c39d70d766b5..f206e1bdd4cffb 100644 --- a/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts +++ b/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts @@ -8,7 +8,6 @@ import { type PrefetchCacheEntry, PrefetchKind, type ReadonlyReducerState, - PREFETCH_CACHE_MODE, } from './router-reducer-types' import { prefetchQueue } from './reducers/prefetch-reducer' @@ -244,24 +243,20 @@ export function prunePrefetchCache( } } -const FIVE_MINUTES = 5 * 60 * 1000 -const THIRTY_SECONDS = 30 * 1000 +// These values are set by `define-env-plugin` and default to 5 minutes (static) / 30 seconds (dynamic) +const DYNAMIC_STALETIME_MS = + Number(process.env.__NEXT_CLIENT_ROUTER_DYNAMIC_STALETIME) * 1000 + +const STATIC_STALETIME_MS = + Number(process.env.__NEXT_CLIENT_ROUTER_STATIC_STALETIME) * 1000 function getPrefetchEntryCacheStatus({ kind, prefetchTime, lastUsedTime, }: PrefetchCacheEntry): PrefetchCacheEntryStatus { - if (kind !== PrefetchKind.FULL && PREFETCH_CACHE_MODE === 'live') { - // When the cache mode is set to "live", we only want to re-use the loading state. We mark the entry as stale - // regardless of the lastUsedTime so that the router will not attempt to apply the cache node data and will instead only - // re-use the loading state while lazy fetching the page data. - // We don't do this for a full prefetch, as if there's explicit caching intent it should respect existing heuristics. - return PrefetchCacheEntryStatus.stale - } - // if the cache entry was prefetched or read less than the specified staletime window, then we want to re-use it - if (Date.now() < (lastUsedTime ?? prefetchTime) + THIRTY_SECONDS) { + if (Date.now() < (lastUsedTime ?? prefetchTime) + DYNAMIC_STALETIME_MS) { return lastUsedTime ? PrefetchCacheEntryStatus.reusable : PrefetchCacheEntryStatus.fresh @@ -269,14 +264,14 @@ function getPrefetchEntryCacheStatus({ // if the cache entry was prefetched less than 5 mins ago, then we want to re-use only the loading state if (kind === 'auto') { - if (Date.now() < prefetchTime + FIVE_MINUTES) { + if (Date.now() < prefetchTime + STATIC_STALETIME_MS) { return PrefetchCacheEntryStatus.stale } } - // if the cache entry was prefetched less than 5 mins ago and was a "full" prefetch, then we want to re-use it "full + // if the cache entry was prefetched less than 5 mins ago and was a "full" prefetch, then we want to re-use it if (kind === 'full') { - if (Date.now() < prefetchTime + FIVE_MINUTES) { + if (Date.now() < prefetchTime + STATIC_STALETIME_MS) { return PrefetchCacheEntryStatus.reusable } } diff --git a/packages/next/src/client/components/router-reducer/router-reducer-types.ts b/packages/next/src/client/components/router-reducer/router-reducer-types.ts index 1c43bfb9b21b26..3576281b3d32ec 100644 --- a/packages/next/src/client/components/router-reducer/router-reducer-types.ts +++ b/packages/next/src/client/components/router-reducer/router-reducer-types.ts @@ -282,11 +282,3 @@ export function isThenable(value: any): value is Promise { typeof value.then === 'function' ) } - -/** - * A `live` value will indicate that the client router should always fetch the latest data from the server when - * navigating to a new route when auto prefetching is used. A `default` value will use existing - * cache heuristics (router cache will persist for 30s before being invalidated). Defaults to `default`. - */ -export const PREFETCH_CACHE_MODE = - process.env.__NEXT_CLIENT_ROUTER_CACHE_MODE === 'live' ? 'live' : 'default' diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index d19d732197a2dd..22179d9dca80fd 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -247,10 +247,12 @@ export const configSchema: zod.ZodType = z.lazy(() => validator: z.string().optional(), }) .optional(), - clientRouterCacheMode: z.union([ - z.literal('default'), - z.literal('live'), - ]), + staleTimes: z + .object({ + dynamic: z.number().optional(), + static: z.number().optional(), + }) + .optional(), clientRouterFilter: z.boolean().optional(), clientRouterFilterRedirects: z.boolean().optional(), clientRouterFilterAllowedRate: z.number().optional(), diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 5afaf4591f18a2..7ae8af4569bba4 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -188,12 +188,15 @@ export interface ExperimentalConfig { clientRouterFilter?: boolean clientRouterFilterRedirects?: boolean /** - * This value can be used to override the cache behavior for the client router. A `live` value - * will indicate that the client router should always fetch the latest data from the server when - * navigating to a new route when auto prefetching is used. A `default` value will use existing - * cache heuristics (router cache will persist for 30s before being invalidated). Defaults to `default`. - */ - clientRouterCacheMode?: 'live' | 'default' + * This config can be used to override the cache behavior for the client router. + * These values indicate the time, in seconds, that the cache should be considered + * reusable. When the `prefetch` Link prop is left unspecified, this will use the `dynamic` value. + * When the `prefetch` Link prop is set to `true`, this will use the `static` value. + */ + staleTimes?: { + dynamic?: number + static?: number + } // decimal for percent for possible false positives // e.g. 0.01 for 10% potential false matches lower // percent increases size of the filter @@ -933,7 +936,10 @@ export const defaultConfig: NextConfig = { missingSuspenseWithCSRBailout: true, optimizeServerReact: true, useEarlyImport: false, - clientRouterCacheMode: 'default', + staleTimes: { + dynamic: 30, + static: 300, + }, }, } diff --git a/test/e2e/app-dir/app-client-cache/client-cache.experimental.test.ts b/test/e2e/app-dir/app-client-cache/client-cache.experimental.test.ts index 5b0f4b0bcbbf5f..6b65beeeb91f01 100644 --- a/test/e2e/app-dir/app-client-cache/client-cache.experimental.test.ts +++ b/test/e2e/app-dir/app-client-cache/client-cache.experimental.test.ts @@ -1,13 +1,12 @@ import { nextTestSetup } from 'e2e-utils' import { browserConfigWithFixedTime, fastForwardTo } from './test-utils' -import { runTests } from './client-cache.test' -describe('app dir client cache semantics (experimental clientRouterCache)', () => { - describe('clientRouterCache = live', () => { +describe('app dir client cache semantics (experimental staleTimes)', () => { + describe('dynamic: 0', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, nextConfig: { - experimental: { clientRouterCacheMode: 'live' }, + experimental: { staleTimes: { dynamic: 0 } }, }, }) @@ -19,7 +18,7 @@ describe('app dir client cache semantics (experimental clientRouterCache)', () = } describe('prefetch={true}', () => { - it('should re-use the cache for 5 minutes', async () => { + it('should re-use the cache for 5 minutes (default "static" time)', async () => { const browser = await next.browser('/', browserConfigWithFixedTime) let initialRandomNumber = await browser @@ -170,11 +169,11 @@ describe('app dir client cache semantics (experimental clientRouterCache)', () = }) }) - describe('clientRouterCache = default', () => { + describe('static: 180', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, nextConfig: { - experimental: { clientRouterCacheMode: 'default' }, + experimental: { staleTimes: { static: 180 } }, }, }) @@ -182,8 +181,89 @@ describe('app dir client cache semantics (experimental clientRouterCache)', () = // since the router behavior is different in development mode (no viewport prefetching + liberal revalidation) // we only check the production behavior it('should skip dev', () => {}) - } else { - runTests(next) + return } + + describe('prefetch={true}', () => { + it('should use the custom static override time (3 minutes)', async () => { + const browser = await next.browser('/', browserConfigWithFixedTime) + + let initialRandomNumber = await browser + .elementByCss('[href="/0?timeout=0"]') + .click() + .waitForElementByCss('#random-number') + .text() + + await browser.elementByCss('[href="/"]').click() + + let newRandomNumber = await browser + .elementByCss('[href="/0?timeout=0"]') + .click() + .waitForElementByCss('#random-number') + .text() + + expect(initialRandomNumber).toBe(newRandomNumber) + + await browser.eval(fastForwardTo, 30 * 1000) // fast forward 30 seconds + + await browser.elementByCss('[href="/"]').click() + + newRandomNumber = await browser + .elementByCss('[href="/0?timeout=0"]') + .click() + .waitForElementByCss('#random-number') + .text() + + expect(initialRandomNumber).toBe(newRandomNumber) + + await browser.eval(fastForwardTo, 3 * 60 * 1000) // fast forward 3 minutes + + await browser.elementByCss('[href="/"]').click() + + newRandomNumber = await browser + .elementByCss('[href="/0?timeout=0"]') + .click() + .waitForElementByCss('#random-number') + .text() + + expect(initialRandomNumber).not.toBe(newRandomNumber) + }) + }) + + describe('prefetch={undefined} - default', () => { + it('should re-use the loading boundary for the custom static override time (3 minutes)', async () => { + const browser = await next.browser('/', browserConfigWithFixedTime) + + const loadingRandomNumber = await browser + .elementByCss('[href="/1?timeout=1000"]') + .click() + .waitForElementByCss('#loading') + .text() + + await browser.elementByCss('[href="/"]').click() + + await browser.eval(fastForwardTo, 2 * 60 * 1000) // fast forward 2 minutes + + let newLoadingNumber = await browser + .elementByCss('[href="/1?timeout=1000"]') + .click() + .waitForElementByCss('#loading') + .text() + + expect(loadingRandomNumber).toBe(newLoadingNumber) + + await browser.elementByCss('[href="/"]').click() + + await browser.eval(fastForwardTo, 2 * 60 * 1000) // fast forward 2 minutes + + newLoadingNumber = await browser + .elementByCss('[href="/1?timeout=1000"]') + .click() + .waitForElementByCss('#loading') + .text() + + expect(loadingRandomNumber).not.toBe(newLoadingNumber) + }) + }) }) }) diff --git a/test/e2e/app-dir/app-client-cache/client-cache.test.ts b/test/e2e/app-dir/app-client-cache/client-cache.test.ts index 1d29bb06408ec3..67a576aa8d237c 100644 --- a/test/e2e/app-dir/app-client-cache/client-cache.test.ts +++ b/test/e2e/app-dir/app-client-cache/client-cache.test.ts @@ -1,4 +1,4 @@ -import { NextInstance, createNextDescribe } from 'e2e-utils' +import { createNextDescribe } from 'e2e-utils' import { check } from 'next-test-utils' import { BrowserInterface } from 'test/lib/browsers/base' import { @@ -8,406 +8,406 @@ import { getPathname, } from './test-utils' -export function runTests(next: NextInstance) { - describe('prefetch={true}', () => { - let browser: BrowserInterface - - beforeEach(async () => { - browser = (await next.browser( - '/', - browserConfigWithFixedTime - )) as BrowserInterface - }) - - it('should prefetch the full page', async () => { - const { getRequests, clearRequests } = await createRequestsListener( - browser - ) - await check(() => { - return getRequests().some( - ([url, didPartialPrefetch]) => - getPathname(url) === '/0' && !didPartialPrefetch - ) - ? 'success' - : 'fail' - }, 'success') - - clearRequests() - - await browser - .elementByCss('[href="/0?timeout=0"]') - .click() - .waitForElementByCss('#random-number') - - expect(getRequests().every(([url]) => getPathname(url) !== '/0')).toEqual( - true - ) - }) - it('should re-use the cache for the full page, only for 5 mins', async () => { - const randomNumber = await browser - .elementByCss('[href="/0?timeout=0"]') - .click() - .waitForElementByCss('#random-number') - .text() - - await browser.elementByCss('[href="/"]').click() - - const number = await browser - .elementByCss('[href="/0?timeout=0"]') - .click() - .waitForElementByCss('#random-number') - .text() - - expect(number).toBe(randomNumber) - - await browser.eval(fastForwardTo, 5 * 60 * 1000) - - await browser.elementByCss('[href="/"]').click() - - const newNumber = await browser - .elementByCss('[href="/0?timeout=0"]') - .click() - .waitForElementByCss('#random-number') - .text() - - expect(newNumber).not.toBe(randomNumber) - }) - - it('should prefetch again after 5 mins if the link is visible again', async () => { - const { getRequests, clearRequests } = await createRequestsListener( - browser - ) - - await check(() => { - return getRequests().some( - ([url, didPartialPrefetch]) => - getPathname(url) === '/0' && !didPartialPrefetch - ) - ? 'success' - : 'fail' - }, 'success') - - const randomNumber = await browser - .elementByCss('[href="/0?timeout=0"]') - .click() - .waitForElementByCss('#random-number') - .text() - - await browser.eval(fastForwardTo, 5 * 60 * 1000) - clearRequests() - - await browser.elementByCss('[href="/"]').click() - - await check(() => { - return getRequests().some( - ([url, didPartialPrefetch]) => - getPathname(url) === '/0' && !didPartialPrefetch - ) - ? 'success' - : 'fail' - }, 'success') - - const number = await browser - .elementByCss('[href="/0?timeout=0"]') - .click() - .waitForElementByCss('#random-number') - .text() - - expect(number).not.toBe(randomNumber) - }) - }) - describe('prefetch={false}', () => { - let browser: BrowserInterface - - beforeEach(async () => { - browser = (await next.browser( - '/', - browserConfigWithFixedTime - )) as BrowserInterface - }) - it('should not prefetch the page at all', async () => { - const { getRequests } = await createRequestsListener(browser) - - await browser - .elementByCss('[href="/2"]') - .click() - .waitForElementByCss('#random-number') - - expect( - getRequests().filter(([url]) => getPathname(url) === '/2') - ).toHaveLength(1) - - expect( - getRequests().some( - ([url, didPartialPrefetch]) => - getPathname(url) === '/2' && didPartialPrefetch - ) - ).toBe(false) - }) - it('should re-use the cache only for 30 seconds', async () => { - const randomNumber = await browser - .elementByCss('[href="/2"]') - .click() - .waitForElementByCss('#random-number') - .text() - - await browser.elementByCss('[href="/"]').click() - - const number = await browser - .elementByCss('[href="/2"]') - .click() - .waitForElementByCss('#random-number') - .text() - - expect(number).toBe(randomNumber) - - await browser.eval(fastForwardTo, 30 * 1000) - - await browser.elementByCss('[href="/"]').click() - - const newNumber = await browser - .elementByCss('[href="/2"]') - .click() - .waitForElementByCss('#random-number') - .text() - - expect(newNumber).not.toBe(randomNumber) - }) - }) - describe('prefetch={undefined} - default', () => { - let browser: BrowserInterface - - beforeEach(async () => { - browser = (await next.browser( - '/', - browserConfigWithFixedTime - )) as BrowserInterface - }) - - it('should prefetch partially a dynamic page', async () => { - const { getRequests, clearRequests } = await createRequestsListener( - browser - ) - - await check(() => { - return getRequests().some( - ([url, didPartialPrefetch]) => - getPathname(url) === '/1' && didPartialPrefetch - ) - ? 'success' - : 'fail' - }, 'success') - - clearRequests() - - await browser - .elementByCss('[href="/1"]') - .click() - .waitForElementByCss('#random-number') - - expect( - getRequests().some( - ([url, didPartialPrefetch]) => - getPathname(url) === '/1' && !didPartialPrefetch - ) - ).toBe(true) - }) - it('should re-use the full cache for only 30 seconds', async () => { - const randomNumber = await browser - .elementByCss('[href="/1"]') - .click() - .waitForElementByCss('#random-number') - .text() - - await browser.elementByCss('[href="/"]').click() - - const number = await browser - .elementByCss('[href="/1"]') - .click() - .waitForElementByCss('#random-number') - .text() - - expect(number).toBe(randomNumber) - - await browser.eval(fastForwardTo, 5 * 1000) - - await browser.elementByCss('[href="/"]').click() - - const newNumber = await browser - .elementByCss('[href="/1"]') - .click() - .waitForElementByCss('#random-number') - .text() - - expect(newNumber).toBe(randomNumber) - - await browser.eval(fastForwardTo, 30 * 1000) - - await browser.elementByCss('[href="/"]').click() - - const newNumber2 = await browser - .elementByCss('[href="/1"]') - .click() - .waitForElementByCss('#random-number') - .text() - - expect(newNumber2).not.toBe(newNumber) - }) - - it('should renew the 30s cache once the data is revalidated', async () => { - // navigate to prefetch-auto page - await browser.elementByCss('[href="/1"]').click() - - let initialNumber = await browser.elementById('random-number').text() - - // Navigate back to the index, and then back to the prefetch-auto page - await browser.elementByCss('[href="/"]').click() - await browser.eval(fastForwardTo, 5 * 1000) - await browser.elementByCss('[href="/1"]').click() - - let newNumber = await browser.elementById('random-number').text() - - // the number should be the same, as we navigated within 30s. - expect(newNumber).toBe(initialNumber) - - // Fast forward to expire the cache - await browser.eval(fastForwardTo, 30 * 1000) - - // Navigate back to the index, and then back to the prefetch-auto page - await browser.elementByCss('[href="/"]').click() - await browser.elementByCss('[href="/1"]').click() +createNextDescribe( + 'app dir client cache semantics', + { + files: __dirname, + }, + ({ next, isNextDev }) => { + if (isNextDev) { + // since the router behavior is different in development mode (no viewport prefetching + liberal revalidation) + // we only check the production behavior + it('should skip dev', () => {}) + } else { + describe('prefetch={true}', () => { + let browser: BrowserInterface + + beforeEach(async () => { + browser = (await next.browser( + '/', + browserConfigWithFixedTime + )) as BrowserInterface + }) + + it('should prefetch the full page', async () => { + const { getRequests, clearRequests } = await createRequestsListener( + browser + ) + await check(() => { + return getRequests().some( + ([url, didPartialPrefetch]) => + getPathname(url) === '/0' && !didPartialPrefetch + ) + ? 'success' + : 'fail' + }, 'success') + + clearRequests() + + await browser + .elementByCss('[href="/0?timeout=0"]') + .click() + .waitForElementByCss('#random-number') + + expect( + getRequests().every(([url]) => getPathname(url) !== '/0') + ).toEqual(true) + }) + it('should re-use the cache for the full page, only for 5 mins', async () => { + const randomNumber = await browser + .elementByCss('[href="/0?timeout=0"]') + .click() + .waitForElementByCss('#random-number') + .text() + + await browser.elementByCss('[href="/"]').click() + + const number = await browser + .elementByCss('[href="/0?timeout=0"]') + .click() + .waitForElementByCss('#random-number') + .text() + + expect(number).toBe(randomNumber) + + await browser.eval(fastForwardTo, 5 * 60 * 1000) + + await browser.elementByCss('[href="/"]').click() + + const newNumber = await browser + .elementByCss('[href="/0?timeout=0"]') + .click() + .waitForElementByCss('#random-number') + .text() + + expect(newNumber).not.toBe(randomNumber) + }) + + it('should prefetch again after 5 mins if the link is visible again', async () => { + const { getRequests, clearRequests } = await createRequestsListener( + browser + ) + + await check(() => { + return getRequests().some( + ([url, didPartialPrefetch]) => + getPathname(url) === '/0' && !didPartialPrefetch + ) + ? 'success' + : 'fail' + }, 'success') + + const randomNumber = await browser + .elementByCss('[href="/0?timeout=0"]') + .click() + .waitForElementByCss('#random-number') + .text() + + await browser.eval(fastForwardTo, 5 * 60 * 1000) + clearRequests() + + await browser.elementByCss('[href="/"]').click() + + await check(() => { + return getRequests().some( + ([url, didPartialPrefetch]) => + getPathname(url) === '/0' && !didPartialPrefetch + ) + ? 'success' + : 'fail' + }, 'success') + + const number = await browser + .elementByCss('[href="/0?timeout=0"]') + .click() + .waitForElementByCss('#random-number') + .text() + + expect(number).not.toBe(randomNumber) + }) + }) + describe('prefetch={false}', () => { + let browser: BrowserInterface + + beforeEach(async () => { + browser = (await next.browser( + '/', + browserConfigWithFixedTime + )) as BrowserInterface + }) + it('should not prefetch the page at all', async () => { + const { getRequests } = await createRequestsListener(browser) + + await browser + .elementByCss('[href="/2"]') + .click() + .waitForElementByCss('#random-number') + + expect( + getRequests().filter(([url]) => getPathname(url) === '/2') + ).toHaveLength(1) + + expect( + getRequests().some( + ([url, didPartialPrefetch]) => + getPathname(url) === '/2' && didPartialPrefetch + ) + ).toBe(false) + }) + it('should re-use the cache only for 30 seconds', async () => { + const randomNumber = await browser + .elementByCss('[href="/2"]') + .click() + .waitForElementByCss('#random-number') + .text() + + await browser.elementByCss('[href="/"]').click() + + const number = await browser + .elementByCss('[href="/2"]') + .click() + .waitForElementByCss('#random-number') + .text() + + expect(number).toBe(randomNumber) + + await browser.eval(fastForwardTo, 30 * 1000) + + await browser.elementByCss('[href="/"]').click() + + const newNumber = await browser + .elementByCss('[href="/2"]') + .click() + .waitForElementByCss('#random-number') + .text() + + expect(newNumber).not.toBe(randomNumber) + }) + }) + describe('prefetch={undefined} - default', () => { + let browser: BrowserInterface + + beforeEach(async () => { + browser = (await next.browser( + '/', + browserConfigWithFixedTime + )) as BrowserInterface + }) + + it('should prefetch partially a dynamic page', async () => { + const { getRequests, clearRequests } = await createRequestsListener( + browser + ) + + await check(() => { + return getRequests().some( + ([url, didPartialPrefetch]) => + getPathname(url) === '/1' && didPartialPrefetch + ) + ? 'success' + : 'fail' + }, 'success') + + clearRequests() + + await browser + .elementByCss('[href="/1"]') + .click() + .waitForElementByCss('#random-number') + + expect( + getRequests().some( + ([url, didPartialPrefetch]) => + getPathname(url) === '/1' && !didPartialPrefetch + ) + ).toBe(true) + }) + it('should re-use the full cache for only 30 seconds', async () => { + const randomNumber = await browser + .elementByCss('[href="/1"]') + .click() + .waitForElementByCss('#random-number') + .text() + + await browser.elementByCss('[href="/"]').click() + + const number = await browser + .elementByCss('[href="/1"]') + .click() + .waitForElementByCss('#random-number') + .text() + + expect(number).toBe(randomNumber) + + await browser.eval(fastForwardTo, 5 * 1000) + + await browser.elementByCss('[href="/"]').click() + + const newNumber = await browser + .elementByCss('[href="/1"]') + .click() + .waitForElementByCss('#random-number') + .text() + + expect(newNumber).toBe(randomNumber) + + await browser.eval(fastForwardTo, 30 * 1000) + + await browser.elementByCss('[href="/"]').click() - newNumber = await browser.elementById('random-number').text() - - // ~35s have passed, so the cache should be expired and the number should be different - expect(newNumber).not.toBe(initialNumber) + const newNumber2 = await browser + .elementByCss('[href="/1"]') + .click() + .waitForElementByCss('#random-number') + .text() - // once the number is updated, we should have a renewed 30s cache for this entry - // store this new number so we can check that it stays the same - initialNumber = newNumber + expect(newNumber2).not.toBe(newNumber) + }) + + it('should renew the 30s cache once the data is revalidated', async () => { + // navigate to prefetch-auto page + await browser.elementByCss('[href="/1"]').click() - await browser.eval(fastForwardTo, 5 * 1000) + let initialNumber = await browser.elementById('random-number').text() - // Navigate back to the index, and then back to the prefetch-auto page - await browser.elementByCss('[href="/"]').click() - await browser.elementByCss('[href="/1"]').click() + // Navigate back to the index, and then back to the prefetch-auto page + await browser.elementByCss('[href="/"]').click() + await browser.eval(fastForwardTo, 5 * 1000) + await browser.elementByCss('[href="/1"]').click() - newNumber = await browser.elementById('random-number').text() + let newNumber = await browser.elementById('random-number').text() + + // the number should be the same, as we navigated within 30s. + expect(newNumber).toBe(initialNumber) - // the number should be the same, as we navigated within 30s (part 2). - expect(newNumber).toBe(initialNumber) - }) + // Fast forward to expire the cache + await browser.eval(fastForwardTo, 30 * 1000) - it('should refetch below the fold after 30 seconds', async () => { - const randomLoadingNumber = await browser - .elementByCss('[href="/1?timeout=1000"]') - .click() - .waitForElementByCss('#loading') - .text() + // Navigate back to the index, and then back to the prefetch-auto page + await browser.elementByCss('[href="/"]').click() + await browser.elementByCss('[href="/1"]').click() - const randomNumber = await browser - .waitForElementByCss('#random-number') - .text() + newNumber = await browser.elementById('random-number').text() + + // ~35s have passed, so the cache should be expired and the number should be different + expect(newNumber).not.toBe(initialNumber) + + // once the number is updated, we should have a renewed 30s cache for this entry + // store this new number so we can check that it stays the same + initialNumber = newNumber + + await browser.eval(fastForwardTo, 5 * 1000) - await browser.elementByCss('[href="/"]').click() + // Navigate back to the index, and then back to the prefetch-auto page + await browser.elementByCss('[href="/"]').click() + await browser.elementByCss('[href="/1"]').click() + + newNumber = await browser.elementById('random-number').text() - await browser.eval(fastForwardTo, 30 * 1000) + // the number should be the same, as we navigated within 30s (part 2). + expect(newNumber).toBe(initialNumber) + }) - const newLoadingNumber = await browser - .elementByCss('[href="/1?timeout=1000"]') - .click() - .waitForElementByCss('#loading') - .text() + it('should refetch below the fold after 30 seconds', async () => { + const randomLoadingNumber = await browser + .elementByCss('[href="/1?timeout=1000"]') + .click() + .waitForElementByCss('#loading') + .text() - const newNumber = await browser - .waitForElementByCss('#random-number') - .text() + const randomNumber = await browser + .waitForElementByCss('#random-number') + .text() - expect(newLoadingNumber).toBe(randomLoadingNumber) + await browser.elementByCss('[href="/"]').click() - expect(newNumber).not.toBe(randomNumber) - }) - it('should refetch the full page after 5 mins', async () => { - const randomLoadingNumber = await browser - .elementByCss('[href="/1?timeout=1000"]') - .click() - .waitForElementByCss('#loading') - .text() + await browser.eval(fastForwardTo, 30 * 1000) - const randomNumber = await browser - .waitForElementByCss('#random-number') - .text() + const newLoadingNumber = await browser + .elementByCss('[href="/1?timeout=1000"]') + .click() + .waitForElementByCss('#loading') + .text() - await browser.eval(fastForwardTo, 5 * 60 * 1000) + const newNumber = await browser + .waitForElementByCss('#random-number') + .text() - await browser - .elementByCss('[href="/"]') - .click() - .waitForElementByCss('[href="/1?timeout=1000"]') + expect(newLoadingNumber).toBe(randomLoadingNumber) - const newLoadingNumber = await browser - .elementByCss('[href="/1?timeout=1000"]') - .click() - .waitForElementByCss('#loading') - .text() + expect(newNumber).not.toBe(randomNumber) + }) + it('should refetch the full page after 5 mins', async () => { + const randomLoadingNumber = await browser + .elementByCss('[href="/1?timeout=1000"]') + .click() + .waitForElementByCss('#loading') + .text() - const newNumber = await browser - .waitForElementByCss('#random-number') - .text() + const randomNumber = await browser + .waitForElementByCss('#random-number') + .text() - expect(newLoadingNumber).not.toBe(randomLoadingNumber) + await browser.eval(fastForwardTo, 5 * 60 * 1000) - expect(newNumber).not.toBe(randomNumber) - }) + await browser + .elementByCss('[href="/"]') + .click() + .waitForElementByCss('[href="/1?timeout=1000"]') - it('should respect a loading boundary that returns `null`', async () => { - await browser.elementByCss('[href="/null-loading"]').click() + const newLoadingNumber = await browser + .elementByCss('[href="/1?timeout=1000"]') + .click() + .waitForElementByCss('#loading') + .text() - // the page content should disappear immediately - expect( - await browser.hasElementByCssSelector('[href="/null-loading"]') - ).toBeFalse() + const newNumber = await browser + .waitForElementByCss('#random-number') + .text() - // the root layout should still be visible - expect(await browser.hasElementByCssSelector('#root-layout')).toBeTrue() + expect(newLoadingNumber).not.toBe(randomLoadingNumber) - // the dynamic content should eventually appear - await browser.waitForElementByCss('#random-number') - expect(await browser.hasElementByCssSelector('#random-number')).toBeTrue() - }) - }) - it('should seed the prefetch cache with the fetched page data', async () => { - const browser = (await next.browser( - '/1', - browserConfigWithFixedTime - )) as BrowserInterface + expect(newNumber).not.toBe(randomNumber) + }) - const initialNumber = await browser.elementById('random-number').text() + it('should respect a loading boundary that returns `null`', async () => { + await browser.elementByCss('[href="/null-loading"]').click() - // Move forward a few seconds, navigate off the page and then back to it - await browser.eval(fastForwardTo, 5 * 1000) - await browser.elementByCss('[href="/"]').click() - await browser.elementByCss('[href="/1"]').click() - - const newNumber = await browser.elementById('random-number').text() + // the page content should disappear immediately + expect( + await browser.hasElementByCssSelector('[href="/null-loading"]') + ).toBeFalse() - // The number should be the same as we've seeded it in the prefetch cache when we loaded the full page - expect(newNumber).toBe(initialNumber) - }) -} + // the root layout should still be visible + expect( + await browser.hasElementByCssSelector('#root-layout') + ).toBeTrue() -createNextDescribe( - 'app dir client cache semantics', - { - files: __dirname, - }, - ({ next, isNextDev }) => { - if (isNextDev) { - // since the router behavior is different in development mode (no viewport prefetching + liberal revalidation) - // we only check the production behavior - it('should skip dev', () => {}) - } else { - runTests(next) + // the dynamic content should eventually appear + await browser.waitForElementByCss('#random-number') + expect( + await browser.hasElementByCssSelector('#random-number') + ).toBeTrue() + }) + }) + it('should seed the prefetch cache with the fetched page data', async () => { + const browser = (await next.browser( + '/1', + browserConfigWithFixedTime + )) as BrowserInterface + + const initialNumber = await browser.elementById('random-number').text() + + // Move forward a few seconds, navigate off the page and then back to it + await browser.eval(fastForwardTo, 5 * 1000) + await browser.elementByCss('[href="/"]').click() + await browser.elementByCss('[href="/1"]').click() + + const newNumber = await browser.elementById('random-number').text() + + // The number should be the same as we've seeded it in the prefetch cache when we loaded the full page + expect(newNumber).toBe(initialNumber) + }) } } )