diff --git a/packages/react-client/flight.js b/packages/react-client/flight.js index 7d0a0b03ba920..2b9b3f45d67bb 100644 --- a/packages/react-client/flight.js +++ b/packages/react-client/flight.js @@ -7,4 +7,4 @@ * @flow */ -export * from './src/ReactFlightClient'; +export * from './src/ReactFlightClientStream'; diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 3adab738369df..b158c0039d1ba 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -7,25 +7,17 @@ * @flow */ -import type {Source, StringDecoder} from './ReactFlightClientHostConfig'; - -import { - supportsBinaryStreams, - createStringDecoder, - readPartialStringChunk, - readFinalStringChunk, -} from './ReactFlightClientHostConfig'; - export type ReactModelRoot = {| model: T, |}; -type JSONValue = +export type JSONValue = | number | null | boolean | string - | {[key: string]: JSONValue, ...}; + | {[key: string]: JSONValue} + | Array; const PENDING = 0; const RESOLVED = 1; @@ -48,39 +40,23 @@ type ErroredChunk = {| |}; type Chunk = PendingChunk | ResolvedChunk | ErroredChunk; -type OpaqueResponseWithoutDecoder = { - source: Source, +export type Response = { partialRow: string, modelRoot: ReactModelRoot, chunks: Map, - fromJSON: (key: string, value: JSONValue) => any, - ... }; -type OpaqueResponse = OpaqueResponseWithoutDecoder & { - stringDecoder: StringDecoder, - ... -}; - -export function createResponse(source: Source): OpaqueResponse { +export function createResponse(): Response { let modelRoot: ReactModelRoot = ({}: any); let rootChunk: Chunk = createPendingChunk(); definePendingProperty(modelRoot, 'model', rootChunk); let chunks: Map = new Map(); chunks.set(0, rootChunk); - - let response: OpaqueResponse = (({ - source, + let response = { partialRow: '', modelRoot, chunks: chunks, - fromJSON: function(key, value) { - return parseFromJSON(response, this, key, value); - }, - }: OpaqueResponseWithoutDecoder): any); - if (supportsBinaryStreams) { - response.stringDecoder = createStringDecoder(); - } + }; return response; } @@ -138,10 +114,7 @@ function resolveChunk(chunk: Chunk, value: mixed): void { // 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: OpaqueResponse, - error: Error, -): void { +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 @@ -168,8 +141,8 @@ function definePendingProperty( }); } -function parseFromJSON( - response: OpaqueResponse, +export function parseModelFromJSON( + response: Response, targetObj: Object, key: string, value: JSONValue, @@ -195,12 +168,11 @@ function parseFromJSON( return value; } -function resolveJSONRow( - response: OpaqueResponse, +export function resolveModelChunk( + response: Response, id: number, - json: string, + model: T, ): void { - let model = JSON.parse(json, response.fromJSON); let chunks = response.chunks; let chunk = chunks.get(id); if (!chunk) { @@ -210,81 +182,24 @@ function resolveJSONRow( } } -function processFullRow(response: OpaqueResponse, row: string): void { - if (row === '') { - return; - } - let tag = row[0]; - switch (tag) { - case 'J': { - let colon = row.indexOf(':', 1); - let id = parseInt(row.substring(1, colon), 16); - let json = row.substring(colon + 1); - resolveJSONRow(response, id, json); - return; - } - case 'E': { - let colon = row.indexOf(':', 1); - let id = parseInt(row.substring(1, colon), 16); - let json = row.substring(colon + 1); - let errorInfo = JSON.parse(json); - let error = new Error(errorInfo.message); - error.stack = errorInfo.stack; - let chunks = response.chunks; - let chunk = chunks.get(id); - if (!chunk) { - chunks.set(id, createErrorChunk(error)); - } else { - triggerErrorOnChunk(chunk, error); - } - return; - } - default: { - // Assume this is the root model. - resolveJSONRow(response, 0, row); - return; - } - } -} - -export function processStringChunk( - response: OpaqueResponse, - chunk: string, - offset: number, -): void { - let linebreak = chunk.indexOf('\n', offset); - while (linebreak > -1) { - let fullrow = response.partialRow + chunk.substring(offset, linebreak); - processFullRow(response, fullrow); - response.partialRow = ''; - offset = linebreak + 1; - linebreak = chunk.indexOf('\n', offset); - } - response.partialRow += chunk.substring(offset); -} - -export function processBinaryChunk( - response: OpaqueResponse, - chunk: Uint8Array, +export function resolveErrorChunk( + response: Response, + id: number, + message: string, + stack: string, ): void { - if (!supportsBinaryStreams) { - throw new Error("This environment don't support binary chunks."); - } - let stringDecoder = response.stringDecoder; - let linebreak = chunk.indexOf(10); // newline - while (linebreak > -1) { - let fullrow = - response.partialRow + - readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak)); - processFullRow(response, fullrow); - response.partialRow = ''; - chunk = chunk.subarray(linebreak + 1); - linebreak = chunk.indexOf(10); // newline + let error = new Error(message); + error.stack = stack; + let chunks = response.chunks; + let chunk = chunks.get(id); + if (!chunk) { + chunks.set(id, createErrorChunk(error)); + } else { + triggerErrorOnChunk(chunk, error); } - response.partialRow += readPartialStringChunk(stringDecoder, chunk); } -export function complete(response: OpaqueResponse): 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 @@ -292,6 +207,6 @@ export function complete(response: OpaqueResponse): void { reportGlobalError(response, new Error('Connection closed.')); } -export function getModelRoot(response: OpaqueResponse): ReactModelRoot { +export function getModelRoot(response: Response): ReactModelRoot { return response.modelRoot; } diff --git a/packages/react-client/src/ReactFlightClientHostConfigBrowser.js b/packages/react-client/src/ReactFlightClientHostConfigBrowser.js index a3ba45faee0e3..d5aef79df514d 100644 --- a/packages/react-client/src/ReactFlightClientHostConfigBrowser.js +++ b/packages/react-client/src/ReactFlightClientHostConfigBrowser.js @@ -7,8 +7,6 @@ * @flow */ -export type Source = Promise | ReadableStream | XMLHttpRequest; - export type StringDecoder = TextDecoder; export const supportsBinaryStreams = true; diff --git a/packages/react-client/src/ReactFlightClientStream.js b/packages/react-client/src/ReactFlightClientStream.js new file mode 100644 index 0000000000000..27e5eabaa8f8f --- /dev/null +++ b/packages/react-client/src/ReactFlightClientStream.js @@ -0,0 +1,116 @@ +/** + * 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 {Response as ResponseBase, JSONValue} from './ReactFlightClient'; + +import type {StringDecoder} from './ReactFlightClientHostConfig'; + +import { + createResponse as createResponseImpl, + resolveModelChunk, + resolveErrorChunk, + parseModelFromJSON, +} from './ReactFlightClient'; + +import { + supportsBinaryStreams, + createStringDecoder, + readPartialStringChunk, + readFinalStringChunk, +} from './ReactFlightClientHostConfig'; + +export type ReactModelRoot = {| + model: T, +|}; + +type Response = ResponseBase & { + fromJSON: (key: string, value: JSONValue) => any, + stringDecoder: StringDecoder, +}; + +export function createResponse(): Response { + let response: Response = (createResponseImpl(): any); + response.fromJSON = function(key: string, value: JSONValue) { + return parseModelFromJSON(response, this, key, value); + }; + if (supportsBinaryStreams) { + response.stringDecoder = createStringDecoder(); + } + return response; +} + +function processFullRow(response: Response, row: string): void { + if (row === '') { + return; + } + let tag = row[0]; + switch (tag) { + case 'J': { + let colon = row.indexOf(':', 1); + let id = parseInt(row.substring(1, colon), 16); + let json = row.substring(colon + 1); + let model = JSON.parse(json, response.fromJSON); + resolveModelChunk(response, id, model); + return; + } + case 'E': { + let colon = row.indexOf(':', 1); + let id = parseInt(row.substring(1, colon), 16); + let json = row.substring(colon + 1); + let errorInfo = JSON.parse(json); + resolveErrorChunk(response, id, errorInfo.message, errorInfo.stack); + return; + } + default: { + // Assume this is the root model. + let model = JSON.parse(row, response.fromJSON); + resolveModelChunk(response, 0, model); + return; + } + } +} + +export function processStringChunk( + response: Response, + chunk: string, + offset: number, +): void { + let linebreak = chunk.indexOf('\n', offset); + while (linebreak > -1) { + let fullrow = response.partialRow + chunk.substring(offset, linebreak); + processFullRow(response, fullrow); + response.partialRow = ''; + offset = linebreak + 1; + linebreak = chunk.indexOf('\n', offset); + } + response.partialRow += chunk.substring(offset); +} + +export function processBinaryChunk( + response: Response, + chunk: Uint8Array, +): void { + if (!supportsBinaryStreams) { + throw new Error("This environment don't support binary chunks."); + } + let stringDecoder = response.stringDecoder; + let linebreak = chunk.indexOf(10); // newline + while (linebreak > -1) { + let fullrow = + response.partialRow + + readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak)); + processFullRow(response, fullrow); + response.partialRow = ''; + chunk = chunk.subarray(linebreak + 1); + linebreak = chunk.indexOf(10); // newline + } + response.partialRow += readPartialStringChunk(stringDecoder, chunk); +} + +export {reportGlobalError, close, getModelRoot} from './ReactFlightClient'; diff --git a/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClient.js b/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClient.js index 9507e654d607d..f26373b8f9266 100644 --- a/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClient.js +++ b/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClient.js @@ -9,21 +9,59 @@ import type {ReactModelRoot} from 'react-client/src/ReactFlightClient'; +import type {Chunk} from './ReactFlightDOMRelayClientHostConfig'; + import { createResponse, getModelRoot, - processStringChunk, - complete, + parseModelFromJSON, + resolveModelChunk, + resolveErrorChunk, + close, } from 'react-client/src/ReactFlightClient'; -type EncodedData = Array; +type EncodedData = Array; + +function parseModel(response, targetObj, key, value) { + if (typeof value === 'object' && value !== null) { + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + value[i] = parseModel(response, value, '' + i, value[i]); + } + } else { + for (let innerKey in value) { + value[innerKey] = parseModel( + response, + value, + innerKey, + value[innerKey], + ); + } + } + } + return parseModelFromJSON(response, targetObj, key, value); +} function read(data: EncodedData): ReactModelRoot { - let response = createResponse(data); + let response = createResponse(); for (let i = 0; i < data.length; i++) { - processStringChunk(response, data[i], 0); + let chunk = data[i]; + if (chunk.type === 'json') { + resolveModelChunk( + response, + chunk.id, + parseModel(response, {}, '', chunk.json), + ); + } else { + resolveErrorChunk( + response, + chunk.id, + chunk.json.message, + chunk.json.stack, + ); + } } - complete(response); + close(response); return getModelRoot(response); } diff --git a/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js b/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js index 22aa7844c44ad..89dbc53fb0b0d 100644 --- a/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js +++ b/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js @@ -7,7 +7,29 @@ * @flow */ -export type Source = Array; +type JSONValue = + | string + | number + | boolean + | null + | {[key: string]: JSONValue} + | Array; + +export type Chunk = + | { + type: 'json', + id: number, + json: JSONValue, + } + | { + type: 'error', + id: number, + json: { + message: string, + stack: string, + ... + }, + }; export type StringDecoder = void; diff --git a/packages/react-flight-dom-webpack/src/ReactFlightDOMClient.js b/packages/react-flight-dom-webpack/src/ReactFlightDOMClient.js index 6a3e42e60ac09..38bdec83baac2 100644 --- a/packages/react-flight-dom-webpack/src/ReactFlightDOMClient.js +++ b/packages/react-flight-dom-webpack/src/ReactFlightDOMClient.js @@ -7,7 +7,7 @@ * @flow */ -import type {ReactModelRoot} from 'react-client/src/ReactFlightClient'; +import type {ReactModelRoot} from 'react-client/src/ReactFlightClientStream'; import { createResponse, @@ -15,14 +15,14 @@ import { reportGlobalError, processStringChunk, processBinaryChunk, - complete, -} from 'react-client/src/ReactFlightClient'; + close, +} from 'react-client/src/ReactFlightClientStream'; function startReadingFromStream(response, stream: ReadableStream): void { let reader = stream.getReader(); function progress({done, value}) { if (done) { - complete(response); + close(response); return; } let buffer: Uint8Array = (value: any); @@ -36,7 +36,7 @@ function startReadingFromStream(response, stream: ReadableStream): void { } function readFromReadableStream(stream: ReadableStream): ReactModelRoot { - let response = createResponse(stream); + let response = createResponse(); startReadingFromStream(response, stream); return getModelRoot(response); } @@ -44,7 +44,7 @@ function readFromReadableStream(stream: ReadableStream): ReactModelRoot { function readFromFetch( promiseForResponse: Promise, ): ReactModelRoot { - let response = createResponse(promiseForResponse); + let response = createResponse(); promiseForResponse.then( function(r) { startReadingFromStream(response, (r.body: any)); @@ -57,7 +57,7 @@ function readFromFetch( } function readFromXHR(request: XMLHttpRequest): ReactModelRoot { - let response = createResponse(request); + let response = createResponse(); let processedLength = 0; function progress(e: ProgressEvent): void { let chunk = request.responseText; @@ -66,7 +66,7 @@ function readFromXHR(request: XMLHttpRequest): ReactModelRoot { } function load(e: ProgressEvent): void { progress(e); - complete(response); + close(response); } function error(e: ProgressEvent): void { reportGlobalError(response, new TypeError('Network error')); diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js index 99a062017b812..51c5d73fc3c37 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightClient.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js @@ -24,7 +24,7 @@ const { createResponse, getModelRoot, processStringChunk, - complete, + close, } = ReactFlightClient({ supportsBinaryStreams: false, }); @@ -34,7 +34,7 @@ function read(source: Source): ReactModelRoot { for (let i = 0; i < source.length; i++) { processStringChunk(response, source[i], 0); } - complete(response); + close(response); return getModelRoot(response); }