Skip to content

Commit caef001

Browse files
wyattjohijjk
authored andcommitted
Shared Revalidate Timings (#64370)
<!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: - Run `pnpm prettier-fix` to fix formatting issues before opening the PR. - Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide - The "examples guidelines" are followed from our contributing doc https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md - Make sure the linting passes by running `pnpm build && pnpm lint`. See https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md - Related issues linked using `fixes #number` - Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md - Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas) - Related issues/discussions are linked using `fixes #number` - e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) - Documentation added - Telemetry added. In case of a feature if it's used or not. - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md - Minimal description (aim for explaining to someone not on the team to understand the PR) - When linking to a Slack thread, you might want to share details of the conclusion - Link both the Linear (Fixes NEXT-xxx) and the GitHub issues - Add review comments if necessary to explain to the reviewer the logic behind a change Closes NEXT- Fixes # --> This creates a new `SharedRevalidateTimings` type that is safe to share amongst different points within the framework for sharing revalidation timings. This is a precursor to #64313 which freezes loaded manifests. Using the `SharedRevalidateTimings` type, we no-longer have to modify the in-memory instance of the prerender manifest to share the revalidation timings for different routes. Closes NEXT-3083
1 parent 9051bc4 commit caef001

File tree

7 files changed

+203
-30
lines changed

7 files changed

+203
-30
lines changed

