diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 14f4aaaa05939..cda19b8e505b6 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -14,12 +14,15 @@ import type {LazyComponent} from 'react/src/ReactLazy'; import type { ModuleReference, ModuleMetaData, + UninitializedModel, + Response, } from './ReactFlightClientHostConfig'; import { resolveModuleReference, preloadModule, requireModule, + parseModel, } from './ReactFlightClientHostConfig'; import { @@ -33,33 +36,48 @@ export type JSONValue = | null | boolean | string - | {[key: string]: JSONValue} - | Array; + | {+[key: string]: JSONValue} + | $ReadOnlyArray; const PENDING = 0; -const RESOLVED = 1; -const ERRORED = 2; +const RESOLVED_MODEL = 1; +const INITIALIZED = 2; +const ERRORED = 3; type PendingChunk = { _status: 0, _value: null | Array<() => mixed>, + _response: Response, then(resolve: () => mixed): void, }; -type ResolvedChunk = { +type ResolvedModelChunk = { _status: 1, + _value: UninitializedModel, + _response: Response, + then(resolve: () => mixed): void, +}; +type InitializedChunk = { + _status: 2, _value: T, + _response: Response, then(resolve: () => mixed): void, }; type ErroredChunk = { - _status: 2, + _status: 3, _value: Error, + _response: Response, then(resolve: () => mixed): void, }; -type SomeChunk = PendingChunk | ResolvedChunk | ErroredChunk; +type SomeChunk = + | PendingChunk + | ResolvedModelChunk + | InitializedChunk + | ErroredChunk; -function Chunk(status: any, value: any) { +function Chunk(status: any, value: any, response: Response) { this._status = status; this._value = value; + this._response = response; } Chunk.prototype.then = function(resolve: () => mixed) { const chunk: SomeChunk = this; @@ -73,45 +91,40 @@ Chunk.prototype.then = function(resolve: () => mixed) { } }; -export type Response = { - partialRow: string, - rootChunk: SomeChunk, - chunks: Map>, - readRoot(): T, +export type ResponseBase = { + _chunks: Map>, + readRoot(): T, + ... }; -function readRoot(): T { - const response: Response = this; - const rootChunk = response.rootChunk; - if (rootChunk._status === RESOLVED) { - return rootChunk._value; - } else if (rootChunk._status === PENDING) { - // eslint-disable-next-line no-throw-literal - throw (rootChunk: Wakeable); - } else { - throw rootChunk._value; +export type {Response}; + +function readChunk(chunk: SomeChunk): T { + switch (chunk._status) { + case INITIALIZED: + return chunk._value; + case RESOLVED_MODEL: + return initializeModelChunk(chunk); + case PENDING: + // eslint-disable-next-line no-throw-literal + throw (chunk: Wakeable); + default: + throw chunk._value; } } -export function createResponse(): Response { - const rootChunk: SomeChunk = createPendingChunk(); - const chunks: Map> = new Map(); - chunks.set(0, rootChunk); - const response = { - partialRow: '', - rootChunk, - chunks: chunks, - readRoot: readRoot, - }; - return response; +function readRoot(): T { + const response: Response = this; + const chunk = getChunk(response, 0); + return readChunk(chunk); } -function createPendingChunk(): PendingChunk { - return new Chunk(PENDING, null); +function createPendingChunk(response: Response): PendingChunk { + return new Chunk(PENDING, null, response); } -function createErrorChunk(error: Error): ErroredChunk { - return new Chunk(ERRORED, error); +function createErrorChunk(response: Response, error: Error): ErroredChunk { + return new Chunk(ERRORED, error, response); } function wakeChunk(listeners: null | Array<() => mixed>) { @@ -135,29 +148,40 @@ function triggerErrorOnChunk(chunk: SomeChunk, error: Error): void { wakeChunk(listeners); } -function createResolvedChunk(value: T): ResolvedChunk { - return new Chunk(RESOLVED, value); +function createResolvedModelChunk( + response: Response, + value: UninitializedModel, +): ResolvedModelChunk { + return new Chunk(RESOLVED_MODEL, value, response); } -function resolveChunk(chunk: SomeChunk, value: T): void { +function resolveModelChunk( + chunk: SomeChunk, + value: UninitializedModel, +): void { if (chunk._status !== PENDING) { // We already resolved. We didn't expect to see this. return; } const listeners = chunk._value; - const resolvedChunk: ResolvedChunk = (chunk: any); - resolvedChunk._status = RESOLVED; + const resolvedChunk: ResolvedModelChunk = (chunk: any); + resolvedChunk._status = RESOLVED_MODEL; resolvedChunk._value = value; wakeChunk(listeners); } +function initializeModelChunk(chunk: ResolvedModelChunk): T { + const value: T = parseModel(chunk._response, chunk._value); + const initializedChunk: InitializedChunk = (chunk: any); + initializedChunk._status = INITIALIZED; + initializedChunk._value = value; + return value; +} + // Report that any missing chunks in the model is now going to throw this // error upon read. Also notify any pending promises. -export function reportGlobalError( - response: Response, - error: Error, -): void { - response.chunks.forEach(chunk => { +export function reportGlobalError(response: Response, error: Error): void { + response._chunks.forEach(chunk => { // If this chunk was already resolved or errored, it won't // trigger an error but if it wasn't then we need to // because we won't be getting any new data to resolve it. @@ -171,14 +195,7 @@ function readMaybeChunk(maybeChunk: SomeChunk | T): T { return maybeChunk; } const chunk: SomeChunk = (maybeChunk: any); - if (chunk._status === RESOLVED) { - return chunk._value; - } else if (chunk._status === PENDING) { - // eslint-disable-next-line no-throw-literal - throw (chunk: Wakeable); - } else { - throw chunk._value; - } + return readChunk(chunk); } function createElement(type, key, props): React$Element { @@ -226,6 +243,7 @@ type UninitializedBlockPayload = [ mixed, ModuleMetaData | SomeChunk, Data | SomeChunk, + Response, ]; function initializeBlock( @@ -267,83 +285,102 @@ function createLazyBlock( return lazyType; } -export function parseModelFromJSON( - response: Response, - targetObj: Object, - key: string, - value: JSONValue, -): mixed { - if (typeof value === 'string') { - if (value[0] === '$') { - if (value === '$') { - return REACT_ELEMENT_TYPE; - } else if (value[1] === '$' || value[1] === '@') { - // This was an escaped string value. - return value.substring(1); - } else { - const id = parseInt(value.substring(1), 16); - const chunks = response.chunks; - let chunk = chunks.get(id); - if (!chunk) { - chunk = createPendingChunk(); - chunks.set(id, chunk); - } +function getChunk(response: Response, id: number): SomeChunk { + const chunks = response._chunks; + let chunk = chunks.get(id); + if (!chunk) { + chunk = createPendingChunk(response); + chunks.set(id, chunk); + } + return chunk; +} + +export function parseModelString( + response: Response, + parentObject: Object, + value: string, +): any { + if (value[0] === '$') { + if (value === '$') { + return REACT_ELEMENT_TYPE; + } else if (value[1] === '$' || value[1] === '@') { + // This was an escaped string value. + return value.substring(1); + } else { + const id = parseInt(value.substring(1), 16); + const chunk = getChunk(response, id); + if (parentObject[0] === REACT_BLOCK_TYPE) { + // Block types know how to deal with lazy values. return chunk; } - } - if (value === '@') { - return REACT_BLOCK_TYPE; + // For anything else we must Suspend this block if + // we don't yet have the value. + return readChunk(chunk); } } - if (typeof value === 'object' && value !== null) { - const tuple: [mixed, mixed, mixed, mixed] = (value: any); - switch (tuple[0]) { - case REACT_ELEMENT_TYPE: { - // TODO: Consider having React just directly accept these arrays as elements. - // Or even change the ReactElement type to be an array. - return createElement(tuple[1], tuple[2], tuple[3]); - } - case REACT_BLOCK_TYPE: { - // TODO: Consider having React just directly accept these arrays as blocks. - return createLazyBlock((tuple: any)); - } - } + if (value === '@') { + return REACT_BLOCK_TYPE; } return value; } -export function resolveModelChunk( - response: Response, +export function parseModelTuple( + response: Response, + value: {+[key: string]: JSONValue} | $ReadOnlyArray, +): any { + const tuple: [mixed, mixed, mixed, mixed] = (value: any); + if (tuple[0] === REACT_ELEMENT_TYPE) { + // TODO: Consider having React just directly accept these arrays as elements. + // Or even change the ReactElement type to be an array. + return createElement(tuple[1], tuple[2], tuple[3]); + } else if (tuple[0] === REACT_BLOCK_TYPE) { + // TODO: Consider having React just directly accept these arrays as blocks. + return createLazyBlock((tuple: any)); + } + return value; +} + +export function createResponse(): ResponseBase { + const chunks: Map> = new Map(); + const response = { + _chunks: chunks, + readRoot: readRoot, + }; + return response; +} + +export function resolveModel( + response: Response, id: number, - model: M, + model: UninitializedModel, ): void { - const chunks = response.chunks; + const chunks = response._chunks; const chunk = chunks.get(id); if (!chunk) { - chunks.set(id, createResolvedChunk(model)); + chunks.set(id, createResolvedModelChunk(response, model)); } else { - resolveChunk(chunk, model); + resolveModelChunk(chunk, model); } } -export function resolveErrorChunk( - response: Response, +export function resolveError( + response: Response, id: number, message: string, stack: string, ): void { const error = new Error(message); error.stack = stack; - const chunks = response.chunks; + const chunks = response._chunks; const chunk = chunks.get(id); if (!chunk) { - chunks.set(id, createErrorChunk(error)); + chunks.set(id, createErrorChunk(response, error)); } else { triggerErrorOnChunk(chunk, error); } } -export function close(response: Response): void { +export function close(response: Response): void { // In case there are any remaining unresolved chunks, they won't // be resolved now. So we need to issue an error to those. // Ideally we should be able to early bail out if we kept a diff --git a/packages/react-client/src/ReactFlightClientHostConfigStream.js b/packages/react-client/src/ReactFlightClientHostConfigStream.js new file mode 100644 index 0000000000000..2005e8a98a9ed --- /dev/null +++ b/packages/react-client/src/ReactFlightClientHostConfigStream.js @@ -0,0 +1,23 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ResponseBase} from './ReactFlightClient'; +import type {StringDecoder} from './ReactFlightClientHostConfig'; + +export type Response = ResponseBase & { + _partialRow: string, + _fromJSON: (key: string, value: JSONValue) => any, + _stringDecoder: StringDecoder, +}; + +export type UninitializedModel = string; + +export function parseModel(response: Response, json: UninitializedModel): T { + return JSON.parse(json, response._fromJSON); +} diff --git a/packages/react-client/src/ReactFlightClientStream.js b/packages/react-client/src/ReactFlightClientStream.js index 2753814b7ec57..af73c297ec377 100644 --- a/packages/react-client/src/ReactFlightClientStream.js +++ b/packages/react-client/src/ReactFlightClientStream.js @@ -7,41 +7,26 @@ * @flow */ -import type {Response as ResponseBase, JSONValue} from './ReactFlightClient'; - -import type {StringDecoder} from './ReactFlightClientHostConfig'; +import type {Response} from './ReactFlightClientHostConfigStream'; import { - createResponse as createResponseImpl, - resolveModelChunk, - resolveErrorChunk, - parseModelFromJSON, + resolveModel, + resolveError, + createResponse as createResponseBase, + parseModelString, + parseModelTuple, } from './ReactFlightClient'; import { - supportsBinaryStreams, - createStringDecoder, readPartialStringChunk, readFinalStringChunk, + supportsBinaryStreams, + createStringDecoder, } from './ReactFlightClientHostConfig'; -export type Response = ResponseBase & { - fromJSON: (key: string, value: JSONValue) => any, - stringDecoder: StringDecoder, -}; - -export function createResponse(): Response { - const response: Response = (createResponseImpl(): any); - response.fromJSON = function(key: string, value: JSONValue) { - return parseModelFromJSON(response, this, key, value); - }; - if (supportsBinaryStreams) { - response.stringDecoder = createStringDecoder(); - } - return response; -} +export type {Response}; -function processFullRow(response: Response, row: string): void { +function processFullRow(response: Response, row: string): void { if (row === '') { return; } @@ -51,8 +36,7 @@ function processFullRow(response: Response, row: string): void { const colon = row.indexOf(':', 1); const id = parseInt(row.substring(1, colon), 16); const json = row.substring(colon + 1); - const model = JSON.parse(json, response.fromJSON); - resolveModelChunk(response, id, model); + resolveModel(response, id, json); return; } case 'E': { @@ -60,53 +44,79 @@ function processFullRow(response: Response, row: string): void { const id = parseInt(row.substring(1, colon), 16); const json = row.substring(colon + 1); const errorInfo = JSON.parse(json); - resolveErrorChunk(response, id, errorInfo.message, errorInfo.stack); + resolveError(response, id, errorInfo.message, errorInfo.stack); return; } default: { // Assume this is the root model. - const model = JSON.parse(row, response.fromJSON); - resolveModelChunk(response, 0, model); + resolveModel(response, 0, row); return; } } } -export function processStringChunk( - response: Response, +export function processStringChunk( + response: Response, chunk: string, offset: number, ): void { let linebreak = chunk.indexOf('\n', offset); while (linebreak > -1) { - const fullrow = response.partialRow + chunk.substring(offset, linebreak); + const fullrow = response._partialRow + chunk.substring(offset, linebreak); processFullRow(response, fullrow); - response.partialRow = ''; + response._partialRow = ''; offset = linebreak + 1; linebreak = chunk.indexOf('\n', offset); } - response.partialRow += chunk.substring(offset); + response._partialRow += chunk.substring(offset); } -export function processBinaryChunk( - response: Response, +export function processBinaryChunk( + response: Response, chunk: Uint8Array, ): void { if (!supportsBinaryStreams) { throw new Error("This environment don't support binary chunks."); } - const stringDecoder = response.stringDecoder; + const stringDecoder = response._stringDecoder; let linebreak = chunk.indexOf(10); // newline while (linebreak > -1) { const fullrow = - response.partialRow + + response._partialRow + readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak)); processFullRow(response, fullrow); - response.partialRow = ''; + response._partialRow = ''; chunk = chunk.subarray(linebreak + 1); linebreak = chunk.indexOf(10); // newline } - response.partialRow += readPartialStringChunk(stringDecoder, chunk); + response._partialRow += readPartialStringChunk(stringDecoder, chunk); +} + +function createFromJSONCallback(response: Response) { + return function(key: string, value: JSONValue) { + if (typeof value === 'string') { + // We can't use .bind here because we need the "this" value. + return parseModelString(response, this, value); + } + if (typeof value === 'object' && value !== null) { + return parseModelTuple(response, value); + } + return value; + }; +} + +export function createResponse(): Response { + // NOTE: CHECK THE COMPILER OUTPUT EACH TIME YOU CHANGE THIS. + // It should be inlined to one object literal but minor changes can break it. + const stringDecoder = supportsBinaryStreams ? createStringDecoder() : null; + const response: any = createResponseBase(); + response._partialRow = ''; + if (supportsBinaryStreams) { + response._stringDecoder = stringDecoder; + } + // Don't inline this call because it causes closure to outline the call above. + response._fromJSON = createFromJSONCallback(response); + return response; } export {reportGlobalError, close} from './ReactFlightClient'; diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js index 4ba779ad1f94b..5f0b9d2c7114a 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js @@ -25,6 +25,7 @@ declare var $$$hostConfig: any; +export type Response = any; export opaque type ModuleMetaData = mixed; // eslint-disable-line no-undef export opaque type ModuleReference = mixed; // eslint-disable-line no-undef export const resolveModuleReference = $$$hostConfig.resolveModuleReference; @@ -32,6 +33,10 @@ export const preloadModule = $$$hostConfig.preloadModule; export const requireModule = $$$hostConfig.requireModule; export opaque type Source = mixed; // eslint-disable-line no-undef + +export type UninitializedModel = string; +export const parseModel = $$$hostConfig.parseModel; + export opaque type StringDecoder = mixed; // eslint-disable-line no-undef export const supportsBinaryStreams = $$$hostConfig.supportsBinaryStreams; diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser.js index 80d967d3ecdba..6ef78deee2162 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser.js @@ -8,4 +8,5 @@ */ export * from 'react-client/src/ReactFlightClientHostConfigBrowser'; +export * from 'react-client/src/ReactFlightClientHostConfigStream'; export * from 'react-flight-dom-webpack/src/ReactFlightClientWebpackBundlerConfig'; diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom.js index 80d967d3ecdba..6ef78deee2162 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom.js @@ -8,4 +8,5 @@ */ export * from 'react-client/src/ReactFlightClientHostConfigBrowser'; +export * from 'react-client/src/ReactFlightClientHostConfigStream'; export * from 'react-flight-dom-webpack/src/ReactFlightClientWebpackBundlerConfig'; diff --git a/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClient.js b/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClient.js index 4ac520d5b4e74..3ca80460c8d5f 100644 --- a/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClient.js +++ b/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClient.js @@ -7,51 +7,9 @@ * @flow */ -import type {Response, JSONValue} from 'react-client/src/ReactFlightClient'; - -import { +export { createResponse, - parseModelFromJSON, - resolveModelChunk, - resolveErrorChunk, + resolveModel, + resolveError, close, } from 'react-client/src/ReactFlightClient'; - -function parseModel(response: Response, targetObj, key, value) { - if (typeof value === 'object' && value !== null) { - if (Array.isArray(value)) { - for (let i = 0; i < value.length; i++) { - (value: any)[i] = parseModel(response, value, '' + i, value[i]); - } - } else { - for (const innerKey in value) { - (value: any)[innerKey] = parseModel( - response, - value, - innerKey, - value[innerKey], - ); - } - } - } - return parseModelFromJSON(response, targetObj, key, value); -} - -export {createResponse, close}; - -export function resolveModel( - response: Response, - id: number, - json: JSONValue, -) { - resolveModelChunk(response, id, parseModel(response, {}, '', json)); -} - -export function resolveError( - response: Response, - id: number, - message: string, - stack: string, -) { - resolveErrorChunk(response, id, message, stack); -} diff --git a/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js b/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js index 61345b0934646..81c86e94ad96e 100644 --- a/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js +++ b/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js @@ -7,6 +7,13 @@ * @flow */ +import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient'; + +import { + parseModelString, + parseModelTuple, +} from 'react-client/src/ReactFlightClient'; + export { resolveModuleReference, preloadModule, @@ -17,3 +24,36 @@ export type { ModuleReference, ModuleMetaData, } from 'ReactFlightDOMRelayClientIntegration'; + +export opaque type UninitializedModel = JSONValue; + +export type Response = ResponseBase; + +function parseModelRecursively(response: Response, parentObj, value) { + if (typeof value === 'string') { + return parseModelString(response, parentObj, value); + } + if (typeof value === 'object' && value !== null) { + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + (value: any)[i] = parseModelRecursively(response, value, value[i]); + } + return parseModelTuple(response, value); + } else { + for (const innerKey in value) { + (value: any)[innerKey] = parseModelRecursively( + response, + value, + value[innerKey], + ); + } + } + } + return value; +} + +const dummy = {}; + +export function parseModel(response: Response, json: UninitializedModel): T { + return (parseModelRecursively(response, dummy, json): any); +} diff --git a/packages/react-flight-dom-webpack/src/ReactFlightDOMClient.js b/packages/react-flight-dom-webpack/src/ReactFlightDOMClient.js index bf43f475540c2..9c9d17c11f532 100644 --- a/packages/react-flight-dom-webpack/src/ReactFlightDOMClient.js +++ b/packages/react-flight-dom-webpack/src/ReactFlightDOMClient.js @@ -17,8 +17,8 @@ import { close, } from 'react-client/src/ReactFlightClientStream'; -function startReadingFromStream( - response: FlightResponse, +function startReadingFromStream( + response: FlightResponse, stream: ReadableStream, ): void { const reader = stream.getReader(); @@ -37,18 +37,16 @@ function startReadingFromStream( reader.read().then(progress, error); } -function createFromReadableStream( - stream: ReadableStream, -): FlightResponse { - const response: FlightResponse = createResponse(); +function createFromReadableStream(stream: ReadableStream): FlightResponse { + const response: FlightResponse = createResponse(); startReadingFromStream(response, stream); return response; } -function createFromFetch( +function createFromFetch( promiseForResponse: Promise, -): FlightResponse { - const response: FlightResponse = createResponse(); +): FlightResponse { + const response: FlightResponse = createResponse(); promiseForResponse.then( function(r) { startReadingFromStream(response, (r.body: any)); @@ -60,8 +58,8 @@ function createFromFetch( return response; } -function createFromXHR(request: XMLHttpRequest): FlightResponse { - const response: FlightResponse = createResponse(); +function createFromXHR(request: XMLHttpRequest): FlightResponse { + const response: FlightResponse = createResponse(); let processedLength = 0; function progress(e: ProgressEvent): void { const chunk = request.responseText; diff --git a/packages/react-flight-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-flight-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 871e124efa611..8f8b4820e09a2 100644 --- a/packages/react-flight-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-flight-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -286,7 +286,7 @@ describe('ReactFlightDOM', () => { function Text({children}) { return children; } - function makeDelayedText() { + function makeDelayedTextBlock() { let error, _resolve, _reject; let promise = new Promise((resolve, reject) => { _resolve = () => { @@ -315,11 +315,36 @@ describe('ReactFlightDOM', () => { return [loadBlock(), _resolve, _reject]; } + function makeDelayedText() { + let error, _resolve, _reject; + let promise = new Promise((resolve, reject) => { + _resolve = () => { + promise = null; + resolve(); + }; + _reject = e => { + error = e; + promise = null; + reject(e); + }; + }); + function DelayedText({children}, data) { + if (promise) { + throw promise; + } + if (error) { + throw error; + } + return {children}; + } + return [DelayedText, _resolve, _reject]; + } + const [FriendsModel, resolveFriendsModel] = makeDelayedText(); const [NameModel, resolveNameModel] = makeDelayedText(); - const [PostsModel, resolvePostsModel] = makeDelayedText(); - const [PhotosModel, resolvePhotosModel] = makeDelayedText(); - const [GamesModel, , rejectGamesModel] = makeDelayedText(); + const [PostsModel, resolvePostsModel] = makeDelayedTextBlock(); + const [PhotosModel, resolvePhotosModel] = makeDelayedTextBlock(); + const [GamesModel, , rejectGamesModel] = makeDelayedTextBlock(); function ProfileMore() { return { avatar: :avatar:, diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js index bbc87546773e5..df586c6efb2cb 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightClient.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js @@ -29,6 +29,9 @@ const {createResponse, processStringChunk, close} = ReactFlightClient({ requireModule(idx: string) { return readModule(idx); }, + parseModel(response: Response, json) { + return JSON.parse(json, response._fromJSON); + }, }); function read(source: Source): T { diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 14ae0916c265f..184daacc1524e 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -237,9 +237,23 @@ export function resolveModelToJSON( value.$$typeof === REACT_ELEMENT_TYPE ) { // TODO: Concatenate keys of parents onto children. - // TODO: Allow elements to suspend independently and serialize as references to future elements. const element: React$Element = (value: any); - value = attemptResolveElement(element); + try { + // Attempt to render the server component. + value = attemptResolveElement(element); + } 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. + request.pendingChunks++; + const newSegment = createSegment(request, () => value); + const ping = newSegment.ping; + x.then(ping, ping); + return serializeIDRef(newSegment.id); + } else { + // Something errored. Don't bother encoding anything up to here. + throw x; + } + } } return value; @@ -268,8 +282,22 @@ function emitErrorChunk(request: Request, id: number, error: mixed): void { function retrySegment(request: Request, segment: Segment): void { const query = segment.query; + let value; try { - const value = query(); + value = query(); + while ( + typeof value === 'object' && + value !== null && + value.$$typeof === REACT_ELEMENT_TYPE + ) { + // TODO: Concatenate keys of parents onto children. + const element: React$Element = (value: any); + // Attempt to render the server component. + // Doing this here lets us reuse this same segment if the next component + // also suspends. + segment.query = () => value; + value = attemptResolveElement(element); + } const processedChunk = processModelChunk(request, segment.id, value); request.completedJSONChunks.push(processedChunk); } catch (x) { diff --git a/scripts/eslint/index.js b/scripts/eslint/index.js index d9acaf913edf8..9511161464fa1 100644 --- a/scripts/eslint/index.js +++ b/scripts/eslint/index.js @@ -68,10 +68,11 @@ function runESLint({onlyChanged}) { if (typeof onlyChanged !== 'boolean') { throw new Error('Pass options.onlyChanged as a boolean.'); } - const {errorCount, warningCount, output} = runESLintOnFilesWithOptions( - allPaths, - onlyChanged - ); + const { + errorCount, + warningCount, + output, + } = runESLintOnFilesWithOptions(allPaths, onlyChanged, {fix: true}); console.log(output); return errorCount === 0 && warningCount === 0; } diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 19036f3343bec..ab4fe5bacb3d3 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -27,6 +27,7 @@ module.exports = [ 'react-flight-dom-webpack/server.node', 'react-flight-dom-webpack/server-runtime', 'react-flight-dom-webpack/src/ReactFlightDOMServerNode.js', // react-flight-dom-webpack/server.browser + 'react-client/src/ReactFlightClientStream.js', // We can only type check this in streaming configurations. 'react-interactions', ], isFlowTyped: true, @@ -51,6 +52,7 @@ module.exports = [ 'react-flight-dom-webpack/server.browser', 'react-flight-dom-webpack/server-runtime', 'react-flight-dom-webpack/src/ReactFlightDOMServerBrowser.js', // react-flight-dom-webpack/server.browser + 'react-client/src/ReactFlightClientStream.js', // We can only type check this in streaming configurations. ], isFlowTyped: true, isServerSupported: true, @@ -103,7 +105,12 @@ module.exports = [ 'react-server/flight', 'react-server/flight-server-runtime', ], - paths: [], + paths: [ + 'react-client/flight', + 'react-server/flight', + 'react-server/flight-server-runtime', + 'react-client/src/ReactFlightClientStream.js', // We can only type check this in streaming configurations. + ], isFlowTyped: true, isServerSupported: true, },