Skip to content

Support temporary references for server actions #71230

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

Merged
merged 5 commits into from
Oct 14, 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
12 changes: 6 additions & 6 deletions packages/next/src/client/app-index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,10 @@ const readable = new ReadableStream({
},
})

const initialServerResponse = createFromReadableStream(readable, {
callServer,
findSourceMapURL,
})
const initialServerResponse = createFromReadableStream<InitialRSCPayload>(
readable,
{ callServer, findSourceMapURL }
)

// React overrides `.then` and doesn't return a new promise chain,
// so we wrap the action queue in a promise to ensure that its value
Expand All @@ -151,7 +151,7 @@ const initialServerResponse = createFromReadableStream(readable, {
const pendingActionQueue: Promise<AppRouterActionQueue> = new Promise(
(resolve, reject) => {
initialServerResponse.then(
(initialRSCPayload: InitialRSCPayload) => {
(initialRSCPayload) => {
resolve(
createMutableActionQueue(
createInitialRouterState({
Expand All @@ -173,7 +173,7 @@ const pendingActionQueue: Promise<AppRouterActionQueue> = new Promise(
)

function ServerRoot(): React.ReactNode {
const initialRSCPayload = use<InitialRSCPayload>(initialServerResponse)
const initialRSCPayload = use(initialServerResponse)
const actionQueue = use<AppRouterActionQueue>(pendingActionQueue)

const router = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
// import { createFromFetch } from 'react-server-dom-webpack/client'
// // eslint-disable-next-line import/no-extraneous-dependencies
// import { encodeReply } from 'react-server-dom-webpack/client'
const { createFromFetch, encodeReply } = (
const { createFromFetch, createTemporaryReferenceSet, encodeReply } = (
!!process.env.NEXT_RUNTIME
? // eslint-disable-next-line import/no-extraneous-dependencies
require('react-server-dom-webpack/client.edge')
Expand Down Expand Up @@ -70,7 +70,8 @@ async function fetchServerAction(
nextUrl: ReadonlyReducerState['nextUrl'],
{ actionId, actionArgs }: ServerActionAction
): Promise<FetchServerActionResult> {
const body = await encodeReply(actionArgs)
const temporaryReferences = createTemporaryReferenceSet()
const body = await encodeReply(actionArgs, { temporaryReferences })

const res = await fetch('', {
method: 'POST',
Expand Down Expand Up @@ -140,7 +141,7 @@ async function fetchServerAction(
if (contentType?.startsWith(RSC_CONTENT_TYPE_HEADER)) {
const response: ActionFlightResponse = await createFromFetch(
Promise.resolve(res),
{ callServer, findSourceMapURL }
{ callServer, findSourceMapURL, temporaryReferences }
)

if (location) {
Expand Down
70 changes: 54 additions & 16 deletions packages/next/src/server/app-render/action-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { selectWorkerForForwarding } from './action-utils'
import { isNodeNextRequest, isWebNextRequest } from '../base-http/helpers'
import { RedirectStatusCode } from '../../client/components/redirect-status-code'
import { synchronizeMutableCookies } from '../async-storage/request-store'
import type { TemporaryReferenceSet } from 'react-server-dom-webpack/server.edge'

function formDataFromSearchQueryString(query: string) {
const searchParams = new URLSearchParams(query)
Expand Down Expand Up @@ -394,12 +395,11 @@ function limitUntrustedHeaderValueForLogs(value: string) {

type ServerModuleMap = Record<
string,
| {
id: string
chunks: string[]
name: string
}
| undefined
{
id: string
chunks: string[]
name: string
}
>

type ServerActionsConfig = {
Expand Down Expand Up @@ -460,6 +460,8 @@ export async function handleAction({
)
}

let temporaryReferences: TemporaryReferenceSet | undefined

const finalizeAndGenerateFlight: GenerateFlight = (...args) => {
// When we switch to the render phase, cookies() will return
// `workUnitStore.cookies` instead of `workUnitStore.userspaceMutableCookies`.
Expand Down Expand Up @@ -562,6 +564,7 @@ export async function handleAction({
actionResult: promise,
// if the page was not revalidated, we can skip the rendering the flight tree
skipFlight: !workStore.pathWasRevalidated,
temporaryReferences,
}),
}
}
Expand Down Expand Up @@ -617,19 +620,31 @@ export async function handleAction({
process.env.NEXT_RUNTIME === 'edge' &&
isWebNextRequest(req)
) {
// Use react-server-dom-webpack/server.edge
const { decodeReply, decodeAction, decodeFormState } = ComponentMod
if (!req.body) {
throw new Error('invariant: Missing request body.')
}

// TODO: add body limit

// Use react-server-dom-webpack/server.edge
const {
createTemporaryReferenceSet,
decodeReply,
decodeAction,
decodeFormState,
} = ComponentMod

temporaryReferences = createTemporaryReferenceSet()

if (isMultipartAction) {
// TODO-APP: Add streaming support
const formData = await req.request.formData()
if (isFetchAction) {
boundActionArguments = await decodeReply(formData, serverModuleMap)
boundActionArguments = await decodeReply(
formData,
serverModuleMap,
{ temporaryReferences }
)
} else {
const action = await decodeAction(formData, serverModuleMap)
if (typeof action === 'function') {
Expand Down Expand Up @@ -672,11 +687,16 @@ export async function handleAction({

if (isURLEncodedAction) {
const formData = formDataFromSearchQueryString(actionData)
boundActionArguments = await decodeReply(formData, serverModuleMap)
boundActionArguments = await decodeReply(
formData,
serverModuleMap,
{ temporaryReferences }
)
} else {
boundActionArguments = await decodeReply(
actionData,
serverModuleMap
serverModuleMap,
{ temporaryReferences }
)
}
}
Expand All @@ -688,11 +708,16 @@ export async function handleAction({
) {
// Use react-server-dom-webpack/server.node which supports streaming
const {
createTemporaryReferenceSet,
decodeReply,
decodeReplyFromBusboy,
decodeAction,
decodeFormState,
} = require(`./react-server.node`)
} = require(
`./react-server.node`
) as typeof import('./react-server.node')

temporaryReferences = createTemporaryReferenceSet()

const { Transform } =
require('node:stream') as typeof import('node:stream')
Expand Down Expand Up @@ -742,7 +767,8 @@ export async function handleAction({

boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap
serverModuleMap,
{ temporaryReferences }
)
} else {
// React doesn't yet publish a busboy version of decodeAction
Expand Down Expand Up @@ -772,7 +798,11 @@ export async function handleAction({
// Only warn if it's a server action, otherwise skip for other post requests
warnBadServerActionRequest()
const actionReturnedState = await action()
formState = await decodeFormState(actionReturnedState, formData)
formState = await decodeFormState(
actionReturnedState,
formData,
serverModuleMap
)
}

// Skip the fetch path
Expand All @@ -799,11 +829,16 @@ export async function handleAction({

if (isURLEncodedAction) {
const formData = formDataFromSearchQueryString(actionData)
boundActionArguments = await decodeReply(formData, serverModuleMap)
boundActionArguments = await decodeReply(
formData,
serverModuleMap,
{ temporaryReferences }
)
} else {
boundActionArguments = await decodeReply(
actionData,
serverModuleMap
serverModuleMap,
{ temporaryReferences }
)
}
}
Expand Down Expand Up @@ -855,6 +890,7 @@ export async function handleAction({
actionResult: Promise.resolve(returnVal),
// if the page was not revalidated, or if the action was forwarded from another worker, we can skip the rendering the flight tree
skipFlight: !workStore.pathWasRevalidated || actionWasForwarded,
temporaryReferences,
})
}
})
Expand Down Expand Up @@ -929,6 +965,7 @@ export async function handleAction({
result: await finalizeAndGenerateFlight(req, ctx, {
skipFlight: false,
actionResult: promise,
temporaryReferences,
}),
}
}
Expand Down Expand Up @@ -963,6 +1000,7 @@ export async function handleAction({
actionResult: promise,
// if the page was not revalidated, or if the action was forwarded from another worker, we can skip the rendering the flight tree
skipFlight: !workStore.pathWasRevalidated || actionWasForwarded,
temporaryReferences,
}),
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ async function generateDynamicFlightRenderResult(
skipFlight: boolean
componentTree?: CacheNodeSeedData
preloadCallbacks?: PreloadCallbacks
temporaryReferences?: WeakMap<any, string>
}
): Promise<RenderResult> {
const renderOpts = ctx.renderOpts
Expand All @@ -517,6 +518,7 @@ async function generateDynamicFlightRenderResult(
ctx.clientReferenceManifest.clientModules,
{
onError,
temporaryReferences: options?.temporaryReferences,
}
)

Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/app-render/entry-base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// eslint-disable-next-line import/no-extraneous-dependencies
export {
createTemporaryReferenceSet,
renderToReadableStream,
decodeReply,
decodeAction,
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/app-render/react-server.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

// eslint-disable-next-line import/no-extraneous-dependencies
export {
createTemporaryReferenceSet,
decodeReply,
decodeReplyFromBusboy,
decodeAction,
Expand Down
Loading
Loading