Revert "app-router: new client-side cache semantics" (#48678)

Reverts vercel/next.js#48383
fix NEXT-1011

revert and re-land later
This commit is contained in:
Jiachi Liu 2023-04-21 19:21:58 +02:00 committed by GitHub
parent f779f10f38
commit 52fcc59717
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 271 additions and 951 deletions

View file

@ -25,7 +25,6 @@ import {
ACTION_REFRESH,
ACTION_RESTORE,
ACTION_SERVER_PATCH,
PrefetchKind,
} from './router-reducer/router-reducer-types'
import { createHrefFromUrl } from './router-reducer/create-href-from-url'
import {
@ -235,7 +234,7 @@ function Router({
const routerInstance: AppRouterInstance = {
back: () => window.history.back(),
forward: () => window.history.forward(),
prefetch: async (href, options) => {
prefetch: async (href) => {
// If prefetch has already been triggered, don't trigger it again.
if (isBot(window.navigator.userAgent)) {
return
@ -245,12 +244,12 @@ function Router({
if (isExternalURL(url)) {
return
}
// @ts-ignore startTransition exists
React.startTransition(() => {
dispatch({
type: ACTION_PREFETCH,
url,
kind: options?.kind ?? PrefetchKind.FULL,
})
})
},

View file

@ -277,7 +277,16 @@ function InnerLayoutRouter({
// TODO-APP: verify if this can be null based on user code
childProp.current !== null
) {
if (!childNode) {
if (childNode) {
if (childNode.status === CacheStates.LAZY_INITIALIZED) {
// @ts-expect-error we're changing it's type!
childNode.status = CacheStates.READY
// @ts-expect-error
childNode.subTreeData = childProp.current
// Mutates the prop in order to clean up the memory associated with the subTreeData as it is now part of the cache.
childProp.current = null
}
} else {
// Add the segment's subTreeData to the cache.
// This writes to the cache when there is no item in the cache yet. It never *overwrites* existing cache items which is why it's safe in concurrent mode.
childNodes.set(cacheKey, {
@ -286,15 +295,10 @@ function InnerLayoutRouter({
subTreeData: childProp.current,
parallelRoutes: new Map(),
})
// Mutates the prop in order to clean up the memory associated with the subTreeData as it is now part of the cache.
childProp.current = null
// In the above case childNode was set on childNodes, so we have to get it from the cacheNodes again.
childNode = childNodes.get(cacheKey)
} else {
if (childNode.status === CacheStates.LAZY_INITIALIZED) {
// @ts-expect-error we're changing it's type!
childNode.status = CacheStates.READY
// @ts-expect-error
childNode.subTreeData = childProp.current
}
}
}

View file

@ -7,7 +7,7 @@ export function applyFlightData(
existingCache: CacheNode,
cache: CacheNode,
flightDataPath: FlightDataPath,
wasPrefetched: boolean = false
wasPrefetched?: boolean
): boolean {
// The one before last item is the router state tree patch
const [treePatch, subTreeData, head] = flightDataPath.slice(-3)
@ -33,12 +33,7 @@ export function applyFlightData(
cache.subTreeData = existingCache.subTreeData
cache.parallelRoutes = new Map(existingCache.parallelRoutes)
// Create a copy of the existing cache with the subTreeData applied.
fillCacheWithNewSubTreeData(
cache,
existingCache,
flightDataPath,
wasPrefetched
)
fillCacheWithNewSubTreeData(cache, existingCache, flightDataPath)
}
return true

View file

@ -14,7 +14,6 @@ import {
} from '../app-router-headers'
import { urlToUrlWithoutFlightMarker } from '../app-router'
import { callServer } from '../../app-call-server'
import { PrefetchKind } from './router-reducer-types'
/**
* Fetch the flight data for the provided url. Takes in the current router state to decide what to render server-side.
@ -24,7 +23,7 @@ export async function fetchServerResponse(
url: URL,
flightRouterState: FlightRouterState,
nextUrl: string | null,
prefetchKind?: PrefetchKind
prefetch?: true
): Promise<[FlightData: FlightData, canonicalUrlOverride: URL | undefined]> {
const headers: {
[RSC]: '1'
@ -37,14 +36,8 @@ export async function fetchServerResponse(
// Provide the current router state
[NEXT_ROUTER_STATE_TREE]: JSON.stringify(flightRouterState),
}
/**
* Three cases:
* - `prefetchKind` is `undefined`, it means it's a normal navigation, so we want to prefetch the page data fully
* - `prefetchKind` is `full` - we want to prefetch the whole page so same as above
* - `prefetchKind` is `auto` - if the page is dynamic, prefetch the page data partially, if static prefetch the page data fully
*/
if (prefetchKind === PrefetchKind.AUTO) {
if (prefetch) {
// Enable prefetch response
headers[NEXT_ROUTER_PREFETCH] = '1'
}

View file

@ -1,6 +1,4 @@
import { FlightSegmentPath } from '../../../server/app-render/types'
import { CacheNode, CacheStates } from '../../../shared/lib/app-router-context'
import { createRouterCacheKey } from './create-router-cache-key'
import { fetchServerResponse } from './fetch-server-response'
/**
@ -9,24 +7,19 @@ import { fetchServerResponse } from './fetch-server-response'
export function fillCacheWithDataProperty(
newCache: CacheNode,
existingCache: CacheNode,
flightSegmentPath: FlightSegmentPath,
fetchResponse: () => ReturnType<typeof fetchServerResponse>,
bailOnParallelRoutes: boolean = false
segments: string[],
fetchResponse: () => ReturnType<typeof fetchServerResponse>
): { bailOptimistic: boolean } | undefined {
const isLastEntry = flightSegmentPath.length <= 2
const isLastEntry = segments.length === 1
const [parallelRouteKey, segment] = flightSegmentPath
const cacheKey = createRouterCacheKey(segment)
const parallelRouteKey = 'children'
const [segment] = segments
const existingChildSegmentMap =
existingCache.parallelRoutes.get(parallelRouteKey)
if (
!existingChildSegmentMap ||
(bailOnParallelRoutes && existingCache.parallelRoutes.size > 1)
) {
if (!existingChildSegmentMap) {
// Bailout because the existing cache does not have the path to the leaf node
// or the existing cache has multiple parallel routes
// Will trigger lazy fetch in layout-router because of missing segment
return { bailOptimistic: true }
}
@ -38,8 +31,8 @@ export function fillCacheWithDataProperty(
newCache.parallelRoutes.set(parallelRouteKey, childSegmentMap)
}
const existingChildCacheNode = existingChildSegmentMap.get(cacheKey)
let childCacheNode = childSegmentMap.get(cacheKey)
const existingChildCacheNode = existingChildSegmentMap.get(segment)
let childCacheNode = childSegmentMap.get(segment)
// In case of last segment start off the fetch at this level and don't copy further down.
if (isLastEntry) {
@ -48,7 +41,7 @@ export function fillCacheWithDataProperty(
!childCacheNode.data ||
childCacheNode === existingChildCacheNode
) {
childSegmentMap.set(cacheKey, {
childSegmentMap.set(segment, {
status: CacheStates.DATA_FETCH,
data: fetchResponse(),
subTreeData: null,
@ -61,7 +54,7 @@ export function fillCacheWithDataProperty(
if (!childCacheNode || !existingChildCacheNode) {
// Start fetch in the place where the existing cache doesn't have the data yet.
if (!childCacheNode) {
childSegmentMap.set(cacheKey, {
childSegmentMap.set(segment, {
status: CacheStates.DATA_FETCH,
data: fetchResponse(),
subTreeData: null,
@ -78,13 +71,13 @@ export function fillCacheWithDataProperty(
subTreeData: childCacheNode.subTreeData,
parallelRoutes: new Map(childCacheNode.parallelRoutes),
} as CacheNode
childSegmentMap.set(cacheKey, childCacheNode)
childSegmentMap.set(segment, childCacheNode)
}
return fillCacheWithDataProperty(
childCacheNode,
existingChildCacheNode,
flightSegmentPath.slice(2),
segments.slice(1),
fetchResponse
)
}

View file

@ -78,7 +78,7 @@ describe('fillCacheWithNewSubtreeData', () => {
// Mirrors the way router-reducer values are passed in.
const flightDataPath = flightData[0]
fillCacheWithNewSubTreeData(cache, existingCache, flightDataPath, false)
fillCacheWithNewSubTreeData(cache, existingCache, flightDataPath)
const expectedCache: CacheNode = {
data: null,

View file

@ -10,8 +10,7 @@ import { createRouterCacheKey } from './create-router-cache-key'
export function fillCacheWithNewSubTreeData(
newCache: CacheNode,
existingCache: CacheNode,
flightDataPath: FlightDataPath,
wasPrefetched?: boolean
flightDataPath: FlightDataPath
): void {
const isLastEntry = flightDataPath.length <= 5
const [parallelRouteKey, segment] = flightDataPath
@ -64,8 +63,7 @@ export function fillCacheWithNewSubTreeData(
childCacheNode,
existingChildCacheNode,
flightDataPath[2],
flightDataPath[4],
wasPrefetched
flightDataPath[4]
)
childSegmentMap.set(cacheKey, childCacheNode)
@ -92,7 +90,6 @@ export function fillCacheWithNewSubTreeData(
fillCacheWithNewSubTreeData(
childCacheNode,
existingChildCacheNode,
flightDataPath.slice(2),
wasPrefetched
flightDataPath.slice(2)
)
}

View file

@ -1,40 +0,0 @@
import { PrefetchCacheEntry } from './router-reducer-types'
const FIVE_MINUTES = 5 * 60 * 1000
const THIRTY_SECONDS = 30 * 1000
export enum PrefetchCacheEntryStatus {
fresh = 'fresh',
reusable = 'reusable',
expired = 'expired',
stale = 'stale',
}
export function getPrefetchEntryCacheStatus({
kind,
prefetchTime,
lastUsedTime,
}: PrefetchCacheEntry): PrefetchCacheEntryStatus {
// if the cache entry was prefetched or read less than 30s ago, then we want to re-use it
if (Date.now() < (lastUsedTime ?? prefetchTime) + THIRTY_SECONDS) {
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) {
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 (kind === 'full') {
if (Date.now() < prefetchTime + FIVE_MINUTES) {
return PrefetchCacheEntryStatus.reusable
}
}
return PrefetchCacheEntryStatus.expired
}

View file

@ -86,7 +86,7 @@ describe('invalidateCacheBelowFlightSegmentPath', () => {
// @ts-expect-error TODO-APP: investigate why this is not a TS error in router-reducer.
cache.subTreeData = existingCache.subTreeData
// Create a copy of the existing cache with the subTreeData applied.
fillCacheWithNewSubTreeData(cache, existingCache, flightDataPath, false)
fillCacheWithNewSubTreeData(cache, existingCache, flightDataPath)
// Invalidate the cache below the flight segment path. This should remove the 'about' node.
invalidateCacheBelowFlightSegmentPath(

View file

@ -80,7 +80,6 @@ import {
ACTION_NAVIGATE,
ACTION_PREFETCH,
PrefetchAction,
PrefetchKind,
} from '../router-reducer-types'
import { navigateReducer } from './navigate-reducer'
import { prefetchReducer } from './prefetch-reducer'
@ -1006,7 +1005,6 @@ describe('navigateReducer', () => {
const prefetchAction: PrefetchAction = {
type: ACTION_PREFETCH,
url,
kind: PrefetchKind.AUTO,
}
const state = createInitialRouterState({
@ -1088,9 +1086,6 @@ describe('navigateReducer', () => {
'/linking/about',
{
data: record,
kind: PrefetchKind.AUTO,
lastUsedTime: null,
prefetchTime: expect.any(Number),
treeAtTimeOfPrefetch: [
'',
{

View file

@ -1,7 +1,4 @@
import {
CacheNode,
CacheStates,
} from '../../../../shared/lib/app-router-context'
import { CacheStates } from '../../../../shared/lib/app-router-context'
import type {
FlightRouterState,
FlightSegmentPath,
@ -16,20 +13,14 @@ import { createOptimisticTree } from '../create-optimistic-tree'
import { applyRouterStatePatchToTree } from '../apply-router-state-patch-to-tree'
import { shouldHardNavigate } from '../should-hard-navigate'
import { isNavigatingToNewRootLayout } from '../is-navigating-to-new-root-layout'
import {
import type {
Mutable,
NavigateAction,
PrefetchKind,
ReadonlyReducerState,
ReducerState,
} from '../router-reducer-types'
import { handleMutable } from '../handle-mutable'
import { applyFlightData } from '../apply-flight-data'
import {
PrefetchCacheEntryStatus,
getPrefetchEntryCacheStatus,
} from '../get-prefetch-cache-entry-status'
import { prunePrefetchCache } from './prune-prefetch-cache'
export function handleExternalUrl(
state: ReadonlyReducerState,
@ -72,37 +63,6 @@ function generateSegmentsFromPatch(
return segments
}
function addRefetchToLeafSegments(
newCache: CacheNode,
currentCache: CacheNode,
flightSegmentPath: FlightSegmentPath,
treePatch: FlightRouterState,
data: () => ReturnType<typeof fetchServerResponse>
) {
let appliedPatch = false
newCache.status = CacheStates.READY
newCache.subTreeData = currentCache.subTreeData
newCache.parallelRoutes = new Map(currentCache.parallelRoutes)
const segmentPathsToFill = generateSegmentsFromPatch(treePatch).map(
(segment) => [...flightSegmentPath, ...segment]
)
for (const segmentPaths of segmentPathsToFill) {
const res = fillCacheWithDataProperty(
newCache,
currentCache,
segmentPaths,
data
)
if (!res?.bailOptimistic) {
appliedPatch = true
}
}
return appliedPatch
}
export function navigateReducer(
state: ReadonlyReducerState,
action: NavigateAction
@ -118,8 +78,6 @@ export function navigateReducer(
const { pathname, hash } = url
const href = createHrefFromUrl(url)
const pendingPush = navigateType === 'push'
// we want to prune the prefetch cache on every navigation to avoid it growing too large
prunePrefetchCache(state.prefetchCache)
const isForCurrentTree =
JSON.stringify(mutable.previousTree) === JSON.stringify(state.tree)
@ -132,105 +90,14 @@ export function navigateReducer(
return handleExternalUrl(state, mutable, url.toString(), pendingPush)
}
let prefetchValues = state.prefetchCache.get(createHrefFromUrl(url, false))
if (
forceOptimisticNavigation &&
prefetchValues?.kind !== PrefetchKind.TEMPORARY
) {
const segments = pathname.split('/')
// TODO-APP: figure out something better for index pages
segments.push('')
// Optimistic tree case.
// If the optimistic tree is deeper than the current state leave that deeper part out of the fetch
const optimisticTree = createOptimisticTree(segments, state.tree, false)
// we need a copy of the cache in case we need to revert to it
const temporaryCacheNode: CacheNode = {
...cache,
}
// Copy subTreeData for the root node of the cache.
// Note: didn't do it above because typescript doesn't like it.
temporaryCacheNode.status = CacheStates.READY
temporaryCacheNode.subTreeData = state.cache.subTreeData
temporaryCacheNode.parallelRoutes = new Map(state.cache.parallelRoutes)
const data = createRecordFromThenable(
fetchServerResponse(url, optimisticTree, state.nextUrl)
)
// TODO-APP: segments.slice(1) strips '', we can get rid of '' altogether.
// TODO-APP: re-evaluate if we need to strip the last segment
const optimisticFlightSegmentPath = segments
.slice(1)
.map((segment) => ['children', segment === '' ? '__PAGE__' : segment])
.flat()
// Copy existing cache nodes as far as possible and fill in `data` property with the started data fetch.
// The `data` property is used to suspend in layout-router during render if it hasn't resolved yet by the time it renders.
const res = fillCacheWithDataProperty(
temporaryCacheNode,
state.cache,
optimisticFlightSegmentPath,
() => data,
true
)
// If optimistic fetch couldn't happen it falls back to the non-optimistic case.
if (!res?.bailOptimistic) {
mutable.previousTree = state.tree
mutable.patchedTree = optimisticTree
mutable.pendingPush = pendingPush
mutable.hashFragment = hash
mutable.scrollableSegments = []
mutable.cache = temporaryCacheNode
mutable.canonicalUrl = href
state.prefetchCache.set(createHrefFromUrl(url, false), {
data: Promise.resolve(data),
// this will make sure that the entry will be discarded after 30s
kind: PrefetchKind.TEMPORARY,
prefetchTime: Date.now(),
treeAtTimeOfPrefetch: state.tree,
lastUsedTime: Date.now(),
})
return handleMutable(state, mutable)
}
}
// If we don't have a prefetch value, we need to create one
if (!prefetchValues) {
const data = createRecordFromThenable(
fetchServerResponse(url, state.tree, state.nextUrl)
)
const newPrefetchValue = {
data: Promise.resolve(data),
// this will make sure that the entry will be discarded after 30s
kind: PrefetchKind.TEMPORARY,
prefetchTime: Date.now(),
treeAtTimeOfPrefetch: state.tree,
lastUsedTime: null,
}
state.prefetchCache.set(createHrefFromUrl(url, false), newPrefetchValue)
prefetchValues = newPrefetchValue
}
const prefetchEntryCacheStatus = getPrefetchEntryCacheStatus(prefetchValues)
const prefetchValues = state.prefetchCache.get(createHrefFromUrl(url, false))
if (prefetchValues) {
// The one before last item is the router state tree patch
const { treeAtTimeOfPrefetch, data } = prefetchValues
// Unwrap cache data with `use` to suspend here (in the reducer) until the fetch resolves.
const [flightData, canonicalUrlOverride] = readRecordValue(data!)
// important: we should only mark the cache node as dirty after we unsuspend from the call above
prefetchValues.lastUsedTime = Date.now()
// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
return handleExternalUrl(state, mutable, flightData, pendingPush)
@ -242,8 +109,9 @@ export function navigateReducer(
for (const flightDataPath of flightData) {
const flightSegmentPath = flightDataPath.slice(
0,
-4
-3
) as unknown as FlightSegmentPath
// The one before last item is the router state tree patch
const [treePatch] = flightDataPath.slice(-3) as [FlightRouterState]
@ -271,27 +139,13 @@ export function navigateReducer(
return handleExternalUrl(state, mutable, href, pendingPush)
}
let applied = applyFlightData(
const applied = applyFlightData(
currentCache,
cache,
flightDataPath,
prefetchValues.kind === 'auto' &&
prefetchEntryCacheStatus === PrefetchCacheEntryStatus.reusable
true
)
if (
!applied &&
prefetchEntryCacheStatus === PrefetchCacheEntryStatus.stale
) {
applied = addRefetchToLeafSegments(
cache,
currentCache,
flightSegmentPath,
treePatch,
() => fetchServerResponse(url, newTree!, state.nextUrl)
)
}
const hardNavigate = shouldHardNavigate(
// TODO-APP: remove ''
['', ...flightSegmentPath],
@ -319,7 +173,8 @@ export function navigateReducer(
for (const subSegment of generateSegmentsFromPatch(treePatch)) {
scrollableSegments.push(
[...flightSegmentPath, ...subSegment].filter(
// the last segment is the same as the first segment in the patch
[...flightSegmentPath.slice(0, -1), ...subSegment].filter(
(segment) => segment !== '__PAGE__'
)
)
@ -336,5 +191,123 @@ export function navigateReducer(
mutable.pendingPush = pendingPush
mutable.hashFragment = hash
return handleMutable(state, mutable)
}
// When doing a hard push there can be two cases: with optimistic tree and without
// The with optimistic tree case only happens when the layouts have a loading state (loading.js)
// The without optimistic tree case happens when there is no loading state, in that case we suspend in this reducer
// forceOptimisticNavigation is used for links that have `prefetch={false}`.
if (forceOptimisticNavigation) {
const segments = pathname.split('/')
// TODO-APP: figure out something better for index pages
segments.push('')
// Optimistic tree case.
// If the optimistic tree is deeper than the current state leave that deeper part out of the fetch
const optimisticTree = createOptimisticTree(segments, state.tree, false)
// Copy subTreeData for the root node of the cache.
cache.status = CacheStates.READY
cache.subTreeData = state.cache.subTreeData
// Copy existing cache nodes as far as possible and fill in `data` property with the started data fetch.
// The `data` property is used to suspend in layout-router during render if it hasn't resolved yet by the time it renders.
const res = fillCacheWithDataProperty(
cache,
state.cache,
// TODO-APP: segments.slice(1) strips '', we can get rid of '' altogether.
segments.slice(1),
() => fetchServerResponse(url, optimisticTree, state.nextUrl)
)
// If optimistic fetch couldn't happen it falls back to the non-optimistic case.
if (!res?.bailOptimistic) {
mutable.previousTree = state.tree
mutable.patchedTree = optimisticTree
mutable.pendingPush = pendingPush
mutable.hashFragment = hash
mutable.scrollableSegments = []
mutable.cache = cache
mutable.canonicalUrl = href
return handleMutable(state, mutable)
}
}
// Below is the not-optimistic case. Data is fetched at the root and suspended there without a suspense boundary.
// If no in-flight fetch at the top, start it.
if (!cache.data) {
cache.data = createRecordFromThenable(
fetchServerResponse(url, state.tree, state.nextUrl)
)
}
// Unwrap cache data with `use` to suspend here (in the reducer) until the fetch resolves.
const [flightData, canonicalUrlOverride] = readRecordValue(cache.data!)
// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
return handleExternalUrl(state, mutable, flightData, pendingPush)
}
// Remove cache.data as it has been resolved at this point.
cache.data = null
let currentTree = state.tree
let currentCache = state.cache
let scrollableSegments: FlightSegmentPath[] = []
for (const flightDataPath of flightData) {
// The one before last item is the router state tree patch
const [treePatch] = flightDataPath.slice(-3, -2)
// Path without the last segment, router state, and the subTreeData
const flightSegmentPath = flightDataPath.slice(0, -4)
// Create new tree based on the flightSegmentPath and router state patch
const newTree = applyRouterStatePatchToTree(
// TODO-APP: remove ''
['', ...flightSegmentPath],
currentTree,
treePatch
)
if (newTree === null) {
throw new Error('SEGMENT MISMATCH')
}
if (isNavigatingToNewRootLayout(currentTree, newTree)) {
return handleExternalUrl(state, mutable, href, pendingPush)
}
mutable.canonicalUrl = canonicalUrlOverride
? createHrefFromUrl(canonicalUrlOverride)
: href
const applied = applyFlightData(currentCache, cache, flightDataPath)
if (applied) {
mutable.cache = cache
currentCache = cache
}
currentTree = newTree
for (const subSegment of generateSegmentsFromPatch(treePatch)) {
scrollableSegments.push(
[...flightSegmentPath, ...subSegment].filter(
(segment) => segment !== '__PAGE__'
)
)
}
}
mutable.previousTree = state.tree
mutable.patchedTree = currentTree
mutable.scrollableSegments = scrollableSegments
mutable.pendingPush = pendingPush
mutable.hashFragment = hash
return handleMutable(state, mutable)
}

View file

@ -38,11 +38,7 @@ import {
CacheStates,
} from '../../../../shared/lib/app-router-context'
import { createInitialRouterState } from '../create-initial-router-state'
import {
PrefetchAction,
ACTION_PREFETCH,
PrefetchKind,
} from '../router-reducer-types'
import { PrefetchAction, ACTION_PREFETCH } from '../router-reducer-types'
import { prefetchReducer } from './prefetch-reducer'
import { fetchServerResponse } from '../fetch-server-response'
import { createRecordFromThenable } from '../create-record-from-thenable'
@ -132,12 +128,11 @@ describe('prefetchReducer', () => {
url,
initialTree,
null,
PrefetchKind.AUTO
true
)
const action: PrefetchAction = {
type: ACTION_PREFETCH,
url,
kind: PrefetchKind.AUTO,
}
const newState = await runPromiseThrowChain(() =>
@ -154,9 +149,6 @@ describe('prefetchReducer', () => {
'/linking/about',
{
data: record,
kind: PrefetchKind.AUTO,
lastUsedTime: null,
prefetchTime: expect.any(Number),
treeAtTimeOfPrefetch: [
'',
{
@ -281,12 +273,11 @@ describe('prefetchReducer', () => {
url,
initialTree,
null,
PrefetchKind.AUTO
true
)
const action: PrefetchAction = {
type: ACTION_PREFETCH,
url,
kind: PrefetchKind.AUTO,
}
await runPromiseThrowChain(() => prefetchReducer(state, action))
@ -305,9 +296,6 @@ describe('prefetchReducer', () => {
'/linking/about',
{
data: record,
prefetchTime: expect.any(Number),
kind: PrefetchKind.AUTO,
lastUsedTime: null,
treeAtTimeOfPrefetch: [
'',
{

View file

@ -4,18 +4,13 @@ import {
PrefetchAction,
ReducerState,
ReadonlyReducerState,
PrefetchKind,
} from '../router-reducer-types'
import { createRecordFromThenable } from '../create-record-from-thenable'
import { prunePrefetchCache } from './prune-prefetch-cache'
export function prefetchReducer(
state: ReadonlyReducerState,
action: PrefetchAction
): ReducerState {
// let's prune the prefetch cache before we do anything else
prunePrefetchCache(state.prefetchCache)
const { url } = action
const href = createHrefFromUrl(
url,
@ -23,33 +18,10 @@ export function prefetchReducer(
false
)
const cacheEntry = state.prefetchCache.get(href)
if (cacheEntry) {
/**
* If the cache entry present was marked as temporary, it means that we prefetched it from the navigate reducer,
* where we didn't have the prefetch intent. We want to update it to the new, more accurate, kind here.
*/
if (cacheEntry.kind === PrefetchKind.TEMPORARY) {
console.log(href, action.kind, cacheEntry)
state.prefetchCache.set(href, {
...cacheEntry,
kind: action.kind,
})
}
/**
* if the prefetch action was a full prefetch and that the current cache entry wasn't one, we want to re-prefetch,
* otherwise we can re-use the current cache entry
**/
if (
!(
cacheEntry.kind === PrefetchKind.AUTO &&
action.kind === PrefetchKind.FULL
)
) {
// If the href was already prefetched it is not necessary to prefetch it again
if (state.prefetchCache.has(href)) {
return state
}
}
// fetchServerResponse is intentionally not awaited so that it can be unwrapped in the navigate-reducer
const serverResponse = createRecordFromThenable(
@ -58,7 +30,7 @@ export function prefetchReducer(
// initialTree is used when history.state.tree is missing because the history state is set in `useEffect` below, it being missing means this is the hydration case.
state.tree,
state.nextUrl,
action.kind
true
)
)
@ -67,9 +39,6 @@ export function prefetchReducer(
// Create new tree based on the flightSegmentPath and router state patch
treeAtTimeOfPrefetch: state.tree,
data: serverResponse,
kind: action.kind,
prefetchTime: Date.now(),
lastUsedTime: null,
})
return state

View file

@ -1,18 +0,0 @@
import type { ReducerState } from '../router-reducer-types'
import {
PrefetchCacheEntryStatus,
getPrefetchEntryCacheStatus,
} from '../get-prefetch-cache-entry-status'
export function prunePrefetchCache(
prefetchCache: ReducerState['prefetchCache']
) {
for (const [href, prefetchCacheEntry] of prefetchCache) {
if (
getPrefetchEntryCacheStatus(prefetchCacheEntry) ===
PrefetchCacheEntryStatus.expired
) {
prefetchCache.delete(href)
}
}
}

View file

@ -117,19 +117,6 @@ export interface ServerPatchAction {
mutable: Mutable
}
/**
* PrefetchKind defines the type of prefetching that should be done.
* - `auto` - if the page is dynamic, prefetch the page data partially, if static prefetch the page data fully.
* - `full` - prefetch the page data fully.
* - `temporary` - a temporary prefetch entry is added to the cache, this is used when prefetch={false} is used in next/link or when you push a route programmatically.
*/
export enum PrefetchKind {
AUTO = 'auto',
FULL = 'full',
TEMPORARY = 'temporary',
}
/**
* Prefetch adds the provided FlightData to the prefetch cache
* - Creates the router state tree based on the patch in FlightData
@ -139,7 +126,6 @@ export enum PrefetchKind {
export interface PrefetchAction {
type: typeof ACTION_PREFETCH
url: URL
kind: PrefetchKind
}
interface PushRef {
@ -168,14 +154,6 @@ export type FocusAndScrollRef = {
segmentPaths: FlightSegmentPath[]
}
export type PrefetchCacheEntry = {
treeAtTimeOfPrefetch: FlightRouterState
data: ReturnType<typeof fetchServerResponse> | null
kind: PrefetchKind
prefetchTime: number
lastUsedTime: number | null
}
/**
* Handles keeping the state of app-router.
*/
@ -195,7 +173,13 @@ export type AppRouterState = {
/**
* Cache that holds prefetched Flight responses keyed by url.
*/
prefetchCache: Map<string, PrefetchCacheEntry>
prefetchCache: Map<
string,
{
treeAtTimeOfPrefetch: FlightRouterState
data: ReturnType<typeof fetchServerResponse> | null
}
>
/**
* Decides if the update should create a new history entry and if the navigation has to trigger a browser navigation.
*/

View file

@ -16,12 +16,10 @@ import { RouterContext } from '../shared/lib/router-context'
import {
AppRouterContext,
AppRouterInstance,
PrefetchOptions as AppRouterPrefetchOptions,
} from '../shared/lib/app-router-context'
import { useIntersection } from './use-intersection'
import { getDomainLocale } from './get-domain-locale'
import { addBasePath } from './add-base-path'
import { PrefetchKind } from './components/router-reducer/router-reducer-types'
type Url = string | UrlObject
type RequiredKeys<T> = {
@ -122,7 +120,6 @@ function prefetch(
href: string,
as: string,
options: PrefetchOptions,
appOptions: AppRouterPrefetchOptions,
isAppRouter: boolean
): void {
if (typeof window === 'undefined') {
@ -157,15 +154,11 @@ function prefetch(
prefetched.add(prefetchedKey)
}
const prefetchPromise = isAppRouter
? (router as AppRouterInstance).prefetch(href, appOptions)
: (router as NextRouter).prefetch(href, as, options)
// Prefetch the JSON page if asked (only in the client)
// We need to handle a prefetch error here since we may be
// loading with priority which can reject but we don't
// want to force navigation since this is only a prefetch
Promise.resolve(prefetchPromise).catch((err) => {
Promise.resolve(router.prefetch(href, as, options)).catch((err) => {
if (process.env.NODE_ENV !== 'production') {
// rethrow to show invalid URL errors
throw err
@ -255,51 +248,6 @@ function formatStringOrUrl(urlObjOrString: UrlObject | string): string {
*/
const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
function LinkComponent(props, forwardedRef) {
let children: React.ReactNode
const {
href: hrefProp,
as: asProp,
children: childrenProp,
prefetch: prefetchProp = null,
passHref,
replace,
shallow,
scroll,
locale,
onClick,
onMouseEnter: onMouseEnterProp,
onTouchStart: onTouchStartProp,
// @ts-expect-error this is inlined as a literal boolean not a string
legacyBehavior = process.env.__NEXT_NEW_LINK_BEHAVIOR === false,
...restProps
} = props
children = childrenProp
if (
legacyBehavior &&
(typeof children === 'string' || typeof children === 'number')
) {
children = <a>{children}</a>
}
const prefetchEnabled = prefetchProp !== false
/**
* The possible states for prefetch are:
* - null: this is the default "auto" mode, where we will prefetch partially if the link is in the viewport
* - true: we will prefetch if the link is visible and prefetch the full page, not just partially
* - false: we will not prefetch if in the viewport at all
*/
const appPrefetchKind =
prefetchProp === null ? PrefetchKind.AUTO : PrefetchKind.FULL
const pagesRouter = React.useContext(RouterContext)
const appRouter = React.useContext(AppRouterContext)
const router = pagesRouter ?? appRouter
// We're in the app directory if there is no pages router.
const isAppRouter = !pagesRouter
if (process.env.NODE_ENV !== 'production') {
function createPropError(args: {
key: string
@ -413,7 +361,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
// This hook is in a conditional but that is ok because `process.env.NODE_ENV` never changes
// eslint-disable-next-line react-hooks/rules-of-hooks
const hasWarned = React.useRef(false)
if (props.prefetch && !hasWarned.current && !isAppRouter) {
if (props.prefetch && !hasWarned.current) {
hasWarned.current = true
console.warn(
'Next.js auto-prefetches automatically based on viewport. The prefetch attribute is no longer needed. More: https://nextjs.org/docs/messages/prefetch-true-deprecated'
@ -421,6 +369,44 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
}
}
let children: React.ReactNode
const {
href: hrefProp,
as: asProp,
children: childrenProp,
prefetch: prefetchProp,
passHref,
replace,
shallow,
scroll,
locale,
onClick,
onMouseEnter: onMouseEnterProp,
onTouchStart: onTouchStartProp,
// @ts-expect-error this is inlined as a literal boolean not a string
legacyBehavior = process.env.__NEXT_NEW_LINK_BEHAVIOR === false,
...restProps
} = props
children = childrenProp
if (
legacyBehavior &&
(typeof children === 'string' || typeof children === 'number')
) {
children = <a>{children}</a>
}
const prefetchEnabled = prefetchProp !== false
const pagesRouter = React.useContext(RouterContext)
const appRouter = React.useContext(AppRouterContext)
const router = pagesRouter ?? appRouter
// We're in the app directory if there is no pages router.
const isAppRouter = !pagesRouter
if (process.env.NODE_ENV !== 'production') {
if (isAppRouter && !asProp) {
let href: string | undefined
@ -560,16 +546,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
}
// Prefetch the URL.
prefetch(
router,
href,
as,
{ locale },
{
kind: appPrefetchKind,
},
isAppRouter
)
prefetch(router, href, as, { locale }, isAppRouter)
}, [
as,
href,
@ -579,7 +556,6 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
pagesRouter?.locale,
router,
isAppRouter,
appPrefetchKind,
])
const childProps: {
@ -663,9 +639,6 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
// @see {https://github.com/vercel/next.js/discussions/40268?sort=top#discussioncomment-3572642}
bypassPrefetchedCheck: true,
},
{
kind: appPrefetchKind,
},
isAppRouter
)
},
@ -700,9 +673,6 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
// @see {https://github.com/vercel/next.js/discussions/40268?sort=top#discussioncomment-3572642}
bypassPrefetchedCheck: true,
},
{
kind: appPrefetchKind,
},
isAppRouter
)
},

View file

@ -1,9 +1,6 @@
'use client'
import {
FocusAndScrollRef,
PrefetchKind,
} from '../../client/components/router-reducer/router-reducer-types'
import { FocusAndScrollRef } from '../../client/components/router-reducer/router-reducer-types'
import type { fetchServerResponse } from '../../client/components/router-reducer/fetch-server-response'
import type {
FlightRouterState,
@ -71,10 +68,6 @@ export interface NavigateOptions {
forceOptimisticNavigation?: boolean
}
export interface PrefetchOptions {
kind: PrefetchKind
}
export interface AppRouterInstance {
/**
* Navigate to the previous history entry.
@ -101,7 +94,7 @@ export interface AppRouterInstance {
/**
* Prefetch the provided href.
*/
prefetch(href: string, options?: PrefetchOptions): void
prefetch(href: string): void
}
export const AppRouterContext = React.createContext<AppRouterInstance | null>(

View file

@ -1,9 +0,0 @@
export default async function Page() {
const randomNumber = Math.random()
return (
<div>
<div>LOADING</div>
<div id="loading">{randomNumber}</div>
</div>
)
}

View file

@ -1,21 +0,0 @@
import Link from 'next/link'
export default async function Page({ searchParams: { timeout } }) {
const randomNumber = await new Promise((resolve) => {
setTimeout(
() => {
resolve(Math.random())
},
timeout !== undefined ? Number.parseInt(timeout, 10) : 0
)
})
return (
<>
<div>
<Link href="/"> Back to Home </Link>
</div>
<div id="random-number">{randomNumber}</div>
</>
)
}

View file

@ -1,8 +0,0 @@
export default function Root({ children }) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}

View file

@ -1,25 +0,0 @@
import Link from 'next/link'
export default function HomePage() {
return (
<>
<div>
<Link href="/0?timeout=0" prefetch={true}>
To Random Number - prefetch: true
</Link>
</div>
<div>
<Link href="/1">To Random Number - prefetch: auto</Link>
</div>
<div>
<Link href="/2" prefetch={false}>
To Random Number 2 - prefetch: false
</Link>
</div>
<div>
<Link href="/1?timeout=1000">
To Random Number - prefetch: auto, slow
</Link>
</div>
</>
)
}

View file

@ -1,386 +0,0 @@
import { createNextDescribe } from 'e2e-utils'
import { check } from 'next-test-utils'
import { BrowserInterface } from 'test/lib/browsers/base'
import { Request } from 'playwright-chromium'
const getPathname = (url: string) => {
const urlObj = new URL(url)
return urlObj.pathname
}
const browserConfigWithFixedTime = {
beforePageLoad: (page) => {
page.addInitScript(() => {
const startTime = new Date()
const fixedTime = new Date('2023-04-17T00:00:00Z')
// Override the Date constructor
// @ts-ignore
// eslint-disable-next-line no-native-reassign
Date = class extends Date {
constructor() {
super()
// @ts-ignore
return new startTime.constructor(fixedTime)
}
static now() {
return fixedTime.getTime()
}
}
})
},
}
const fastForwardTo = (ms) => {
// Increment the fixed time by the specified duration
const currentTime = new Date()
currentTime.setTime(currentTime.getTime() + ms)
// Update the Date constructor to use the new fixed time
// @ts-ignore
// eslint-disable-next-line no-native-reassign
Date = class extends Date {
constructor() {
super()
// @ts-ignore
return new currentTime.constructor(currentTime)
}
static now() {
return currentTime.getTime()
}
}
}
const createRequestsListener = async (browser: BrowserInterface) => {
// wait for network idle
await browser.waitForIdleNetwork()
let requests = []
browser.on('request', (req: Request) => {
requests.push([req.url(), !!req.headers()['next-router-prefetch']])
})
await browser.refresh()
return {
getRequests: () => requests,
clearRequests: () => {
requests = []
},
}
}
createNextDescribe(
'app dir client cache semantics',
{
files: __dirname,
},
({ next, isNextDev }) => {
if (isNextDev) {
// since the router behavior is different in dev 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, 30 * 1000)
await browser.elementByCss('[href="/"]').click()
const newNumber = await browser
.elementByCss('[href="/1"]')
.click()
.waitForElementByCss('#random-number')
.text()
expect(newNumber).not.toBe(randomNumber)
})
it('should refetch below the fold after 30 seconds', async () => {
const randomLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const randomNumber = await browser
.waitForElementByCss('#random-number')
.text()
await browser.elementByCss('[href="/"]').click()
await browser.eval(fastForwardTo, 30 * 1000)
const newLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const newNumber = await browser
.waitForElementByCss('#random-number')
.text()
expect(newLoadingNumber).toBe(randomLoadingNumber)
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 randomNumber = await browser
.waitForElementByCss('#random-number')
.text()
await browser.eval(fastForwardTo, 5 * 60 * 1000)
await browser
.elementByCss('[href="/"]')
.click()
.waitForElementByCss('[href="/1?timeout=1000"]')
const newLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const newNumber = await browser
.waitForElementByCss('#random-number')
.text()
expect(newLoadingNumber).not.toBe(randomLoadingNumber)
expect(newNumber).not.toBe(randomNumber)
})
})
describe('router.push', () => {
it('should re-use the cache for 30 seconds', async () => {})
it('should fully refetch the page after 30 seconds', async () => {})
})
}
}
)

View file

@ -1,5 +0,0 @@
module.exports = {
experimental: {
appDir: true,
},
}

View file

@ -1,30 +1,6 @@
import { createNextDescribe } from 'e2e-utils'
import { check, waitFor } from 'next-test-utils'
const browserConfigWithFixedTime = {
beforePageLoad: (page) => {
page.addInitScript(() => {
const startTime = new Date()
const fixedTime = new Date('2023-04-17T00:00:00Z')
// Override the Date constructor
// @ts-ignore
// eslint-disable-next-line no-native-reassign
Date = class extends Date {
constructor() {
super()
// @ts-ignore
return new startTime.constructor(fixedTime)
}
static now() {
return fixedTime.getTime()
}
}
})
},
}
createNextDescribe(
'app dir prefetching',
{
@ -39,7 +15,7 @@ createNextDescribe(
}
it('should show layout eagerly when prefetched with loading one level down', async () => {
const browser = await next.browser('/', browserConfigWithFixedTime)
const browser = await next.browser('/')
// Ensure the page is prefetched
await waitFor(1000)
@ -74,7 +50,7 @@ createNextDescribe(
})
it('should not fetch again when a static page was prefetched', async () => {
const browser = await next.browser('/404', browserConfigWithFixedTime)
const browser = await next.browser('/404')
let requests: string[] = []
browser.on('request', (req) => {
@ -102,7 +78,7 @@ createNextDescribe(
})
it('should not fetch again when a static page was prefetched when navigating to it twice', async () => {
const browser = await next.browser('/404', browserConfigWithFixedTime)
const browser = await next.browser('/404')
let requests: string[] = []
browser.on('request', (req) => {

View file

@ -522,10 +522,17 @@ createNextDescribe(
await browser.waitForElementByCss('#render-id-456')
expect(await browser.eval('window.history.length')).toBe(3)
// Get the id on the rendered page.
const firstID = await browser.elementById('render-id-456').text()
// Go back, and redo the navigation by clicking the link.
await browser.back()
await browser.elementById('link').click()
await browser.waitForElementByCss('#render-id-456')
// Get the id again, and compare, they should not be the same.
const secondID = await browser.elementById('render-id-456').text()
expect(secondID).not.toBe(firstID)
} finally {
await browser.close()
}
@ -542,6 +549,9 @@ createNextDescribe(
await browser.waitForElementByCss('#render-id-456')
expect(await browser.eval('window.history.length')).toBe(2)
// Get the date again, and compare, they should not be the same.
const firstId = await browser.elementById('render-id-456').text()
// Navigate to the subpage, verify that the history entry was NOT added.
await browser.elementById('link').click()
await browser.waitForElementByCss('#render-id-123')
@ -551,6 +561,10 @@ createNextDescribe(
await browser.elementById('link').click()
await browser.waitForElementByCss('#render-id-456')
expect(await browser.eval('window.history.length')).toBe(2)
// Get the date again, and compare, they should not be the same.
const secondId = await browser.elementById('render-id-456').text()
expect(firstId).not.toBe(secondId)
} finally {
await browser.close()
}

View file

@ -122,13 +122,10 @@ export class BrowserInterface implements PromiseLike<any> {
async getAttribute<T = any>(name: string): Promise<T> {
return
}
async eval<T = any>(snippet: string | Function, ...args: any[]): Promise<T> {
async eval<T = any>(snippet: string | Function): Promise<T> {
return
}
async evalAsync<T = any>(
snippet: string | Function,
...args: any[]
): Promise<T> {
async evalAsync<T = any>(snippet: string | Function): Promise<T> {
return
}
async text(): Promise<string> {
@ -151,6 +148,4 @@ export class BrowserInterface implements PromiseLike<any> {
async url(): Promise<string> {
return ''
}
async waitForIdleNetwork(): Promise<void> {}
}

View file

@ -350,10 +350,10 @@ export class Playwright extends BrowserInterface {
})
}
eval<T = any>(fn: any, ...args: any[]): Promise<T> {
eval<T = any>(snippet): Promise<T> {
return this.chainWithReturnValue(() =>
page
.evaluate(fn, ...args)
.evaluate(snippet)
.catch((err) => {
console.error('eval error:', err)
return null
@ -365,15 +365,15 @@ export class Playwright extends BrowserInterface {
)
}
async evalAsync<T = any>(fn: any, ...args: any[]) {
if (typeof fn === 'function') {
fn = fn.toString()
async evalAsync<T = any>(snippet) {
if (typeof snippet === 'function') {
snippet = snippet.toString()
}
if (fn.includes(`var callback = arguments[arguments.length - 1]`)) {
fn = `(function() {
if (snippet.includes(`var callback = arguments[arguments.length - 1]`)) {
snippet = `(function() {
return new Promise((resolve, reject) => {
const origFunc = ${fn}
const origFunc = ${snippet}
try {
origFunc(resolve)
} catch (err) {
@ -383,7 +383,7 @@ export class Playwright extends BrowserInterface {
})()`
}
return page.evaluate<T>(fn).catch(() => null)
return page.evaluate<T>(snippet).catch(() => null)
}
async log() {
@ -397,10 +397,4 @@ export class Playwright extends BrowserInterface {
async url() {
return this.chain(() => page.evaluate('window.location.href')) as any
}
async waitForIdleNetwork(): Promise<void> {
return this.chain(() => {
return page.waitForLoadState('networkidle')
})
}
}