@samitouri / QOS-React-2 / commits / b9c4a01f71

Allow the streaming config to decide how to precompute or compute chunks (#21008)

Some legacy environments can not encode non-strings. Those would specify both as strings. They'll throw for binary data. Some environments have to encode strings (like web streams). Those would encode both as uint8array. Some environments (like Node) can do either. It can be beneficial to leave things as strings in case the native stream can do something smart with it.

Sebastian Markbåge committed Mar 15, 2021 at 13:36 UTC b9c4a01f71f59dc1ddf5944b0b11ec86b8218c73
9 files changed +115 -70
packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+51 -42
@@ -7,11 +7,16 @@
7 * @flow
8 */
9
10 -import type {Destination} from 'react-server/src/ReactServerStreamConfig';
10 +import type {
11 + Destination,
12 + Chunk,
13 + PrecomputedChunk,
14 +} from 'react-server/src/ReactServerStreamConfig';
15
16 import {
17 writeChunk,
14 - convertStringToBuffer,
18 + stringToChunk,
19 + stringToPrecomputedChunk,
20 } from 'react-server/src/ReactServerStreamConfig';
21
22 import escapeTextForBrowser from './escapeTextForBrowser';
@@ -55,43 +60,43 @@ function encodeHTMLTextNode(text: string): string {
60 }
61
62 export function pushTextInstance(
58 - target: Array<Uint8Array>,
63 + target: Array<Chunk | PrecomputedChunk>,
64 text: string,
65 ): void {
61 - target.push(convertStringToBuffer(encodeHTMLTextNode(text)));
66 + target.push(stringToChunk(encodeHTMLTextNode(text)));
67 }
68
64 -const startTag1 = convertStringToBuffer('<');
65 -const startTag2 = convertStringToBuffer('>');
69 +const startTag1 = stringToPrecomputedChunk('<');
70 +const startTag2 = stringToPrecomputedChunk('>');
71
72 export function pushStartInstance(
68 - target: Array<Uint8Array>,
73 + target: Array<Chunk | PrecomputedChunk>,
74 type: string,
75 props: Object,
76 ): void {
77 // TODO: Figure out if it's self closing and everything else.
73 - target.push(startTag1, convertStringToBuffer(type), startTag2);
78 + target.push(startTag1, stringToChunk(type), startTag2);
79 }
80
76 -const endTag1 = convertStringToBuffer('</');
77 -const endTag2 = convertStringToBuffer('>');
81 +const endTag1 = stringToPrecomputedChunk('</');
82 +const endTag2 = stringToPrecomputedChunk('>');
83
84 export function pushEndInstance(
80 - target: Array<Uint8Array>,
85 + target: Array<Chunk | PrecomputedChunk>,
86 type: string,
87 props: Object,
88 ): void {
89 // TODO: Figure out if it was self closing.
85 - target.push(endTag1, convertStringToBuffer(type), endTag2);
90 + target.push(endTag1, stringToChunk(type), endTag2);
91 }
92
93 // Structural Nodes
94
95 // A placeholder is a node inside a hidden partial tree that can be filled in later, but before
96 // display. It's never visible to users.
92 -const placeholder1 = convertStringToBuffer('<span id="');
93 -const placeholder2 = convertStringToBuffer('P:');
94 -const placeholder3 = convertStringToBuffer('"></span>');
97 +const placeholder1 = stringToPrecomputedChunk('<span id="');
98 +const placeholder2 = stringToPrecomputedChunk('P:');
99 +const placeholder3 = stringToPrecomputedChunk('"></span>');
100 export function writePlaceholder(
101 destination: Destination,
102 id: number,
@@ -101,16 +106,18 @@ export function writePlaceholder(
106 writeChunk(destination, placeholder1);
107 // TODO: Use the identifierPrefix option to make the prefix configurable.
108 writeChunk(destination, placeholder2);
104 - const formattedID = convertStringToBuffer(id.toString(16));
109 + const formattedID = stringToChunk(id.toString(16));
110 writeChunk(destination, formattedID);
111 return writeChunk(destination, placeholder3);
112 }
113
114 // Suspense boundaries are encoded as comments.
110 -const startCompletedSuspenseBoundary = convertStringToBuffer('<!--$-->');
111 -const startPendingSuspenseBoundary = convertStringToBuffer('<!--$?-->');
112 -const startClientRenderedSuspenseBoundary = convertStringToBuffer('<!--$!-->');
113 -const endSuspenseBoundary = convertStringToBuffer('<!--/$-->');
115 +const startCompletedSuspenseBoundary = stringToPrecomputedChunk('<!--$-->');
116 +const startPendingSuspenseBoundary = stringToPrecomputedChunk('<!--$?-->');
117 +const startClientRenderedSuspenseBoundary = stringToPrecomputedChunk(
118 + '<!--$!-->',
119 +);
120 +const endSuspenseBoundary = stringToPrecomputedChunk('<!--/$-->');
121
122 export function writeStartCompletedSuspenseBoundary(
123 destination: Destination,
@@ -134,10 +141,10 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
141 return writeChunk(destination, endSuspenseBoundary);
142 }
143
137 -const startSegment = convertStringToBuffer('<div hidden id="');
138 -const startSegment2 = convertStringToBuffer('S:');
139 -const startSegment3 = convertStringToBuffer('">');
140 -const endSegment = convertStringToBuffer('"></div>');
144 +const startSegment = stringToPrecomputedChunk('<div hidden id="');
145 +const startSegment2 = stringToPrecomputedChunk('S:');
146 +const startSegment3 = stringToPrecomputedChunk('">');
147 +const endSegment = stringToPrecomputedChunk('"></div>');
148 export function writeStartSegment(
149 destination: Destination,
150 id: number,
@@ -146,7 +153,7 @@ export function writeStartSegment(
153 writeChunk(destination, startSegment);
154 // TODO: Use the identifierPrefix option to make the prefix configurable.
155 writeChunk(destination, startSegment2);
149 - const formattedID = convertStringToBuffer(id.toString(16));
156 + const formattedID = stringToChunk(id.toString(16));
157 writeChunk(destination, formattedID);
158 return writeChunk(destination, startSegment3);
159 }
@@ -276,12 +283,14 @@ const completeBoundaryFunction =
283 const clientRenderFunction =
284 'function $RX(b){if(b=document.getElementById(b)){do b=b.previousSibling;while(8!==b.nodeType||"$?"!==b.data);b.data="$!";b._reactRetry&&b._reactRetry()}}';
285
279 -const completeSegmentScript1Full = convertStringToBuffer(
286 +const completeSegmentScript1Full = stringToPrecomputedChunk(
287 '<script>' + completeSegmentFunction + ';$RS("S:',
288 );
282 -const completeSegmentScript1Partial = convertStringToBuffer('<script>$RS("S:');
283 -const completeSegmentScript2 = convertStringToBuffer('","P:');
284 -const completeSegmentScript3 = convertStringToBuffer('")</script>');
289 +const completeSegmentScript1Partial = stringToPrecomputedChunk(
290 + '<script>$RS("S:',
291 +);
292 +const completeSegmentScript2 = stringToPrecomputedChunk('","P:');
293 +const completeSegmentScript3 = stringToPrecomputedChunk('")</script>');
294
295 export function writeCompletedSegmentInstruction(
296 destination: Destination,
@@ -297,19 +306,21 @@ export function writeCompletedSegmentInstruction(
306 writeChunk(destination, completeSegmentScript1Partial);
307 }
308 // TODO: Use the identifierPrefix option to make the prefix configurable.
300 - const formattedID = convertStringToBuffer(contentSegmentID.toString(16));
309 + const formattedID = stringToChunk(contentSegmentID.toString(16));
310 writeChunk(destination, formattedID);
311 writeChunk(destination, completeSegmentScript2);
312 writeChunk(destination, formattedID);
313 return writeChunk(destination, completeSegmentScript3);
314 }
315
307 -const completeBoundaryScript1Full = convertStringToBuffer(
316 +const completeBoundaryScript1Full = stringToPrecomputedChunk(
317 '<script>' + completeBoundaryFunction + ';$RC("',
318 );
310 -const completeBoundaryScript1Partial = convertStringToBuffer('<script>$RC("');
311 -const completeBoundaryScript2 = convertStringToBuffer('","S:');
312 -const completeBoundaryScript3 = convertStringToBuffer('")</script>');
319 +const completeBoundaryScript1Partial = stringToPrecomputedChunk(
320 + '<script>$RC("',
321 +);
322 +const completeBoundaryScript2 = stringToPrecomputedChunk('","S:');
323 +const completeBoundaryScript3 = stringToPrecomputedChunk('")</script>');
324
325 export function writeCompletedBoundaryInstruction(
326 destination: Destination,
@@ -330,23 +341,21 @@ export function writeCompletedBoundaryInstruction(
341 boundaryID.id !== null,
342 'An ID must have been assigned before we can complete the boundary.',
343 );
333 - const formattedBoundaryID = convertStringToBuffer(
344 + const formattedBoundaryID = stringToChunk(
345 encodeHTMLIDAttribute(boundaryID.id),
346 );
336 - const formattedContentID = convertStringToBuffer(
337 - contentSegmentID.toString(16),
338 - );
347 + const formattedContentID = stringToChunk(contentSegmentID.toString(16));
348 writeChunk(destination, formattedBoundaryID);
349 writeChunk(destination, completeBoundaryScript2);
350 writeChunk(destination, formattedContentID);
351 return writeChunk(destination, completeBoundaryScript3);
352 }
353
345 -const clientRenderScript1Full = convertStringToBuffer(
354 +const clientRenderScript1Full = stringToPrecomputedChunk(
355 '<script>' + clientRenderFunction + ';$RX("',
356 );
348 -const clientRenderScript1Partial = convertStringToBuffer('<script>$RX("');
349 -const clientRenderScript2 = convertStringToBuffer('")</script>');
357 +const clientRenderScript1Partial = stringToPrecomputedChunk('<script>$RX("');
358 +const clientRenderScript2 = stringToPrecomputedChunk('")</script>');
359
360 export function writeClientRenderBoundaryInstruction(
361 destination: Destination,
@@ -365,7 +374,7 @@ export function writeClientRenderBoundaryInstruction(
374 boundaryID.id !== null,
375 'An ID must have been assigned before we can complete the boundary.',
376 );
368 - const formattedBoundaryID = convertStringToBuffer(
377 + const formattedBoundaryID = stringToPrecomputedChunk(
378 encodeHTMLIDAttribute(boundaryID.id),
379 );
380 writeChunk(destination, formattedBoundaryID);
packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+12 -7
@@ -7,11 +7,16 @@
7 * @flow
8 */
9
10 -import type {Destination} from 'react-server/src/ReactServerStreamConfig';
10 +import type {
11 + Destination,
12 + Chunk,
13 + PrecomputedChunk,
14 +} from 'react-server/src/ReactServerStreamConfig';
15
16 import {
17 writeChunk,
14 - convertStringToBuffer,
18 + stringToChunk,
19 + stringToPrecomputedChunk,
20 } from 'react-server/src/ReactServerStreamConfig';
21
22 import invariant from 'shared/invariant';
@@ -71,10 +76,10 @@ export function createSuspenseBoundaryID(
76 return responseState.nextSuspenseID++;
77 }
78
74 -const RAW_TEXT = convertStringToBuffer('RCTRawText');
79 +const RAW_TEXT = stringToPrecomputedChunk('RCTRawText');
80
81 export function pushTextInstance(
77 - target: Array<Uint8Array>,
82 + target: Array<Chunk | PrecomputedChunk>,
83 text: string,
84 ): void {
85 target.push(
@@ -87,20 +92,20 @@ export function pushTextInstance(
92 }
93
94 export function pushStartInstance(
90 - target: Array<Uint8Array>,
95 + target: Array<Chunk | PrecomputedChunk>,
96 type: string,
97 props: Object,
98 ): void {
99 target.push(
100 INSTANCE,
96 - convertStringToBuffer(type),
101 + stringToChunk(type),
102 END, // Null terminated type string
103 // TODO: props
104 );
105 }
106
107 export function pushEndInstance(
103 - target: Array<Uint8Array>,
108 + target: Array<Chunk | PrecomputedChunk>,
109 type: string,
110 props: Object,
111 ): void {
packages/react-noop-renderer/src/ReactNoopFlightServer.js
+7 -4
@@ -27,15 +27,18 @@ const ReactNoopFlightServer = ReactFlightServer({
27 callback();
28 },
29 beginWriting(destination: Destination): void {},
30 - writeChunk(destination: Destination, buffer: Uint8Array): void {
31 - destination.push(Buffer.from((buffer: any)).toString('utf8'));
30 + writeChunk(destination: Destination, chunk: string): void {
31 + destination.push(chunk);
32 },
33 completeWriting(destination: Destination): void {},
34 close(destination: Destination): void {},
35 closeWithError(destination: Destination, error: mixed): void {},
36 flushBuffered(destination: Destination): void {},
37 - convertStringToBuffer(content: string): Uint8Array {
38 - return Buffer.from(content, 'utf8');
37 + stringToChunk(content: string): string {
38 + return content;
39 + },
40 + stringToPrecomputedChunk(content: string): string {
41 + return content;
42 },
43 isModuleReference(reference: Object): boolean {
44 return reference.$$typeof === Symbol.for('react.module.reference');
packages/react-server/src/ReactFizzServer.js
+6 -2
@@ -8,7 +8,11 @@
8 */
9
10 import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes';
11 -import type {Destination} from './ReactServerStreamConfig';
11 +import type {
12 + Destination,
13 + Chunk,
14 + PrecomputedChunk,
15 +} from './ReactServerStreamConfig';
16 import type {ReactNodeList} from 'shared/ReactTypes';
17 import type {
18 SuspenseBoundaryID,
@@ -78,7 +82,7 @@ type Segment = {
82 parentFlushed: boolean, // typically a segment will be flushed by its parent, except if its parent was already flushed
83 id: number, // starts as 0 and is lazily assigned if the parent flushes early
84 +index: number, // the index within the parent's chunks or 0 at the root
81 - +chunks: Array<Uint8Array>,
85 + +chunks: Array<Chunk | PrecomputedChunk>,
86 +children: Array<Segment>,
87 // If this segment represents a fallback, this is the content that will replace that fallback.
88 +boundary: null | SuspenseBoundary,
packages/react-server/src/ReactFlightServerConfigStream.js
+7 -7
@@ -66,11 +66,11 @@ ByteSize
66
67 import type {Request, ReactModel} from 'react-server/src/ReactFlightServer';
68
69 -import {convertStringToBuffer} from './ReactServerStreamConfig';
69 +import {stringToChunk} from './ReactServerStreamConfig';
70
71 -export type {Destination} from './ReactServerStreamConfig';
71 +import type {Chunk} from './ReactServerStreamConfig';
72
73 -export type Chunk = Uint8Array;
73 +export type {Destination, Chunk} from './ReactServerStreamConfig';
74
75 const stringify = JSON.stringify;
76
@@ -86,7 +86,7 @@ export function processErrorChunk(
86 ): Chunk {
87 const errorInfo = {message, stack};
88 const row = serializeRowHeader('E', id) + stringify(errorInfo) + '\n';
89 - return convertStringToBuffer(row);
89 + return stringToChunk(row);
90 }
91
92 export function processModelChunk(
@@ -96,7 +96,7 @@ export function processModelChunk(
96 ): Chunk {
97 const json = stringify(model, request.toJSON);
98 const row = serializeRowHeader('J', id) + json + '\n';
99 - return convertStringToBuffer(row);
99 + return stringToChunk(row);
100 }
101
102 export function processModuleChunk(
@@ -106,7 +106,7 @@ export function processModuleChunk(
106 ): Chunk {
107 const json = stringify(moduleMetaData);
108 const row = serializeRowHeader('M', id) + json + '\n';
109 - return convertStringToBuffer(row);
109 + return stringToChunk(row);
110 }
111
112 export function processSymbolChunk(
@@ -116,7 +116,7 @@ export function processSymbolChunk(
116 ): Chunk {
117 const json = stringify(name);
118 const row = serializeRowHeader('S', id) + json + '\n';
119 - return convertStringToBuffer(row);
119 + return stringToChunk(row);
120 }
121
122 export {
packages/react-server/src/ReactServerStreamConfigBrowser.js
+10 -3
@@ -9,6 +9,9 @@
9
10 export type Destination = ReadableStreamController;
11
12 +export type PrecomputedChunk = Uint8Array;
13 +export type Chunk = Uint8Array;
14 +
15 export function scheduleWork(callback: () => void) {
16 callback();
17 }
@@ -22,9 +25,9 @@ export function beginWriting(destination: Destination) {}
25
26 export function writeChunk(
27 destination: Destination,
25 - buffer: Uint8Array,
28 + chunk: PrecomputedChunk | Chunk,
29 ): boolean {
27 - destination.enqueue(buffer);
30 + destination.enqueue(chunk);
31 return destination.desiredSize > 0;
32 }
33
@@ -36,7 +39,11 @@ export function close(destination: Destination) {
39
40 const textEncoder = new TextEncoder();
41
39 -export function convertStringToBuffer(content: string): Uint8Array {
42 +export function stringToChunk(content: string): Chunk {
43 + return textEncoder.encode(content);
44 +}
45 +
46 +export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
47 return textEncoder.encode(content);
48 }
49
packages/react-server/src/ReactServerStreamConfigNode.js
+10 -3
@@ -18,6 +18,9 @@ type MightBeFlushable = {
18
19 export type Destination = Writable & MightBeFlushable;
20
21 +export type PrecomputedChunk = Uint8Array;
22 +export type Chunk = string;
23 +
24 export function scheduleWork(callback: () => void) {
25 setImmediate(callback);
26 }
@@ -44,9 +47,9 @@ export function beginWriting(destination: Destination) {
47
48 export function writeChunk(
49 destination: Destination,
47 - buffer: Uint8Array,
50 + chunk: Chunk | PrecomputedChunk,
51 ): boolean {
49 - const nodeBuffer = ((buffer: any): Buffer); // close enough
52 + const nodeBuffer = ((chunk: any): Buffer | string); // close enough
53 return destination.write(nodeBuffer);
54 }
55
@@ -61,7 +64,11 @@ export function close(destination: Destination) {
64 destination.end();
65 }
66
64 -export function convertStringToBuffer(content: string): Uint8Array {
67 +export function stringToChunk(content: string): Chunk {
68 + return content;
69 +}
70 +
71 +export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
72 return Buffer.from(content, 'utf8');
73 }
74
packages/react-server/src/forks/ReactServerStreamConfig.custom.js
+5 -1
@@ -26,6 +26,9 @@
26 declare var $$$hostConfig: any;
27 export opaque type Destination = mixed; // eslint-disable-line no-undef
28
29 +export opaque type PrecomputedChunk = mixed; // eslint-disable-line no-undef
30 +export opaque type Chunk = mixed; // eslint-disable-line no-undef
31 +
32 export const scheduleWork = $$$hostConfig.scheduleWork;
33 export const beginWriting = $$$hostConfig.beginWriting;
34 export const writeChunk = $$$hostConfig.writeChunk;
@@ -33,4 +36,5 @@ export const completeWriting = $$$hostConfig.completeWriting;
36 export const flushBuffered = $$$hostConfig.flushBuffered;
37 export const close = $$$hostConfig.close;
38 export const closeWithError = $$$hostConfig.closeWithError;
36 -export const convertStringToBuffer = $$$hostConfig.convertStringToBuffer;
39 +export const stringToChunk = $$$hostConfig.stringToChunk;
40 +export const stringToPrecomputedChunk = $$$hostConfig.stringToPrecomputedChunk;
scripts/shared/inlinedHostConfigs.js
+7 -1
@@ -94,7 +94,13 @@ module.exports = [
94 'react-server-native-relay',
95 'react-server-native-relay/server',
96 ],
97 - paths: ['react-native-renderer', 'react-server-native-relay'],
97 + paths: [
98 + 'react-native-renderer',
99 + 'react-server-native-relay',
100 + // this is included here so that it's not included in the main native check
101 + // remove this when it's added to the main native renderer.
102 + 'react-native-renderer/src/server',
103 + ],
104 isFlowTyped: true,
105 isServerSupported: true,
106 },