Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add connection() as a new dynamic API #69949

Merged
merged 1 commit into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/next/server.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export { URLPattern } from 'next/dist/compiled/@edge-runtime/primitives/url'
export { ImageResponse } from 'next/dist/server/web/spec-extension/image-response'
export type { ImageResponseOptions } from 'next/dist/compiled/@vercel/og/types'
export { unstable_after } from 'next/dist/server/after'
export { connection } from 'next/dist/server/request/connection'
export type { UnsafeUnwrappedSearchParams } from 'next/dist/server/request/search-params'
export type { UnsafeUnwrappedParams } from 'next/dist/server/request/params'
2 changes: 2 additions & 0 deletions packages/next/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const serverExports = {
URLPattern: require('next/dist/server/web/spec-extension/url-pattern')
.URLPattern,
unstable_after: require('next/dist/server/after').unstable_after,
connection: require('next/dist/server/request/connection').connection,
}

// https://nodejs.org/api/esm.html#commonjs-namespaces
Expand All @@ -26,3 +27,4 @@ exports.userAgentFromString = serverExports.userAgentFromString
exports.userAgent = serverExports.userAgent
exports.URLPattern = serverExports.URLPattern
exports.unstable_after = serverExports.unstable_after
exports.connection = serverExports.connection
72 changes: 72 additions & 0 deletions packages/next/src/server/request/connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { staticGenerationAsyncStorage } from '../../client/components/static-generation-async-storage.external'
import {
isDynamicIOPrerender,
prerenderAsyncStorage,
} from '../app-render/prerender-async-storage.external'
import {
postponeWithTracking,
throwToInterruptStaticGeneration,
trackDynamicDataInDynamicRender,
} from '../app-render/dynamic-rendering'
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout'
import { makeHangingPromise } from '../dynamic-rendering-utils'

