Skip to content

Commit

Permalink
switch to a global time configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
ztanner committed Mar 28, 2024
1 parent e9c1b18 commit 5d280ab
Show file tree
Hide file tree
Showing 7 changed files with 500 additions and 417 deletions.
12 changes: 10 additions & 2 deletions packages/next/src/build/webpack/plugins/define-env-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
type PrefetchCacheEntry,
PrefetchKind,
type ReadonlyReducerState,
PREFETCH_CACHE_MODE,
} from './router-reducer-types'
import { prefetchQueue } from './reducers/prefetch-reducer'

Expand Down Expand Up @@ -244,39 +243,35 @@ 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
}

// 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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,3 @@ export function isThenable(value: any): value is Promise<AppRouterState> {
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'
10 changes: 6 additions & 4 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,12 @@ export const configSchema: zod.ZodType<NextConfig> = 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(),
Expand Down
20 changes: 13 additions & 7 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -933,7 +936,10 @@ export const defaultConfig: NextConfig = {
missingSuspenseWithCSRBailout: true,
optimizeServerReact: true,
useEarlyImport: false,
clientRouterCacheMode: 'default',
staleTimes: {
dynamic: 30,
static: 300,
},
},
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 } },
},
})

Expand All @@ -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
Expand Down Expand Up @@ -170,20 +169,101 @@ 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 } },
},
})

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)
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)
})
})
})
})

0 comments on commit 5d280ab

Please sign in to comment.