@samitouri / QOS-React-2 / commits / 6c3202b1e1

[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

Sebastian Markbåge committed Mar 22, 2021 at 16:10 UTC 6c3202b1e1a4382b91de4fdc3b9bc9ed7a77619b
8 files changed +133 -31
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+80
@@ -320,4 +320,84 @@ describe('ReactDOMFizzServer', () => {
320 </div>,
321 );
322 });
323 +
324 + // @gate experimental
325 + it('should allow for two containers to be written to the same document', async () => {
326 + // We create two passthrough streams for each container to write into.
327 + // Notably we don't implement a end() call for these. Because we don't want to
328 + // close the underlying stream just because one of the streams is done. Instead
329 + // we manually close when both are done.
330 + const writableA = new Stream.Writable();
331 + writableA._write = (chunk, encoding, next) => {
332 + writable.write(chunk, encoding, next);
333 + };
334 + const writableB = new Stream.Writable();
335 + writableB._write = (chunk, encoding, next) => {
336 + writable.write(chunk, encoding, next);
337 + };
338 +
339 + writable.write('<div id="container-A">');
340 + await act(async () => {
341 + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
342 + <Suspense fallback={<Text text="Loading A..." />}>
343 + <Text text="This will show A: " />
344 + <div>
345 + <AsyncText text="A" />
346 + </div>
347 + </Suspense>,
348 + writableA,
349 + {identifierPrefix: 'A_'},
350 + );
351 + startWriting();
352 + });
353 + writable.write('</div>');
354 +
355 + writable.write('<div id="container-B">');
356 + await act(async () => {
357 + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
358 + <Suspense fallback={<Text text="Loading B..." />}>
359 + <Text text="This will show B: " />
360 + <div>
361 + <AsyncText text="B" />
362 + </div>
363 + </Suspense>,
364 + writableB,
365 + {identifierPrefix: 'B_'},
366 + );
367 + startWriting();
368 + });
369 + writable.write('</div>');
370 +
371 + expect(getVisibleChildren(container)).toEqual([
372 + <div id="container-A">Loading A...</div>,
373 + <div id="container-B">Loading B...</div>,
374 + ]);
375 +
376 + await act(async () => {
377 + resolveText('B');
378 + });
379 +
380 + expect(getVisibleChildren(container)).toEqual([
381 + <div id="container-A">Loading A...</div>,
382 + <div id="container-B">
383 + This will show B: <div>B</div>
384 + </div>,
385 + ]);
386 +
387 + await act(async () => {
388 + resolveText('A');
389 + });
390 +
391 + // We're done writing both streams now.
392 + writable.end();
393 +
394 + expect(getVisibleChildren(container)).toEqual([
395 + <div id="container-A">
396 + This will show A: <div>A</div>
397 + </div>,
398 + <div id="container-B">
399 + This will show B: <div>B</div>
400 + </div>,
401 + ]);
402 + });
403 });
packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
+5 -1
@@ -16,9 +16,12 @@ import {
16 abort,
17 } from 'react-server/src/ReactFizzServer';
18
19 +import {createResponseState} from './ReactDOMServerFormatConfig';
20 +
21 type Options = {
20 - signal?: AbortSignal,
22 + identifierPrefix?: string,
23 progressiveChunkSize?: number,
24 + signal?: AbortSignal,
25 };
26
27 function renderToReadableStream(
@@ -39,6 +42,7 @@ function renderToReadableStream(
42 request = createRequest(
43 children,
44 controller,
45 + createResponseState(options ? options.identifierPrefix : undefined),
46 options ? options.progressiveChunkSize : undefined,
47 );
48 startWork(request);
packages/react-dom/src/server/ReactDOMFizzServerNode.js
+4
@@ -17,11 +17,14 @@ import {
17 abort,
18 } from 'react-server/src/ReactFizzServer';
19
20 +import {createResponseState} from './ReactDOMServerFormatConfig';
21 +
22 function createDrainHandler(destination, request) {
23 return () => startFlowing(request);
24 }
25
26 type Options = {
27 + identifierPrefix?: string,
28 progressiveChunkSize?: number,
29 };
30
@@ -39,6 +42,7 @@ function pipeToNodeWritable(
42 const request = createRequest(
43 children,
44 destination,
45 + createResponseState(options ? options.identifierPrefix : undefined),
46 options ? options.progressiveChunkSize : undefined,
47 );
48 let hasStartedFlowing = false;
packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+27 -20
@@ -24,6 +24,10 @@ import invariant from 'shared/invariant';
24
25 // Per response,
26 export type ResponseState = {
27 + placeholderPrefix: PrecomputedChunk,
28 + segmentPrefix: PrecomputedChunk,
29 + boundaryPrefix: string,
30 + opaqueIdentifierPrefix: PrecomputedChunk,
31 nextSuspenseID: number,
32 sentCompleteSegmentFunction: boolean,
33 sentCompleteBoundaryFunction: boolean,
@@ -31,8 +35,14 @@ export type ResponseState = {
35 };
36
37 // Allows us to keep track of what we've already written so we can refer back to it.
34 -export function createResponseState(): ResponseState {
38 +export function createResponseState(
39 + identifierPrefix: string = '',
40 +): ResponseState {
41 return {
42 + placeholderPrefix: stringToPrecomputedChunk(identifierPrefix + 'P:'),
43 + segmentPrefix: stringToPrecomputedChunk(identifierPrefix + 'S:'),
44 + boundaryPrefix: identifierPrefix + 'B:',
45 + opaqueIdentifierPrefix: stringToPrecomputedChunk(identifierPrefix + 'R:'),
46 nextSuspenseID: 0,
47 sentCompleteSegmentFunction: false,
48 sentCompleteBoundaryFunction: false,
@@ -68,7 +78,7 @@ function assignAnID(
78 // TODO: This approach doesn't yield deterministic results since this is assigned during render.
79 const generatedID = responseState.nextSuspenseID++;
80 return (id.formattedID = stringToPrecomputedChunk(
71 - 'B:' + generatedID.toString(16),
81 + responseState.boundaryPrefix + generatedID.toString(16),
82 ));
83 }
84
@@ -160,20 +170,19 @@ export function pushEndInstance(
170 // A placeholder is a node inside a hidden partial tree that can be filled in later, but before
171 // display. It's never visible to users.
172 const placeholder1 = stringToPrecomputedChunk('<span id="');
163 -const placeholder2 = stringToPrecomputedChunk('P:');
164 -const placeholder3 = stringToPrecomputedChunk('"></span>');
173 +const placeholder2 = stringToPrecomputedChunk('"></span>');
174 export function writePlaceholder(
175 destination: Destination,
176 + responseState: ResponseState,
177 id: number,
178 ): boolean {
179 // TODO: This needs to be contextually aware and switch tag since not all parents allow for spans like
180 // <select> or <tbody>. E.g. suspending a component that renders a table row.
181 writeChunk(destination, placeholder1);
172 - // TODO: Use the identifierPrefix option to make the prefix configurable.
173 - writeChunk(destination, placeholder2);
182 + writeChunk(destination, responseState.placeholderPrefix);
183 const formattedID = stringToChunk(id.toString(16));
184 writeChunk(destination, formattedID);
176 - return writeChunk(destination, placeholder3);
185 + return writeChunk(destination, placeholder2);
186 }
187
188 // Suspense boundaries are encoded as comments.
@@ -207,20 +216,19 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
216 }
217
218 const startSegment = stringToPrecomputedChunk('<div hidden id="');
210 -const startSegment2 = stringToPrecomputedChunk('S:');
211 -const startSegment3 = stringToPrecomputedChunk('">');
219 +const startSegment2 = stringToPrecomputedChunk('">');
220 const endSegment = stringToPrecomputedChunk('</div>');
221 export function writeStartSegment(
222 destination: Destination,
223 + responseState: ResponseState,
224 id: number,
225 ): boolean {
226 // TODO: What happens with special children like <tr> if they're inserted in a div? Maybe needs contextually aware containers.
227 writeChunk(destination, startSegment);
219 - // TODO: Use the identifierPrefix option to make the prefix configurable.
220 - writeChunk(destination, startSegment2);
228 + writeChunk(destination, responseState.segmentPrefix);
229 const formattedID = stringToChunk(id.toString(16));
230 writeChunk(destination, formattedID);
223 - return writeChunk(destination, startSegment3);
231 + return writeChunk(destination, startSegment2);
232 }
233 export function writeEndSegment(destination: Destination): boolean {
234 return writeChunk(destination, endSegment);
@@ -349,12 +357,10 @@ const clientRenderFunction =
357 'function $RX(b){if(b=document.getElementById(b)){do b=b.previousSibling;while(8!==b.nodeType||"$?"!==b.data);b.data="$!";b._reactRetry&&b._reactRetry()}}';
358
359 const completeSegmentScript1Full = stringToPrecomputedChunk(
352 - '<script>' + completeSegmentFunction + ';$RS("S:',
353 -);
354 -const completeSegmentScript1Partial = stringToPrecomputedChunk(
355 - '<script>$RS("S:',
360 + '<script>' + completeSegmentFunction + ';$RS("',
361 );
357 -const completeSegmentScript2 = stringToPrecomputedChunk('","P:');
362 +const completeSegmentScript1Partial = stringToPrecomputedChunk('<script>$RS("');
363 +const completeSegmentScript2 = stringToPrecomputedChunk('","');
364 const completeSegmentScript3 = stringToPrecomputedChunk('")</script>');
365
366 export function writeCompletedSegmentInstruction(
@@ -370,10 +376,11 @@ export function writeCompletedSegmentInstruction(
376 // Future calls can just reuse the same function.
377 writeChunk(destination, completeSegmentScript1Partial);
378 }
373 - // TODO: Use the identifierPrefix option to make the prefix configurable.
379 + writeChunk(destination, responseState.segmentPrefix);
380 const formattedID = stringToChunk(contentSegmentID.toString(16));
381 writeChunk(destination, formattedID);
382 writeChunk(destination, completeSegmentScript2);
383 + writeChunk(destination, responseState.placeholderPrefix);
384 writeChunk(destination, formattedID);
385 return writeChunk(destination, completeSegmentScript3);
386 }
@@ -384,7 +391,7 @@ const completeBoundaryScript1Full = stringToPrecomputedChunk(
391 const completeBoundaryScript1Partial = stringToPrecomputedChunk(
392 '<script>$RC("',
393 );
387 -const completeBoundaryScript2 = stringToPrecomputedChunk('","S:');
394 +const completeBoundaryScript2 = stringToPrecomputedChunk('","');
395 const completeBoundaryScript3 = stringToPrecomputedChunk('")</script>');
396
397 export function writeCompletedBoundaryInstruction(
@@ -401,7 +408,6 @@ export function writeCompletedBoundaryInstruction(
408 // Future calls can just reuse the same function.
409 writeChunk(destination, completeBoundaryScript1Partial);
410 }
404 - // TODO: Use the identifierPrefix option to make the prefix configurable.
411 const formattedBoundaryID = boundaryID.formattedID;
412 invariant(
413 formattedBoundaryID !== null,
@@ -410,6 +416,7 @@ export function writeCompletedBoundaryInstruction(
416 const formattedContentID = stringToChunk(contentSegmentID.toString(16));
417 writeChunk(destination, formattedBoundaryID);
418 writeChunk(destination, completeBoundaryScript2);
419 + writeChunk(destination, responseState.segmentPrefix);
420 writeChunk(destination, formattedContentID);
421 return writeChunk(destination, completeBoundaryScript3);
422 }
packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+2
@@ -145,6 +145,7 @@ function formatID(id: number): Uint8Array {
145 // display. It's never visible to users.
146 export function writePlaceholder(
147 destination: Destination,
148 + responseState: ResponseState,
149 id: number,
150 ): boolean {
151 writeChunk(destination, PLACEHOLDER);
@@ -179,6 +180,7 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
180
181 export function writeStartSegment(
182 destination: Destination,
183 + responseState: ResponseState,
184 id: number,
185 ): boolean {
186 writeChunk(destination, SEGMENT);
packages/react-noop-renderer/src/ReactNoopServer.js
+11 -5
@@ -77,9 +77,6 @@ const ReactNoopServer = ReactFizzServer({
77 closeWithError(destination: Destination, error: mixed): void {},
78 flushBuffered(destination: Destination): void {},
79
80 - createResponseState(): null {
81 - return null;
82 - },
80 createSuspenseBoundaryID(): SuspenseInstance {
81 // The ID is a pointer to the boundary itself.
82 return {state: 'pending', children: []};
@@ -114,7 +111,11 @@ const ReactNoopServer = ReactFizzServer({
111 target.push(POP);
112 },
113
117 - writePlaceholder(destination: Destination, id: number): boolean {
114 + writePlaceholder(
115 + destination: Destination,
116 + responseState: ResponseState,
117 + id: number,
118 + ): boolean {
119 const parent = destination.stack[destination.stack.length - 1];
120 destination.placeholders.set(id, {
121 parent: parent,
@@ -153,7 +154,11 @@ const ReactNoopServer = ReactFizzServer({
154 destination.stack.pop();
155 },
156
156 - writeStartSegment(destination: Destination, id: number): boolean {
157 + writeStartSegment(
158 + destination: Destination,
159 + responseState: ResponseState,
160 + id: number,
161 + ): boolean {
162 const segment = {
163 children: [],
164 };
@@ -227,6 +232,7 @@ function render(children: React$Element<any>, options?: Options): Destination {
232 const request = ReactNoopServer.createRequest(
233 children,
234 destination,
235 + null,
236 options ? options.progressiveChunkSize : undefined,
237 );
238 ReactNoopServer.startWork(request);
packages/react-server/src/ReactFizzServer.js
+4 -4
@@ -44,7 +44,6 @@ import {
44 pushStartInstance,
45 pushEndInstance,
46 createSuspenseBoundaryID,
47 - createResponseState,
47 } from './ReactServerFormatConfig';
48 import {REACT_ELEMENT_TYPE, REACT_SUSPENSE_TYPE} from 'shared/ReactSymbols';
49 import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -133,13 +132,14 @@ const DEFAULT_PROGRESSIVE_CHUNK_SIZE = 12800;
132 export function createRequest(
133 children: ReactNodeList,
134 destination: Destination,
135 + responseState: ResponseState,
136 progressiveChunkSize: number = DEFAULT_PROGRESSIVE_CHUNK_SIZE,
137 ): Request {
138 const pingedWork = [];
139 const abortSet: Set<SuspendedWork> = new Set();
140 const request = {
141 destination,
142 - responseState: createResponseState(),
142 + responseState,
143 progressiveChunkSize,
144 status: BUFFERING,
145 nextSegmentId: 0,
@@ -590,7 +590,7 @@ function flushSubtree(
590 // We're emitting a placeholder for this segment to be filled in later.
591 // Therefore we'll need to assign it an ID - to refer to it by.
592 const segmentID = (segment.id = request.nextSegmentId++);
593 - return writePlaceholder(destination, segmentID);
593 + return writePlaceholder(destination, request.responseState, segmentID);
594 }
595 case COMPLETED: {
596 segment.status = FLUSHED;
@@ -712,7 +712,7 @@ function flushSegmentContainer(
712 destination: Destination,
713 segment: Segment,
714 ): boolean {
715 - writeStartSegment(destination, segment.id);
715 + writeStartSegment(destination, request.responseState, segment.id);
716 flushSegment(request, destination, segment);
717 return writeEndSegment(destination);
718 }
packages/react-server/src/forks/ReactServerFormatConfig.custom.js
-1
@@ -28,7 +28,6 @@ export opaque type Destination = mixed; // eslint-disable-line no-undef
28 export opaque type ResponseState = mixed;
29 export opaque type SuspenseBoundaryID = mixed;
30
31 -export const createResponseState = $$$hostConfig.createResponseState;
31 export const createSuspenseBoundaryID = $$$hostConfig.createSuspenseBoundaryID;
32 export const pushEmpty = $$$hostConfig.pushEmpty;
33 export const pushTextInstance = $$$hostConfig.pushTextInstance;