Skip to content

Commit

Permalink
renew router cache after update from server
Browse files Browse the repository at this point in the history
  • Loading branch information
ztanner committed Feb 2, 2024
1 parent 86c1971 commit 058c955
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 51 deletions.
14 changes: 3 additions & 11 deletions packages/next/src/client/components/app-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ import type {
CacheNode,
AppRouterInstance,
} from '../../shared/lib/app-router-context.shared-runtime'
import type {
FlightRouterState,
FlightData,
} from '../../server/app-render/types'
import type { ErrorComponent } from './error-boundary'
import {
ACTION_FAST_REFRESH,
Expand Down Expand Up @@ -178,17 +174,13 @@ function useChangeByServerResponse(
dispatch: React.Dispatch<ReducerActions>
): RouterChangeByServerResponse {
return useCallback(
(
previousTree: FlightRouterState,
flightData: FlightData,
overrideCanonicalUrl: URL | undefined
) => {
({ previousTree, serverResponse, url }) => {
startTransition(() => {
dispatch({
type: ACTION_SERVER_PATCH,
flightData,
previousTree,
overrideCanonicalUrl,
serverResponse,
url,
})
})
},
Expand Down
13 changes: 10 additions & 3 deletions packages/next/src/client/components/layout-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -399,14 +399,17 @@ function InnerLayoutRouter({

// Check if there's already a pending request.
let lazyData = childNode.lazyData
// TODO: Can this just be derived from `context.nextUrl`?
const fetchUrl = new URL(url, location.origin)

if (lazyData === null) {
/**
* Router state with refetch marker added
*/
// TODO-APP: remove ''
const refetchTree = walkAddRefetch(['', ...segmentPath], fullTree)
childNode.lazyData = lazyData = fetchServerResponse(
new URL(url, location.origin),
fetchUrl,
refetchTree,
context.nextUrl,
buildId
Expand All @@ -417,15 +420,19 @@ function InnerLayoutRouter({
* Flight response data
*/
// When the data has not resolved yet `use` will suspend here.
const [flightData, overrideCanonicalUrl] = use(lazyData)
const serverResponse = use(lazyData)

// segmentPath from the server does not match the layout's segmentPath
childNode.lazyData = null

// setTimeout is used to start a new transition during render, this is an intentional hack around React.
setTimeout(() => {
startTransition(() => {
changeByServerResponse(fullTree, flightData, overrideCanonicalUrl)
changeByServerResponse({
previousTree: fullTree,
serverResponse,
url: fetchUrl,
})
})
})
// Suspend infinitely as `changeByServerResponse` will cause a different part of the tree to be rendered.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ describe('serverPatchReducer', () => {
],
])

const url = new URL('/linking/about', 'https://localhost') as any
const state = createInitialRouterState({
buildId,
initialTree,
Expand All @@ -137,11 +138,11 @@ describe('serverPatchReducer', () => {
initialSeedData: ['', {}, children],
initialFlightData: [['']],
initialParallelRoutes,
location: new URL('/linking/about', 'https://localhost') as any,
location: url,
})
const action: ServerPatchAction = {
type: ACTION_SERVER_PATCH,
flightData: flightDataForPatch,
serverResponse: [flightDataForPatch, undefined],
previousTree: [
'',
{
Expand All @@ -156,7 +157,7 @@ describe('serverPatchReducer', () => {
undefined,
true,
],
overrideCanonicalUrl: undefined,
url,
}

const newState = await serverPatchReducer(state, action)
Expand Down Expand Up @@ -232,7 +233,7 @@ describe('serverPatchReducer', () => {
"data": Promise {},
"key": "/linking/about",
"kind": "auto",
"lastUsedTime": null,
"lastUsedTime": 1690329600000,
"prefetchTime": 1690329600000,
"treeAtTimeOfPrefetch": [
"",
Expand Down Expand Up @@ -339,6 +340,7 @@ describe('serverPatchReducer', () => {
shouldScroll: true,
}

const url = new URL(initialCanonicalUrl, 'https://localhost') as any
const state = createInitialRouterState({
buildId,
initialTree,
Expand All @@ -347,14 +349,14 @@ describe('serverPatchReducer', () => {
initialSeedData: ['', {}, children],
initialFlightData: [['']],
initialParallelRoutes,
location: new URL(initialCanonicalUrl, 'https://localhost') as any,
location: url,
})

const stateAfterNavigate = await navigateReducer(state, navigateAction)

const action: ServerPatchAction = {
type: ACTION_SERVER_PATCH,
flightData: flightDataForPatch,
serverResponse: [flightDataForPatch, undefined],
previousTree: [
'',
{
Expand All @@ -369,7 +371,7 @@ describe('serverPatchReducer', () => {
undefined,
true,
],
overrideCanonicalUrl: undefined,
url,
}

const newState = await serverPatchReducer(stateAfterNavigate, action)
Expand Down Expand Up @@ -476,7 +478,7 @@ describe('serverPatchReducer', () => {
"data": Promise {},
"key": "/linking",
"kind": "auto",
"lastUsedTime": null,
"lastUsedTime": 1690329600000,
"prefetchTime": 1690329600000,
"treeAtTimeOfPrefetch": [
"",
Expand Down Expand Up @@ -572,6 +574,7 @@ describe('serverPatchReducer', () => {
</html>
)

const url = new URL('/linking/about', 'https://localhost') as any
const state = createInitialRouterState({
buildId,
initialTree,
Expand All @@ -580,22 +583,25 @@ describe('serverPatchReducer', () => {
initialSeedData: ['', {}, children],
initialFlightData: [['']],
initialParallelRoutes: new Map(),
location: new URL('/linking/about', 'https://localhost') as any,
location: url,
})

const action: ServerPatchAction = {
type: ACTION_SERVER_PATCH,
// this flight data is intentionally completely unrelated to the existing tree
flightData: [
serverResponse: [
[
'children',
'tree-patch-failure',
'children',
'new-page',
['new-page', { children: ['__PAGE__', {}] }],
null,
null,
[
'children',
'tree-patch-failure',
'children',
'new-page',
['new-page', { children: ['__PAGE__', {}] }],
null,
null,
],
],
undefined,
],
previousTree: [
'',
Expand All @@ -611,7 +617,7 @@ describe('serverPatchReducer', () => {
undefined,
true,
],
overrideCanonicalUrl: undefined,
url,
}

const newState = await serverPatchReducer(state, action)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import { handleMutable } from '../handle-mutable'
import type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime'
import { createEmptyCacheNode } from '../../app-router'
import { handleSegmentMismatch } from '../handle-segment-mismatch'
import { createPrefetchCacheKey } from './prefetch-cache-utils'

export function serverPatchReducer(
state: ReadonlyReducerState,
action: ServerPatchAction
): ReducerState {
const { flightData, overrideCanonicalUrl } = action
const { serverResponse, url } = action
const [flightData, overrideCanonicalUrl, , intercept] = serverResponse

const mutable: Mutable = {}

Expand Down Expand Up @@ -80,5 +82,18 @@ export function serverPatchReducer(
currentTree = newTree
}

const prefetchCacheKey = createPrefetchCacheKey(
url,
// routes that could be intercepted / are interception routes get prefixed with the nextUrl
intercept ? state.nextUrl : undefined
)
const prefetchValues = state.prefetchCache.get(prefetchCacheKey)

// If we applied a patch from the server, we want to renew the prefetch cache entry
// Otherwise it'll remain stale and we'll keep refetching the page data
if (prefetchValues) {
prefetchValues.lastUsedTime = Date.now()
}

return handleMutable(state, mutable)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime'
import type {
FlightRouterState,
FlightData,
FlightSegmentPath,
} from '../../../server/app-render/types'
import type { FetchServerResponseResult } from './fetch-server-response'
Expand All @@ -14,11 +13,15 @@ export const ACTION_PREFETCH = 'prefetch'
export const ACTION_FAST_REFRESH = 'fast-refresh'
export const ACTION_SERVER_ACTION = 'server-action'

export type RouterChangeByServerResponse = (
previousTree: FlightRouterState,
flightData: FlightData,
overrideCanonicalUrl: URL | undefined
) => void
export type RouterChangeByServerResponse = ({
previousTree,
serverResponse,
url,
}: {
previousTree: FlightRouterState
serverResponse: FetchServerResponseResult
url: URL
}) => void

export type RouterNavigate = (
href: string,
Expand Down Expand Up @@ -133,9 +136,9 @@ export interface RestoreAction {
*/
export interface ServerPatchAction {
type: typeof ACTION_SERVER_PATCH
flightData: FlightData
serverResponse: FetchServerResponseResult
previousTree: FlightRouterState
overrideCanonicalUrl: URL | undefined
url: URL
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
import type {
FocusAndScrollRef,
PrefetchKind,
RouterChangeByServerResponse,
} from '../../client/components/router-reducer/router-reducer-types'
import type { FetchServerResponseResult } from '../../client/components/router-reducer/fetch-server-response'
import type {
FlightRouterState,
FlightData,
} from '../../server/app-render/types'
import type { FlightRouterState } from '../../server/app-render/types'
import React from 'react'

export type ChildSegmentMap = Map<string, CacheNode>
Expand Down Expand Up @@ -144,11 +142,7 @@ export const LayoutRouterContext = React.createContext<{
export const GlobalLayoutRouterContext = React.createContext<{
buildId: string
tree: FlightRouterState
changeByServerResponse: (
previousTree: FlightRouterState,
flightData: FlightData,
overrideCanonicalUrl: URL | undefined
) => void
changeByServerResponse: RouterChangeByServerResponse
focusAndScrollRef: FocusAndScrollRef
nextUrl: string | null
}>(null as any)
Expand Down
45 changes: 45 additions & 0 deletions test/e2e/app-dir/app-client-cache/client-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,51 @@ createNextDescribe(

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

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)

// 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()

// the number should be the same, as we navigated within 30s (part 2).
expect(newNumber).toBe(initialNumber)
})

it('should refetch below the fold after 30 seconds', async () => {
const randomLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
Expand Down
3 changes: 2 additions & 1 deletion test/ppr-tests-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"test/e2e/app-dir/app-client-cache/client-cache.test.ts": {
"failed": [
"app dir client cache semantics prefetch={undefined} - default should re-use the full cache for only 30 seconds",
"app dir client cache semantics prefetch={undefined} - default should refetch below the fold after 30 seconds"
"app dir client cache semantics prefetch={undefined} - default should refetch below the fold after 30 seconds",
"app dir client cache semantics prefetch={undefined} - default should renew the 30s cache once the data is revalidated"
]
},
"test/e2e/app-dir/headers-static-bailout/headers-static-bailout.test.ts": {
Expand Down

0 comments on commit 058c955

Please sign in to comment.