From 724205f06e0487ad23412b16b0d5a4330605d9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 15 Mar 2021 13:36:23 -0400 Subject: [PATCH] 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. --- .../src/server/ReactDOMServerFormatConfig.js | 93 ++++++++++--------- .../server/ReactNativeServerFormatConfig.js | 19 ++-- .../src/ReactNoopFlightServer.js | 11 ++- packages/react-server/src/ReactFizzServer.js | 8 +- .../src/ReactFlightServerConfigStream.js | 14 +-- .../src/ReactServerStreamConfigBrowser.js | 13 ++- .../src/ReactServerStreamConfigNode.js | 13 ++- .../forks/ReactServerStreamConfig.custom.js | 6 +- scripts/shared/inlinedHostConfigs.js | 8 +- 9 files changed, 115 insertions(+), 70 deletions(-) diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js index b06196c452a60..37ee8feae3096 100644 --- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js @@ -7,11 +7,16 @@ * @flow */ -import type {Destination} from 'react-server/src/ReactServerStreamConfig'; +import type { + Destination, + Chunk, + PrecomputedChunk, +} from 'react-server/src/ReactServerStreamConfig'; import { writeChunk, - convertStringToBuffer, + stringToChunk, + stringToPrecomputedChunk, } from 'react-server/src/ReactServerStreamConfig'; import escapeTextForBrowser from './escapeTextForBrowser'; @@ -55,43 +60,43 @@ function encodeHTMLTextNode(text: string): string { } export function pushTextInstance( - target: Array, + target: Array, text: string, ): void { - target.push(convertStringToBuffer(encodeHTMLTextNode(text))); + target.push(stringToChunk(encodeHTMLTextNode(text))); } -const startTag1 = convertStringToBuffer('<'); -const startTag2 = convertStringToBuffer('>'); +const startTag1 = stringToPrecomputedChunk('<'); +const startTag2 = stringToPrecomputedChunk('>'); export function pushStartInstance( - target: Array, + target: Array, type: string, props: Object, ): void { // TODO: Figure out if it's self closing and everything else. - target.push(startTag1, convertStringToBuffer(type), startTag2); + target.push(startTag1, stringToChunk(type), startTag2); } -const endTag1 = convertStringToBuffer(''); +const endTag1 = stringToPrecomputedChunk(''); export function pushEndInstance( - target: Array, + target: Array, type: string, props: Object, ): void { // TODO: Figure out if it was self closing. - target.push(endTag1, convertStringToBuffer(type), endTag2); + target.push(endTag1, stringToChunk(type), endTag2); } // Structural Nodes // 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 = convertStringToBuffer(''); +const placeholder1 = stringToPrecomputedChunk(''); export function writePlaceholder( destination: Destination, id: number, @@ -101,16 +106,18 @@ export function writePlaceholder( writeChunk(destination, placeholder1); // TODO: Use the identifierPrefix option to make the prefix configurable. writeChunk(destination, placeholder2); - const formattedID = convertStringToBuffer(id.toString(16)); + const formattedID = stringToChunk(id.toString(16)); writeChunk(destination, formattedID); return writeChunk(destination, placeholder3); } // Suspense boundaries are encoded as comments. -const startCompletedSuspenseBoundary = convertStringToBuffer(''); -const startPendingSuspenseBoundary = convertStringToBuffer(''); -const startClientRenderedSuspenseBoundary = convertStringToBuffer(''); -const endSuspenseBoundary = convertStringToBuffer(''); +const startCompletedSuspenseBoundary = stringToPrecomputedChunk(''); +const startPendingSuspenseBoundary = stringToPrecomputedChunk(''); +const startClientRenderedSuspenseBoundary = stringToPrecomputedChunk( + '', +); +const endSuspenseBoundary = stringToPrecomputedChunk(''); export function writeStartCompletedSuspenseBoundary( destination: Destination, @@ -134,10 +141,10 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean { return writeChunk(destination, endSuspenseBoundary); } -const startSegment = convertStringToBuffer(''); +const startSegment = stringToPrecomputedChunk(''); export function writeStartSegment( destination: Destination, id: number, @@ -146,7 +153,7 @@ export function writeStartSegment( writeChunk(destination, startSegment); // TODO: Use the identifierPrefix option to make the prefix configurable. writeChunk(destination, startSegment2); - const formattedID = convertStringToBuffer(id.toString(16)); + const formattedID = stringToChunk(id.toString(16)); writeChunk(destination, formattedID); return writeChunk(destination, startSegment3); } @@ -276,12 +283,14 @@ const completeBoundaryFunction = const clientRenderFunction = 'function $RX(b){if(b=document.getElementById(b)){do b=b.previousSibling;while(8!==b.nodeType||"$?"!==b.data);b.data="$!";b._reactRetry&&b._reactRetry()}}'; -const completeSegmentScript1Full = convertStringToBuffer( +const completeSegmentScript1Full = stringToPrecomputedChunk( ''); +const completeSegmentScript1Partial = stringToPrecomputedChunk( + ''); export function writeCompletedSegmentInstruction( destination: Destination, @@ -297,19 +306,21 @@ export function writeCompletedSegmentInstruction( writeChunk(destination, completeSegmentScript1Partial); } // TODO: Use the identifierPrefix option to make the prefix configurable. - const formattedID = convertStringToBuffer(contentSegmentID.toString(16)); + const formattedID = stringToChunk(contentSegmentID.toString(16)); writeChunk(destination, formattedID); writeChunk(destination, completeSegmentScript2); writeChunk(destination, formattedID); return writeChunk(destination, completeSegmentScript3); } -const completeBoundaryScript1Full = convertStringToBuffer( +const completeBoundaryScript1Full = stringToPrecomputedChunk( ''); +const completeBoundaryScript1Partial = stringToPrecomputedChunk( + ''); export function writeCompletedBoundaryInstruction( destination: Destination, @@ -330,23 +341,21 @@ export function writeCompletedBoundaryInstruction( boundaryID.id !== null, 'An ID must have been assigned before we can complete the boundary.', ); - const formattedBoundaryID = convertStringToBuffer( + const formattedBoundaryID = stringToChunk( encodeHTMLIDAttribute(boundaryID.id), ); - const formattedContentID = convertStringToBuffer( - contentSegmentID.toString(16), - ); + const formattedContentID = stringToChunk(contentSegmentID.toString(16)); writeChunk(destination, formattedBoundaryID); writeChunk(destination, completeBoundaryScript2); writeChunk(destination, formattedContentID); return writeChunk(destination, completeBoundaryScript3); } -const clientRenderScript1Full = convertStringToBuffer( +const clientRenderScript1Full = stringToPrecomputedChunk( ''); +const clientRenderScript1Partial = stringToPrecomputedChunk(''); export function writeClientRenderBoundaryInstruction( destination: Destination, @@ -365,7 +374,7 @@ export function writeClientRenderBoundaryInstruction( boundaryID.id !== null, 'An ID must have been assigned before we can complete the boundary.', ); - const formattedBoundaryID = convertStringToBuffer( + const formattedBoundaryID = stringToPrecomputedChunk( encodeHTMLIDAttribute(boundaryID.id), ); writeChunk(destination, formattedBoundaryID); diff --git a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js index f4e58a03c7efb..224f28e4fa945 100644 --- a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js +++ b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js @@ -7,11 +7,16 @@ * @flow */ -import type {Destination} from 'react-server/src/ReactServerStreamConfig'; +import type { + Destination, + Chunk, + PrecomputedChunk, +} from 'react-server/src/ReactServerStreamConfig'; import { writeChunk, - convertStringToBuffer, + stringToChunk, + stringToPrecomputedChunk, } from 'react-server/src/ReactServerStreamConfig'; import invariant from 'shared/invariant'; @@ -71,10 +76,10 @@ export function createSuspenseBoundaryID( return responseState.nextSuspenseID++; } -const RAW_TEXT = convertStringToBuffer('RCTRawText'); +const RAW_TEXT = stringToPrecomputedChunk('RCTRawText'); export function pushTextInstance( - target: Array, + target: Array, text: string, ): void { target.push( @@ -87,20 +92,20 @@ export function pushTextInstance( } export function pushStartInstance( - target: Array, + target: Array, type: string, props: Object, ): void { target.push( INSTANCE, - convertStringToBuffer(type), + stringToChunk(type), END, // Null terminated type string // TODO: props ); } export function pushEndInstance( - target: Array, + target: Array, type: string, props: Object, ): void { diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index 411e6094164c3..7614ba3e8dfbc 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -27,15 +27,18 @@ const ReactNoopFlightServer = ReactFlightServer({ callback(); }, beginWriting(destination: Destination): void {}, - writeChunk(destination: Destination, buffer: Uint8Array): void { - destination.push(Buffer.from((buffer: any)).toString('utf8')); + writeChunk(destination: Destination, chunk: string): void { + destination.push(chunk); }, completeWriting(destination: Destination): void {}, close(destination: Destination): void {}, closeWithError(destination: Destination, error: mixed): void {}, flushBuffered(destination: Destination): void {}, - convertStringToBuffer(content: string): Uint8Array { - return Buffer.from(content, 'utf8'); + stringToChunk(content: string): string { + return content; + }, + stringToPrecomputedChunk(content: string): string { + return content; }, isModuleReference(reference: Object): boolean { return reference.$$typeof === Symbol.for('react.module.reference'); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 4f5879b86bff5..4731e965dab23 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -8,7 +8,11 @@ */ import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes'; -import type {Destination} from './ReactServerStreamConfig'; +import type { + Destination, + Chunk, + PrecomputedChunk, +} from './ReactServerStreamConfig'; import type {ReactNodeList} from 'shared/ReactTypes'; import type { SuspenseBoundaryID, @@ -78,7 +82,7 @@ type Segment = { parentFlushed: boolean, // typically a segment will be flushed by its parent, except if its parent was already flushed id: number, // starts as 0 and is lazily assigned if the parent flushes early +index: number, // the index within the parent's chunks or 0 at the root - +chunks: Array, + +chunks: Array, +children: Array, // If this segment represents a fallback, this is the content that will replace that fallback. +boundary: null | SuspenseBoundary, diff --git a/packages/react-server/src/ReactFlightServerConfigStream.js b/packages/react-server/src/ReactFlightServerConfigStream.js index 12241493e57fa..b9a57c16eb277 100644 --- a/packages/react-server/src/ReactFlightServerConfigStream.js +++ b/packages/react-server/src/ReactFlightServerConfigStream.js @@ -66,11 +66,11 @@ ByteSize import type {Request, ReactModel} from 'react-server/src/ReactFlightServer'; -import {convertStringToBuffer} from './ReactServerStreamConfig'; +import {stringToChunk} from './ReactServerStreamConfig'; -export type {Destination} from './ReactServerStreamConfig'; +import type {Chunk} from './ReactServerStreamConfig'; -export type Chunk = Uint8Array; +export type {Destination, Chunk} from './ReactServerStreamConfig'; const stringify = JSON.stringify; @@ -86,7 +86,7 @@ export function processErrorChunk( ): Chunk { const errorInfo = {message, stack}; const row = serializeRowHeader('E', id) + stringify(errorInfo) + '\n'; - return convertStringToBuffer(row); + return stringToChunk(row); } export function processModelChunk( @@ -96,7 +96,7 @@ export function processModelChunk( ): Chunk { const json = stringify(model, request.toJSON); const row = serializeRowHeader('J', id) + json + '\n'; - return convertStringToBuffer(row); + return stringToChunk(row); } export function processModuleChunk( @@ -106,7 +106,7 @@ export function processModuleChunk( ): Chunk { const json = stringify(moduleMetaData); const row = serializeRowHeader('M', id) + json + '\n'; - return convertStringToBuffer(row); + return stringToChunk(row); } export function processSymbolChunk( @@ -116,7 +116,7 @@ export function processSymbolChunk( ): Chunk { const json = stringify(name); const row = serializeRowHeader('S', id) + json + '\n'; - return convertStringToBuffer(row); + return stringToChunk(row); } export { diff --git a/packages/react-server/src/ReactServerStreamConfigBrowser.js b/packages/react-server/src/ReactServerStreamConfigBrowser.js index efcef78509cb0..3714be4302765 100644 --- a/packages/react-server/src/ReactServerStreamConfigBrowser.js +++ b/packages/react-server/src/ReactServerStreamConfigBrowser.js @@ -9,6 +9,9 @@ export type Destination = ReadableStreamController; +export type PrecomputedChunk = Uint8Array; +export type Chunk = Uint8Array; + export function scheduleWork(callback: () => void) { callback(); } @@ -22,9 +25,9 @@ export function beginWriting(destination: Destination) {} export function writeChunk( destination: Destination, - buffer: Uint8Array, + chunk: PrecomputedChunk | Chunk, ): boolean { - destination.enqueue(buffer); + destination.enqueue(chunk); return destination.desiredSize > 0; } @@ -36,7 +39,11 @@ export function close(destination: Destination) { const textEncoder = new TextEncoder(); -export function convertStringToBuffer(content: string): Uint8Array { +export function stringToChunk(content: string): Chunk { + return textEncoder.encode(content); +} + +export function stringToPrecomputedChunk(content: string): PrecomputedChunk { return textEncoder.encode(content); } diff --git a/packages/react-server/src/ReactServerStreamConfigNode.js b/packages/react-server/src/ReactServerStreamConfigNode.js index 00e3364fc93a5..4329554b7f050 100644 --- a/packages/react-server/src/ReactServerStreamConfigNode.js +++ b/packages/react-server/src/ReactServerStreamConfigNode.js @@ -18,6 +18,9 @@ type MightBeFlushable = { export type Destination = Writable & MightBeFlushable; +export type PrecomputedChunk = Uint8Array; +export type Chunk = string; + export function scheduleWork(callback: () => void) { setImmediate(callback); } @@ -44,9 +47,9 @@ export function beginWriting(destination: Destination) { export function writeChunk( destination: Destination, - buffer: Uint8Array, + chunk: Chunk | PrecomputedChunk, ): boolean { - const nodeBuffer = ((buffer: any): Buffer); // close enough + const nodeBuffer = ((chunk: any): Buffer | string); // close enough return destination.write(nodeBuffer); } @@ -61,7 +64,11 @@ export function close(destination: Destination) { destination.end(); } -export function convertStringToBuffer(content: string): Uint8Array { +export function stringToChunk(content: string): Chunk { + return content; +} + +export function stringToPrecomputedChunk(content: string): PrecomputedChunk { return Buffer.from(content, 'utf8'); } diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.custom.js b/packages/react-server/src/forks/ReactServerStreamConfig.custom.js index 2766595967375..e832f19430a20 100644 --- a/packages/react-server/src/forks/ReactServerStreamConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerStreamConfig.custom.js @@ -26,6 +26,9 @@ declare var $$$hostConfig: any; export opaque type Destination = mixed; // eslint-disable-line no-undef +export opaque type PrecomputedChunk = mixed; // eslint-disable-line no-undef +export opaque type Chunk = mixed; // eslint-disable-line no-undef + export const scheduleWork = $$$hostConfig.scheduleWork; export const beginWriting = $$$hostConfig.beginWriting; export const writeChunk = $$$hostConfig.writeChunk; @@ -33,4 +36,5 @@ export const completeWriting = $$$hostConfig.completeWriting; export const flushBuffered = $$$hostConfig.flushBuffered; export const close = $$$hostConfig.close; export const closeWithError = $$$hostConfig.closeWithError; -export const convertStringToBuffer = $$$hostConfig.convertStringToBuffer; +export const stringToChunk = $$$hostConfig.stringToChunk; +export const stringToPrecomputedChunk = $$$hostConfig.stringToPrecomputedChunk; diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 98fc13e4f1204..7f96ae163ad63 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -94,7 +94,13 @@ module.exports = [ 'react-server-native-relay', 'react-server-native-relay/server', ], - paths: ['react-native-renderer', 'react-server-native-relay'], + paths: [ + 'react-native-renderer', + 'react-server-native-relay', + // this is included here so that it's not included in the main native check + // remove this when it's added to the main native renderer. + 'react-native-renderer/src/server', + ], isFlowTyped: true, isServerSupported: true, },