Skip to content

Commit

Permalink
Implements new connection api
Browse files Browse the repository at this point in the history
  • Loading branch information
gnoff committed Sep 27, 2024
1 parent fb15ee9 commit 8e77a5a
Show file tree
Hide file tree
Showing 14 changed files with 337 additions and 6 deletions.
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 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`
)
} 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/conneciton/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

0 comments on commit 8e77a5a

Please sign in to comment.