From 17b8cdf4cd8c634656aa03a154747ca3f0d79864 Mon Sep 17 00:00:00 2001 From: alexandr Date: Mon, 22 Mar 2021 16:10:57 -0400 Subject: [PATCH] [Fizz] Use identifierPrefix to avoid conflicts within the same response (#21037) * Use identifierPrefix to avoid conflicts within the same response identifierPrefix as an option exists to avoid useOpaqueIdentifier conflicting when different renders are used within one HTML response. This lets this be configured for the DOM renderer specifically since it's DOM specific whether they will conflict across trees or not. * Add test for using multiple containers in one HTML document --- .../src/__tests__/ReactDOMFizzServer-test.js | 80 +++++++++++++++++++ .../src/server/ReactDOMFizzServerBrowser.js | 6 +- .../src/server/ReactDOMFizzServerNode.js | 4 + .../src/server/ReactDOMServerFormatConfig.js | 47 ++++++----- .../server/ReactNativeServerFormatConfig.js | 2 + .../src/ReactNoopServer.js | 16 ++-- packages/react-server/src/ReactFizzServer.js | 8 +- .../forks/ReactServerFormatConfig.custom.js | 1 - 8 files changed, 133 insertions(+), 31 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 9f139dd3ba..3a7d7205dc 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -320,4 +320,84 @@ describe('ReactDOMFizzServer', () => { , ); }); + + // @gate experimental + it('should allow for two containers to be written to the same document', async () => { + // We create two passthrough streams for each container to write into. + // Notably we don't implement a end() call for these. Because we don't want to + // close the underlying stream just because one of the streams is done. Instead + // we manually close when both are done. + const writableA = new Stream.Writable(); + writableA._write = (chunk, encoding, next) => { + writable.write(chunk, encoding, next); + }; + const writableB = new Stream.Writable(); + writableB._write = (chunk, encoding, next) => { + writable.write(chunk, encoding, next); + }; + + writable.write('
'); + await act(async () => { + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable( + }> + +
+ +
+
, + writableA, + {identifierPrefix: 'A_'}, + ); + startWriting(); + }); + writable.write('
'); + + writable.write('
'); + await act(async () => { + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable( + }> + +
+ +
+
, + writableB, + {identifierPrefix: 'B_'}, + ); + startWriting(); + }); + writable.write('
'); + + expect(getVisibleChildren(container)).toEqual([ +
Loading A...
, +
Loading B...
, + ]); + + await act(async () => { + resolveText('B'); + }); + + expect(getVisibleChildren(container)).toEqual([ +
Loading A...
, +
+ This will show B:
B
+
, + ]); + + await act(async () => { + resolveText('A'); + }); + + // We're done writing both streams now. + writable.end(); + + expect(getVisibleChildren(container)).toEqual([ +
+ This will show A:
A
+
, +
+ This will show B:
B
+
, + ]); + }); }); diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index b310271291..902989fefc 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js @@ -16,9 +16,12 @@ import { abort, } from 'react-server/src/ReactFizzServer'; +import {createResponseState} from './ReactDOMServerFormatConfig'; + type Options = { - signal?: AbortSignal, + identifierPrefix?: string, progressiveChunkSize?: number, + signal?: AbortSignal, }; function renderToReadableStream( @@ -39,6 +42,7 @@ function renderToReadableStream( request = createRequest( children, controller, + createResponseState(options ? options.identifierPrefix : undefined), options ? options.progressiveChunkSize : undefined, ); startWork(request); diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js index 1bc2506927..8de76f7095 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js @@ -17,11 +17,14 @@ import { abort, } from 'react-server/src/ReactFizzServer'; +import {createResponseState} from './ReactDOMServerFormatConfig'; + function createDrainHandler(destination, request) { return () => startFlowing(request); } type Options = { + identifierPrefix?: string, progressiveChunkSize?: number, }; @@ -39,6 +42,7 @@ function pipeToNodeWritable( const request = createRequest( children, destination, + createResponseState(options ? options.identifierPrefix : undefined), options ? options.progressiveChunkSize : undefined, ); let hasStartedFlowing = false; diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js index 250892b31d..e9d337724e 100644 --- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js @@ -24,6 +24,10 @@ import invariant from 'shared/invariant'; // Per response, export type ResponseState = { + placeholderPrefix: PrecomputedChunk, + segmentPrefix: PrecomputedChunk, + boundaryPrefix: string, + opaqueIdentifierPrefix: PrecomputedChunk, nextSuspenseID: number, sentCompleteSegmentFunction: boolean, sentCompleteBoundaryFunction: boolean, @@ -31,8 +35,14 @@ export type ResponseState = { }; // Allows us to keep track of what we've already written so we can refer back to it. -export function createResponseState(): ResponseState { +export function createResponseState( + identifierPrefix: string = '', +): ResponseState { return { + placeholderPrefix: stringToPrecomputedChunk(identifierPrefix + 'P:'), + segmentPrefix: stringToPrecomputedChunk(identifierPrefix + 'S:'), + boundaryPrefix: identifierPrefix + 'B:', + opaqueIdentifierPrefix: stringToPrecomputedChunk(identifierPrefix + 'R:'), nextSuspenseID: 0, sentCompleteSegmentFunction: false, sentCompleteBoundaryFunction: false, @@ -68,7 +78,7 @@ function assignAnID( // TODO: This approach doesn't yield deterministic results since this is assigned during render. const generatedID = responseState.nextSuspenseID++; return (id.formattedID = stringToPrecomputedChunk( - 'B:' + generatedID.toString(16), + responseState.boundaryPrefix + generatedID.toString(16), )); } @@ -160,20 +170,19 @@ export function pushEndInstance( // A placeholder is a node inside a hidden partial tree that can be filled in later, but before // display. It's never visible to users. const placeholder1 = stringToPrecomputedChunk(''); +const placeholder2 = stringToPrecomputedChunk('">'); export function writePlaceholder( destination: Destination, + responseState: ResponseState, id: number, ): boolean { // TODO: This needs to be contextually aware and switch tag since not all parents allow for spans like //