Skip to content

Commit 719de07

Browse files
authored
Support temporary references for server actions (#71230)
Implements https://x.com/sebmarkbage/status/1842240773867126939 supersedes and closes #66054 (with permission)
1 parent e09d5b0 commit 719de07

File tree

16 files changed

+385
-50
lines changed

16 files changed

+385
-50
lines changed

packages/next/src/client/app-index.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,10 @@ const readable = new ReadableStream({
139139
},
140140
})
141141

142-
const initialServerResponse = createFromReadableStream(readable, {
143-
callServer,
144-
findSourceMapURL,
145-
})
142+
const initialServerResponse = createFromReadableStream<InitialRSCPayload>(
143+
readable,
144+
{ callServer, findSourceMapURL }
145+
)
146146

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

175175
function ServerRoot(): React.ReactNode {
176-
const initialRSCPayload = use<InitialRSCPayload>(initialServerResponse)
176+
const initialRSCPayload = use(initialServerResponse)
177177
const actionQueue = use<AppRouterActionQueue>(pendingActionQueue)
178178

179179
const router = (

packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
// import { createFromFetch } from 'react-server-dom-webpack/client'
1717
// // eslint-disable-next-line import/no-extraneous-dependencies
1818
// import { encodeReply } from 'react-server-dom-webpack/client'
19-
const { createFromFetch, encodeReply } = (
19+
const { createFromFetch, createTemporaryReferenceSet, encodeReply } = (
2020
!!process.env.NEXT_RUNTIME
2121
? // eslint-disable-next-line import/no-extraneous-dependencies
2222
require('react-server-dom-webpack/client.edge')
@@ -70,7 +70,8 @@ async function fetchServerAction(
7070
nextUrl: ReadonlyReducerState['nextUrl'],
7171
{ actionId, actionArgs }: ServerActionAction
7272
): Promise<FetchServerActionResult> {
73-
const body = await encodeReply(actionArgs)
73+
const temporaryReferences = createTemporaryReferenceSet()
74+
const body = await encodeReply(actionArgs, { temporaryReferences })
7475

7576
const res = await fetch('', {
7677
method: 'POST',
@@ -140,7 +141,7 @@ async function fetchServerAction(
140141
if (contentType?.startsWith(RSC_CONTENT_TYPE_HEADER)) {
141142
const response: ActionFlightResponse = await createFromFetch(
142143
Promise.resolve(res),
143-
{ callServer, findSourceMapURL }
144+
{ callServer, findSourceMapURL, temporaryReferences }
144145
)
145146

146147
if (location) {

packages/next/src/server/app-render/action-handler.ts

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { selectWorkerForForwarding } from './action-utils'
4444
import { isNodeNextRequest, isWebNextRequest } from '../base-http/helpers'
4545
import { RedirectStatusCode } from '../../client/components/redirect-status-code'
4646
import { synchronizeMutableCookies } from '../async-storage/request-store'
47+
import type { TemporaryReferenceSet } from 'react-server-dom-webpack/server.edge'
4748

4849
function formDataFromSearchQueryString(query: string) {
4950
const searchParams = new URLSearchParams(query)
@@ -394,12 +395,11 @@ function limitUntrustedHeaderValueForLogs(value: string) {
394395

395396
type ServerModuleMap = Record<
396397
string,
397-
| {
398-
id: string
399-
chunks: string[]
400-
name: string
401-
}
402-
| undefined
398+
{
399+
id: string
400+
chunks: string[]
401+
name: string
402+
}
403403
>
404404

405405
type ServerActionsConfig = {
@@ -460,6 +460,8 @@ export async function handleAction({
460460
)
461461
}
462462

463+
let temporaryReferences: TemporaryReferenceSet | undefined
464+
463465
const finalizeAndGenerateFlight: GenerateFlight = (...args) => {
464466
// When we switch to the render phase, cookies() will return
465467
// `workUnitStore.cookies` instead of `workUnitStore.userspaceMutableCookies`.
@@ -562,6 +564,7 @@ export async function handleAction({
562564
actionResult: promise,
563565
// if the page was not revalidated, we can skip the rendering the flight tree
564566
skipFlight: !workStore.pathWasRevalidated,
567+
temporaryReferences,
565568
}),
566569
}
567570
}
@@ -617,19 +620,31 @@ export async function handleAction({
617620
process.env.NEXT_RUNTIME === 'edge' &&
618621
isWebNextRequest(req)
619622
) {
620-
// Use react-server-dom-webpack/server.edge
621-
const { decodeReply, decodeAction, decodeFormState } = ComponentMod
622623
if (!req.body) {
623624
throw new Error('invariant: Missing request body.')
624625
}
625626

626627
// TODO: add body limit
627628

629+
// Use react-server-dom-webpack/server.edge
630+
const {
631+
createTemporaryReferenceSet,
632+
decodeReply,
633+
decodeAction,
634+
decodeFormState,
635+
} = ComponentMod
636+
637+
temporaryReferences = createTemporaryReferenceSet()
638+
628639
if (isMultipartAction) {
629640
// TODO-APP: Add streaming support
630641
const formData = await req.request.formData()
631642
if (isFetchAction) {
632-
boundActionArguments = await decodeReply(formData, serverModuleMap)
643+
boundActionArguments = await decodeReply(
644+
formData,
645+
serverModuleMap,
646+
{ temporaryReferences }
647+
)
633648
} else {
634649
const action = await decodeAction(formData, serverModuleMap)
635650
if (typeof action === 'function') {
@@ -672,11 +687,16 @@ export async function handleAction({
672687

673688
if (isURLEncodedAction) {
674689
const formData = formDataFromSearchQueryString(actionData)
675-
boundActionArguments = await decodeReply(formData, serverModuleMap)
690+
boundActionArguments = await decodeReply(
691+
formData,
692+
serverModuleMap,
693+
{ temporaryReferences }
694+
)
676695
} else {
677696
boundActionArguments = await decodeReply(
678697
actionData,
679-
serverModuleMap
698+
serverModuleMap,
699+
{ temporaryReferences }
680700
)
681701
}
682702
}
@@ -688,11 +708,16 @@ export async function handleAction({
688708
) {
689709
// Use react-server-dom-webpack/server.node which supports streaming
690710
const {
711+
createTemporaryReferenceSet,
691712
decodeReply,
692713
decodeReplyFromBusboy,
693714
decodeAction,
694715
decodeFormState,
695-
} = require(`./react-server.node`)
716+
} = require(
717+
`./react-server.node`
718+
) as typeof import('./react-server.node')
719+
720+
temporaryReferences = createTemporaryReferenceSet()
696721

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

743768
boundActionArguments = await decodeReplyFromBusboy(
744769
busboy,
745-
serverModuleMap
770+
serverModuleMap,
771+
{ temporaryReferences }
746772
)
747773
} else {
748774
// React doesn't yet publish a busboy version of decodeAction
@@ -772,7 +798,11 @@ export async function handleAction({
772798
// Only warn if it's a server action, otherwise skip for other post requests
773799
warnBadServerActionRequest()
774800
const actionReturnedState = await action()
775-
formState = await decodeFormState(actionReturnedState, formData)
801+
formState = await decodeFormState(
802+
actionReturnedState,
803+
formData,
804+
serverModuleMap
805+
)
776806
}
777807

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

800830
if (isURLEncodedAction) {
801831
const formData = formDataFromSearchQueryString(actionData)
802-
boundActionArguments = await decodeReply(formData, serverModuleMap)
832+
boundActionArguments = await decodeReply(
833+
formData,
834+
serverModuleMap,
835+
{ temporaryReferences }
836+
)
803837
} else {
804838
boundActionArguments = await decodeReply(
805839
actionData,
806-
serverModuleMap
840+
serverModuleMap,
841+
{ temporaryReferences }
807842
)
808843
}
809844
}
@@ -855,6 +890,7 @@ export async function handleAction({
855890
actionResult: Promise.resolve(returnVal),
856891
// if the page was not revalidated, or if the action was forwarded from another worker, we can skip the rendering the flight tree
857892
skipFlight: !workStore.pathWasRevalidated || actionWasForwarded,
893+
temporaryReferences,
858894
})
859895
}
860896
})
@@ -929,6 +965,7 @@ export async function handleAction({
929965
result: await finalizeAndGenerateFlight(req, ctx, {
930966
skipFlight: false,
931967
actionResult: promise,
968+
temporaryReferences,
932969
}),
933970
}
934971
}
@@ -963,6 +1000,7 @@ export async function handleAction({
9631000
actionResult: promise,
9641001
// if the page was not revalidated, or if the action was forwarded from another worker, we can skip the rendering the flight tree
9651002
skipFlight: !workStore.pathWasRevalidated || actionWasForwarded,
1003+
temporaryReferences,
9661004
}),
9671005
}
9681006
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@ async function generateDynamicFlightRenderResult(
492492
skipFlight: boolean
493493
componentTree?: CacheNodeSeedData
494494
preloadCallbacks?: PreloadCallbacks
495+
temporaryReferences?: WeakMap<any, string>
495496
}
496497
): Promise<RenderResult> {
497498
const renderOpts = ctx.renderOpts
@@ -517,6 +518,7 @@ async function generateDynamicFlightRenderResult(
517518
ctx.clientReferenceManifest.clientModules,
518519
{
519520
onError,
521+
temporaryReferences: options?.temporaryReferences,
520522
}
521523
)
522524

packages/next/src/server/app-render/entry-base.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// eslint-disable-next-line import/no-extraneous-dependencies
22
export {
3+
createTemporaryReferenceSet,
34
renderToReadableStream,
45
decodeReply,
56
decodeAction,

packages/next/src/server/app-render/react-server.node.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
// eslint-disable-next-line import/no-extraneous-dependencies
44
export {
5+
createTemporaryReferenceSet,
56
decodeReply,
67
decodeReplyFromBusboy,
78
decodeAction,

0 commit comments

Comments
 (0)