packages/next/src/server/base-server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ import {
124124
import { matchNextDataPathname } from './lib/match-next-data-pathname'
125125
import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path'
126126
import { stripInternalHeaders } from './internal-utils'
127+
import { toRoute } from './lib/to-route'
127128

128129
export type FindComponentsResult = {
129130
components: LoadComponentsReturnType
@@ -1676,8 +1677,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
16761677
) {
16771678
isSSG = true
16781679
} else if (!this.renderOpts.dev) {
1679-
isSSG ||=
1680-
!!prerenderManifest.routes[pathname === '/index' ? '/' : pathname]
1680+
isSSG ||= !!prerenderManifest.routes[toRoute(pathname)]
16811681
}
16821682

16831683
// Toggle whether or not this is a Data request

packages/next/src/server/lib/incremental-cache/index.ts

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { CacheFs } from '../../../shared/lib/utils'
22
import type { PrerenderManifest } from '../../../build'
3+
import type { Revalidate } from '../revalidate'
34
import type {
45
IncrementalCacheValue,
56
IncrementalCacheEntry,
67
} from '../../response-cache'
78
import FetchCache from './fetch-cache'
89
import FileSystemCache from './file-system-cache'
9-
import path from '../../../shared/lib/isomorphic/path'
1010
import { encodeText } from '../../stream-utils/encode-decode'
1111
import { encode } from '../../../shared/lib/base64-arraybuffer'
1212
import { normalizePagePath } from '../../../shared/lib/page-path/normalize-page-path'
@@ -17,10 +17,8 @@ import {
1717
NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER,
1818
PRERENDER_REVALIDATE_HEADER,
1919
} from '../../../lib/constants'
20-
21-
function toRoute(pathname: string): string {
22-
return pathname.replace(/\/$/, '').replace(/\/index$/, '') || '/'
23-
}
20+
import { toRoute } from '../to-route'
21+
import { SharedRevalidateTimings } from './shared-revalidate-timings'
2422

2523
export interface CacheHandlerContext {
2624
fs?: CacheFs
@@ -72,6 +70,11 @@ export class IncrementalCache {
7270
isOnDemandRevalidate?: boolean
7371
private locks = new Map<string, Promise<void>>()
7472
private unlocks = new Map<string, () => Promise<void>>()
73+
/**
74+
* The revalidate timings for routes. This will source the timings from the
75+
* prerender manifest until the in-memory cache is updated with new timings.
76+
*/
77+
private readonly revalidateTimings: SharedRevalidateTimings
7578

7679
constructor({
7780
fs,
@@ -139,6 +142,7 @@ export class IncrementalCache {
139142
this.requestProtocol = requestProtocol
140143
this.allowedRevalidateHeaderKeys = allowedRevalidateHeaderKeys
141144
this.prerenderManifest = getPrerenderManifest()
145+
this.revalidateTimings = new SharedRevalidateTimings(this.prerenderManifest)
142146
this.fetchCacheKeyPrefix = fetchCacheKeyPrefix
143147
let revalidatedTags: string[] = []
144148

@@ -178,18 +182,16 @@ export class IncrementalCache {
178182
pathname: string,
179183
fromTime: number,
180184
dev?: boolean
181-
): number | false {
185+
): Revalidate {
182186
// in development we don't have a prerender-manifest
183187
// and default to always revalidating to allow easier debugging
184188
if (dev) return new Date().getTime() - 1000
185189

186190
// if an entry isn't present in routes we fallback to a default
187-
// of revalidating after 1 second
188-
const { initialRevalidateSeconds } = this.prerenderManifest.routes[
189-
toRoute(pathname)
190-
] || {
191-
initialRevalidateSeconds: 1,
192-
}
191+
// of revalidating after 1 second.
192+
const initialRevalidateSeconds =
193+
this.revalidateTimings.get(toRoute(pathname)) ?? 1
194+
193195
const revalidateAfter =
194196
typeof initialRevalidateSeconds === 'number'
195197
? initialRevalidateSeconds * 1000 + fromTime
@@ -464,11 +466,10 @@ export class IncrementalCache {
464466
}
465467
}
466468

467-
const curRevalidate =
468-
this.prerenderManifest.routes[toRoute(cacheKey)]?.initialRevalidateSeconds
469+
const curRevalidate = this.revalidateTimings.get(toRoute(cacheKey))
469470

470471
let isStale: boolean | -1 | undefined
471-
let revalidateAfter: false | number
472+
let revalidateAfter: Revalidate
472473

473474
if (cacheData?.lastModified === -1) {
474475
isStale = -1
@@ -554,19 +555,12 @@ export class IncrementalCache {
554555
pathname = this._getPathname(pathname, ctx.fetchCache)
555556

556557
try {
557-
// we use the prerender manifest memory instance
558-
// to store revalidate timings for calculating
559-
// revalidateAfter values so we update this on set
558+
// Set the value for the revalidate seconds so if it changes we can
559+
// update the cache with the new value.
560560
if (typeof ctx.revalidate !== 'undefined' && !ctx.fetchCache) {
561-
this.prerenderManifest.routes[pathname] = {
562-
dataRoute: path.posix.join(
563-
'/_next/data',
564-
`${normalizePagePath(pathname)}.json`
565-
),
566-
srcRoute: null, // FIXME: provide actual source route, however, when dynamically appending it doesn't really matter
567-
initialRevalidateSeconds: ctx.revalidate,
568-
}
561+
this.revalidateTimings.set(pathname, ctx.revalidate)
569562
}
563+
570564
await this.cacheHandler?.set(pathname, data, ctx)
571565
} catch (error) {
572566
console.warn('Failed to update prerender cache for', pathname, error)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { SharedRevalidateTimings } from './shared-revalidate-timings'
2+
3+
describe('SharedRevalidateTimings', () => {
4+
let sharedRevalidateTimings: SharedRevalidateTimings
5+
let prerenderManifest
6+
7+
beforeEach(() => {
8+
prerenderManifest = {
9+
routes: {
10+
'/route1': {
11+
initialRevalidateSeconds: 10,
12+
dataRoute: null,
13+
srcRoute: null,
14+
},
15+
'/route2': {
16+
initialRevalidateSeconds: 20,
17+
dataRoute: null,
18+
srcRoute: null,
19+
},
20+
},
21+
}
22+
sharedRevalidateTimings = new SharedRevalidateTimings(prerenderManifest)
23+
})
24+
25+
afterEach(() => {
26+
sharedRevalidateTimings.clear()
27+
})
28+
29+
it('should get revalidate timing from in-memory cache', () => {
30+
sharedRevalidateTimings.set('/route1', 15)
31+
const revalidate = sharedRevalidateTimings.get('/route1')
32+
expect(revalidate).toBe(15)
33+
})
34+
35+
it('should get revalidate timing from prerender manifest if not in cache', () => {
36+
const revalidate = sharedRevalidateTimings.get('/route2')
37+
expect(revalidate).toBe(20)
38+
})
39+
40+
it('should return undefined if revalidate timing not found', () => {
41+
const revalidate = sharedRevalidateTimings.get('/route3')
42+
expect(revalidate).toBeUndefined()
43+
})
44+
45+
it('should set revalidate timing in cache', () => {
46+
sharedRevalidateTimings.set('/route3', 30)
47+
const revalidate = sharedRevalidateTimings.get('/route3')
48+
expect(revalidate).toBe(30)
49+
})
50+
51+
it('should clear the in-memory cache', () => {
52+
sharedRevalidateTimings.set('/route3', 30)
53+
sharedRevalidateTimings.clear()
54+
const revalidate = sharedRevalidateTimings.get('/route3')
55+
expect(revalidate).toBeUndefined()
56+
})
57+
})
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { PrerenderManifest } from '../../../build'
2+
import type { Revalidate } from '../revalidate'
3+
4+
/**
5+
* A shared cache of revalidate timings for routes. This cache is used so we
6+
* don't have to modify the prerender manifest when we want to update the
7+
* revalidate timings for a route.
8+
*/
9+
export class SharedRevalidateTimings {
10+
/**
11+
* The in-memory cache of revalidate timings for routes. This cache is
12+
* populated when the cache is updated with new timings.
13+
*/
14+
private static readonly timings = new Map<string, Revalidate>()
15+
16+
constructor(
17+
/**
18+
* The prerender manifest that contains the initial revalidate timings for
19+
* routes.
20+
*/
21+
private readonly prerenderManifest: Pick<PrerenderManifest, 'routes'>
22+
) {}
23+
24+
/**
25+
* Try to get the revalidate timings for a route. This will first try to get
26+
* the timings from the in-memory cache. If the timings are not present in the
27+
* in-memory cache, then the timings will be sourced from the prerender
28+
* manifest.
29+
*
30+
* @param route the route to get the revalidate timings for
31+
* @returns the revalidate timings for the route, or undefined if the timings
32+
* are not present in the in-memory cache or the prerender manifest
33+
*/
34+
public get(route: string): Revalidate | undefined {
35+
// This is a copy on write cache that is updated when the cache is updated.
36+
// If the cache is never written to, then the timings will be sourced from
37+
// the prerender manifest.
38+
let revalidate = SharedRevalidateTimings.timings.get(route)
39+
if (typeof revalidate !== 'undefined') return revalidate
40+
41+
revalidate = this.prerenderManifest.routes[route]?.initialRevalidateSeconds
42+
if (typeof revalidate !== 'undefined') return revalidate
43+
44+
return undefined
45+
}
46+
47+
/**
48+
* Set the revalidate timings for a route.
49+
*
50+
* @param route the route to set the revalidate timings for
51+
* @param revalidate the revalidate timings for the route
52+
*/
53+
public set(route: string, revalidate: Revalidate) {
54+
SharedRevalidateTimings.timings.set(route, revalidate)
55+
}
56+
57+
/**
58+
* Clear the in-memory cache of revalidate timings for routes.
59+
*/
60+
public clear() {
61+
SharedRevalidateTimings.timings.clear()
62+
}
63+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { toRoute } from './to-route'
2+
3+
describe('toRoute Function', () => {
4+
it('should remove trailing slash', () => {
5+
const result = toRoute('/example/')
6+
expect(result).toBe('/example')
7+
})
8+
9+
it('should remove trailing `/index`', () => {
10+
const result = toRoute('/example/index')
11+
expect(result).toBe('/example')
12+
})
13+
14+
it('should return `/` when input is `/index`', () => {
15+
const result = toRoute('/index')
16+
expect(result).toBe('/')
17+
})
18+
19+
it('should return `/` when input is `/index/`', () => {
20+
const result = toRoute('/index/')
21+
expect(result).toBe('/')
22+
})
23+
24+
it('should return `/` when input is only a slash', () => {
25+
const result = toRoute('/')
26+
expect(result).toBe('/')
27+
})
28+
29+
it('should return `/` when input is empty', () => {
30+
const result = toRoute('')
31+
expect(result).toBe('/')
32+
})
33+
})
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* This transforms a URL pathname into a route. It removes any trailing slashes
3+
* and the `/index` suffix.
4+
*
5+
* @param {string} pathname - The URL path that needs to be optimized.
6+
* @returns {string} - The route
7+
*
8+
* @example
9+
* // returns '/example'
10+
* toRoute('/example/index/');
11+
*
12+
* @example
13+
* // returns '/example'
14+
* toRoute('/example/');
15+
*
16+
* @example
17+
* // returns '/'
18+
* toRoute('/index/');
19+
*
20+
* @example
21+
* // returns '/'
22+
* toRoute('/');
23+
*/
24+
export function toRoute(pathname: string): string {
25+
return pathname.replace(/(?:\/index)?\/?$/, '') || '/'
26+
}

packages/next/src/server/response-cache/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ interface IncrementalCachedPageValue {
7272
}
7373

7474
export type IncrementalCacheEntry = {
75-
curRevalidate?: number | false
75+
curRevalidate?: Revalidate
7676
// milliseconds to revalidate after
77-
revalidateAfter: number | false
77+
revalidateAfter: Revalidate
7878
// -1 here dictates a blocking revalidate should be used
7979
isStale?: boolean | -1
8080
value: IncrementalCacheValue | null

0 commit comments

Comments
 (0)