/**
* This function allows you to indicate that you require an actual user Request before continuing.
*
* During prerendering it will never resolve and during rendering it resolves immediately.
*/
export function connection(): Promise<void> {
const staticGenerationStore = staticGenerationAsyncStorage.getStore()
const prerenderStore = prerenderAsyncStorage.getStore()

if (staticGenerationStore) {
if (staticGenerationStore.forceStatic) {
// When using forceStatic we override all other logic and always just return an empty
// headers object without tracking
return Promise.resolve(undefined)
}

if (staticGenerationStore.isUnstableCacheCallback) {
throw new Error(
`Route ${staticGenerationStore.route} used "connection" inside a function cached with "unstable_cache(...)". The \`connection()\` function is used to 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`
)
} else if (staticGenerationStore.dynamicShouldError) {
throw new StaticGenBailoutError(
`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`
)
}

if (prerenderStore) {
// We are in PPR and/or dynamicIO mode and prerendering

if (isDynamicIOPrerender(prerenderStore)) {
// We use the controller and cacheSignal as an indication we are in dynamicIO mode.
// When resolving headers for a prerender with dynamic IO we return a forever promise
// along with property access tracked synchronous headers.

// We don't track dynamic access here because access will be tracked when you access
// one of the properties of the headers object.
return makeHangingPromise()
} else {
// We are prerendering with PPR. We need track dynamic access here eagerly
// to keep continuity with how headers has worked in PPR without dynamicIO.
// TODO consider switching the semantic to throw on property access intead
postponeWithTracking(
staticGenerationStore.route,
'connection',
prerenderStore.dynamicTracking
)
}
} else if (staticGenerationStore.isStaticGeneration) {
// We are in a legacy static generation mode while prerendering
// We treat this function call as a bailout of static generation
throwToInterruptStaticGeneration('connection', staticGenerationStore)
}
// We fall through to the dynamic context below but we still track dynamic access
// because in dev we can still error for things like using headers inside a cache context
trackDynamicDataInDynamicRender(staticGenerationStore)
}

return Promise.resolve(undefined)
}
1 change: 1 addition & 0 deletions packages/next/src/server/web/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { NextResponse } from '../spec-extension/response'
export { userAgent, userAgentFromString } from '../spec-extension/user-agent'
export { URLPattern } from '../spec-extension/url-pattern'
export { unstable_after } from '../../after'
export { connection } from '../../request/connection'
26 changes: 26 additions & 0 deletions test/e2e/app-dir/dynamic-data/dynamic-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,16 @@ describe('dynamic-data with dynamic = "error"', () => {
await browser.close()
}

browser = await next.browser('/connection')
try {
await assertHasRedbox(browser)
expect(await getRedboxHeader(browser)).toMatch(
'Error: Route /connection with `dynamic = "error"` couldn\'t be rendered statically because it used `connection`'
)
} finally {
await browser.close()
}

browser = await next.browser('/headers?foo=foosearch')
try {
await assertHasRedbox(browser)
Expand Down Expand Up @@ -230,6 +240,9 @@ describe('dynamic-data with dynamic = "error"', () => {
expect(next.cliOutput).toMatch(
'Error: Route /cookies with `dynamic = "error"` couldn\'t be rendered statically because it used `cookies`'
)
expect(next.cliOutput).toMatch(
'Error: Route /connection with `dynamic = "error"` couldn\'t be rendered statically because it used `connection`'
)
expect(next.cliOutput).toMatch(
'Error: Route /headers with `dynamic = "error"` couldn\'t be rendered statically because it used `headers`'
)
Expand Down Expand Up @@ -277,6 +290,16 @@ describe('dynamic-data inside cache scope', () => {
await browser.close()
}

browser = await next.browser('/connection')
try {
await assertHasRedbox(browser)
expect(await getRedboxHeader(browser)).toMatch(
'Error: Route /connection used "connection" inside a function cached with "unstable_cache(...)".'
)
} finally {
await browser.close()
}

browser = await next.browser('/headers')
try {
await assertHasRedbox(browser)
Expand All @@ -297,6 +320,9 @@ describe('dynamic-data inside cache scope', () => {
expect(next.cliOutput).toMatch(
'Error: Route /cookies used "cookies" inside a function cached with "unstable_cache(...)".'
)
expect(next.cliOutput).toMatch(
'Error: Route /connection used "connection" inside a function cached with "unstable_cache(...)".'
)
expect(next.cliOutput).toMatch(
'Error: Route /headers used "headers" inside a function cached with "unstable_cache(...)".'
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { connection } from 'next/server'
import { unstable_cache as cache } from 'next/cache'

const cachedConnection = cache(async () => connection())

export default async function Page({ searchParams }) {
await cachedConnection()
return (
<div>
<section>
This example uses `connection()` inside `unstable_cache` which should
cause the build to fail
</section>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { headers, cookies } from 'next/headers'
import { connection } from 'next/server'

import { PageSentinel } from '../getSentinelValue'

export const dynamic = 'force-dynamic'

export default async function Page({ searchParams }) {
await connection()
return (
<div>
<PageSentinel />
<section>
This example uses headers/cookies/searchParams directly in a Page
configured with `dynamic = 'force-dynamic'`. This should cause the page
to always render dynamically regardless of dynamic APIs used
This example uses headers/cookies/connection/searchParams directly in a
Page configured with `dynamic = 'force-dynamic'`. This should cause the
page to always render dynamically regardless of dynamic APIs used
</section>
<section id="headers">
<h3>headers</h3>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { headers, cookies } from 'next/headers'
import { connection } from 'next/server'

import { PageSentinel } from '../getSentinelValue'

export const dynamic = 'force-static'

export default async function Page({ searchParams }) {
await connection()
return (
<div>
<PageSentinel />
<section>
This example uses headers/cookies/searchParams directly in a Page
configured with `dynamic = 'force-static'`. This should cause the page
to always statically render but without exposing dynamic data
This example uses headers/cookies/connection/searchParams directly in a
Page configured with `dynamic = 'force-static'`. This should cause the
page to always statically render but without exposing dynamic data
</section>
<section id="headers">
<h3>headers</h3>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { headers, cookies } from 'next/headers'
import { connection } from 'next/server'

import { PageSentinel } from '../getSentinelValue'

export default async function Page({ searchParams }) {
await connection()
return (
<div>
<PageSentinel />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Server, { connection } from 'next/server'

console.log('Server', Server)

export const dynamic = 'error'

export default async function Page({ searchParams }) {
await connection()
return (
<div>
<section>
This example uses `connection()` but is configured with `dynamic =
'error'` which should cause the page to fail to build
</section>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Suspense } from 'react'
import { connection } from 'next/server'

import { getSentinelValue } from '../../../getSentinelValue'
/**
* This test case is constructed to demonstrate how using the async form of cookies can lead to a better
* prerender with dynamic IO when PPR is on. There is no difference when PPR is off. When PPR is on the second component
* can finish rendering before the prerender completes and so we can produce a static shell where the Fallback closest
* to Cookies access is read
*/
export default async function Page() {
return (
<>
<Suspense fallback="loading...">
<Component />
</Suspense>
<ComponentTwo />
<div id="page">{getSentinelValue()}</div>
</>
)
}

async function Component() {
await connection()
return (
<div>
cookie <span id="foo">foo</span>
</div>
)
}

function ComponentTwo() {
return <p>footer</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Suspense } from 'react'
import { connection } from 'next/server'

import { getSentinelValue } from '../../../getSentinelValue'

export default async function Page() {
const pendingConnection = connection()
return (
<section>
<h1>Deep Connection Reader</h1>
<p>
This component was passed the connection promise returned by
`connection()`. It is rendered inside a Suspense boundary.
</p>
<p>
If dynamicIO is turned off the `connection()` call would trigger a
dynamic point at the callsite and the suspense boundary would also be
blocked for over one second
</p>
<Suspense
fallback={
<>
<p>loading connection...</p>
<div id="fallback">{getSentinelValue()}</div>
</>
}
>
<DeepConnectionReader pendingConnection={pendingConnection} />
</Suspense>
</section>
)
}

async function DeepConnectionReader({
pendingConnection,
}: {
pendingConnection: ReturnType<typeof connection>
}) {
await pendingConnection
return (
<>
<p>The connection was awaited</p>
<div id="page">{getSentinelValue()}</div>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { connection } from 'next/server'

import { getSentinelValue } from '../../../getSentinelValue'
/**
* This test case is constructed to demonstrate how using the async form of cookies can lead to a better
* prerender with dynamic IO when PPR is on. There is no difference when PPR is off. When PPR is on the second component
* can finish rendering before the prerender completes and so we can produce a static shell where the Fallback closest
* to Cookies access is read
*/
export default async function Page() {
return (
<>
<Component />
<ComponentTwo />
<div id="page">{getSentinelValue()}</div>
</>
)
}

async function Component() {
await connection()
return (
<div>
cookie <span id="foo">foo</span>
</div>
)
}

function ComponentTwo() {
return <p>footer</p>
}
Loading
Loading