1
- import type { CacheNodeSeedData , FlightRouterState , Segment } from './types'
1
+ import type {
2
+ CacheNodeSeedData ,
3
+ FlightRouterState ,
4
+ InitialRSCPayload ,
5
+ Segment ,
6
+ } from './types'
2
7
import type { ManifestNode } from '../../build/webpack/plugins/flight-manifest-plugin'
3
8
4
9
// eslint-disable-next-line import/no-extraneous-dependencies
@@ -53,56 +58,44 @@ type SegmentPrefetch = {
53
58
}
54
59
55
60
export async function collectSegmentData (
56
- flightRouterState : FlightRouterState ,
57
- seedData : CacheNodeSeedData ,
58
61
fullPageDataBuffer : Buffer ,
59
62
staleTime : number ,
60
63
clientModules : ManifestNode ,
61
64
serverConsumerManifest : any
62
65
) : Promise < Map < string , Buffer > > {
63
66
// Traverse the router tree and generate a prefetch response for each segment.
64
67
68
+ // A mutable map to collect the results as we traverse the route tree.
69
+ const resultMap = new Map < string , Buffer > ( )
70
+
65
71
// Before we start, warm up the module cache by decoding the page data once.
66
72
// Then we can assume that any remaining async tasks that occur the next time
67
73
// are due to hanging promises caused by dynamic data access. Note we only
68
74
// have to do this once per page, not per individual segment.
69
75
//
70
- // Based on similar strategy in warmFlightResponse.
71
76
try {
72
77
await createFromReadableStream ( streamFromBuffer ( fullPageDataBuffer ) , {
73
78
serverConsumerManifest,
74
79
} )
75
80
await waitAtLeastOneReactRenderTask ( )
76
81
} catch { }
77
82
78
- // A mutable map to collect the results as we traverse the route tree.
79
- const resultMap = new Map < string , Buffer > ( )
80
-
81
- const tree = await collectSegmentDataImpl (
82
- flightRouterState ,
83
- seedData ,
84
- fullPageDataBuffer ,
85
- clientModules ,
86
- serverConsumerManifest ,
87
- '' ,
88
- '' ,
89
- resultMap
90
- )
91
-
92
- // Render the route tree to a special `/_tree` segment.
93
- const treePrefetch : RootTreePrefetch = {
94
- tree,
95
- staleTime,
96
- }
83
+ // Generate a stream for the route tree prefetch. While we're walking the
84
+ // tree, we'll also spawn additional tasks to generate the segment prefetches.
85
+ // The promises for these tasks are pushed to a mutable array that we will
86
+ // await once the route tree is fully rendered.
87
+ const segmentTasks : Array < Promise < [ string , Buffer ] > > = [ ]
97
88
const treeStream = await renderToReadableStream (
98
- // SegmentPrefetch is not a valid return type for a React component, but
89
+ // RootTreePrefetch is not a valid return type for a React component, but
99
90
// we need to use a component so that when we decode the original stream
100
91
// inside of it, the side effects are transferred to the new stream.
101
92
// @ts -expect-error
102
93
< PrefetchTreeData
103
94
fullPageDataBuffer = { fullPageDataBuffer }
104
95
serverConsumerManifest = { serverConsumerManifest }
105
- treePrefetch = { treePrefetch }
96
+ clientModules = { clientModules }
97
+ staleTime = { staleTime }
98
+ segmentTasks = { segmentTasks }
106
99
/> ,
107
100
clientModules ,
108
101
{
@@ -116,12 +109,80 @@ export async function collectSegmentData(
116
109
} ,
117
110
}
118
111
)
112
+
113
+ // Write the route tree to a special `/_tree` segment.
119
114
const treeBuffer = await streamToBuffer ( treeStream )
120
115
resultMap . set ( '/_tree' , treeBuffer )
121
116
117
+ // Now that we've finished rendering the route tree, all the segment tasks
118
+ // should have been spawned. Await them in parallel and write the segment
119
+ // prefetches to the result map.
120
+ for ( const [ segmentPath , buffer ] of await Promise . all ( segmentTasks ) ) {
121
+ resultMap . set ( segmentPath , buffer )
122
+ }
123
+
122
124
return resultMap
123
125
}
124
126
127
+ async function PrefetchTreeData ( {
128
+ fullPageDataBuffer,
129
+ serverConsumerManifest,
130
+ clientModules,
131
+ staleTime,
132
+ segmentTasks,
133
+ } : {
134
+ fullPageDataBuffer : Buffer
135
+ serverConsumerManifest : any
136
+ clientModules : ManifestNode
137
+ staleTime : number
138
+ segmentTasks : Array < Promise < [ string , Buffer ] > >
139
+ } ) : Promise < RootTreePrefetch | null > {
140
+ // We're currently rendering a Flight response for the route tree prefetch.
141
+ // Inside this component, decode the Flight stream for the whole page. This is
142
+ // a hack to transfer the side effects from the original Flight stream (e.g.
143
+ // Float preloads) onto the Flight stream for the tree prefetch.
144
+ // TODO: React needs a better way to do this. Needed for Server Actions, too.
145
+ const initialRSCPayload : InitialRSCPayload = await createFromReadableStream (
146
+ streamFromBuffer ( fullPageDataBuffer ) ,
147
+ {
148
+ serverConsumerManifest,
149
+ }
150
+ )
151
+
152
+ // FlightDataPath is an unsound type, hence the additional checks.
153
+ const flightDataPaths = initialRSCPayload . f
154
+ if ( flightDataPaths . length !== 1 && flightDataPaths [ 0 ] . length !== 3 ) {
155
+ console . error (
156
+ 'Internal Next.js error: InitialRSCPayload does not match the expected ' +
157
+ 'shape for a prerendered page during segment prefetch generation.'
158
+ )
159
+ return null
160
+ }
161
+ const flightRouterState : FlightRouterState = flightDataPaths [ 0 ] [ 0 ]
162
+ const seedData : CacheNodeSeedData = flightDataPaths [ 0 ] [ 1 ]
163
+
164
+ // Compute the route metadata tree by traversing the FlightRouterState. As we
165
+ // walk the tree, we will also spawn a task to produce a prefetch response for
166
+ // each segment.
167
+ const tree = await collectSegmentDataImpl (
168
+ flightRouterState ,
169
+ seedData ,
170
+ fullPageDataBuffer ,
171
+ clientModules ,
172
+ serverConsumerManifest ,
173
+ '' ,
174
+ '' ,
175
+ segmentTasks
176
+ )
177
+
178
+ // Render the route tree to a special `/_tree` segment.
179
+ const treePrefetch : RootTreePrefetch = {
180
+ tree,
181
+ staleTime,
182
+ }
183
+ return treePrefetch
184
+ }
185
+
125
186
async function collectSegmentDataImpl (
126
187
route : FlightRouterState ,
127
188
seedData : CacheNodeSeedData ,
@@ -130,7 +191,7 @@ async function collectSegmentDataImpl(
130
191
serverConsumerManifest : any ,
131
192
segmentPathStr : string ,
132
193
accessToken : string ,
133
- segmentBufferMap : Map < string , Buffer >
194
+ segmentTasks : Array < Promise < [ string , Buffer ] > >
134
195
) : Promise < TreePrefetch > {
135
196
// Metadata about the segment. Sent as part of the tree prefetch. Null if
136
197
// there are no children.
@@ -168,7 +229,7 @@ async function collectSegmentDataImpl(
168
229
serverConsumerManifest ,
169
230
childSegmentPathStr ,
170
231
childAccessToken ,
171
- segmentBufferMap
232
+ segmentTasks
172
233
)
173
234
if ( slotMetadata === null ) {
174
235
slotMetadata = { }
@@ -181,6 +242,39 @@ async function collectSegmentDataImpl(
181
242
childAccessTokens [ parallelRouteKey ] = childAccessToken
182
243
}
183
244
245
+ // Spawn a task to write the segment data to a new Flight stream.
246
+ segmentTasks . push (
247
+ // Since we're already in the middle of a render, wait until after the
248
+ // current task to escape the current rendering context.
249
+ waitAtLeastOneReactRenderTask ( ) . then ( ( ) =>
250
+ renderSegmentPrefetch (
251
+ seedData ,
252
+ segmentPathStr ,
253
+ accessToken ,
254
+ childAccessTokens ,
255
+ clientModules
256
+ )
257
+ )
258
+ )
259
+
260
+ // Metadata about the segment. Sent to the client as part of the
261
+ // tree prefetch.
262
+ const segment = route [ 0 ]
263
+ const isRootLayout = route [ 4 ]
264
+ return {
265
+ key : segmentPathStr === '' ? '/' : segmentPathStr ,
266
+ slots : slotMetadata ,
267
+ extra : [ segment , isRootLayout === true ] ,
268
+ }
269
+ }
270
+
271
+ async function renderSegmentPrefetch (
272
+ seedData : CacheNodeSeedData ,
273
+ segmentPathStr : string ,
274
+ accessToken : string ,
275
+ childAccessTokens : { [ parallelRouteKey : string ] : string } | null ,
276
+ clientModules : ManifestNode
277
+ ) : Promise < [ string , Buffer ] > {
184
278
// Render the segment data to a stream.
185
279
// In the future, this is where we can include additional metadata, like the
186
280
// stale time and cache tags.
@@ -210,7 +304,7 @@ async function collectSegmentDataImpl(
210
304
const segmentBuffer = await streamToBuffer ( segmentStream )
211
305
// Add the buffer to the result map.
212
306
if ( segmentPathStr === '' ) {
213
- segmentBufferMap . set ( '/' , segmentBuffer )
307
+ return [ '/' , segmentBuffer ]
214
308
} else {
215
309
// The access token is appended to the end of the segment name. To request
216
310
// a segment, the client sends a header like:
@@ -220,53 +314,8 @@ async function collectSegmentDataImpl(
220
314
// The segment path is provided by the tree prefetch, and the access
221
315
// token is provided in the parent layout's data.
222
316
const fullPath = `${ segmentPathStr } .${ accessToken } `
223
- segmentBufferMap . set ( fullPath , segmentBuffer )
317
+ return [ fullPath , segmentBuffer ]
224
318
}
225
-
226
- // Metadata about the segment. Sent to the client as part of the
227
- // tree prefetch.
228
- const segment = route [ 0 ]
229
- const isRootLayout = route [ 4 ]
230
- return {
231
- key : segmentPathStr === '' ? '/' : segmentPathStr ,
232
- slots : slotMetadata ,
233
- extra : [ segment , isRootLayout === true ] ,
234
- }
235
- }
236
-
237
- async function PrefetchTreeData ( {
238
- fullPageDataBuffer,
239
- serverConsumerManifest,
240
- treePrefetch,
241
- } : {
242
- fullPageDataBuffer : Buffer
243
- serverConsumerManifest : any
244
- treePrefetch : RootTreePrefetch
245
- } ) : Promise < RootTreePrefetch | null > {
246
- // We're currently rendering a Flight response for a segment prefetch. Inside
247
- // this component, decode the Flight stream for the whole page. This is a hack
248
- // to transfer the side effects from the original Flight stream (e.g. Float
249
- // preloads) onto the Flight stream for the tree prefetch.
250
- // TODO: React needs a better way to do this. Needed for Server Actions, too.
251
-
252
- const replayConsoleLogs = true
253
- await Promise . race ( [
254
- createFromReadableStream ( streamFromBuffer ( fullPageDataBuffer ) , {
255
- serverConsumerManifest,
256
- replayConsoleLogs,
257
- } ) ,
258
-
259
- // If the page contains dynamic data, the stream will hang indefinitely. So,
260
- // at the end of the current task, stop waiting and proceed rendering. This
261
- // is similar to the AbortSignal strategy we use for generating segment
262
- // data, except we don't actually want or need to abort the outer stream in
263
- // this case.
264
- waitAtLeastOneReactRenderTask ( ) ,
265
- ] )
266
-
267
- // By this point the side effects have been transfered and we can render the
268
- // tree metadata.
269
- return treePrefetch
270
319
}
271
320
272
321
// TODO: Consider updating or unifying this encoding logic for segments with
0 commit comments