From 0db702966ab97746e2a981b05cc3f06cf6e947bb Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 23 Jan 2024 13:24:20 -0800 Subject: [PATCH] Uses a simpler approach to trackign hoistables and only writes them either during the preamble or when a boundary complets. The cost is that during the preamble flush there is an extra traversal of the segments that will write in the preamble. While this is not ideal for perf it does make the tracking significantly easier to follow and overall probalby strikes a better balance of practicality and maintainability over raw performance. --- .../src/server/ReactFizzConfigDOM.js | 274 +++++++++----- .../src/server/ReactFizzConfigDOMLegacy.js | 19 +- .../src/__tests__/ReactDOMFloat-test.js | 51 --- .../src/ReactNoopServer.js | 15 +- packages/react-server/src/ReactFizzServer.js | 346 +++++++----------- .../src/forks/ReactFizzConfig.custom.js | 12 +- 6 files changed, 336 insertions(+), 381 deletions(-) diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 067f668262be4..47a8c804e0cac 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -147,9 +147,12 @@ export type RenderState = { // external runtime script chunks externalRuntimeScript: null | ExternalRuntimeScript, bootstrapChunks: Array, + importMapChunks: Array, // Hoistable chunks - importMapChunks: Array, + charsetChunks: Array, + viewportChunks: Array, + hoistableChunks: Array, // Headers queues for Resources that can flush early onHeaders: void | ((headers: HeadersDescriptor) => void), @@ -466,6 +469,10 @@ export function createRenderState( style: {}, }, + charsetChunks: [], + viewportChunks: [], + hoistableChunks: [], + // cleared on flush preconnects: new Set(), fontPreloads: new Set(), @@ -485,6 +492,7 @@ export function createRenderState( nonce, // like a module global for currently rendering boundary + boundaryState: null, stylesToHoist: false, }; @@ -2213,7 +2221,8 @@ function pushStartTextArea( function pushMeta( target: Array, props: Object, - hoistableState: HoistableState, + renderState: RenderState, + boundaryState: null | BoundaryState, textEmbedded: boolean, insertionMode: InsertionMode, noscriptTagInScope: boolean, @@ -2233,12 +2242,30 @@ function pushMeta( } if (typeof props.charSet === 'string') { - return pushSelfClosing(hoistableState.charset, props, 'meta'); + return pushSelfClosing( + boundaryState + ? boundaryState.charsetChunks + : renderState.charsetChunks, + props, + 'meta', + ); } else if (props.name === 'viewport') { // "viewport" isn't related to preconnect but it has the right priority - return pushSelfClosing(hoistableState.viewport, props, 'meta'); + return pushSelfClosing( + boundaryState + ? boundaryState.viewportChunks + : renderState.viewportChunks, + props, + 'meta', + ); } else { - return pushSelfClosing(hoistableState.chunks, props, 'meta'); + return pushSelfClosing( + boundaryState + ? boundaryState.hoistableChunks + : renderState.hoistableChunks, + props, + 'meta', + ); } } } else { @@ -2251,8 +2278,7 @@ function pushLink( props: Object, resumableState: ResumableState, renderState: RenderState, - boundaryResources: null | BoundaryResources, - hoistableState: HoistableState, + boundaryState: null | BoundaryState, textEmbedded: boolean, insertionMode: InsertionMode, noscriptTagInScope: boolean, @@ -2373,8 +2399,8 @@ function pushLink( // We add the newly created resource to our StyleQueue and if necessary // track the resource with the currently rendering boundary styleQueue.sheets.set(key, resource); - if (boundaryResources) { - boundaryResources.stylesheets.add(resource); + if (boundaryState) { + boundaryState.stylesheets.add(resource); } } else { // We need to track whether this boundary should wait on this resource or not. @@ -2385,8 +2411,8 @@ function pushLink( if (styleQueue) { const resource = styleQueue.sheets.get(key); if (resource) { - if (boundaryResources) { - boundaryResources.stylesheets.add(resource); + if (boundaryState) { + boundaryState.stylesheets.add(resource); } } } @@ -2411,7 +2437,10 @@ function pushLink( target.push(textSeparator); } - return pushLinkImpl(hoistableState.chunks, props); + const hoistableChunks = boundaryState + ? boundaryState.hoistableChunks + : renderState.hoistableChunks; + return pushLinkImpl(hoistableChunks, props); } } else { return pushLinkImpl(target, props); @@ -2453,7 +2482,7 @@ function pushStyle( props: Object, resumableState: ResumableState, renderState: RenderState, - boundaryResources: null | BoundaryResources, + boundaryState: null | BoundaryState, textEmbedded: boolean, insertionMode: InsertionMode, noscriptTagInScope: boolean, @@ -2553,8 +2582,8 @@ function pushStyle( // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. - if (boundaryResources) { - boundaryResources.styles.add(styleQueue); + if (boundaryState) { + boundaryState.styles.add(styleQueue); } } @@ -2864,7 +2893,8 @@ function pushStartMenuItem( function pushTitle( target: Array, props: Object, - hoistableState: HoistableState, + renderState: RenderState, + boundaryState: null | BoundaryState, insertionMode: InsertionMode, noscriptTagInScope: boolean, ): ReactNodeList { @@ -2922,7 +2952,10 @@ function pushTitle( !noscriptTagInScope && props.itemProp == null ) { - pushTitleImpl(hoistableState.chunks, props); + const hoistableTarget = boundaryState + ? boundaryState.hoistableChunks + : renderState.hoistableChunks; + pushTitleImpl(hoistableTarget, props); return null; } else { return pushTitleImpl(target, props); @@ -3454,8 +3487,7 @@ export function pushStartInstance( props: Object, resumableState: ResumableState, renderState: RenderState, - boundaryResources: null | BoundaryResources, - hoistableState: HoistableState, + boundaryState: null | BoundaryState, formatContext: FormatContext, textEmbedded: boolean, ): ReactNodeList { @@ -3523,7 +3555,8 @@ export function pushStartInstance( ? pushTitle( target, props, - hoistableState, + renderState, + boundaryState, formatContext.insertionMode, !!(formatContext.tagScope & NOSCRIPT_SCOPE), ) @@ -3534,8 +3567,7 @@ export function pushStartInstance( props, resumableState, renderState, - boundaryResources, - hoistableState, + boundaryState, textEmbedded, formatContext.insertionMode, !!(formatContext.tagScope & NOSCRIPT_SCOPE), @@ -3558,7 +3590,7 @@ export function pushStartInstance( props, resumableState, renderState, - boundaryResources, + boundaryState, textEmbedded, formatContext.insertionMode, !!(formatContext.tagScope & NOSCRIPT_SCOPE), @@ -3567,7 +3599,8 @@ export function pushStartInstance( return pushMeta( target, props, - hoistableState, + renderState, + boundaryState, textEmbedded, formatContext.insertionMode, !!(formatContext.tagScope & NOSCRIPT_SCOPE), @@ -4107,7 +4140,7 @@ export function writeCompletedBoundaryInstruction( resumableState: ResumableState, renderState: RenderState, id: number, - boundaryResources: BoundaryResources, + boundaryState: BoundaryState, ): boolean { let requiresStyleInsertion; if (enableFloat) { @@ -4183,11 +4216,11 @@ export function writeCompletedBoundaryInstruction( // e.g. ["A", "B"] if (scriptFormat) { writeChunk(destination, completeBoundaryScript3a); - // boundaryResources encodes an array literal - writeStyleResourceDependenciesInJS(destination, boundaryResources); + // boundaryState encodes an array literal + writeStyleResourceDependenciesInJS(destination, boundaryState); } else { writeChunk(destination, completeBoundaryData3a); - writeStyleResourceDependenciesInAttr(destination, boundaryResources); + writeStyleResourceDependenciesInAttr(destination, boundaryState); } } else { if (scriptFormat) { @@ -4436,9 +4469,9 @@ function hasStylesToHoist(stylesheet: StylesheetResource): boolean { return false; } -export function writeResourcesForBoundary( +export function writeHoistablesForPartialBoundary( destination: Destination, - boundaryResources: BoundaryResources, + boundaryState: BoundaryState, renderState: RenderState, ): boolean { // Reset these on each invocation, they are only safe to read in this function @@ -4446,10 +4479,66 @@ export function writeResourcesForBoundary( destinationHasCapacity = true; // Flush style tags for each precedence this boundary depends on - boundaryResources.styles.forEach(flushStyleTagsLateForBoundary, destination); + boundaryState.styles.forEach(flushStyleTagsLateForBoundary, destination); // Determine if this boundary has stylesheets that need to be awaited upon completion - boundaryResources.stylesheets.forEach(hasStylesToHoist); + boundaryState.stylesheets.forEach(hasStylesToHoist); + + // We don't actually want to flush any hoistables until the boundary is complete so we omit + // any further writing here. This is becuase unlike Resources, Hoistable Elements act more like + // regular elements, each rendered element has a unique representation in the DOM. We don't want + // these elements to appear in the DOM early, before the boundary has actually completed + + if (currentlyRenderingBoundaryHasStylesToHoist) { + renderState.stylesToHoist = true; + } + return destinationHasCapacity; +} + +export function writeHoistablesForCompletedBoundary( + destination: Destination, + boundaryState: BoundaryState, + renderState: RenderState, +): boolean { + // Reset these on each invocation, they are only safe to read in this function + currentlyRenderingBoundaryHasStylesToHoist = false; + destinationHasCapacity = true; + + // Flush style tags for each precedence this boundary depends on + boundaryState.styles.forEach(flushStyleTagsLateForBoundary, destination); + + // Determine if this boundary has stylesheets that need to be awaited upon completion + boundaryState.stylesheets.forEach(hasStylesToHoist); + + // Flush Hoistable Elements + let i; + const charsetChunks = boundaryState.charsetChunks; + for (i = 0; i < charsetChunks.length - 1; i++) { + writeChunk(destination, charsetChunks[i]); + } + if (i < charsetChunks.length) { + destinationHasCapacity = writeChunkAndReturn(destination, charsetChunks[i]); + } + const viewportChunks = boundaryState.viewportChunks; + for (i = 0; i < viewportChunks.length - 1; i++) { + writeChunk(destination, charsetChunks[i]); + } + if (i < viewportChunks.length) { + destinationHasCapacity = writeChunkAndReturn( + destination, + viewportChunks[i], + ); + } + const hoistableChunks = boundaryState.hoistableChunks; + for (i = 0; i < hoistableChunks.length - 1; i++) { + writeChunk(destination, hoistableChunks[i]); + } + if (i < hoistableChunks.length) { + destinationHasCapacity = writeChunkAndReturn( + destination, + hoistableChunks[i], + ); + } if (currentlyRenderingBoundaryHasStylesToHoist) { renderState.stylesToHoist = true; @@ -4561,7 +4650,6 @@ export function writePreamble( destination: Destination, resumableState: ResumableState, renderState: RenderState, - hoistableState: HoistableState, willFlushAllSegments: boolean, ): void { // This function must be called exactly once on every request @@ -4607,7 +4695,7 @@ export function writePreamble( } // Emit high priority Hoistables - const charsetChunks = hoistableState.charset; + const charsetChunks = renderState.charsetChunks; for (i = 0; i < charsetChunks.length; i++) { writeChunk(destination, charsetChunks[i]); } @@ -4617,7 +4705,7 @@ export function writePreamble( renderState.preconnects.forEach(flushResource, destination); renderState.preconnects.clear(); - const viewportChunks = hoistableState.viewport; + const viewportChunks = renderState.viewportChunks; for (i = 0; i < viewportChunks.length; i++) { writeChunk(destination, viewportChunks[i]); } @@ -4646,8 +4734,8 @@ export function writePreamble( renderState.bulkPreloads.forEach(flushResource, destination); renderState.bulkPreloads.clear(); - // Write hoistableState chunks - const hoistableChunks = hoistableState.chunks; + // Write embedding hoistableChunks + const hoistableChunks = renderState.hoistableChunks; for (i = 0; i < hoistableChunks.length; i++) { writeChunk(destination, hoistableChunks[i]); } @@ -4664,23 +4752,15 @@ export function writePreamble( } } -// We don't bother reporting backpressure at the moment because we expect to -// flush the entire preamble in a single pass. This probably should be modified -// in the future to be backpressure sensitive but that requires a larger refactor -// of the flushing code in Fizz. +// This is an opportunity to write hoistables however in the current implemention +// the only hoistables that make sense to write here are Resources. Hoistable Elements +// would have already been written as part of the preamble or will be written as part +// of a boundary completion and thus don't need to be written here. export function writeHoistables( destination: Destination, resumableState: ResumableState, renderState: RenderState, - hoistableState: HoistableState, ): void { - let i = 0; - - // Emit high priority Hoistables - - // We omit charsetChunks because we have already sent the shell and if it wasn't - // already sent it is too late now. - renderState.preconnects.forEach(flushResource, destination); renderState.preconnects.clear(); @@ -4707,13 +4787,6 @@ export function writeHoistables( renderState.bulkPreloads.forEach(flushResource, destination); renderState.bulkPreloads.clear(); - - // Write hoistableState chunks - const hoistableChunks = hoistableState.chunks; - for (i = 0; i < hoistableChunks.length; i++) { - writeChunk(destination, hoistableChunks[i]); - } - hoistableChunks.length = 0; } export function writePostamble( @@ -4738,12 +4811,12 @@ const arrayCloseBracket = stringToPrecomputedChunk(']'); // [["JS_escaped_string1", "JS_escaped_string2"]] function writeStyleResourceDependenciesInJS( destination: Destination, - boundaryResources: BoundaryResources, + boundaryState: BoundaryState, ): void { writeChunk(destination, arrayFirstOpenBracket); let nextArrayOpenBrackChunk = arrayFirstOpenBracket; - boundaryResources.stylesheets.forEach(resource => { + boundaryState.stylesheets.forEach(resource => { if (resource.state === PREAMBLE) { // We can elide this dependency because it was flushed in the shell and // should be ready before content is shown on the client @@ -4931,12 +5004,12 @@ function writeStyleResourceAttributeInJS( // [["JSON_escaped_string1", "JSON_escaped_string2"]] function writeStyleResourceDependenciesInAttr( destination: Destination, - boundaryResources: BoundaryResources, + boundaryState: BoundaryState, ): void { writeChunk(destination, arrayFirstOpenBracket); let nextArrayOpenBrackChunk = arrayFirstOpenBracket; - boundaryResources.stylesheets.forEach(resource => { + boundaryState.stylesheets.forEach(resource => { if (resource.state === PREAMBLE) { // We can elide this dependency because it was flushed in the shell and // should be ready before content is shown on the client @@ -5183,24 +5256,13 @@ type StylesheetResource = { state: StylesheetState, }; -export type HoistableState = { - charset: Array, - viewport: Array, - chunks: Array, -}; - -export function createHoistableState(): HoistableState { - return { - charset: [], - viewport: [], - chunks: [], - }; -} - -export type BoundaryResources = { - // style dependencies +export type BoundaryState = { styles: Set, stylesheets: Set, + // Hoistable chunks + charsetChunks: Array, + viewportChunks: Array, + hoistableChunks: Array, }; export type StyleQueue = { @@ -5210,10 +5272,13 @@ export type StyleQueue = { sheets: Map, }; -export function createBoundaryResources(): BoundaryResources { +export function createBoundaryState(): BoundaryState { return { styles: new Set(), stylesheets: new Set(), + charsetChunks: [], + viewportChunks: [], + hoistableChunks: [], }; } @@ -6063,35 +6128,58 @@ function escapeStringForLinkHeaderQuotedParamValueContextReplacer( } } -export function hoistHoistables( - target: HoistableState, - source: HoistableState, -) { - target.charset.push(...source.charset); - target.viewport.push(...source.viewport); - target.chunks.push(...source.chunks); -} - function hoistStyleQueueDependency( - this: BoundaryResources, + this: BoundaryState, styleQueue: StyleQueue, ) { this.styles.add(styleQueue); } function hoistStylesheetDependency( - this: BoundaryResources, + this: BoundaryState, stylesheet: StylesheetResource, ) { this.stylesheets.add(stylesheet); } -export function hoistBoundaryResources( - target: BoundaryResources, - source: BoundaryResources, +export function hoistToBoundary( + parentState: BoundaryState, + childState: BoundaryState, ): void { - source.styles.forEach(hoistStyleQueueDependency, target); - source.stylesheets.forEach(hoistStylesheetDependency, target); + childState.styles.forEach(hoistStyleQueueDependency, parentState); + childState.stylesheets.forEach(hoistStylesheetDependency, parentState); + let i; + const charsetChunks = childState.charsetChunks; + for (i = 0; i < charsetChunks.length; i++) { + parentState.charsetChunks.push(charsetChunks[i]); + } + const viewportChunks = childState.viewportChunks; + for (i = 0; i < charsetChunks.length; i++) { + parentState.viewportChunks.push(viewportChunks[i]); + } + const hoistableChunks = childState.hoistableChunks; + for (i = 0; i < hoistableChunks.length; i++) { + parentState.hoistableChunks.push(hoistableChunks[i]); + } +} + +export function hoistToRoot( + renderState: RenderState, + boundaryState: BoundaryState, +): void { + let i; + const charsetChunks = boundaryState.charsetChunks; + for (i = 0; i < charsetChunks.length; i++) { + renderState.charsetChunks.push(charsetChunks[i]); + } + const viewportChunks = boundaryState.viewportChunks; + for (i = 0; i < charsetChunks.length; i++) { + renderState.viewportChunks.push(viewportChunks[i]); + } + const hoistableChunks = boundaryState.hoistableChunks; + for (i = 0; i < hoistableChunks.length; i++) { + renderState.hoistableChunks.push(hoistableChunks[i]); + } } // This function is called at various times depending on whether we are rendering diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js index fb0fef7b3cc14..4a9ff6a56b7f6 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js @@ -56,6 +56,9 @@ export type RenderState = { remainingCapacity: number, }, resets: BaseRenderState['resets'], + charsetChunks: Array, + viewportChunks: Array, + hoistableChunks: Array, preconnects: Set, fontPreloads: Set, highImagePreloads: Set, @@ -101,6 +104,9 @@ export function createRenderState( onHeaders: renderState.onHeaders, headers: renderState.headers, resets: renderState.resets, + charsetChunks: renderState.charsetChunks, + viewportChunks: renderState.viewportChunks, + hoistableChunks: renderState.hoistableChunks, preconnects: renderState.preconnects, fontPreloads: renderState.fontPreloads, highImagePreloads: renderState.highImagePreloads, @@ -127,8 +133,7 @@ export const doctypeChunk: PrecomputedChunk = stringToPrecomputedChunk(''); export type { ResumableState, - HoistableState, - BoundaryResources, + BoundaryState, FormatContext, } from './ReactFizzConfigDOM'; @@ -148,18 +153,18 @@ export { writeClientRenderBoundaryInstruction, writeStartPendingSuspenseBoundary, writeEndPendingSuspenseBoundary, - writeResourcesForBoundary, + writeHoistablesForPartialBoundary, + writeHoistablesForCompletedBoundary, writePlaceholder, writeCompletedRoot, createRootFormatContext, createResumableState, - createBoundaryResources, - createHoistableState, + createBoundaryState, writePreamble, writeHoistables, writePostamble, - hoistBoundaryResources, - hoistHoistables, + hoistToBoundary, + hoistToRoot, prepareHostDispatcher, resetResumableState, completeResumableState, diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 7f64612f2b828..cb48446087912 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -7924,57 +7924,6 @@ background-color: green; ); }); - // @gate enableFloat - it('emits hoistables before other content when streaming in late', async () => { - let content = ''; - writable.on('data', chunk => (content += chunk)); - - await act(() => { - const {pipe} = renderToPipeableStream( - - - - - -
foo
- -
-
- - , - ); - pipe(writable); - }); - - expect(getMeaningfulChildren(document)).toEqual( - - - - - - , - ); - content = ''; - - await act(() => { - resolveText('foo'); - }); - - expect(content.slice(0, 30)).toEqual('
- - - - -
foo
- - - , - ); - }); - // @gate enableFloat it('supports rendering hoistables outside of scope', async () => { await act(() => { diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js index 562ecffc47ffa..7263de82a860e 100644 --- a/packages/react-noop-renderer/src/ReactNoopServer.js +++ b/packages/react-noop-renderer/src/ReactNoopServer.js @@ -52,8 +52,7 @@ type Destination = { }; type RenderState = null; -type HoistableState = null; -type BoundaryResources = null; +type BoundaryState = null; const POP = Buffer.from('/', 'utf8'); @@ -266,18 +265,12 @@ const ReactNoopServer = ReactFizzServer({ writeHoistables() {}, writePostamble() {}, - createBoundaryResources(): BoundaryResources { + createBoundaryState(): BoundaryState { return null; }, - createHoistableState(): HoistableState { - return null; - }, - - hoistHoistables( - parentHoistableState: HoistableState, - hoistableState: HoistableState, - ) {}, + hoistToRoot(renderState: RenderState, boundaryState: BoundaryState) {}, + hoistToBoundary(parent: BoundaryState, child: BoundaryState) {}, prepareHostDispatcher() {}, emitEarlyPreloads() {}, diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index c4b15f61e5b37..8cfc7af29f13e 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -25,9 +25,8 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; import type { RenderState, ResumableState, - HoistableState, - BoundaryResources, FormatContext, + BoundaryState, } from './ReactFizzConfig'; import type {ContextSnapshot} from './ReactFizzNewContext'; import type {ComponentStackNode} from './ReactFizzComponentStack'; @@ -65,14 +64,14 @@ import { pushEndCompletedSuspenseBoundary, pushSegmentFinale, getChildFormatContext, - writeResourcesForBoundary, - writePreamble, + writeHoistablesForPartialBoundary, + writeHoistablesForCompletedBoundary, writeHoistables, + writePreamble, writePostamble, - hoistBoundaryResources, - hoistHoistables, - createBoundaryResources, - createHoistableState, + hoistToBoundary, + hoistToRoot, + createBoundaryState, prepareHostDispatcher, supportsRequestStorage, requestStorage, @@ -211,10 +210,10 @@ type SuspenseBoundary = { parentFlushed: boolean, pendingTasks: number, // when it reaches zero we can show this boundary's content completedSegments: Array, // completed but not yet flushed segments. - fallbackHoistables: null | Hoistables, byteSize: number, // used to determine whether to inline children boundaries. fallbackAbortableTasks: Set, // used to cancel task on the fallback if the boundary completes or gets canceled. - resources: BoundaryResources, + contentState: BoundaryState, + fallbackState: BoundaryState, trackedContentKeyPath: null | KeyNode, // used to track the path for replay nodes trackedFallbackNode: null | ReplayNode, // used to track the fallback for replay nodes }; @@ -225,10 +224,8 @@ type RenderTask = { childIndex: number, ping: () => void, blockedBoundary: Root | SuspenseBoundary, - blockedBoundaryResources: null | BoundaryResources, // Conceptually part of the blockedBoundary but split out for performance + blockedBoundaryState: null | BoundaryState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary blockedSegment: Segment, // the segment we'll write to - blockedHoistables: Hoistables, // the hoistables we'll write to - parentHoistables: null | Hoistables, abortSet: Set, // the abortable set that this task belongs to keyPath: Root | KeyNode, // the path of all parent keys currently rendering formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML) @@ -253,10 +250,8 @@ type ReplayTask = { childIndex: number, ping: () => void, blockedBoundary: Root | SuspenseBoundary, - blockedBoundaryResources: null | BoundaryResources, // Conceptually part of the blockedBoundary but split out for performance + blockedBoundaryState: null | BoundaryState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary blockedSegment: null, // we don't write to anything when we replay - blockedHoistables: Hoistables, // contains hoistable state for any child tasks that resume - parentHoistables: null | Hoistables, abortSet: Set, // the abortable set that this task belongs to keyPath: Root | KeyNode, // the path of all parent keys currently rendering formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML) @@ -294,11 +289,6 @@ type Segment = { textEmbedded: boolean, }; -type Hoistables = { - state: HoistableState, - fallbacks: Set, -}; - const OPEN = 0; const CLOSING = 1; const CLOSED = 2; @@ -308,7 +298,6 @@ export opaque type Request = { flushScheduled: boolean, +resumableState: ResumableState, +renderState: RenderState, - +hoistables: Hoistables, +rootFormatContext: FormatContext, +progressiveChunkSize: number, status: 0 | 1 | 2, @@ -387,13 +376,11 @@ export function createRequest( prepareHostDispatcher(); const pingedTasks: Array = []; const abortSet: Set = new Set(); - const hoistables = createHoistables(); const request: Request = { destination: null, flushScheduled: false, resumableState, renderState, - hoistables, rootFormatContext, progressiveChunkSize: progressiveChunkSize === undefined @@ -437,9 +424,8 @@ export function createRequest( children, -1, null, - rootSegment, - hoistables, null, + rootSegment, abortSet, null, rootFormatContext, @@ -502,13 +488,11 @@ export function resumeRequest( prepareHostDispatcher(); const pingedTasks: Array = []; const abortSet: Set = new Set(); - const hoistables = createHoistables(); const request: Request = { destination: null, flushScheduled: false, resumableState: postponedState.resumableState, renderState, - hoistables, rootFormatContext: postponedState.rootFormatContext, progressiveChunkSize: postponedState.progressiveChunkSize, status: OPEN, @@ -552,9 +536,8 @@ export function resumeRequest( children, -1, null, - rootSegment, - hoistables, null, + rootSegment, abortSet, null, postponedState.rootFormatContext, @@ -579,7 +562,6 @@ export function resumeRequest( children, -1, null, - hoistables, null, abortSet, null, @@ -616,7 +598,6 @@ function pingTask(request: Request, task: Task): void { function createSuspenseBoundary( request: Request, fallbackAbortableTasks: Set, - fallbackHoistables: null | Hoistables, ): SuspenseBoundary { return { status: PENDING, @@ -624,11 +605,11 @@ function createSuspenseBoundary( parentFlushed: false, pendingTasks: 0, completedSegments: [], - fallbackHoistables, byteSize: 0, fallbackAbortableTasks, errorDigest: null, - resources: createBoundaryResources(), + contentState: createBoundaryState(), + fallbackState: createBoundaryState(), trackedContentKeyPath: null, trackedFallbackNode: null, }; @@ -640,9 +621,8 @@ function createRenderTask( node: ReactNodeList, childIndex: number, blockedBoundary: Root | SuspenseBoundary, + blockedBoundaryState: null | BoundaryState, blockedSegment: Segment, - blockedHoistables: Hoistables, - parentHoistables: null | Hoistables, abortSet: Set, keyPath: Root | KeyNode, formatContext: FormatContext, @@ -652,13 +632,10 @@ function createRenderTask( componentStack: null | ComponentStackNode, ): RenderTask { request.allPendingTasks++; - let blockedBoundaryResources; if (blockedBoundary === null) { request.pendingRootTasks++; - blockedBoundaryResources = null; } else { blockedBoundary.pendingTasks++; - blockedBoundaryResources = blockedBoundary.resources; } const task: RenderTask = { replay: null, @@ -666,10 +643,8 @@ function createRenderTask( childIndex, ping: () => pingTask(request, task), blockedBoundary, - blockedBoundaryResources, + blockedBoundaryState, blockedSegment, - blockedHoistables, - parentHoistables, abortSet, keyPath, formatContext, @@ -690,8 +665,7 @@ function createReplayTask( node: ReactNodeList, childIndex: number, blockedBoundary: Root | SuspenseBoundary, - blockedHoistables: Hoistables, - parentHoistables: null | Hoistables, + blockedBoundaryState: null | BoundaryState, abortSet: Set, keyPath: Root | KeyNode, formatContext: FormatContext, @@ -701,13 +675,10 @@ function createReplayTask( componentStack: null | ComponentStackNode, ): ReplayTask { request.allPendingTasks++; - let blockedBoundaryResources; if (blockedBoundary === null) { request.pendingRootTasks++; - blockedBoundaryResources = null; } else { blockedBoundary.pendingTasks++; - blockedBoundaryResources = blockedBoundary.resources; } replay.pendingTasks++; const task: ReplayTask = { @@ -716,10 +687,8 @@ function createReplayTask( childIndex, ping: () => pingTask(request, task), blockedBoundary, - blockedBoundaryResources, + blockedBoundaryState, blockedSegment: null, - blockedHoistables, - parentHoistables, abortSet, keyPath, formatContext, @@ -755,13 +724,6 @@ function createPendingSegment( }; } -function createHoistables(): Hoistables { - return { - state: createHoistableState(), - fallbacks: new Set(), - }; -} - // DEV-only global reference to the currently executing task let currentTaskInDEV: null | Task = null; function getCurrentStackInDEV(): string { @@ -942,10 +904,8 @@ function renderSuspenseBoundary( const prevKeyPath = task.keyPath; const parentBoundary = task.blockedBoundary; - const parentBoundaryResources = task.blockedBoundaryResources; + const parentBoundaryState = task.blockedBoundaryState; const parentSegment = task.blockedSegment; - const parentHoistables = task.blockedHoistables; - const grandParentHoistables = task.parentHoistables; // Each time we enter a suspense boundary, we split out into a new segment for // the fallback so that we can later replace that segment with the content. @@ -955,13 +915,7 @@ function renderSuspenseBoundary( const content: ReactNodeList = props.children; const fallbackAbortSet: Set = new Set(); - const fallbackHoistables = createHoistables(); - parentHoistables.fallbacks.add(fallbackHoistables); - const newBoundary = createSuspenseBoundary( - request, - fallbackAbortSet, - fallbackHoistables, - ); + const newBoundary = createSuspenseBoundary(request, fallbackAbortSet); if (request.trackedPostpones !== null) { newBoundary.trackedContentKeyPath = keyPath; } @@ -1003,10 +957,8 @@ function renderSuspenseBoundary( // context switching. We just need to temporarily switch which boundary and which segment // we're writing to. If something suspends, it'll spawn new suspended task with that context. task.blockedBoundary = newBoundary; - task.blockedBoundaryResources = newBoundary.resources; + task.blockedBoundaryState = newBoundary.contentState; task.blockedSegment = contentRootSegment; - task.blockedHoistables = createHoistables(); - task.parentHoistables = parentHoistables; task.keyPath = keyPath; try { @@ -1028,7 +980,6 @@ function renderSuspenseBoundary( // We are returning early so we need to restore the task.componentStack = previousComponentStack; - mergeHoistables.call(parentHoistables, task.blockedHoistables); return; } } catch (error: mixed) { @@ -1058,10 +1009,8 @@ function renderSuspenseBoundary( // We do need to fallthrough to create the fallback though. } finally { task.blockedBoundary = parentBoundary; - task.blockedBoundaryResources = parentBoundaryResources; + task.blockedBoundaryState = parentBoundaryState; task.blockedSegment = parentSegment; - task.blockedHoistables = parentHoistables; - task.parentHoistables = grandParentHoistables; task.keyPath = prevKeyPath; task.componentStack = previousComponentStack; } @@ -1088,7 +1037,6 @@ function renderSuspenseBoundary( newBoundary.trackedFallbackNode = fallbackReplayNode; } } - // We create suspended task for the fallback because we don't want to actually work // on it yet in case we finish the main content, so we queue for later. const suspendedFallbackTask = createRenderTask( @@ -1097,9 +1045,8 @@ function renderSuspenseBoundary( fallback, -1, parentBoundary, + newBoundary.fallbackState, boundarySegment, - fallbackHoistables, - null, fallbackAbortSet, fallbackKeyPath, task.formatContext, @@ -1136,21 +1083,13 @@ function replaySuspenseBoundary( const previousReplaySet: ReplaySet = task.replay; const parentBoundary = task.blockedBoundary; - const parentBoundaryResources = task.blockedBoundaryResources; - const parentHoistables = task.blockedHoistables; - const grandParentHoistables = task.parentHoistables; + const parentBoundaryState = task.blockedBoundaryState; const content: ReactNodeList = props.children; const fallback: ReactNodeList = props.fallback; const fallbackAbortSet: Set = new Set(); - const fallbackHoistables = createHoistables(); - parentHoistables.fallbacks.add(fallbackHoistables); - const resumedBoundary = createSuspenseBoundary( - request, - fallbackAbortSet, - fallbackHoistables, - ); + const resumedBoundary = createSuspenseBoundary(request, fallbackAbortSet); resumedBoundary.parentFlushed = true; // We restore the same id of this boundary as was used during prerender. resumedBoundary.rootSegmentID = id; @@ -1159,10 +1098,9 @@ function replaySuspenseBoundary( // context switching. We just need to temporarily switch which boundary and replay node // we're writing to. If something suspends, it'll spawn new suspended task with that context. task.blockedBoundary = resumedBoundary; - task.blockedBoundaryResources = resumedBoundary.resources; - task.blockedHoistables = createHoistables(); - task.parentHoistables = parentHoistables; + task.blockedBoundaryState = resumedBoundary.contentState; task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1}; + try { // We use the safe form because we don't handle suspending here. Only error handling. renderNode(request, task, content, -1); @@ -1217,9 +1155,7 @@ function replaySuspenseBoundary( // We do need to fallthrough to create the fallback though. } finally { task.blockedBoundary = parentBoundary; - task.blockedBoundaryResources = parentBoundaryResources; - task.blockedHoistables = parentHoistables; - task.parentHoistables = grandParentHoistables; + task.blockedBoundaryState = parentBoundaryState; task.replay = previousReplaySet; task.keyPath = prevKeyPath; task.componentStack = previousComponentStack; @@ -1241,8 +1177,7 @@ function replaySuspenseBoundary( fallback, -1, parentBoundary, - fallbackHoistables, - null, + resumedBoundary.fallbackState, fallbackAbortSet, fallbackKeyPath, task.formatContext, @@ -1312,15 +1247,13 @@ function renderHostElement( task.keyPath = prevKeyPath; } else { // Render - const hoistables = task.blockedHoistables; const children = pushStartInstance( segment.chunks, type, props, request.resumableState, request.renderState, - task.blockedBoundaryResources, - hoistables.state, + task.blockedBoundaryState, task.formatContext, segment.lastPushedText, ); @@ -2805,8 +2738,7 @@ function spawnNewSuspendedReplayTask( task.node, task.childIndex, task.blockedBoundary, - task.blockedHoistables, - task.parentHoistables, + task.blockedBoundaryState, task.abortSet, task.keyPath, task.formatContext, @@ -2850,9 +2782,8 @@ function spawnNewSuspendedRenderTask( task.node, task.childIndex, task.blockedBoundary, + task.blockedBoundaryState, newSegment, - task.blockedHoistables, - task.parentHoistables, task.abortSet, task.keyPath, task.formatContext, @@ -3130,13 +3061,11 @@ function abortTaskSoft(this: Request, task: Task): void { // It's used for when we didn't need this task to complete the tree. // If task was needed, then it should use abortTask instead. const request: Request = this; + const boundary = task.blockedBoundary; const segment = task.blockedSegment; if (segment !== null) { - const boundary = task.blockedBoundary; - const hoistables = task.blockedHoistables; - const parentHoistables = task.parentHoistables; segment.status = ABORTED; - finishedTask(request, boundary, segment, hoistables, parentHoistables); + finishedTask(request, boundary, segment); } } @@ -3147,7 +3076,7 @@ function abortRemainingSuspenseBoundary( errorDigest: ?string, errorInfo: ThrownInfo, ): void { - const resumedBoundary = createSuspenseBoundary(request, new Set(), null); + const resumedBoundary = createSuspenseBoundary(request, new Set()); resumedBoundary.parentFlushed = true; // We restore the same id of this boundary as was used during prerender. resumedBoundary.rootSegmentID = rootSegmentID; @@ -3395,31 +3324,10 @@ function queueCompletedSegment( } } -// Merges the internal state of Hoistables from a source to a target. afterwards -// both source and target will share the same unified state. This technique is used -// so we can resolve tasks in any order and always accumulate up to the root Hoistable. -// This function uses the `this` argument to allow for slightly optimized calling from -// a forEach over a Set. -function mergeHoistables(this: Hoistables, source: Hoistables) { - if (enableFloat) { - const target = this; - hoistHoistables(target.state, source.state); - source.fallbacks.forEach(h => { - target.fallbacks.add(h); - }); - // we assign the parent state and fallbacks to the child state so if a child task completes - // it will hoist and merge with the parent Hoistables - source.state = target.state; - source.fallbacks = target.fallbacks; - } -} - function finishedTask( request: Request, boundary: Root | SuspenseBoundary, segment: null | Segment, - hoistables: Hoistables, - parentHoistables: null | Hoistables, ) { if (boundary === null) { if (segment !== null && segment.parentFlushed) { @@ -3458,20 +3366,6 @@ function finishedTask( request.completedBoundaries.push(boundary); } - if (enableFloat && parentHoistables) { - // We have completed a boundary and need to merge this boundary's Hoistables with the parent - // Hoistables for this task. First we remove the fallback Hoistables. If they already flushed - // we can't do anythign about it but we don't want to flush them now if unflushed because the fallback - // will never show - if (boundary.fallbackHoistables) { - parentHoistables.fallbacks.delete(boundary.fallbackHoistables); - } - // Next we merge the boundary Hoistables into the task Hoistables. In the process the boundary assumes - // the task Hoistables internal state so later if a child task also completes it will merge with - // the appropriate sets - mergeHoistables.call(parentHoistables, hoistables); - } - // We can now cancel any pending task on the fallback since we won't need to show it anymore. // This needs to happen after we read the parentFlushed flags because aborting can finish // work which can trigger user code, which can start flushing, which can change those flags. @@ -3572,13 +3466,7 @@ function retryRenderTask( task.abortSet.delete(task); segment.status = COMPLETED; - finishedTask( - request, - task.blockedBoundary, - segment, - task.blockedHoistables, - task.parentHoistables, - ); + finishedTask(request, task.blockedBoundary, segment); } catch (thrownValue) { resetHooksState(); @@ -3619,13 +3507,7 @@ function retryRenderTask( const postponeInfo = getThrownInfo(request, task.componentStack); logPostpone(request, postponeInstance.message, postponeInfo); trackPostpone(request, trackedPostpones, task, segment); - finishedTask( - request, - task.blockedBoundary, - segment, - task.blockedHoistables, - task.parentHoistables, - ); + finishedTask(request, task.blockedBoundary, segment); return; } } @@ -3685,13 +3567,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void { task.replay.pendingTasks--; task.abortSet.delete(task); - finishedTask( - request, - task.blockedBoundary, - null, - task.blockedHoistables, - task.parentHoistables, - ); + finishedTask(request, task.blockedBoundary, null); } catch (thrownValue) { resetHooksState(); @@ -3804,11 +3680,66 @@ export function performWork(request: Request): void { } } +function preparePreambleForSubtree(request: Request, segment: Segment): void { + if (segment.status === COMPLETED) { + const children = segment.children; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + prepareSegmentForPreamble(request, child); + } + } +} + +function prepareSegmentForPreamble(request: Request, segment: Segment): void { + const boundary = segment.boundary; + if (boundary === null) { + // Not a suspense boundary. + preparePreambleForSubtree(request, segment); + } else { + if (boundary.status === COMPLETED) { + // we are going to flush this boundary's primary content rather than a fallback + hoistToRoot(request.renderState, boundary.contentState); + // Traverse down the primary content path. + const completedSegments = boundary.completedSegments; + const contentSegment = completedSegments[0]; + if (contentSegment) { + // It is an invariant that a previously unvisited boundary have a single root + // segment however we know this will be caught in the normal flushing path + // so we simply guard the condition here and avoid throwing + preparePreambleForSubtree(request, segment); + } + } else { + // We are going to flush this boundary's fallback content and should include + // it's hoistables as well + hoistToRoot(request.renderState, boundary.fallbackState); + // Traverse the fallback path and see if there are any deeper boundaries with hoistables + // to collect + preparePreambleForSubtree(request, segment); + } + } +} + +function flushPreamble( + request: Request, + destination: Destination, + rootSegment: Segment, +) { + prepareSegmentForPreamble(request, rootSegment); + const willFlushAllSegments = + request.allPendingTasks === 0 && request.trackedPostpones === null; + writePreamble( + destination, + request.resumableState, + request.renderState, + willFlushAllSegments, + ); +} + function flushSubtree( request: Request, destination: Destination, segment: Segment, - rootBoundary: null | SuspenseBoundary, + boundaryState: null | BoundaryState, ): boolean { segment.parentFlushed = true; switch (segment.status) { @@ -3838,7 +3769,7 @@ function flushSubtree( for (; chunkIdx < nextChild.index; chunkIdx++) { writeChunk(destination, chunks[chunkIdx]); } - r = flushSegment(request, destination, nextChild, rootBoundary); + r = flushSegment(request, destination, nextChild, boundaryState); } // Finally just write all the remaining chunks for (; chunkIdx < chunks.length - 1; chunkIdx++) { @@ -3861,12 +3792,12 @@ function flushSegment( request: Request, destination: Destination, segment: Segment, - rootBoundary: null | SuspenseBoundary, + boundaryState: null | BoundaryState, ): boolean { const boundary = segment.boundary; if (boundary === null) { // Not a suspense boundary. - return flushSubtree(request, destination, segment, rootBoundary); + return flushSubtree(request, destination, segment, boundaryState); } boundary.parentFlushed = true; @@ -3884,7 +3815,7 @@ function flushSegment( boundary.errorComponentStack, ); // Flush the fallback. - flushSubtree(request, destination, segment, rootBoundary); + flushSubtree(request, destination, segment, boundaryState); return writeEndClientRenderedSuspenseBoundary( destination, @@ -3907,8 +3838,15 @@ function flushSegment( const id = boundary.rootSegmentID; writeStartPendingSuspenseBoundary(destination, request.renderState, id); + // We are going to flush the fallback so we need to hoist the fallback + // state to the parent boundary + if (enableFloat) { + if (boundaryState) { + hoistToBoundary(boundaryState, boundary.fallbackState); + } + } // Flush the fallback. - flushSubtree(request, destination, segment, rootBoundary); + flushSubtree(request, destination, segment, boundaryState); return writeEndPendingSuspenseBoundary(destination, request.renderState); } else if (boundary.byteSize > request.progressiveChunkSize) { @@ -3929,13 +3867,20 @@ function flushSegment( boundary.rootSegmentID, ); + // While we are going to flush the fallback we are going to follow it up with + // the completed boundary immediately so we make the choice to omit fallback + // boundary state from the parent since it will be replaced when the boundary + // flushes later in this pass or in a future flush + // Flush the fallback. - flushSubtree(request, destination, segment, rootBoundary); + flushSubtree(request, destination, segment, boundaryState); return writeEndPendingSuspenseBoundary(destination, request.renderState); } else { - if (enableFloat && rootBoundary && rootBoundary !== boundary) { - hoistBoundaryResources(rootBoundary.resources, boundary.resources); + if (enableFloat) { + if (boundaryState) { + hoistToBoundary(boundaryState, boundary.contentState); + } } // We can inline this boundary's content as a complete boundary. writeStartCompletedSuspenseBoundary(destination, request.renderState); @@ -3949,7 +3894,7 @@ function flushSegment( } const contentSegment = completedSegments[0]; - flushSegment(request, destination, contentSegment, rootBoundary); + flushSegment(request, destination, contentSegment, boundaryState); return writeEndCompletedSuspenseBoundary(destination, request.renderState); } @@ -3975,7 +3920,7 @@ function flushSegmentContainer( request: Request, destination: Destination, segment: Segment, - boundary: SuspenseBoundary, + boundaryState: BoundaryState, ): boolean { writeStartSegment( destination, @@ -3983,7 +3928,7 @@ function flushSegmentContainer( segment.parentFormatContext, segment.id, ); - flushSegment(request, destination, segment, boundary); + flushSegment(request, destination, segment, boundaryState); return writeEndSegment(destination, segment.parentFormatContext); } @@ -4001,9 +3946,9 @@ function flushCompletedBoundary( completedSegments.length = 0; if (enableFloat) { - writeResourcesForBoundary( + writeHoistablesForCompletedBoundary( destination, - boundary.resources, + boundary.contentState, request.renderState, ); } @@ -4013,7 +3958,7 @@ function flushCompletedBoundary( request.resumableState, request.renderState, boundary.rootSegmentID, - boundary.resources, + boundary.contentState, ); } @@ -4039,13 +3984,9 @@ function flushPartialBoundary( completedSegments.splice(0, i); if (enableFloat) { - // The way this is structured we only write resources for partial boundaries - // if there is no backpressure. Later before we complete the boundary we - // will write resources regardless of backpressure before we emit the - // completion instruction - return writeResourcesForBoundary( + return writeHoistablesForPartialBoundary( destination, - boundary.resources, + boundary.contentState, request.renderState, ); } else { @@ -4064,6 +4005,8 @@ function flushPartiallyCompletedSegment( return true; } + const boundaryState = boundary.contentState; + const segmentID = segment.id; if (segmentID === -1) { // This segment wasn't previously referred to. This happens at the root of @@ -4076,13 +4019,13 @@ function flushPartiallyCompletedSegment( ); } - return flushSegmentContainer(request, destination, segment, boundary); + return flushSegmentContainer(request, destination, segment, boundaryState); } else if (segmentID === boundary.rootSegmentID) { // When we emit postponed boundaries, we might have assigned the ID already // but it's still the root segment so we can't inject it into the parent yet. - return flushSegmentContainer(request, destination, segment, boundary); + return flushSegmentContainer(request, destination, segment, boundaryState); } else { - flushSegmentContainer(request, destination, segment, boundary); + flushSegmentContainer(request, destination, segment, boundaryState); return writeCompletedSegmentInstruction( destination, request.resumableState, @@ -4092,17 +4035,6 @@ function flushPartiallyCompletedSegment( } } -function prepareToFlushHoistables(request: Request) { - // At the moment we flush we merge all fallback Hoistables visible to the request's Hoistables - // object. These represent hoistables for Boundaries that will flush a fallback because the - // primary content isn't ready yet. If a boundary completes before this step then the fallback - // Hoistables would have already been removed from this set so we know it only includes necessary - // fallback hoistables - const requestHoistables = request.hoistables; - requestHoistables.fallbacks.forEach(mergeHoistables, requestHoistables); - requestHoistables.fallbacks.clear(); -} - function flushCompletedQueues( request: Request, destination: Destination, @@ -4122,14 +4054,7 @@ function flushCompletedQueues( return; } else if (request.pendingRootTasks === 0) { if (enableFloat) { - prepareToFlushHoistables(request); - writePreamble( - destination, - request.resumableState, - request.renderState, - request.hoistables.state, - request.allPendingTasks === 0 && request.trackedPostpones === null, - ); + flushPreamble(request, destination, completedRootSegment); } flushSegment(request, destination, completedRootSegment, null); @@ -4140,15 +4065,8 @@ function flushCompletedQueues( return; } } - if (enableFloat) { - prepareToFlushHoistables(request); - writeHoistables( - destination, - request.resumableState, - request.renderState, - request.hoistables.state, - ); + writeHoistables(destination, request.resumableState, request.renderState); } // We emit client rendering instructions for already emitted boundaries first. diff --git a/packages/react-server/src/forks/ReactFizzConfig.custom.js b/packages/react-server/src/forks/ReactFizzConfig.custom.js index 7a2c256fc025f..6c2dc0f1524c2 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.custom.js +++ b/packages/react-server/src/forks/ReactFizzConfig.custom.js @@ -31,7 +31,7 @@ export opaque type Destination = mixed; // eslint-disable-line no-undef export opaque type RenderState = mixed; export opaque type HoistableState = mixed; export opaque type ResumableState = mixed; -export opaque type BoundaryResources = mixed; +export opaque type BoundaryState = mixed; export opaque type FormatContext = mixed; export opaque type HeadersDescriptor = mixed; export type {TransitionStatus}; @@ -88,9 +88,11 @@ export const NotPendingTransition = $$$config.NotPendingTransition; export const writePreamble = $$$config.writePreamble; export const writeHoistables = $$$config.writeHoistables; export const writePostamble = $$$config.writePostamble; -export const hoistBoundaryResources = $$$config.hoistBoundaryResources; +export const hoistBoundaryState = $$$config.hoistBoundaryState; export const hoistHoistables = $$$config.hoistHoistables; -export const createHoistableState = $$$config.createHoistableState; -export const createBoundaryResources = $$$config.createBoundaryResources; -export const writeResourcesForBoundary = $$$config.writeResourcesForBoundary; +export const createBoundaryState = $$$config.createBoundaryState; +export const writeHoistablesForPartialBoundary = + $$$config.writeHoistablesForPartialBoundary; +export const writeHoistablesForCompletedBoundary = + $$$config.writeHoistablesForCompletedBoundary; export const emitEarlyPreloads = $$$config.emitEarlyPreloads;