Skip to content

Commit 5398481

Browse files
committed
[Cache Components] defer cookies(), headers(), and draftMode() to a new task in dev
When resolving these Request Data APIs in dev we now do so in a new Task. This allows the environment tracking to properly move stuff that was previously in the Prerender environment to the Server environment when a Request Data API is used before them. This also has the nice side effect of causing these functions to appear as IO in the new suspended-by features of React Devtools. In Prod we don't need to do any additional delaying because the all of the features this is designed to interact with are dev only
1 parent 0b2c3b1 commit 5398481

File tree

11 files changed

+201
-93
lines changed

11 files changed

+201
-93
lines changed

packages/next/src/server/app-render/app-render.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1765,6 +1765,8 @@ async function renderToHTMLOrFlightImpl(
17651765
renderOpts.renderResumeDataCache ?? postponedState?.renderResumeDataCache
17661766

17671767
const rootParams = getRootParams(loaderTree, ctx.getDynamicParamFromSegment)
1768+
const devValidatingFallbackParams =
1769+
getRequestMeta(req, 'devValidatingFallbackParams') || null
17681770
const requestStore = createRequestStoreForRender(
17691771
req,
17701772
res,
@@ -1775,7 +1777,8 @@ async function renderToHTMLOrFlightImpl(
17751777
renderOpts.previewProps,
17761778
isHmrRefresh,
17771779
serverComponentsHmrCache,
1778-
renderResumeDataCache
1780+
renderResumeDataCache,
1781+
devValidatingFallbackParams
17791782
)
17801783

17811784
if (
@@ -1847,7 +1850,8 @@ async function renderToHTMLOrFlightImpl(
18471850
notFoundLoaderTree,
18481851
formState,
18491852
postponedState,
1850-
metadata
1853+
metadata,
1854+
devValidatingFallbackParams
18511855
)
18521856

18531857
return new RenderResult(stream, {
@@ -1878,7 +1882,8 @@ async function renderToHTMLOrFlightImpl(
18781882
loaderTree,
18791883
formState,
18801884
postponedState,
1881-
metadata
1885+
metadata,
1886+
devValidatingFallbackParams
18821887
)
18831888

18841889
// Invalid dynamic usages should only error the request in development.
@@ -2064,7 +2069,8 @@ async function renderToStream(
20642069
tree: LoaderTree,
20652070
formState: any,
20662071
postponedState: PostponedState | null,
2067-
metadata: AppPageRenderResultMetadata
2072+
metadata: AppPageRenderResultMetadata,
2073+
devValidatingFallbackParams: FallbackRouteParams | null
20682074
): Promise<ReadableStream<Uint8Array>> {
20692075
const { assetPrefix, nonce, pagePath, renderOpts } = ctx
20702076

@@ -2212,9 +2218,6 @@ async function renderToStream(
22122218
}
22132219
)
22142220

2215-
const devValidatingFallbackParams =
2216-
getRequestMeta(req, 'devValidatingFallbackParams') || null
2217-
22182221
spawnDynamicValidationInDev(
22192222
resolveValidation,
22202223
tree,

packages/next/src/server/app-render/work-unit-async-storage.external.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { Params } from '../request/params'
1818
import type { ImplicitTags } from '../lib/implicit-tags'
1919
import type { WorkStore } from './work-async-storage.external'
2020
import { NEXT_HMR_REFRESH_HASH_COOKIE } from '../../client/components/app-router-headers'
21+
import { InvariantError } from '../../shared/lib/invariant-error'
2122

2223
export type WorkUnitPhase = 'action' | 'render' | 'after'
2324

@@ -67,6 +68,7 @@ export interface RequestStore extends CommonWorkUnitStore {
6768
// DEV-only
6869
usedDynamic?: boolean
6970
prerenderPhase?: boolean
71+
devFallbackParams?: FallbackRouteParams | null
7072
}
7173

7274
/**
@@ -311,6 +313,10 @@ export function throwForMissingRequestStore(callingExpression: string): never {
311313
)
312314
}
313315

316+
export function throwInvariantForMissingStore(): never {
317+
throw new InvariantError('Expected workUnitAsyncStorage to have a store.')
318+
}
319+
314320
export function getPrerenderResumeDataCache(
315321
workUnitStore: WorkUnitStore
316322
): PrerenderResumeDataCache | null {

packages/next/src/server/async-storage/request-store.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type { ServerComponentsHmrCache } from '../response-cache'
2424
import type { RenderResumeDataCache } from '../resume-data-cache/resume-data-cache'
2525
import type { Params } from '../request/params'
2626
import type { ImplicitTags } from '../lib/implicit-tags'
27+
import type { FallbackRouteParams } from '../request/fallback-params'
2728

2829
function getHeaders(headers: Headers | IncomingHttpHeaders): ReadonlyHeaders {
2930
const cleaned = HeadersAdapter.from(headers)
@@ -114,7 +115,8 @@ export function createRequestStoreForRender(
114115
previewProps: WrapperRenderOpts['previewProps'],
115116
isHmrRefresh: RequestContext['isHmrRefresh'],
116117
serverComponentsHmrCache: RequestContext['serverComponentsHmrCache'],
117-
renderResumeDataCache: RenderResumeDataCache | undefined
118+
renderResumeDataCache: RenderResumeDataCache | undefined,
119+
devFallbackParams: FallbackRouteParams | null
118120
): RequestStore {
119121
return createRequestStoreImpl(
120122
// Pages start in render phase by default
@@ -128,7 +130,8 @@ export function createRequestStoreForRender(
128130
renderResumeDataCache,
129131
previewProps,
130132
isHmrRefresh,
131-
serverComponentsHmrCache
133+
serverComponentsHmrCache,
134+
devFallbackParams
132135
)
133136
}
134137

@@ -151,7 +154,8 @@ export function createRequestStoreForAPI(
151154
undefined,
152155
previewProps,
153156
false,
154-
undefined
157+
undefined,
158+
null
155159
)
156160
}
157161

@@ -166,7 +170,8 @@ function createRequestStoreImpl(
166170
renderResumeDataCache: RenderResumeDataCache | undefined,
167171
previewProps: WrapperRenderOpts['previewProps'],
168172
isHmrRefresh: RequestContext['isHmrRefresh'],
169-
serverComponentsHmrCache: RequestContext['serverComponentsHmrCache']
173+
serverComponentsHmrCache: RequestContext['serverComponentsHmrCache'],
174+
devFallbackParams: FallbackRouteParams | null | undefined
170175
): RequestStore {
171176
function defaultOnUpdateCookies(cookies: string[]) {
172177
if (res) {
@@ -258,6 +263,7 @@ function createRequestStoreImpl(
258263
serverComponentsHmrCache:
259264
serverComponentsHmrCache ||
260265
(globalThis as any).__serverComponentsHmrCache,
266+
devFallbackParams,
261267
}
262268
}
263269

packages/next/src/server/dynamic-rendering-utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,14 @@ export function makeHangingPromise<T>(
7272
}
7373

7474
function ignoreReject() {}
75+
76+
export function makeDevtoolsIOAwarePromise<T>(underlying: T): Promise<T> {
77+
// in React DevTools if we resolve in a setTimeout we will observe
78+
// the promise resolution as something that can suspend a boundary or root.
79+
return new Promise<T>((resolve) => {
80+
// Must use setTimeout to be considered IO React DevTools. setImmediate will not work.
81+
setTimeout(() => {
82+
resolve(underlying)
83+
}, 0)
84+
})
85+
}

packages/next/src/server/request/connection.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { workAsyncStorage } from '../app-render/work-async-storage.external'
2-
import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external'
2+
import {
3+
throwForMissingRequestStore,
4+
workUnitAsyncStorage,
5+
} from '../app-render/work-unit-async-storage.external'
36
import {
47
postponeWithTracking,
58
throwToInterruptStaticGeneration,
69
trackDynamicDataInDynamicRender,
710
} from '../app-render/dynamic-rendering'
811
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout'
9-
import { makeHangingPromise } from '../dynamic-rendering-utils'
12+
import {
13+
makeHangingPromise,
14+
makeDevtoolsIOAwarePromise,
15+
} from '../dynamic-rendering-utils'
1016
import { isRequestAPICallableInsideAfter } from './utils'
1117

1218
/**
@@ -15,6 +21,7 @@ import { isRequestAPICallableInsideAfter } from './utils'
1521
* During prerendering it will never resolve and during rendering it resolves immediately.
1622
*/
1723
export function connection(): Promise<void> {
24+
const callingExpression = 'connection'
1825
const workStore = workAsyncStorage.getStore()
1926
const workUnitStore = workUnitAsyncStorage.getStore()
2027

@@ -94,12 +101,20 @@ export function connection(): Promise<void> {
94101
)
95102
case 'request':
96103
trackDynamicDataInDynamicRender(workUnitStore)
97-
break
104+
if (process.env.NODE_ENV === 'development') {
105+
// Semantically we only need the dev tracking when running in `next dev`
106+
// but since you would never use next dev with production NODE_ENV we use this
107+
// as a proxy so we can statically exclude this code from production builds.
108+
return makeDevtoolsIOAwarePromise(undefined)
109+
} else {
110+
return Promise.resolve(undefined)
111+
}
98112
default:
99113
workUnitStore satisfies never
100114
}
101115
}
102116
}
103117

104-
return Promise.resolve(undefined)
118+
// If we end up here, there was no work store or work unit store present.
119+
throwForMissingRequestStore(callingExpression)
105120
}

packages/next/src/server/request/cookies.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import {
2121
trackSynchronousRequestDataAccessInDev,
2222
} from '../app-render/dynamic-rendering'
2323
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout'
24-
import { makeHangingPromise } from '../dynamic-rendering-utils'
24+
import {
25+
makeDevtoolsIOAwarePromise,
26+
makeHangingPromise,
27+
} from '../dynamic-rendering-utils'
2528
import { createDedupedByCallsiteServerErrorLoggerDev } from '../create-deduped-by-callsite-server-error-logger'
26-
import { scheduleImmediate } from '../../lib/scheduler'
2729
import { isRequestAPICallableInsideAfter } from './utils'
2830
import { InvariantError } from '../../shared/lib/invariant-error'
2931
import { ReflectAdapter } from '../web/spec-extension/adapters/reflect'
@@ -134,10 +136,10 @@ export function cookies(): Promise<ReadonlyRequestCookies> {
134136
underlyingCookies = workUnitStore.cookies
135137
}
136138

137-
if (
138-
process.env.NODE_ENV === 'development' &&
139-
!workStore?.isPrefetchRequest
140-
) {
139+
if (process.env.NODE_ENV === 'development') {
140+
// Semantically we only need the dev tracking when running in `next dev`
141+
// but since you would never use next dev with production NODE_ENV we use this
142+
// as a proxy so we can statically exclude this code from production builds.
141143
if (process.env.__NEXT_CACHE_COMPONENTS) {
142144
return makeUntrackedCookiesWithDevWarnings(
143145
underlyingCookies,
@@ -263,9 +265,7 @@ function makeUntrackedExoticCookiesWithDevWarnings(
263265
return cachedCookies
264266
}
265267

266-
const promise = new Promise<ReadonlyRequestCookies>((resolve) =>
267-
scheduleImmediate(() => resolve(underlyingCookies))
268-
)
268+
const promise = makeDevtoolsIOAwarePromise(underlyingCookies)
269269
CachedCookies.set(underlyingCookies, promise)
270270

271271
Object.defineProperties(promise, {
@@ -416,9 +416,7 @@ function makeUntrackedCookiesWithDevWarnings(
416416
return cachedCookies
417417
}
418418

419-
const promise = new Promise<ReadonlyRequestCookies>((resolve) =>
420-
scheduleImmediate(() => resolve(underlyingCookies))
421-
)
419+
const promise = makeDevtoolsIOAwarePromise(underlyingCookies)
422420

423421
const proxiedPromise = new Proxy(promise, {
424422
get(target, prop, receiver) {

packages/next/src/server/request/headers.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
workAsyncStorage,
77
type WorkStore,
88
} from '../app-render/work-async-storage.external'
9-
import { throwForMissingRequestStore } from '../app-render/work-unit-async-storage.external'
109
import {
10+
throwForMissingRequestStore,
1111
workUnitAsyncStorage,
1212
type PrerenderStoreModern,
1313
} from '../app-render/work-unit-async-storage.external'
@@ -18,9 +18,11 @@ import {
1818
trackSynchronousRequestDataAccessInDev,
1919
} from '../app-render/dynamic-rendering'
2020
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout'
21-
import { makeHangingPromise } from '../dynamic-rendering-utils'
21+
import {
22+
makeDevtoolsIOAwarePromise,
23+
makeHangingPromise,
24+
} from '../dynamic-rendering-utils'
2225
import { createDedupedByCallsiteServerErrorLoggerDev } from '../create-deduped-by-callsite-server-error-logger'
23-
import { scheduleImmediate } from '../../lib/scheduler'
2426
import { isRequestAPICallableInsideAfter } from './utils'
2527
import { InvariantError } from '../../shared/lib/invariant-error'
2628
import { ReflectAdapter } from '../web/spec-extension/adapters/reflect'
@@ -153,10 +155,10 @@ export function headers(): Promise<ReadonlyHeaders> {
153155
case 'request':
154156
trackDynamicDataInDynamicRender(workUnitStore)
155157

156-
if (
157-
process.env.NODE_ENV === 'development' &&
158-
!workStore?.isPrefetchRequest
159-
) {
158+
if (process.env.NODE_ENV === 'development') {
159+
// Semantically we only need the dev tracking when running in `next dev`
160+
// but since you would never use next dev with production NODE_ENV we use this
161+
// as a proxy so we can statically exclude this code from production builds.
160162
if (process.env.__NEXT_CACHE_COMPONENTS) {
161163
return makeUntrackedHeadersWithDevWarnings(
162164
workUnitStore.headers,
@@ -263,9 +265,7 @@ function makeUntrackedExoticHeadersWithDevWarnings(
263265
return cachedHeaders
264266
}
265267

266-
const promise = new Promise<ReadonlyHeaders>((resolve) =>
267-
scheduleImmediate(() => resolve(underlyingHeaders))
268-
)
268+
const promise = makeDevtoolsIOAwarePromise(underlyingHeaders)
269269

270270
CachedHeaders.set(underlyingHeaders, promise)
271271

@@ -384,9 +384,7 @@ function makeUntrackedHeadersWithDevWarnings(
384384
return cachedHeaders
385385
}
386386

387-
const promise = new Promise<ReadonlyHeaders>((resolve) =>
388-
scheduleImmediate(() => resolve(underlyingHeaders))
389-
)
387+
const promise = makeDevtoolsIOAwarePromise(underlyingHeaders)
390388

391389
const proxiedPromise = new Proxy(promise, {
392390
get(target, prop, receiver) {

0 commit comments

Comments
 (0)