@@ -275,49 +275,127 @@ function fatalError(request: Request, error: mixed): void {
275275 closeWithError ( request . destination , error ) ;
276276}
277277
278- function renderNode ( request : Request , task : Task , node : ReactNodeList ) : void {
279- if ( typeof node === 'string' ) {
280- pushTextInstance (
281- task . blockedSegment . chunks ,
282- node ,
283- request . responseState ,
284- task . assignID ,
285- ) ;
286- task . assignID = null ;
287- return ;
288- }
278+ function renderSuspenseBoundary (
279+ request : Request ,
280+ task : Task ,
281+ props : Object ,
282+ ) : void {
283+ const parentBoundary = task . blockedBoundary ;
284+ const parentSegment = task . blockedSegment ;
285+
286+ // We need to push an "empty" thing here to identify the parent suspense boundary.
287+ pushEmpty ( parentSegment . chunks , request . responseState , task . assignID ) ;
288+ task . assignID = null ;
289+ // Each time we enter a suspense boundary, we split out into a new segment for
290+ // the fallback so that we can later replace that segment with the content.
291+ // This also lets us split out the main content even if it doesn't suspend,
292+ // in case it ends up generating a large subtree of content.
293+ const fallback : ReactNodeList = props . fallback ;
294+ const content : ReactNodeList = props . children ;
295+
296+ const fallbackAbortSet : Set < Task > = new Set ( ) ;
297+ const newBoundary = createSuspenseBoundary ( request , fallbackAbortSet ) ;
298+ const insertionIndex = parentSegment . chunks . length ;
299+ // The children of the boundary segment is actually the fallback.
300+ const boundarySegment = createPendingSegment (
301+ request ,
302+ insertionIndex ,
303+ newBoundary ,
304+ parentSegment . formatContext ,
305+ ) ;
306+ parentSegment . children . push ( boundarySegment ) ;
289307
290- if ( Array . isArray ( node ) ) {
291- if ( node . length > 0 ) {
292- for ( let i = 0 ; i < node . length ; i ++ ) {
293- renderNode ( request , task , node [ i ] ) ;
294- }
295- } else {
296- pushEmpty (
297- task . blockedSegment . chunks ,
298- request . responseState ,
299- task . assignID ,
300- ) ;
301- task . assignID = null ;
308+ // This segment is the actual child content. We can start rendering that immediately.
309+ const contentRootSegment = createPendingSegment (
310+ request ,
311+ 0 ,
312+ null ,
313+ parentSegment . formatContext ,
314+ ) ;
315+ // We mark the root segment as having its parent flushed. It's not really flushed but there is
316+ // no parent segment so there's nothing to wait on.
317+ contentRootSegment . parentFlushed = true ;
318+
319+ // Currently this is running synchronously. We could instead schedule this to pingedTasks.
320+ // I suspect that there might be some efficiency benefits from not creating the suspended task
321+ // and instead just using the stack if possible.
322+ // TODO: Call this directly instead of messing with saving and restoring contexts.
323+
324+ // We can reuse the current context and task to render the content immediately without
325+ // context switching. We just need to temporarily switch which boundary and which segment
326+ // we're writing to. If something suspends, it'll spawn new suspended task with that context.
327+ task . blockedBoundary = newBoundary ;
328+ task . blockedSegment = contentRootSegment ;
329+ try {
330+ renderNode ( request , task , content ) ;
331+ contentRootSegment . status = COMPLETED ;
332+ newBoundary . completedSegments . push ( contentRootSegment ) ;
333+ if ( newBoundary . pendingTasks === 0 ) {
334+ // This must have been the last segment we were waiting on. This boundary is now complete.
335+ // Therefore we won't need the fallback. We early return so that we don't have to create
336+ // the fallback.
337+ return;
302338 }
303- return ;
339+ } catch ( error ) {
340+ contentRootSegment . status = ERRORED ;
341+ reportError ( request , error ) ;
342+ newBoundary . forceClientRender = true ;
343+ // We don't need to decrement any task numbers because we didn't spawn any new task.
344+ // We don't need to schedule any task because we know the parent has written yet.
345+ // We do need to fallthrough to create the fallback though.
346+ } finally {
347+ task . blockedBoundary = parentBoundary ;
348+ task . blockedSegment = parentSegment ;
304349 }
305350
306- if ( node === null ) {
307- pushEmpty ( task . blockedSegment . chunks , request . responseState , task . assignID ) ;
308- return ;
309- }
351+ // We create suspended task for the fallback because we don't want to actually task
352+ // on it yet in case we finish the main content, so we queue for later.
353+ const suspendedFallbackTask = createTask (
354+ request ,
355+ fallback ,
356+ parentBoundary ,
357+ boundarySegment ,
358+ fallbackAbortSet ,
359+ newBoundary . id , // This is the ID we want to give this fallback so we can replace it later.
360+ ) ;
361+ // TODO: This should be queued at a separate lower priority queue so that we only task
362+ // on preparing fallbacks if we don't have any more main content to task on.
363+ request . pingedTasks . push ( suspendedFallbackTask ) ;
364+ }
310365
311- if (
312- typeof node !== 'object' ||
313- ! node ||
314- ( node : any ) . $$typeof !== REACT_ELEMENT_TYPE
315- ) {
316- throw new Error ( 'Not yet implemented node type.' ) ;
317- }
318- const element : React$Element < any > = ( node : any ) ;
319- const type = element . type ;
320- const props = element . props ;
366+ function renderHostElement (
367+ request : Request ,
368+ task : Task ,
369+ type : string ,
370+ props : Object ,
371+ ) : void {
372+ const segment = task . blockedSegment ;
373+ const children = pushStartInstance (
374+ segment . chunks ,
375+ type ,
376+ props ,
377+ request . responseState ,
378+ segment . formatContext ,
379+ task . assignID ,
380+ ) ;
381+ // We must have assigned it already above so we don't need this anymore.
382+ task . assignID = null ;
383+ const prevContext = segment . formatContext ;
384+ segment . formatContext = getChildFormatContext ( prevContext , type , props ) ;
385+ renderNode ( request , task , children ) ;
386+ // We expect that errors will fatal the whole task and that we don't need
387+ // the correct context. Therefore this is not in a finally.
388+ segment . formatContext = prevContext ;
389+ pushEndInstance ( segment . chunks , type , props ) ;
390+ }
391+
392+ function renderElement (
393+ request : Request ,
394+ task : Task ,
395+ type : any ,
396+ props : Object ,
397+ node : ReactNodeList ,
398+ ) : void {
321399 if ( typeof type === 'function' ) {
322400 try {
323401 const result = type ( props ) ;
@@ -352,109 +430,60 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
352430 }
353431 }
354432 } else if ( typeof type === 'string' ) {
355- const segment = task . blockedSegment ;
356- const children = pushStartInstance (
357- segment . chunks ,
358- type ,
359- props ,
433+ renderHostElement ( request , task , type , props ) ;
434+ } else if ( type === REACT_SUSPENSE_TYPE ) {
435+ renderSuspenseBoundary ( request , task , props ) ;
436+ } else {
437+ throw new Error ( 'Not yet implemented element type.' ) ;
438+ }
439+ }
440+
441+ function renderNode ( request : Request , task : Task , node : ReactNodeList ) : void {
442+ if ( typeof node === 'string' ) {
443+ pushTextInstance (
444+ task . blockedSegment . chunks ,
445+ node ,
360446 request . responseState ,
361- segment . formatContext ,
362447 task . assignID ,
363448 ) ;
364- // We must have assigned it already above so we don't need this anymore.
365449 task . assignID = null ;
366- const prevContext = segment . formatContext ;
367- segment . formatContext = getChildFormatContext ( prevContext , type , props ) ;
368- renderNode ( request , task , children ) ;
369- // We expect that errors will fatal the whole task and that we don't need
370- // the correct context. Therefore this is not in a finally.
371- segment . formatContext = prevContext ;
372- pushEndInstance ( segment . chunks , type , props ) ;
373- } else if ( type === REACT_SUSPENSE_TYPE ) {
374- const parentBoundary = task . blockedBoundary ;
375- const parentSegment = task . blockedSegment ;
450+ return ;
451+ }
376452
377- // We need to push an "empty" thing here to identify the parent suspense boundary.
378- pushEmpty ( parentSegment . chunks , request . responseState , task . assignID ) ;
379- task . assignID = null ;
380- // Each time we enter a suspense boundary, we split out into a new segment for
381- // the fallback so that we can later replace that segment with the content.
382- // This also lets us split out the main content even if it doesn't suspend,
383- // in case it ends up generating a large subtree of content.
384- const fallback : ReactNodeList = props . fallback ;
385- const content : ReactNodeList = props . children ;
386-
387- const fallbackAbortSet : Set < Task > = new Set ( ) ;
388- const newBoundary = createSuspenseBoundary ( request , fallbackAbortSet ) ;
389- const insertionIndex = parentSegment . chunks . length ;
390- // The children of the boundary segment is actually the fallback.
391- const boundarySegment = createPendingSegment (
392- request ,
393- insertionIndex ,
394- newBoundary ,
395- parentSegment . formatContext ,
396- ) ;
397- parentSegment . children . push ( boundarySegment ) ;
398-
399- // This segment is the actual child content. We can start rendering that immediately.
400- const contentRootSegment = createPendingSegment (
401- request ,
402- 0 ,
403- null ,
404- parentSegment . formatContext ,
405- ) ;
406- // We mark the root segment as having its parent flushed. It's not really flushed but there is
407- // no parent segment so there's nothing to wait on.
408- contentRootSegment . parentFlushed = true ;
409-
410- // Currently this is running synchronously. We could instead schedule this to pingedTasks.
411- // I suspect that there might be some efficiency benefits from not creating the suspended task
412- // and instead just using the stack if possible.
413- // TODO: Call this directly instead of messing with saving and restoring contexts.
414-
415- // We can reuse the current context and task to render the content immediately without
416- // context switching. We just need to temporarily switch which boundary and which segment
417- // we're writing to. If something suspends, it'll spawn new suspended task with that context.
418- task . blockedBoundary = newBoundary ;
419- task . blockedSegment = contentRootSegment ;
420- try {
421- renderNode ( request , task , content ) ;
422- contentRootSegment . status = COMPLETED ;
423- newBoundary . completedSegments . push ( contentRootSegment ) ;
424- if ( newBoundary . pendingTasks === 0 ) {
425- // This must have been the last segment we were waiting on. This boundary is now complete.
426- // Therefore we won't need the fallback. We early return so that we don't have to create
427- // the fallback.
428- return ;
453+ if ( Array . isArray ( node ) ) {
454+ if ( node . length > 0 ) {
455+ for ( let i = 0 ; i < node . length ; i ++ ) {
456+ renderNode ( request , task , node [ i ] ) ;
429457 }
430- } catch ( error ) {
431- contentRootSegment . status = ERRORED ;
432- reportError ( request , error ) ;
433- newBoundary . forceClientRender = true ;
434- // We don't need to decrement any task numbers because we didn't spawn any new task.
435- // We don't need to schedule any task because we know the parent has written yet.
436- // We do need to fallthrough to create the fallback though.
437- } finally {
438- task . blockedBoundary = parentBoundary ;
439- task . blockedSegment = parentSegment ;
458+ } else {
459+ pushEmpty (
460+ task . blockedSegment . chunks ,
461+ request . responseState ,
462+ task . assignID ,
463+ ) ;
464+ task . assignID = null ;
440465 }
466+ return ;
467+ }
441468
442- // We create suspended task for the fallback because we don't want to actually task
443- // on it yet in case we finish the main content, so we queue for later.
444- const suspendedFallbackTask = createTask (
445- request ,
446- fallback ,
447- parentBoundary ,
448- boundarySegment ,
449- fallbackAbortSet ,
450- newBoundary . id , // This is the ID we want to give this fallback so we can replace it later.
451- ) ;
452- // TODO: This should be queued at a separate lower priority queue so that we only task
453- // on preparing fallbacks if we don't have any more main content to task on.
454- request . pingedTasks . push ( suspendedFallbackTask ) ;
455- } else {
456- throw new Error ( 'Not yet implemented element type.' ) ;
469+ if ( node === null ) {
470+ pushEmpty ( task . blockedSegment . chunks , request . responseState , task . assignID ) ;
471+ return ;
457472 }
473+
474+ if (
475+ typeof node === 'object' &&
476+ node &&
477+ ( node : any ) . $$typeof === REACT_ELEMENT_TYPE
478+ ) {
479+ const element : React$Element < any > = ( node : any ) ;
480+ const type = element . type ;
481+ const props = element . props ;
482+ renderElement ( request , task , type , props , node ) ;
483+ return ;
484+ }
485+
486+ throw new Error ( 'Not yet implemented node type.' ) ;
458487}
459488
460489function erroredTask (
0 commit comments