Skip to content

Commit eaa80b5

Browse files
committed
Implements new connection api
1 parent ab56c12 commit eaa80b5

File tree

14 files changed

+335
-6
lines changed

14 files changed

+335
-6
lines changed

packages/next/server.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ export { URLPattern } from 'next/dist/compiled/@edge-runtime/primitives/url'
1414
export { ImageResponse } from 'next/dist/server/web/spec-extension/image-response'
1515
export type { ImageResponseOptions } from 'next/dist/compiled/@vercel/og/types'
1616
export { unstable_after } from 'next/dist/server/after'
17+
export { connection } from 'next/dist/server/request/connection'
1718
export type { UnsafeUnwrappedSearchParams } from 'next/dist/server/request/search-params'
1819
export type { UnsafeUnwrappedParams } from 'next/dist/server/request/params'

packages/next/server.js

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const serverExports = {
1212
URLPattern: require('next/dist/server/web/spec-extension/url-pattern')
1313
.URLPattern,
1414
unstable_after: require('next/dist/server/after').unstable_after,
15+
connection: require('next/dist/server/request/connection').connection,
1516
}
1617

1718
// https://nodejs.org/api/esm.html#commonjs-namespaces
@@ -26,3 +27,4 @@ exports.userAgentFromString = serverExports.userAgentFromString
2627
exports.userAgent = serverExports.userAgent
2728
exports.URLPattern = serverExports.URLPattern
2829
exports.unstable_after = serverExports.unstable_after
30+
exports.connection = serverExports.connection
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { staticGenerationAsyncStorage } from '../../client/components/static-generation-async-storage.external'
2+
import { prerenderAsyncStorage } from '../app-render/prerender-async-storage.external'
3+
import {
4+
postponeWithTracking,
5+
interruptStaticGeneration,
6+
trackDynamicDataInDynamicRender,
7+
} from '../app-render/dynamic-rendering'
8+
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout'
9+
10+
/**
11+
* This function allows you to indicate that you require an actual user Request before continuing.
12+
*
13+
* During prerendering it will never resolve and during rendering it resolves immediately.
14+
*/
15+
export function connection(): Promise<void> {
16+
const staticGenerationStore = staticGenerationAsyncStorage.getStore()
17+
const prerenderStore = prerenderAsyncStorage.getStore()
18+
19+
if (staticGenerationStore) {
20+
if (staticGenerationStore.forceStatic) {
21+
// When using forceStatic we override all other logic and always just return an empty
22+
// headers object without tracking
23+
return Promise.resolve(undefined)
24+
}
25+
26+
if (staticGenerationStore.isUnstableCacheCallback) {
27+
throw new Error(
28+
`Route ${staticGenerationStore.route} used "connection" inside a function cached with "unstable_cache(...)". The \`connection()\` function is used to wait indicate the subsequent code must only run when there is an actual Request but caches must be able to be produced before a Request so this function is not allowed in this scope. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache`
29+
)
30+
} else if (staticGenerationStore.dynamicShouldError) {
31+
throw new StaticGenBailoutError(
32+
`Route ${staticGenerationStore.route} with \`dynamic = "error"\` couldn't be rendered statically because it used \`connection\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
33+
)
34+
}
35+
36+
if (prerenderStore) {
37+
// We are in PPR and/or dynamicIO mode and prerendering
38+
39+
if (prerenderStore.controller || prerenderStore.cacheSignal) {
40+
// We use the controller and cacheSignal as an indication we are in dynamicIO mode.
41+
// When resolving headers for a prerender with dynamic IO we return a forever promise
42+
// along with property access tracked synchronous headers.
43+
44+
// We don't track dynamic access here because access will be tracked when you access
45+
// one of the properties of the headers object.
46+
return new Promise(hangForever)
47+
} else {
48+
// We are prerendering with PPR. We need track dynamic access here eagerly
49+
// to keep continuity with how headers has worked in PPR without dynamicIO.
50+
// TODO consider switching the semantic to throw on property access intead
51+
postponeWithTracking(
52+
staticGenerationStore.route,
53+
'connection',
54+
prerenderStore.dynamicTracking
55+
)
56+
}
57+
} else if (staticGenerationStore.isStaticGeneration) {
58+
// We are in a legacy static generation mode while prerendering
59+
// We treat this function call as a bailout of static generation
60+
interruptStaticGeneration('connection', staticGenerationStore)
61+
}
62+
// We fall through to the dynamic context below but we still track dynamic access
63+
// because in dev we can still error for things like using headers inside a cache context
64+
trackDynamicDataInDynamicRender(staticGenerationStore)
65+
}
66+
67+
return Promise.resolve(undefined)
68+
}
69+
70+
function hangForever() {}

packages/next/src/server/web/exports/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export { NextResponse } from '../spec-extension/response'
66
export { userAgent, userAgentFromString } from '../spec-extension/user-agent'
77
export { URLPattern } from '../spec-extension/url-pattern'
88
export { unstable_after } from '../../after'
9+
export { connection } from '../../request/connection'

test/e2e/app-dir/dynamic-data/dynamic-data.test.ts

+26
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,16 @@ describe('dynamic-data with dynamic = "error"', () => {
199199
await browser.close()
200200
}
201201

202+
browser = await next.browser('/connection')
203+
try {
204+
await assertHasRedbox(browser)
205+
expect(await getRedboxHeader(browser)).toMatch(
206+
'Error: Route /connection with `dynamic = "error"` couldn\'t be rendered statically because it used `connection`'
207+
)
208+
} finally {
209+
await browser.close()
210+
}
211+
202212
browser = await next.browser('/headers?foo=foosearch')
203213
try {
204214
await assertHasRedbox(browser)
@@ -230,6 +240,9 @@ describe('dynamic-data with dynamic = "error"', () => {
230240
expect(next.cliOutput).toMatch(
231241
'Error: Route /cookies with `dynamic = "error"` couldn\'t be rendered statically because it used `cookies`'
232242
)
243+
expect(next.cliOutput).toMatch(
244+
'Error: Route /connection with `dynamic = "error"` couldn\'t be rendered statically because it used `connection`'
245+
)
233246
expect(next.cliOutput).toMatch(
234247
'Error: Route /headers with `dynamic = "error"` couldn\'t be rendered statically because it used `headers`'
235248
)
@@ -277,6 +290,16 @@ describe('dynamic-data inside cache scope', () => {
277290
await browser.close()
278291
}
279292

293+
browser = await next.browser('/connection')
294+
try {
295+
await assertHasRedbox(browser)
296+
expect(await getRedboxHeader(browser)).toMatch(
297+
'Error: Route /connection used "connection" inside a function cached with "unstable_cache(...)".'
298+
)
299+
} finally {
300+
await browser.close()
301+
}
302+
280303
browser = await next.browser('/headers')
281304
try {
282305
await assertHasRedbox(browser)
@@ -297,6 +320,9 @@ describe('dynamic-data inside cache scope', () => {
297320
expect(next.cliOutput).toMatch(
298321
'Error: Route /cookies used "cookies" inside a function cached with "unstable_cache(...)".'
299322
)
323+
expect(next.cliOutput).toMatch(
324+
'Error: Route /connection used "connection" inside a function cached with "unstable_cache(...)".'
325+
)
300326
expect(next.cliOutput).toMatch(
301327
'Error: Route /headers used "headers" inside a function cached with "unstable_cache(...)".'
302328
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { connection } from 'next/server'
2+
import { unstable_cache as cache } from 'next/cache'
3+
4+
const cachedConnection = cache(async () => connection())
5+
6+
export default async function Page({ searchParams }) {
7+
await cachedConnection()
8+
return (
9+
<div>
10+
<section>
11+
This example uses `connection()` inside `unstable_cache` which should
12+
cause the build to fail
13+
</section>
14+
</div>
15+
)
16+
}

test/e2e/app-dir/dynamic-data/fixtures/main/app/force-dynamic/page.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import { headers, cookies } from 'next/headers'
2+
import { connection } from 'next/server'
23

34
import { PageSentinel } from '../getSentinelValue'
45

56
export const dynamic = 'force-dynamic'
67

78
export default async function Page({ searchParams }) {
9+
await connection()
810
return (
911
<div>
1012
<PageSentinel />
1113
<section>
12-
This example uses headers/cookies/searchParams directly in a Page
13-
configured with `dynamic = 'force-dynamic'`. This should cause the page
14-
to always render dynamically regardless of dynamic APIs used
14+
This example uses headers/cookies/conneciton/searchParams directly in a
15+
Page configured with `dynamic = 'force-dynamic'`. This should cause the
16+
page to always render dynamically regardless of dynamic APIs used
1517
</section>
1618
<section id="headers">
1719
<h3>headers</h3>

test/e2e/app-dir/dynamic-data/fixtures/main/app/force-static/page.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import { headers, cookies } from 'next/headers'
2+
import { connection } from 'next/server'
23

34
import { PageSentinel } from '../getSentinelValue'
45

56
export const dynamic = 'force-static'
67

78
export default async function Page({ searchParams }) {
9+
await connection()
810
return (
911
<div>
1012
<PageSentinel />
1113
<section>
12-
This example uses headers/cookies/searchParams directly in a Page
13-
configured with `dynamic = 'force-static'`. This should cause the page
14-
to always statically render but without exposing dynamic data
14+
This example uses headers/cookies/connection/searchParams directly in a
15+
Page configured with `dynamic = 'force-static'`. This should cause the
16+
page to always statically render but without exposing dynamic data
1517
</section>
1618
<section id="headers">
1719
<h3>headers</h3>

test/e2e/app-dir/dynamic-data/fixtures/main/app/top-level/page.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { headers, cookies } from 'next/headers'
2+
import { connection } from 'next/server'
23

34
import { PageSentinel } from '../getSentinelValue'
45

56
export default async function Page({ searchParams }) {
7+
await connection()
68
return (
79
<div>
810
<PageSentinel />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Server, { connection } from 'next/server'
2+
3+
console.log('Server', Server)
4+
5+
export const dynamic = 'error'
6+
7+
export default async function Page({ searchParams }) {
8+
await connection()
9+
return (
10+
<div>
11+
<section>
12+
This example uses `connection()` but is configured with `dynamic =
13+
'error'` which should cause the page to fail to build
14+
</section>
15+
</div>
16+
)
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Suspense } from 'react'
2+
import { connection } from 'next/server'
3+
4+
import { getSentinelValue } from '../../../getSentinelValue'
5+
/**
6+
* This test case is constructed to demonstrate how using the async form of cookies can lead to a better
7+
* prerender with dynamic IO when PPR is on. There is no difference when PPR is off. When PPR is on the second component
8+
* can finish rendering before the prerender completes and so we can produce a static shell where the Fallback closest
9+
* to Cookies access is read
10+
*/
11+
export default async function Page() {
12+
return (
13+
<>
14+
<Suspense fallback="loading...">
15+
<Component />
16+
</Suspense>
17+
<ComponentTwo />
18+
<div id="page">{getSentinelValue()}</div>
19+
</>
20+
)
21+
}
22+
23+
async function Component() {
24+
await connection()
25+
return (
26+
<div>
27+
cookie <span id="foo">foo</span>
28+
</div>
29+
)
30+
}
31+
32+
function ComponentTwo() {
33+
return <p>footer</p>
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Suspense } from 'react'
2+
import { connection } from 'next/server'
3+
4+
import { getSentinelValue } from '../../../getSentinelValue'
5+
6+
export default async function Page() {
7+
const pendingConnection = connection()
8+
return (
9+
<section>
10+
<h1>Deep Connection Reader</h1>
11+
<p>
12+
This component was passed the connection promise returned by
13+
`connection()`. It is rendered inside a Suspense boundary.
14+
</p>
15+
<p>
16+
If dynamicIO is turned off the `connection()` call would trigger a
17+
dynamic point at the callsite and the suspense boundary would also be
18+
blocked for over one second
19+
</p>
20+
<Suspense
21+
fallback={
22+
<>
23+
<p>loading connection...</p>
24+
<div id="fallback">{getSentinelValue()}</div>
25+
</>
26+
}
27+
>
28+
<DeepConnectionReader pendingConnection={pendingConnection} />
29+
</Suspense>
30+
</section>
31+
)
32+
}
33+
34+
async function DeepConnectionReader({
35+
pendingConnection,
36+
}: {
37+
pendingConnection: ReturnType<typeof connection>
38+
}) {
39+
await pendingConnection
40+
return (
41+
<>
42+
<p>The connection was awaited</p>
43+
<div id="page">{getSentinelValue()}</div>
44+
</>
45+
)
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { connection } from 'next/server'
2+
3+
import { getSentinelValue } from '../../../getSentinelValue'
4+
/**
5+
* This test case is constructed to demonstrate how using the async form of cookies can lead to a better
6+
* prerender with dynamic IO when PPR is on. There is no difference when PPR is off. When PPR is on the second component
7+
* can finish rendering before the prerender completes and so we can produce a static shell where the Fallback closest
8+
* to Cookies access is read
9+
*/
10+
export default async function Page() {
11+
return (
12+
<>
13+
<Component />
14+
<ComponentTwo />
15+
<div id="page">{getSentinelValue()}</div>
16+
</>
17+
)
18+
}
19+
20+
async function Component() {
21+
await connection()
22+
return (
23+
<div>
24+
cookie <span id="foo">foo</span>
25+
</div>
26+
)
27+
}
28+
29+
function ComponentTwo() {
30+
return <p>footer</p>
31+
}

0 commit comments

Comments
 (0)