Basic Fizz Architecture (#20970)

* Copy some infra structure patterns from Flight

* Basic data structures

* Move structural nodes and instruction commands to host config

* Move instruction command to host config

In the DOM this is implemented as script tags. The first time it's emitted
it includes the function. Future calls invoke the same function.

The side of the complete boundary function in particular is unfortunately
large.

* Implement Fizz Noop host configs

This is implemented not as a serialized protocol but by-passing the
serialization when possible and instead it's like a live tree being
built.

* Implement React Native host config

This is not wired up. I just need something for the flow types since
Flight and Fizz are both handled by the isServerSupported flag.

Might as well add something though.

The principle of this format is the same structure as for HTML but a
simpler binary format.

Each entry is a tag followed by some data and terminated by null.

* Check in error codes

* Comment
This commit is contained in:
Sebastian Markbåge
2021-03-11 15:01:41 -05:00
committed by GitHub
parent bd245c1bab
commit 10cc400184
8 changed files with 1564 additions and 71 deletions

View File

@@ -36,12 +36,6 @@ const ReactNoopFlightServer = ReactFlightServer({
convertStringToBuffer(content: string): Uint8Array {
return Buffer.from(content, 'utf8');
},
formatChunkAsString(type: string, props: Object): string {
return JSON.stringify({type, props});
},
formatChunk(type: string, props: Object): Uint8Array {
return Buffer.from(JSON.stringify({type, props}), 'utf8');
},
isModuleReference(reference: Object): boolean {
return reference.$$typeof === Symbol.for('react.module.reference');
},

View File

@@ -16,7 +16,40 @@
import ReactFizzServer from 'react-server';
type Destination = Array<string>;
type Instance = {|
type: string,
children: Array<Instance | TextInstance | SuspenseInstance>,
prop: any,
hidden: boolean,
|};
type TextInstance = {|
text: string,
hidden: boolean,
|};
type SuspenseInstance = {|
state: 'pending' | 'complete' | 'client-render',
children: Array<Instance | TextInstance | SuspenseInstance>,
|};
type Placeholder = {
parent: Instance | SuspenseInstance,
index: number,
};
type Segment = {
children: null | Instance | TextInstance | SuspenseInstance,
};
type Destination = {
root: null | Instance | TextInstance | SuspenseInstance,
placeholders: Map<number, Placeholder>,
segments: Map<number, Segment>,
stack: Array<Segment | Instance | SuspenseInstance>,
};
const POP = Buffer.from('/', 'utf8');
const ReactNoopServer = ReactFizzServer({
scheduleWork(callback: () => void) {
@@ -24,24 +57,165 @@ const ReactNoopServer = ReactFizzServer({
},
beginWriting(destination: Destination): void {},
writeChunk(destination: Destination, buffer: Uint8Array): void {
destination.push(JSON.parse(Buffer.from((buffer: any)).toString('utf8')));
const stack = destination.stack;
if (buffer === POP) {
stack.pop();
return;
}
// We assume one chunk is one instance.
const instance = JSON.parse(Buffer.from((buffer: any)).toString('utf8'));
if (stack.length === 0) {
destination.root = instance;
} else {
const parent = stack[stack.length - 1];
parent.children.push(instance);
}
stack.push(instance);
},
completeWriting(destination: Destination): void {},
close(destination: Destination): void {},
flushBuffered(destination: Destination): void {},
convertStringToBuffer(content: string): Uint8Array {
return Buffer.from(content, 'utf8');
createResponseState(): null {
return null;
},
formatChunkAsString(type: string, props: Object): string {
return JSON.stringify({type, props});
createSuspenseBoundaryID(): SuspenseInstance {
// The ID is a pointer to the boundary itself.
return {state: 'pending', children: []};
},
formatChunk(type: string, props: Object): Uint8Array {
return Buffer.from(JSON.stringify({type, props}), 'utf8');
pushTextInstance(target: Array<Uint8Array>, text: string): void {
const textInstance: TextInstance = {
text,
hidden: false,
};
target.push(Buffer.from(JSON.stringify(textInstance), 'utf8'), POP);
},
pushStartInstance(
target: Array<Uint8Array>,
type: string,
props: Object,
): void {
const instance: Instance = {
type: type,
children: [],
prop: props.prop,
hidden: false,
};
target.push(Buffer.from(JSON.stringify(instance), 'utf8'));
},
pushEndInstance(
target: Array<Uint8Array>,
type: string,
props: Object,
): void {
target.push(POP);
},
writePlaceholder(destination: Destination, id: number): boolean {
const parent = destination.stack[destination.stack.length - 1];
destination.placeholders.set(id, {
parent: parent,
index: parent.children.length,
});
},
writeStartCompletedSuspenseBoundary(
destination: Destination,
suspenseInstance: SuspenseInstance,
): boolean {
suspenseInstance.state = 'complete';
const parent = destination.stack[destination.stack.length - 1];
parent.children.push(suspenseInstance);
destination.stack.push(suspenseInstance);
},
writeStartPendingSuspenseBoundary(
destination: Destination,
suspenseInstance: SuspenseInstance,
): boolean {
suspenseInstance.state = 'pending';
const parent = destination.stack[destination.stack.length - 1];
parent.children.push(suspenseInstance);
destination.stack.push(suspenseInstance);
},
writeStartClientRenderedSuspenseBoundary(
destination: Destination,
suspenseInstance: SuspenseInstance,
): boolean {
suspenseInstance.state = 'client-render';
const parent = destination.stack[destination.stack.length - 1];
parent.children.push(suspenseInstance);
destination.stack.push(suspenseInstance);
},
writeEndSuspenseBoundary(destination: Destination): boolean {
destination.stack.pop();
},
writeStartSegment(destination: Destination, id: number): boolean {
const segment = {
children: [],
};
destination.segments.set(id, segment);
if (destination.stack.length > 0) {
throw new Error('Segments are only expected at the root of the stack.');
}
destination.stack.push(segment);
},
writeEndSegment(destination: Destination): boolean {
destination.stack.pop();
},
writeCompletedSegmentInstruction(
destination: Destination,
responseState: ResponseState,
contentSegmentID: number,
): boolean {
const segment = destination.segments.get(contentSegmentID);
if (!segment) {
throw new Error('Missing segment.');
}
const placeholder = destination.placeholders.get(contentSegmentID);
if (!placeholder) {
throw new Error('Missing placeholder.');
}
placeholder.parent.children.splice(
placeholder.index,
0,
...segment.children,
);
},
writeCompletedBoundaryInstruction(
destination: Destination,
responseState: ResponseState,
boundary: SuspenseInstance,
contentSegmentID: number,
): boolean {
const segment = destination.segments.get(contentSegmentID);
if (!segment) {
throw new Error('Missing segment.');
}
boundary.children = segment.children;
boundary.state = 'complete';
},
writeClientRenderBoundaryInstruction(
destination: Destination,
responseState: ResponseState,
boundary: SuspenseInstance,
): boolean {
boundary.status = 'client-render';
},
});
function render(children: React$Element<any>): Destination {
const destination: Destination = [];
const destination: Destination = {
root: null,
placeholders: new Map(),
segments: new Map(),
stack: [],
};
const request = ReactNoopServer.createRequest(children, destination);
ReactNoopServer.startWork(request);
return destination;

View File

@@ -7,17 +7,368 @@
* @flow
*/
import {convertStringToBuffer} from 'react-server/src/ReactServerStreamConfig';
import type {Destination} from 'react-server/src/ReactServerStreamConfig';
export function formatChunkAsString(type: string, props: Object): string {
let str = '<' + type + '>';
if (typeof props.children === 'string') {
str += props.children;
import {
writeChunk,
convertStringToBuffer,
} from 'react-server/src/ReactServerStreamConfig';
import invariant from 'shared/invariant';
// Per response,
export type ResponseState = {
sentCompleteSegmentFunction: boolean,
sentCompleteBoundaryFunction: boolean,
sentClientRenderFunction: boolean,
};
// Allows us to keep track of what we've already written so we can refer back to it.
export function createResponseState(): ResponseState {
return {
sentCompleteSegmentFunction: false,
sentCompleteBoundaryFunction: false,
sentClientRenderFunction: false,
};
}
// This object is used to lazily reuse the ID of the first generated node, or assign one.
// We can't assign an ID up front because the node we're attaching it to might already
// have one. So we need to lazily use that if it's available.
export type SuspenseBoundaryID = {
id: null | string,
};
export function createSuspenseBoundaryID(
responseState: ResponseState,
): SuspenseBoundaryID {
return {id: null};
}
function encodeHTMLIDAttribute(value: string): string {
// TODO: This needs to be encoded for security purposes.
return value;
}
function encodeHTMLTextNode(text: string): string {
// TOOD: This needs to be encoded for security purposes.
return text;
}
export function pushTextInstance(
target: Array<Uint8Array>,
text: string,
): void {
target.push(convertStringToBuffer(encodeHTMLTextNode(text)));
}
const startTag1 = convertStringToBuffer('<');
const startTag2 = convertStringToBuffer('>');
export function pushStartInstance(
target: Array<Uint8Array>,
type: string,
props: Object,
): void {
// TODO: Figure out if it's self closing and everything else.
target.push(startTag1, convertStringToBuffer(type), startTag2);
}
const endTag1 = convertStringToBuffer('</');
const endTag2 = convertStringToBuffer('>');
export function pushEndInstance(
target: Array<Uint8Array>,
type: string,
props: Object,
): void {
// TODO: Figure out if it was self closing.
target.push(endTag1, convertStringToBuffer(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>');
export function writePlaceholder(
destination: Destination,
id: number,
): boolean {
// TODO: This needs to be contextually aware and switch tag since not all parents allow for spans like
// <select> or <tbody>. E.g. suspending a component that renders a table row.
writeChunk(destination, placeholder1);
// TODO: Use the identifierPrefix option to make the prefix configurable.
writeChunk(destination, placeholder2);
const formattedID = convertStringToBuffer(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('<!--/$-->');
export function writeStartCompletedSuspenseBoundary(
destination: Destination,
id: SuspenseBoundaryID,
): boolean {
return writeChunk(destination, startCompletedSuspenseBoundary);
}
export function writeStartPendingSuspenseBoundary(
destination: Destination,
id: SuspenseBoundaryID,
): boolean {
return writeChunk(destination, startPendingSuspenseBoundary);
}
export function writeStartClientRenderedSuspenseBoundary(
destination: Destination,
id: SuspenseBoundaryID,
): boolean {
return writeChunk(destination, startClientRenderedSuspenseBoundary);
}
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>');
export function writeStartSegment(
destination: Destination,
id: number,
): boolean {
// TODO: What happens with special children like <tr> if they're inserted in a div? Maybe needs contextually aware containers.
writeChunk(destination, startSegment);
// TODO: Use the identifierPrefix option to make the prefix configurable.
writeChunk(destination, startSegment2);
const formattedID = convertStringToBuffer(id.toString(16));
writeChunk(destination, formattedID);
return writeChunk(destination, startSegment3);
}
export function writeEndSegment(destination: Destination): boolean {
return writeChunk(destination, endSegment);
}
// Instruction Set
// The following code is the source scripts that we then minify and inline below,
// with renamed function names that we hope don't collide:
// const COMMENT_NODE = 8;
// const SUSPENSE_START_DATA = '$';
// const SUSPENSE_END_DATA = '/$';
// const SUSPENSE_PENDING_START_DATA = '$?';
// const SUSPENSE_FALLBACK_START_DATA = '$!';
//
// function clientRenderBoundary(suspenseBoundaryID) {
// // Find the fallback's first element.
// let suspenseNode = document.getElementById(suspenseBoundaryID);
// if (!suspenseNode) {
// // The user must have already navigated away from this tree.
// // E.g. because the parent was hydrated.
// return;
// }
// // Find the boundary around the fallback. This might include text nodes.
// do {
// suspenseNode = suspenseNode.previousSibling;
// } while (
// suspenseNode.nodeType !== COMMENT_NODE ||
// suspenseNode.data !== SUSPENSE_PENDING_START_DATA
// );
// // Tag it to be client rendered.
// suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
// // Tell React to retry it if the parent already hydrated.
// if (suspenseNode._reactRetry) {
// suspenseNode._reactRetry();
// }
// }
//
// function completeBoundary(suspenseBoundaryID, contentID) {
// // Find the fallback's first element.
// let suspenseNode = document.getElementById(suspenseBoundaryID);
// const contentNode = document.getElementById(contentID);
// // We'll detach the content node so that regardless of what happens next we don't leave in the tree.
// // This might also help by not causing recalcing each time we move a child from here to the target.
// contentNode.parentNode.removeChild(contentNode);
// if (!suspenseNode) {
// // The user must have already navigated away from this tree.
// // E.g. because the parent was hydrated. That's fine there's nothing to do
// // but we have to make sure that we already deleted the container node.
// return;
// }
// // Find the boundary around the fallback. This might include text nodes.
// do {
// suspenseNode = suspenseNode.previousSibling;
// } while (
// suspenseNode.nodeType !== COMMENT_NODE ||
// suspenseNode.data !== SUSPENSE_PENDING_START_DATA
// );
//
// // Clear all the existing children. This is complicated because
// // there can be embedded Suspense boundaries in the fallback.
// // This is similar to clearSuspenseBoundary in ReactDOMHostConfig.
// // TOOD: We could avoid this if we never emitted suspense boundaries in fallback trees.
// // They never hydrate anyway. However, currently we support incrementally loading the fallback.
// const parentInstance = suspenseNode.parentNode;
// let node = suspenseNode.nextSibling;
// let depth = 0;
// do {
// if (node && node.nodeType === COMMENT_NODE) {
// const data = node.data;
// if (data === SUSPENSE_END_DATA) {
// if (depth === 0) {
// break;
// } else {
// depth--;
// }
// } else if (
// data === SUSPENSE_START_DATA ||
// data === SUSPENSE_PENDING_START_DATA ||
// data === SUSPENSE_FALLBACK_START_DATA
// ) {
// depth++;
// }
// }
//
// const nextNode = node.nextSibling;
// parentInstance.removeChild(node);
// node = nextNode;
// } while (node);
//
// const endOfBoundary = node;
//
// // Insert all the children from the contentNode between the start and end of suspense boundary.
// while (contentNode.firstChild) {
// parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
// }
// suspenseNode.data = SUSPENSE_START_DATA;
// if (suspenseNode._reactRetry) {
// suspenseNode._reactRetry();
// }
// }
//
// function completeSegment(containerID, placeholderID) {
// const segmentContainer = document.getElementById(containerID);
// const placeholderNode = document.getElementById(placeholderID);
// // We always expect both nodes to exist here because, while we might
// // have navigated away from the main tree, we still expect the detached
// // tree to exist.
// segmentContainer.parentNode.removeChild(segmentContainer);
// while (segmentContainer.firstChild) {
// placeholderNode.parentNode.insertBefore(
// segmentContainer.firstChild,
// placeholderNode,
// );
// }
// placeholderNode.parentNode.removeChild(placeholderNode);
// }
const completeSegmentFunction =
'function $RS(b,f){var a=document.getElementById(b),c=document.getElementById(f);for(a.parentNode.removeChild(a);a.firstChild;)c.parentNode.insertBefore(a.firstChild,c);c.parentNode.removeChild(c)}';
const completeBoundaryFunction =
'function $RC(b,f){var a=document.getElementById(b),c=document.getElementById(f);c.parentNode.removeChild(c);if(a){do a=a.previousSibling;while(8!==a.nodeType||"$?"!==a.data);var h=a.parentNode,d=a.nextSibling,g=0;do{if(d&&8===d.nodeType){var e=d.data;if("/$"===e)if(0===g)break;else g--;else"$"!==e&&"$?"!==e&&"$!"!==e||g++}e=d.nextSibling;h.removeChild(d);d=e}while(d);for(;c.firstChild;)h.insertBefore(c.firstChild,d);a.data="$";a._reactRetry&&a._reactRetry()}}';
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(
'<script>' + completeSegmentFunction + ';$RS("S:',
);
const completeSegmentScript1Partial = convertStringToBuffer('<script>$RS("S:');
const completeSegmentScript2 = convertStringToBuffer('","P:');
const completeSegmentScript3 = convertStringToBuffer('")</script>');
export function writeCompletedSegmentInstruction(
destination: Destination,
responseState: ResponseState,
contentSegmentID: number,
): boolean {
if (responseState.sentCompleteSegmentFunction) {
// The first time we write this, we'll need to include the full implementation.
responseState.sentCompleteSegmentFunction = true;
writeChunk(destination, completeSegmentScript1Full);
} else {
// Future calls can just reuse the same function.
writeChunk(destination, completeSegmentScript1Partial);
}
str += '</' + type + '>';
return str;
// TODO: Use the identifierPrefix option to make the prefix configurable.
const formattedID = convertStringToBuffer(contentSegmentID.toString(16));
writeChunk(destination, formattedID);
writeChunk(destination, completeSegmentScript2);
writeChunk(destination, formattedID);
return writeChunk(destination, completeSegmentScript3);
}
export function formatChunk(type: string, props: Object): Uint8Array {
return convertStringToBuffer(formatChunkAsString(type, props));
const completeBoundaryScript1Full = convertStringToBuffer(
'<script>' + completeBoundaryFunction + ';$RC("',
);
const completeBoundaryScript1Partial = convertStringToBuffer('<script>$RC("');
const completeBoundaryScript2 = convertStringToBuffer('","S:');
const completeBoundaryScript3 = convertStringToBuffer('")</script>');
export function writeCompletedBoundaryInstruction(
destination: Destination,
responseState: ResponseState,
boundaryID: SuspenseBoundaryID,
contentSegmentID: number,
): boolean {
if (responseState.sentCompleteBoundaryFunction) {
// The first time we write this, we'll need to include the full implementation.
responseState.sentCompleteBoundaryFunction = true;
writeChunk(destination, completeBoundaryScript1Full);
} else {
// Future calls can just reuse the same function.
writeChunk(destination, completeBoundaryScript1Partial);
}
// TODO: Use the identifierPrefix option to make the prefix configurable.
invariant(
boundaryID.id !== null,
'An ID must have been assigned before we can complete the boundary.',
);
const formattedBoundaryID = convertStringToBuffer(
encodeHTMLIDAttribute(boundaryID.id),
);
const formattedContentID = convertStringToBuffer(
contentSegmentID.toString(16),
);
writeChunk(destination, formattedBoundaryID);
writeChunk(destination, completeBoundaryScript2);
writeChunk(destination, formattedContentID);
return writeChunk(destination, completeBoundaryScript3);
}
const clientRenderScript1Full = convertStringToBuffer(
'<script>' + clientRenderFunction + ';$RX("',
);
const clientRenderScript1Partial = convertStringToBuffer('<script>$RX("');
const clientRenderScript2 = convertStringToBuffer('")</script>');
export function writeClientRenderBoundaryInstruction(
destination: Destination,
responseState: ResponseState,
boundaryID: SuspenseBoundaryID,
): boolean {
if (responseState.sentClientRenderFunction) {
// The first time we write this, we'll need to include the full implementation.
responseState.sentClientRenderFunction = true;
writeChunk(destination, clientRenderScript1Full);
} else {
// Future calls can just reuse the same function.
writeChunk(destination, clientRenderScript1Partial);
}
invariant(
boundaryID.id !== null,
'An ID must have been assigned before we can complete the boundary.',
);
const formattedBoundaryID = convertStringToBuffer(
encodeHTMLIDAttribute(boundaryID.id),
);
writeChunk(destination, formattedBoundaryID);
return writeChunk(destination, clientRenderScript2);
}

View File

@@ -7,8 +7,13 @@
* @flow
*/
import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes';
import type {Destination} from './ReactServerStreamConfig';
import type {ReactNodeList} from 'shared/ReactTypes';
import type {
SuspenseBoundaryID,
ResponseState,
} from './ReactServerFormatConfig';
import {
scheduleWork,
@@ -18,66 +23,819 @@ import {
flushBuffered,
close,
} from './ReactServerStreamConfig';
import {formatChunk} from './ReactServerFormatConfig';
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
import {
writePlaceholder,
writeStartCompletedSuspenseBoundary,
writeStartPendingSuspenseBoundary,
writeStartClientRenderedSuspenseBoundary,
writeEndSuspenseBoundary,
writeStartSegment,
writeEndSegment,
writeClientRenderBoundaryInstruction,
writeCompletedBoundaryInstruction,
writeCompletedSegmentInstruction,
pushTextInstance,
pushStartInstance,
pushEndInstance,
createSuspenseBoundaryID,
createResponseState,
} from './ReactServerFormatConfig';
import {REACT_ELEMENT_TYPE, REACT_SUSPENSE_TYPE} from 'shared/ReactSymbols';
import ReactSharedInternals from 'shared/ReactSharedInternals';
type OpaqueRequest = {
destination: Destination,
children: ReactNodeList,
completedChunks: Array<Uint8Array>,
flowing: boolean,
...
import invariant from 'shared/invariant';
const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
type SuspenseBoundary = {
+id: SuspenseBoundaryID,
rootSegmentID: number,
forceClientRender: boolean, // if it errors or infinitely suspends
parentFlushed: boolean,
pendingWork: number, // when it reaches zero we can show this boundary's content
completedSegments: Array<Segment>, // completed but not yet flushed segments.
byteSize: number, // used to determine whether to inline children boundaries.
};
type SuspendedWork = {
node: ReactNodeList,
ping: () => void,
blockedBoundary: Root | SuspenseBoundary,
blockedSegment: Segment, // the segment we'll write to
assignID: null | SuspenseBoundaryID, // id to assign to the content
};
const PENDING = 0;
const COMPLETED = 1;
const FLUSHED = 2;
const ERRORED = 3;
type Root = null;
type Segment = {
status: 0 | 1 | 2 | 3,
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>,
+children: Array<Segment>,
// If this segment represents a fallback, this is the content that will replace that fallback.
+boundary: null | SuspenseBoundary,
};
const BUFFERING = 0;
const FLOWING = 1;
const CLOSED = 2;
type Request = {
+destination: Destination,
+responseState: ResponseState,
+maxBoundarySize: number,
status: 0 | 1 | 2,
nextSegmentId: number,
allPendingWork: number, // when it reaches zero, we can close the connection.
pendingRootWork: number, // when this reaches zero, we've finished at least the root boundary.
completedRootSegment: null | Segment, // Completed but not yet flushed root segments.
pingedWork: Array<SuspendedWork>,
// Queues to flush in order of priority
clientRenderedBoundaries: Array<SuspenseBoundary>, // Errored or client rendered but not yet flushed.
completedBoundaries: Array<SuspenseBoundary>, // Completed but not yet fully flushed boundaries to show.
partialBoundaries: Array<SuspenseBoundary>, // Partially completed boundaries that can flush its segments early.
};
export function createRequest(
children: ReactNodeList,
destination: Destination,
): OpaqueRequest {
return {destination, children, completedChunks: [], flowing: false};
): Request {
const pingedWork = [];
const request = {
destination,
responseState: createResponseState(),
maxBoundarySize: 1024,
status: BUFFERING,
nextSegmentId: 0,
allPendingWork: 0,
pendingRootWork: 0,
completedRootSegment: null,
pingedWork: pingedWork,
clientRenderedBoundaries: [],
completedBoundaries: [],
partialBoundaries: [],
};
// This segment represents the root fallback.
const rootSegment = createPendingSegment(request, 0, null);
// There is no parent so conceptually, we're unblocked to flush this segment.
rootSegment.parentFlushed = true;
const rootWork = createSuspendedWork(
request,
children,
null,
rootSegment,
null,
);
pingedWork.push(rootWork);
return request;
}
function performWork(request: OpaqueRequest): void {
const element = (request.children: any);
request.children = null;
if (element && element.$$typeof !== REACT_ELEMENT_TYPE) {
function pingSuspendedWork(request: Request, work: SuspendedWork): void {
const pingedWork = request.pingedWork;
pingedWork.push(work);
if (pingedWork.length === 1) {
scheduleWork(() => performWork(request));
}
}
function createSuspenseBoundary(request: Request): SuspenseBoundary {
return {
id: createSuspenseBoundaryID(request.responseState),
rootSegmentID: -1,
parentFlushed: false,
pendingWork: 0,
forceClientRender: false,
completedSegments: [],
byteSize: 0,
};
}
function createSuspendedWork(
request: Request,
node: ReactNodeList,
blockedBoundary: Root | SuspenseBoundary,
blockedSegment: Segment,
assignID: null | SuspenseBoundaryID,
): SuspendedWork {
request.allPendingWork++;
if (blockedBoundary === null) {
request.pendingRootWork++;
} else {
blockedBoundary.pendingWork++;
}
const work = {
node,
ping: () => pingSuspendedWork(request, work),
blockedBoundary,
blockedSegment,
assignID,
};
return work;
}
function createPendingSegment(
request: Request,
index: number,
boundary: null | SuspenseBoundary,
): Segment {
return {
status: PENDING,
id: -1, // lazily assigned later
index,
parentFlushed: false,
chunks: [],
children: [],
boundary,
};
}
function reportError(request: Request, error: mixed): void {
// TODO: Report errors on the server.
}
function fatalError(request: Request, error: mixed): void {
// This is called outside error handling code such as if the root errors outside
// a suspense boundary or if the root suspense boundary's fallback errors.
// It's also called if React itself or its host configs errors.
request.status = CLOSED;
// TODO: Destroy the stream with an error. We weren't able to complete the root.
}
function renderNode(
request: Request,
parentBoundary: Root | SuspenseBoundary,
segment: Segment,
node: ReactNodeList,
): void {
if (typeof node === 'string') {
pushTextInstance(segment.chunks, node);
return;
}
if (
typeof node !== 'object' ||
!node ||
(node: any).$$typeof !== REACT_ELEMENT_TYPE
) {
throw new Error('Not yet implemented node type.');
}
const element: React$Element<any> = (node: any);
const type = element.type;
const props = element.props;
if (typeof type !== 'string') {
if (typeof type === 'function') {
try {
const result = type(props);
renderNode(request, parentBoundary, segment, result);
} catch (x) {
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
// Something suspended, we'll need to create a new segment and resolve it later.
const insertionIndex = segment.chunks.length;
const newSegment = createPendingSegment(request, insertionIndex, null);
const suspendedWork = createSuspendedWork(
request,
node,
parentBoundary,
newSegment,
null,
);
const ping = suspendedWork.ping;
x.then(ping, ping);
// TODO: Emit place holder
} else {
// We can rethrow to terminate the rest of this tree.
throw x;
}
}
} else if (typeof type === 'string') {
pushStartInstance(segment.chunks, type, props);
renderNode(request, parentBoundary, segment, props.children);
pushEndInstance(segment.chunks, type, props);
} else if (type === REACT_SUSPENSE_TYPE) {
// Each time we enter a suspense boundary, we split out into a new segment for
// the fallback so that we can later replace that segment with the content.
// This also lets us split out the main content even if it doesn't suspend,
// in case it ends up generating a large subtree of content.
const fallback: ReactNodeList = props.fallback;
const content: ReactNodeList = props.children;
const newBoundary = createSuspenseBoundary(request);
const insertionIndex = segment.chunks.length;
// The children of the boundary segment is actually the fallback.
const boundarySegment = createPendingSegment(
request,
insertionIndex,
newBoundary,
);
// We create suspended work for the fallback because we don't want to actually work
// on it yet in case we finish the main content, so we queue for later.
const suspendedFallbackWork = createSuspendedWork(
request,
fallback,
parentBoundary,
boundarySegment,
newBoundary.id, // This is the ID we want to give this fallback so we can replace it later.
);
// TODO: This should be queued at a separate lower priority queue so that we only work
// on preparing fallbacks if we don't have any more main content to work on.
request.pingedWork.push(suspendedFallbackWork);
// This segment is the actual child content. We can start rendering that immediately.
const contentRootSegment = createPendingSegment(request, 0, null);
// We mark the root segment as having its parent flushed. It's not really flushed but there is
// no parent segment so there's nothing to wait on.
contentRootSegment.parentFlushed = true;
// TODO: Currently this is running synchronously. We could instead schedule this to pingedWork.
// I suspect that there might be some efficiency benefits from not creating the suspended work
// and instead just using the stack if possible. Particularly when we add contexts.
const contentWork = createSuspendedWork(
request,
content,
newBoundary,
contentRootSegment,
null,
);
retryWork(request, contentWork);
} else {
throw new Error('Not yet implemented element type.');
}
}
function errorWork(
request: Request,
boundary: Root | SuspenseBoundary,
segment: Segment,
error: mixed,
) {
segment.status = ERRORED;
request.allPendingWork--;
if (boundary !== null) {
boundary.pendingWork--;
}
// Report the error to a global handler.
reportError(request, error);
if (boundary === null) {
fatalError(request, error);
} else if (!boundary.forceClientRender) {
boundary.forceClientRender = true;
// Regardless of what happens next, this boundary won't be displayed,
// so we can flush it, if the parent already flushed.
if (boundary.parentFlushed) {
// We don't have a preference where in the queue this goes since it's likely
// to error on the client anyway. However, intentionally client-rendered
// boundaries should be flushed earlier so that they can start on the client.
// We reuse the same queue for errors.
request.clientRenderedBoundaries.push(boundary);
}
}
}
function completeWork(
request: Request,
boundary: Root | SuspenseBoundary,
segment: Segment,
) {
segment.status = COMPLETED;
request.allPendingWork--;
if (boundary === null) {
request.pendingRootWork--;
if (segment.parentFlushed) {
invariant(
request.completedRootSegment === null,
'There can only be one root segment. This is a bug in React.',
);
request.completedRootSegment = segment;
}
return;
}
request.completedChunks.push(formatChunk(type, props));
if (request.flowing) {
flushCompletedChunks(request);
}
flushBuffered(request.destination);
boundary.pendingWork--;
if (boundary.forceClientRender) {
// This already errored.
return;
}
if (boundary.pendingWork === 0) {
// This must have been the last segment we were waiting on. This boundary is now complete.
if (segment.parentFlushed) {
// Our parent segment already flushed, so we need to schedule this segment to be emitted.
boundary.completedSegments.push(segment);
}
if (boundary.parentFlushed) {
// The segment might be part of a segment that didn't flush yet, but if the boundary's
// parent flushed, we need to schedule the boundary to be emitted.
request.completedBoundaries.push(boundary);
}
} else {
if (segment.parentFlushed) {
// Our parent already flushed, so we need to schedule this segment to be emitted.
const completedSegments = boundary.completedSegments;
completedSegments.push(segment);
if (completedSegments.length === 1) {
// This is the first time since we last flushed that we completed anything.
// We can schedule this boundary to emit its partially completed segments early
// in case the parent has already been flushed.
if (boundary.parentFlushed) {
request.partialBoundaries.push(boundary);
}
}
}
}
}
function flushCompletedChunks(request: OpaqueRequest) {
const destination = request.destination;
const chunks = request.completedChunks;
request.completedChunks = [];
function retryWork(request: Request, work: SuspendedWork): void {
const segment = work.blockedSegment;
const boundary = work.blockedBoundary;
try {
let node = work.node;
while (
typeof node === 'object' &&
node !== null &&
(node: any).$$typeof === REACT_ELEMENT_TYPE &&
typeof node.type === 'function'
) {
// Doing this here lets us reuse this same Segment if the next component
// also suspends.
const element: React$Element<any> = (node: any);
work.node = node;
// TODO: Classes and legacy context etc.
node = element.type(element.props);
}
renderNode(request, boundary, segment, node);
completeWork(request, boundary, segment);
} catch (x) {
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
// Something suspended again, let's pick it back up later.
const ping = work.ping;
x.then(ping, ping);
} else {
errorWork(request, boundary, segment, x);
}
}
}
function performWork(request: Request): void {
if (request.status === CLOSED) {
return;
}
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = Dispatcher;
try {
const pingedWork = request.pingedWork;
let i;
for (i = 0; i < pingedWork.length; i++) {
const work = pingedWork[i];
retryWork(request, work);
}
pingedWork.splice(0, i);
if (request.status === FLOWING) {
flushCompletedQueues(request);
}
} catch (error) {
fatalError(request, error);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
}
function flushSubtree(
request: Request,
destination: Destination,
segment: Segment,
): boolean {
segment.parentFlushed = true;
switch (segment.status) {
case PENDING: {
// We're emitting a placeholder for this segment to be filled in later.
// Therefore we'll need to assign it an ID - to refer to it by.
const segmentID = (segment.id = request.nextSegmentId++);
return writePlaceholder(destination, segmentID);
}
case COMPLETED: {
segment.status = FLUSHED;
let r = true;
const chunks = segment.chunks;
let chunkIdx = 0;
const children = segment.children;
for (let childIdx = 0; childIdx < children.length; childIdx++) {
const nextChild = children[childIdx];
// Write all the chunks up until the next child.
for (; chunkIdx < nextChild.index; chunkIdx++) {
writeChunk(destination, chunks[chunkIdx]);
}
r = flushSegment(request, destination, nextChild);
}
// Finally just write all the remaining chunks
for (; chunkIdx < chunks.length; chunkIdx++) {
r = writeChunk(destination, chunks[chunkIdx]);
}
return r;
}
default: {
invariant(
false,
'Errored or already flushed boundaries should not be flushed again. This is a bug in React.',
);
}
}
}
function flushSegment(
request: Request,
destination,
segment: Segment,
): boolean {
const boundary = segment.boundary;
if (boundary === null) {
// Not a suspense boundary.
return flushSubtree(request, destination, segment);
}
boundary.parentFlushed = true;
// This segment is a Suspense boundary. We need to decide whether to
// emit the content or the fallback now.
if (boundary.forceClientRender) {
// Emit a client rendered suspense boundary wrapper.
// We never queue the inner boundary so we'll never emit its content or partial segments.
writeStartClientRenderedSuspenseBoundary(destination, boundary.id);
// Flush the fallback.
flushSubtree(request, destination, segment);
return writeEndSuspenseBoundary(destination);
} else if (boundary.pendingWork > 0) {
// This boundary is still loading. Emit a pending suspense boundary wrapper.
// Assign an ID to refer to the future content by.
boundary.rootSegmentID = request.nextSegmentId++;
if (boundary.completedSegments.length > 0) {
// If this is at least partially complete, we can queue it to be partially emmitted early.
request.partialBoundaries.push(boundary);
}
writeStartPendingSuspenseBoundary(destination, boundary.id);
// Flush the fallback.
flushSubtree(request, destination, segment);
return writeEndSuspenseBoundary(destination);
} else if (boundary.byteSize > request.maxBoundarySize) {
// This boundary is large and will be emitted separately so that we can progressively show
// other content. We add it to the queue during the flush because we have to ensure that
// the parent flushes first so that there's something to inject it into.
// We also have to make sure that it's emitted into the queue in a deterministic slot.
// I.e. we can't insert it here when it completes.
// Assign an ID to refer to the future content by.
boundary.rootSegmentID = request.nextSegmentId++;
request.completedBoundaries.push(boundary);
// Emit a pending rendered suspense boundary wrapper.
writeStartPendingSuspenseBoundary(destination, boundary.id);
// Flush the fallback.
flushSubtree(request, destination, segment);
return writeEndSuspenseBoundary(destination);
} else {
// We can inline this boundary's content as a complete boundary.
writeStartCompletedSuspenseBoundary(destination, boundary.id);
const completedSegments = boundary.completedSegments;
invariant(
completedSegments.length === 1,
'A previously unvisited boundary must have exactly one root segment. This is a bug in React.',
);
const contentSegment = completedSegments[0];
flushSegment(request, destination, contentSegment);
return writeEndSuspenseBoundary(destination);
}
}
function flushClientRenderedBoundary(
request: Request,
destination: Destination,
boundary: SuspenseBoundary,
): boolean {
return writeClientRenderBoundaryInstruction(
destination,
request.responseState,
boundary.id,
);
}
function flushSegmentContainer(
request: Request,
destination: Destination,
segment: Segment,
): boolean {
writeStartSegment(destination, segment.id);
flushSegment(request, destination, segment);
return writeEndSegment(destination);
}
function flushCompletedBoundary(
request: Request,
destination: Destination,
boundary: SuspenseBoundary,
): boolean {
const completedSegments = boundary.completedSegments;
let i = 0;
for (; i < completedSegments.length; i++) {
const segment = completedSegments[i];
flushPartiallyCompletedSegment(request, destination, boundary, segment);
}
completedSegments.length = 0;
return writeCompletedBoundaryInstruction(
destination,
request.responseState,
boundary.id,
boundary.rootSegmentID,
);
}
function flushPartialBoundary(
request: Request,
destination: Destination,
boundary: SuspenseBoundary,
): boolean {
const completedSegments = boundary.completedSegments;
let i = 0;
for (; i < completedSegments.length; i++) {
const segment = completedSegments[i];
if (
!flushPartiallyCompletedSegment(request, destination, boundary, segment)
) {
i++;
completedSegments.splice(0, i);
// Only write as much as the buffer wants. Something higher priority
// might want to write later.
return false;
}
}
completedSegments.splice(0, i);
return true;
}
function flushPartiallyCompletedSegment(
request: Request,
destination: Destination,
boundary: SuspenseBoundary,
segment: Segment,
): boolean {
if (segment.status === FLUSHED) {
// We've already flushed this inline.
return true;
}
const segmentID = segment.id;
if (segmentID === -1) {
// This segment wasn't previously referred to. This happens at the root of
// a boundary. We make kind of a leap here and assume this is the root.
const rootSegmentID = (segment.id = boundary.rootSegmentID);
invariant(
rootSegmentID !== -1,
'A root segment ID must have been assigned by now. This is a bug in React.',
);
return flushSegmentContainer(request, destination, segment);
} else {
flushSegmentContainer(request, destination, segment);
return writeCompletedSegmentInstruction(
destination,
request.responseState,
segmentID,
);
}
}
let reentrant = false;
function flushCompletedQueues(request: Request): void {
if (reentrant) {
return;
}
reentrant = true;
const destination = request.destination;
beginWriting(destination);
try {
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
writeChunk(destination, chunk);
// The structure of this is to go through each queue one by one and write
// until the sink tells us to stop. When we should stop, we still finish writing
// that item fully and then yield. At that point we remove the already completed
// items up until the point we completed them.
// TODO: Emit preloading.
// TODO: It's kind of unfortunate to keep checking this array after we've already
// emitted the root.
const completedRootSegment = request.completedRootSegment;
if (completedRootSegment !== null && request.pendingRootWork === 0) {
flushSegment(request, destination, completedRootSegment);
request.completedRootSegment = null;
}
} finally {
// We emit client rendering instructions for already emitted boundaries first.
// This is so that we can signal to the client to start client rendering them as
// soon as possible.
const clientRenderedBoundaries = request.clientRenderedBoundaries;
let i;
for (i = 0; i < clientRenderedBoundaries.length; i++) {
const boundary = clientRenderedBoundaries[i];
if (!flushClientRenderedBoundary(request, destination, boundary)) {
request.status = BUFFERING;
i++;
clientRenderedBoundaries.splice(0, i);
return;
}
}
clientRenderedBoundaries.splice(0, i);
// Next we emit any complete boundaries. It's better to favor boundaries
// that are completely done since we can actually show them, than it is to emit
// any individual segments from a partially complete boundary.
const completedBoundaries = request.completedBoundaries;
for (i = 0; i < completedBoundaries.length; i++) {
const boundary = completedBoundaries[i];
if (!flushCompletedBoundary(request, destination, boundary)) {
request.status = BUFFERING;
i++;
completedBoundaries.splice(0, i);
return;
}
}
completedBoundaries.splice(0, i);
// Allow anything written so far to flush to the underlying sink before
// we continue with lower priorities.
completeWriting(destination);
beginWriting(destination);
// TODO: Here we'll emit data used by hydration.
// Next we emit any segments of any boundaries that are partially complete
// but not deeply complete.
const partialBoundaries = request.partialBoundaries;
for (i = 0; i < partialBoundaries.length; i++) {
const boundary = partialBoundaries[i];
if (!flushPartialBoundary(request, destination, boundary)) {
request.status = BUFFERING;
i++;
partialBoundaries.splice(0, i);
return;
}
}
partialBoundaries.splice(0, i);
// Next we check the completed boundaries again. This may have had
// boundaries added to it in case they were too larged to be inlined.
// New ones might be added in this loop.
const largeBoundaries = request.completedBoundaries;
for (i = 0; i < largeBoundaries.length; i++) {
const boundary = largeBoundaries[i];
if (!flushCompletedBoundary(request, destination, boundary)) {
request.status = BUFFERING;
i++;
largeBoundaries.splice(0, i);
return;
}
}
largeBoundaries.splice(0, i);
} finally {
reentrant = false;
completeWriting(destination);
flushBuffered(destination);
if (
request.allPendingWork === 0 &&
request.pingedWork.length === 0 &&
request.clientRenderedBoundaries.length === 0 &&
request.completedBoundaries.length === 0
// We don't need to check any partially completed segments because
// either they have pending work or they're complete.
) {
// We're done.
close(destination);
}
}
close(destination);
}
export function startWork(request: OpaqueRequest): void {
request.flowing = true;
// TODO: Expose a way to abort further processing, without closing the connection from the outside.
// This would put all waiting boundaries into client-only mode.
export function startWork(request: Request): void {
// TODO: Don't automatically start flowing. Expose an explicit signal. Auto-start once everything is done.
request.status = FLOWING;
scheduleWork(() => performWork(request));
}
export function startFlowing(request: OpaqueRequest): void {
request.flowing = false;
flushCompletedChunks(request);
export function startFlowing(request: Request): void {
if (request.status === CLOSED) {
return;
}
request.status = FLOWING;
try {
flushCompletedQueues(request);
} catch (error) {
fatalError(request, error);
}
}
function notYetImplemented(): void {
throw new Error('Not yet implemented.');
}
function unsupportedRefresh() {
invariant(false, 'Cache cannot be refreshed during server rendering.');
}
function unsupportedStartTransition() {
invariant(false, 'startTransition cannot be called during server rendering.');
}
function noop(): void {}
const Dispatcher: DispatcherType = {
useMemo<T>(nextCreate: () => T): T {
return nextCreate();
},
useCallback<T>(callback: T): T {
return callback;
},
useDebugValue(): void {},
useDeferredValue<T>(value: T): T {
return value;
},
useTransition(): [(callback: () => void) => void, boolean] {
return [unsupportedStartTransition, false];
},
getCacheForType<T>(resourceType: () => T): T {
throw new Error('Not yet implemented. Should mark as client rendered.');
},
readContext: (notYetImplemented: any),
useContext: (notYetImplemented: any),
useReducer: (notYetImplemented: any),
useRef: (notYetImplemented: any),
useState: (notYetImplemented: any),
useLayoutEffect: noop,
// useImperativeHandle is not run in the server environment
useImperativeHandle: noop,
// Effects are not run in the server environment.
useEffect: noop,
useOpaqueIdentifier: (notYetImplemented: any),
useMutableSource: (notYetImplemented: any),
useCacheRefresh(): <T>(?() => T, ?T) => void {
return unsupportedRefresh;
},
};

View File

@@ -7,17 +7,197 @@
* @flow
*/
import {convertStringToBuffer} from 'react-server/src/ReactServerStreamConfig';
import type {Destination} from 'react-server/src/ReactServerStreamConfig';
export function formatChunkAsString(type: string, props: Object): string {
let str = '<' + type + '>';
if (typeof props.children === 'string') {
str += props.children;
import {
writeChunk,
convertStringToBuffer,
} from 'react-server/src/ReactServerStreamConfig';
import invariant from 'shared/invariant';
// Every list of children or string is null terminated.
const END_TAG = 0;
// Tree node tags.
const INSTANCE_TAG = 1;
const PLACEHOLDER_TAG = 2;
const SUSPENSE_PENDING_TAG = 3;
const SUSPENSE_COMPLETE_TAG = 4;
const SUSPENSE_CLIENT_RENDER_TAG = 5;
// Command tags.
const SEGMENT_TAG = 1;
const SUSPENSE_UPDATE_TO_COMPLETE_TAG = 2;
const SUSPENSE_UPDATE_TO_CLIENT_RENDER_TAG = 3;
const END = new Uint8Array(1);
END[0] = END_TAG;
const PLACEHOLDER = new Uint8Array(1);
PLACEHOLDER[0] = PLACEHOLDER_TAG;
const INSTANCE = new Uint8Array(1);
INSTANCE[0] = INSTANCE_TAG;
const SUSPENSE_PENDING = new Uint8Array(1);
SUSPENSE_PENDING[0] = SUSPENSE_PENDING_TAG;
const SUSPENSE_COMPLETE = new Uint8Array(1);
SUSPENSE_COMPLETE[0] = SUSPENSE_COMPLETE_TAG;
const SUSPENSE_CLIENT_RENDER = new Uint8Array(1);
SUSPENSE_CLIENT_RENDER[0] = SUSPENSE_CLIENT_RENDER_TAG;
const SEGMENT = new Uint8Array(1);
SEGMENT[0] = SEGMENT_TAG;
const SUSPENSE_UPDATE_TO_COMPLETE = new Uint8Array(1);
SUSPENSE_UPDATE_TO_COMPLETE[0] = SUSPENSE_UPDATE_TO_COMPLETE_TAG;
const SUSPENSE_UPDATE_TO_CLIENT_RENDER = new Uint8Array(1);
SUSPENSE_UPDATE_TO_CLIENT_RENDER[0] = SUSPENSE_UPDATE_TO_CLIENT_RENDER_TAG;
// Per response,
export type ResponseState = {
nextSuspenseID: number,
};
// Allows us to keep track of what we've already written so we can refer back to it.
export function createResponseState(): ResponseState {
return {
nextSuspenseID: 0,
};
}
// This object is used to lazily reuse the ID of the first generated node, or assign one.
// This is very specific to DOM where we can't assign an ID to.
export type SuspenseBoundaryID = number;
export function createSuspenseBoundaryID(
responseState: ResponseState,
): SuspenseBoundaryID {
return responseState.nextSuspenseID++;
}
const RAW_TEXT = convertStringToBuffer('RCTRawText');
export function pushTextInstance(
target: Array<Uint8Array>,
text: string,
): void {
target.push(
INSTANCE,
RAW_TEXT, // Type
END, // Null terminated type string
// TODO: props { text: text }
END, // End of children
);
}
export function pushStartInstance(
target: Array<Uint8Array>,
type: string,
props: Object,
): void {
target.push(
INSTANCE,
convertStringToBuffer(type),
END, // Null terminated type string
// TODO: props
);
}
export function pushEndInstance(
target: Array<Uint8Array>,
type: string,
props: Object,
): void {
target.push(END);
}
// IDs are formatted as little endian Uint16
function formatID(id: number): Uint8Array {
if (id > 0xffff) {
invariant(
false,
'More boundaries or placeholders than we expected to ever emit.',
);
}
str += '</' + type + '>';
return str;
const buffer = new Uint8Array(2);
buffer[0] = (id >>> 8) & 0xff;
buffer[1] = id & 0xff;
return buffer;
}
export function formatChunk(type: string, props: Object): Uint8Array {
return convertStringToBuffer(formatChunkAsString(type, props));
// 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.
export function writePlaceholder(
destination: Destination,
id: number,
): boolean {
writeChunk(destination, PLACEHOLDER);
return writeChunk(destination, formatID(id));
}
// Suspense boundaries are encoded as comments.
export function writeStartCompletedSuspenseBoundary(
destination: Destination,
id: SuspenseBoundaryID,
): boolean {
writeChunk(destination, SUSPENSE_COMPLETE);
return writeChunk(destination, formatID(id));
}
export function writeStartPendingSuspenseBoundary(
destination: Destination,
id: SuspenseBoundaryID,
): boolean {
writeChunk(destination, SUSPENSE_PENDING);
return writeChunk(destination, formatID(id));
}
export function writeStartClientRenderedSuspenseBoundary(
destination: Destination,
id: SuspenseBoundaryID,
): boolean {
writeChunk(destination, SUSPENSE_CLIENT_RENDER);
return writeChunk(destination, formatID(id));
}
export function writeEndSuspenseBoundary(destination: Destination): boolean {
return writeChunk(destination, END);
}
export function writeStartSegment(
destination: Destination,
id: number,
): boolean {
writeChunk(destination, SEGMENT);
return writeChunk(destination, formatID(id));
}
export function writeEndSegment(destination: Destination): boolean {
return writeChunk(destination, END);
}
// Instruction Set
export function writeCompletedSegmentInstruction(
destination: Destination,
responseState: ResponseState,
contentSegmentID: number,
): boolean {
// We don't need to emit this. Instead the client will keep track of pending placeholders.
// TODO: Returning true here is not correct. Avoid having to call this function at all.
return true;
}
export function writeCompletedBoundaryInstruction(
destination: Destination,
responseState: ResponseState,
boundaryID: SuspenseBoundaryID,
contentSegmentID: number,
): boolean {
writeChunk(destination, SUSPENSE_UPDATE_TO_COMPLETE);
writeChunk(destination, formatID(boundaryID));
return writeChunk(destination, formatID(contentSegmentID));
}
export function writeClientRenderBoundaryInstruction(
destination: Destination,
responseState: ResponseState,
boundaryID: SuspenseBoundaryID,
): boolean {
writeChunk(destination, SUSPENSE_UPDATE_TO_CLIENT_RENDER);
return writeChunk(destination, formatID(boundaryID));
}

View File

@@ -21,8 +21,15 @@ describe('ReactServer', () => {
ReactNoopServer = require('react-noop-renderer/server');
});
function div(...children) {
children = children.map(c =>
typeof c === 'string' ? {text: c, hidden: false} : c,
);
return {type: 'div', children, prop: undefined, hidden: false};
}
it('can call render', () => {
const result = ReactNoopServer.render(<div>hello world</div>);
expect(result).toEqual([{type: 'div', props: {children: 'hello world'}}]);
expect(result.root).toEqual(div('hello world'));
});
});

View File

@@ -25,6 +25,27 @@
declare var $$$hostConfig: any;
export opaque type Destination = mixed; // eslint-disable-line no-undef
export opaque type ResponseState = mixed;
export opaque type SuspenseBoundaryID = mixed;
export const formatChunkAsString = $$$hostConfig.formatChunkAsString;
export const formatChunk = $$$hostConfig.formatChunk;
export const createResponseState = $$$hostConfig.createResponseState;
export const createSuspenseBoundaryID = $$$hostConfig.createSuspenseBoundaryID;
export const pushTextInstance = $$$hostConfig.pushTextInstance;
export const pushStartInstance = $$$hostConfig.pushStartInstance;
export const pushEndInstance = $$$hostConfig.pushEndInstance;
export const writePlaceholder = $$$hostConfig.writePlaceholder;
export const writeStartCompletedSuspenseBoundary =
$$$hostConfig.writeStartCompletedSuspenseBoundary;
export const writeStartPendingSuspenseBoundary =
$$$hostConfig.writeStartPendingSuspenseBoundary;
export const writeStartClientRenderedSuspenseBoundary =
$$$hostConfig.writeStartClientRenderedSuspenseBoundary;
export const writeEndSuspenseBoundary = $$$hostConfig.writeEndSuspenseBoundary;
export const writeStartSegment = $$$hostConfig.writeStartSegment;
export const writeEndSegment = $$$hostConfig.writeEndSegment;
export const writeCompletedSegmentInstruction =
$$$hostConfig.writeCompletedSegmentInstruction;
export const writeCompletedBoundaryInstruction =
$$$hostConfig.writeCompletedBoundaryInstruction;
export const writeClientRenderBoundaryInstruction =
$$$hostConfig.writeClientRenderBoundaryInstruction;

View File

@@ -376,5 +376,13 @@
"385": "A mutable source was mutated while the %s component was rendering. This is not supported. Move any mutations into event handlers or effects.",
"386": "The current renderer does not support microtasks. This error is likely caused by a bug in React. Please file an issue.",
"387": "Should have a current fiber. This is a bug in React.",
"388": "Expected to find a bailed out fiber. This is a bug in React."
"388": "Expected to find a bailed out fiber. This is a bug in React.",
"389": "There can only be one root segment. This is a bug in React.",
"390": "Errored or already flushed boundaries should not be flushed again. This is a bug in React.",
"391": "A previously unvisited boundary must have exactly one root segment. This is a bug in React.",
"392": "A root segment ID must have been assigned by now. This is a bug in React.",
"393": "Cache cannot be refreshed during server rendering.",
"394": "startTransition cannot be called during server rendering.",
"395": "An ID must have been assigned before we can complete the boundary.",
"396": "More boundaries or placeholders than we expected to ever emit."
}