Skip to content

Commit

Permalink
Allow the streaming config to decide how to precompute or compute chunks
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sebmarkbage committed Mar 15, 2021
1 parent 00d4f95 commit 28d3fca
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 69 deletions.
93 changes: 51 additions & 42 deletions packages/react-dom/src/server/ReactDOMServerFormatConfig.js
Expand Up @@ -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';
Expand Down Expand Up @@ -55,43 +60,43 @@ function encodeHTMLTextNode(text: string): string {
}

export function pushTextInstance(
target: Array<Uint8Array>,
target: Array<Chunk | PrecomputedChunk>,
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<Uint8Array>,
target: Array<Chunk | PrecomputedChunk>,
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 endTag2 = convertStringToBuffer('>');
const endTag1 = stringToPrecomputedChunk('</');
const endTag2 = stringToPrecomputedChunk('>');

export function pushEndInstance(
target: Array<Uint8Array>,
target: Array<Chunk | PrecomputedChunk>,
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('<span id="');
const placeholder2 = convertStringToBuffer('P:');
const placeholder3 = convertStringToBuffer('"></span>');
const placeholder1 = stringToPrecomputedChunk('<span id="');
const placeholder2 = stringToPrecomputedChunk('P:');
const placeholder3 = stringToPrecomputedChunk('"></span>');
export function writePlaceholder(
destination: Destination,
id: number,
Expand All @@ -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,
Expand All @@ -134,10 +141,10 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
return writeChunk(destination, endSuspenseBoundary);
}

const startSegment = convertStringToBuffer('<div hidden id="');
const startSegment2 = convertStringToBuffer('S:');
const startSegment3 = convertStringToBuffer('">');
const endSegment = convertStringToBuffer('"></div>');
const startSegment = stringToPrecomputedChunk('<div hidden id="');
const startSegment2 = stringToPrecomputedChunk('S:');
const startSegment3 = stringToPrecomputedChunk('">');
const endSegment = stringToPrecomputedChunk('"></div>');
export function writeStartSegment(
destination: Destination,
id: number,
Expand All @@ -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);
}
Expand Down Expand Up @@ -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(
'<script>' + completeSegmentFunction + ';$RS("S:',
);
const completeSegmentScript1Partial = convertStringToBuffer('<script>$RS("S:');
const completeSegmentScript2 = convertStringToBuffer('","P:');
const completeSegmentScript3 = convertStringToBuffer('")</script>');
const completeSegmentScript1Partial = stringToPrecomputedChunk(
'<script>$RS("S:',
);
const completeSegmentScript2 = stringToPrecomputedChunk('","P:');
const completeSegmentScript3 = stringToPrecomputedChunk('")</script>');

export function writeCompletedSegmentInstruction(
destination: Destination,
Expand All @@ -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(
'<script>' + completeBoundaryFunction + ';$RC("',
);
const completeBoundaryScript1Partial = convertStringToBuffer('<script>$RC("');
const completeBoundaryScript2 = convertStringToBuffer('","S:');
const completeBoundaryScript3 = convertStringToBuffer('")</script>');
const completeBoundaryScript1Partial = stringToPrecomputedChunk(
'<script>$RC("',
);
const completeBoundaryScript2 = stringToPrecomputedChunk('","S:');
const completeBoundaryScript3 = stringToPrecomputedChunk('")</script>');

export function writeCompletedBoundaryInstruction(
destination: Destination,
Expand All @@ -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(
'<script>' + clientRenderFunction + ';$RX("',
);
const clientRenderScript1Partial = convertStringToBuffer('<script>$RX("');
const clientRenderScript2 = convertStringToBuffer('")</script>');
const clientRenderScript1Partial = stringToPrecomputedChunk('<script>$RX("');
const clientRenderScript2 = stringToPrecomputedChunk('")</script>');

export function writeClientRenderBoundaryInstruction(
destination: Destination,
Expand All @@ -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);
Expand Down
Expand Up @@ -7,11 +7,12 @@
* @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';
Expand Down Expand Up @@ -71,10 +72,10 @@ export function createSuspenseBoundaryID(
return responseState.nextSuspenseID++;
}

const RAW_TEXT = convertStringToBuffer('RCTRawText');
const RAW_TEXT = stringToPrecomputedChunk('RCTRawText');

export function pushTextInstance(
target: Array<Uint8Array>,
target: Array<Chunk | PrecomputedChunk>,
text: string,
): void {
target.push(
Expand All @@ -87,20 +88,20 @@ export function pushTextInstance(
}

export function pushStartInstance(
target: Array<Uint8Array>,
target: Array<Chunk | PrecomputedChunk>,
type: string,
props: Object,
): void {
target.push(
INSTANCE,
convertStringToBuffer(type),
stringToChunk(type),
END, // Null terminated type string
// TODO: props
);
}

export function pushEndInstance(
target: Array<Uint8Array>,
target: Array<Chunk | PrecomputedChunk>,
type: string,
props: Object,
): void {
Expand Down
11 changes: 7 additions & 4 deletions packages/react-noop-renderer/src/ReactNoopFlightServer.js
Expand Up @@ -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');
Expand Down
8 changes: 6 additions & 2 deletions packages/react-server/src/ReactFizzServer.js
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Uint8Array>,
+chunks: Array<Chunk | PrecomputedChunk>,
+children: Array<Segment>,
// If this segment represents a fallback, this is the content that will replace that fallback.
+boundary: null | SuspenseBoundary,
Expand Down
14 changes: 7 additions & 7 deletions packages/react-server/src/ReactFlightServerConfigStream.js
Expand Up @@ -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;

Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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 {
Expand Down
13 changes: 10 additions & 3 deletions packages/react-server/src/ReactServerStreamConfigBrowser.js
Expand Up @@ -9,6 +9,9 @@

export type Destination = ReadableStreamController;

export type PrecomputedChunk = Uint8Array;
export type Chunk = Uint8Array;

export function scheduleWork(callback: () => void) {
callback();
}
Expand All @@ -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;
}

Expand All @@ -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);
}

Expand Down

0 comments on commit 28d3fca

Please sign in to comment.