@samitouri / QOS-React-2 / commits / 99d7371863

[Flight] Split Streaming from Relay Implemenation (#18260)

* Add ReactFlightServerConfig intermediate This just forwards to the stream version of Flight which is itself forked between Node and W3C streams. The dom-relay goes directly to the Relay config though which allows it to avoid the stream part of Flight. * Separate streaming protocol into the Stream config * Split streaming parts into the ReactFlightServerConfigStream This decouples it so that the Relay implementation doesn't have to encode the JSON to strings. Instead it can be fed the values as JSON objects and do its own encoding. * Split FlightClient into a basic part and a stream part Same split as the server. * Expose lower level async hooks to Relay This requires an external helper file that we'll wire up internally.

Sebastian Markbåge committed Mar 10, 2020 at 14:55 UTC 99d73718636d7930a2abf0030c51c1d7710b13d8
26 files changed +615 -269
packages/react-client/flight.js
+1 -1
@@ -7,4 +7,4 @@
7 * @flow
8 */
9
10 -export * from './src/ReactFlightClient';
10 +export * from './src/ReactFlightClientStream';
packages/react-client/src/ReactFlightClient.js
+28 -113
@@ -7,25 +7,17 @@
7 * @flow
8 */
9
10 -import type {Source, StringDecoder} from './ReactFlightClientHostConfig';
11 -
12 -import {
13 - supportsBinaryStreams,
14 - createStringDecoder,
15 - readPartialStringChunk,
16 - readFinalStringChunk,
17 -} from './ReactFlightClientHostConfig';
18 -
10 export type ReactModelRoot<T> = {|
11 model: T,
12 |};
13
23 -type JSONValue =
14 +export type JSONValue =
15 | number
16 | null
17 | boolean
18 | string
28 - | {[key: string]: JSONValue, ...};
19 + | {[key: string]: JSONValue}
20 + | Array<JSONValue>;
21
22 const PENDING = 0;
23 const RESOLVED = 1;
@@ -48,39 +40,23 @@ type ErroredChunk = {|
40 |};
41 type Chunk = PendingChunk | ResolvedChunk | ErroredChunk;
42
51 -type OpaqueResponseWithoutDecoder = {
52 - source: Source,
43 +export type Response = {
44 partialRow: string,
45 modelRoot: ReactModelRoot<any>,
46 chunks: Map<number, Chunk>,
56 - fromJSON: (key: string, value: JSONValue) => any,
57 - ...
47 };
48
60 -type OpaqueResponse = OpaqueResponseWithoutDecoder & {
61 - stringDecoder: StringDecoder,
62 - ...
63 -};
64 -
65 -export function createResponse(source: Source): OpaqueResponse {
49 +export function createResponse(): Response {
50 let modelRoot: ReactModelRoot<any> = ({}: any);
51 let rootChunk: Chunk = createPendingChunk();
52 definePendingProperty(modelRoot, 'model', rootChunk);
53 let chunks: Map<number, Chunk> = new Map();
54 chunks.set(0, rootChunk);
71 -
72 - let response: OpaqueResponse = (({
73 - source,
55 + let response = {
56 partialRow: '',
57 modelRoot,
58 chunks: chunks,
77 - fromJSON: function(key, value) {
78 - return parseFromJSON(response, this, key, value);
79 - },
80 - }: OpaqueResponseWithoutDecoder): any);
81 - if (supportsBinaryStreams) {
82 - response.stringDecoder = createStringDecoder();
83 - }
59 + };
60 return response;
61 }
62
@@ -138,10 +114,7 @@ function resolveChunk(chunk: Chunk, value: mixed): void {
114
115 // Report that any missing chunks in the model is now going to throw this
116 // error upon read. Also notify any pending promises.
141 -export function reportGlobalError(
142 - response: OpaqueResponse,
143 - error: Error,
144 -): void {
117 +export function reportGlobalError(response: Response, error: Error): void {
118 response.chunks.forEach(chunk => {
119 // If this chunk was already resolved or errored, it won't
120 // trigger an error but if it wasn't then we need to
@@ -168,8 +141,8 @@ function definePendingProperty(
141 });
142 }
143
171 -function parseFromJSON(
172 - response: OpaqueResponse,
144 +export function parseModelFromJSON(
145 + response: Response,
146 targetObj: Object,
147 key: string,
148 value: JSONValue,
@@ -195,12 +168,11 @@ function parseFromJSON(
168 return value;
169 }
170
198 -function resolveJSONRow(
199 - response: OpaqueResponse,
171 +export function resolveModelChunk<T>(
172 + response: Response,
173 id: number,
201 - json: string,
174 + model: T,
175 ): void {
203 - let model = JSON.parse(json, response.fromJSON);
176 let chunks = response.chunks;
177 let chunk = chunks.get(id);
178 if (!chunk) {
@@ -210,81 +182,24 @@ function resolveJSONRow(
182 }
183 }
184
213 -function processFullRow(response: OpaqueResponse, row: string): void {
214 - if (row === '') {
215 - return;
216 - }
217 - let tag = row[0];
218 - switch (tag) {
219 - case 'J': {
220 - let colon = row.indexOf(':', 1);
221 - let id = parseInt(row.substring(1, colon), 16);
222 - let json = row.substring(colon + 1);
223 - resolveJSONRow(response, id, json);
224 - return;
225 - }
226 - case 'E': {
227 - let colon = row.indexOf(':', 1);
228 - let id = parseInt(row.substring(1, colon), 16);
229 - let json = row.substring(colon + 1);
230 - let errorInfo = JSON.parse(json);
231 - let error = new Error(errorInfo.message);
232 - error.stack = errorInfo.stack;
233 - let chunks = response.chunks;
234 - let chunk = chunks.get(id);
235 - if (!chunk) {
236 - chunks.set(id, createErrorChunk(error));
237 - } else {
238 - triggerErrorOnChunk(chunk, error);
239 - }
240 - return;
241 - }
242 - default: {
243 - // Assume this is the root model.
244 - resolveJSONRow(response, 0, row);
245 - return;
246 - }
247 - }
248 -}
249 -
250 -export function processStringChunk(
251 - response: OpaqueResponse,
252 - chunk: string,
253 - offset: number,
254 -): void {
255 - let linebreak = chunk.indexOf('\n', offset);
256 - while (linebreak > -1) {
257 - let fullrow = response.partialRow + chunk.substring(offset, linebreak);
258 - processFullRow(response, fullrow);
259 - response.partialRow = '';
260 - offset = linebreak + 1;
261 - linebreak = chunk.indexOf('\n', offset);
262 - }
263 - response.partialRow += chunk.substring(offset);
264 -}
265 -
266 -export function processBinaryChunk(
267 - response: OpaqueResponse,
268 - chunk: Uint8Array,
185 +export function resolveErrorChunk(
186 + response: Response,
187 + id: number,
188 + message: string,
189 + stack: string,
190 ): void {
270 - if (!supportsBinaryStreams) {
271 - throw new Error("This environment don't support binary chunks.");
272 - }
273 - let stringDecoder = response.stringDecoder;
274 - let linebreak = chunk.indexOf(10); // newline
275 - while (linebreak > -1) {
276 - let fullrow =
277 - response.partialRow +
278 - readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak));
279 - processFullRow(response, fullrow);
280 - response.partialRow = '';
281 - chunk = chunk.subarray(linebreak + 1);
282 - linebreak = chunk.indexOf(10); // newline
191 + let error = new Error(message);
192 + error.stack = stack;
193 + let chunks = response.chunks;
194 + let chunk = chunks.get(id);
195 + if (!chunk) {
196 + chunks.set(id, createErrorChunk(error));
197 + } else {
198 + triggerErrorOnChunk(chunk, error);
199 }
284 - response.partialRow += readPartialStringChunk(stringDecoder, chunk);
200 }
201
287 -export function complete(response: OpaqueResponse): void {
202 +export function close(response: Response): void {
203 // In case there are any remaining unresolved chunks, they won't
204 // be resolved now. So we need to issue an error to those.
205 // Ideally we should be able to early bail out if we kept a
@@ -292,6 +207,6 @@ export function complete(response: OpaqueResponse): void {
207 reportGlobalError(response, new Error('Connection closed.'));
208 }
209
295 -export function getModelRoot<T>(response: OpaqueResponse): ReactModelRoot<T> {
210 +export function getModelRoot<T>(response: Response): ReactModelRoot<T> {
211 return response.modelRoot;
212 }
packages/react-client/src/ReactFlightClientHostConfigBrowser.js
-2
@@ -7,8 +7,6 @@
7 * @flow
8 */
9
10 -export type Source = Promise<Response> | ReadableStream | XMLHttpRequest;
11 -
10 export type StringDecoder = TextDecoder;
11
12 export const supportsBinaryStreams = true;
packages/react-client/src/ReactFlightClientStream.js new
+116
@@ -0,0 +1,116 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Response as ResponseBase, JSONValue} from './ReactFlightClient';
11 +
12 +import type {StringDecoder} from './ReactFlightClientHostConfig';
13 +
14 +import {
15 + createResponse as createResponseImpl,
16 + resolveModelChunk,
17 + resolveErrorChunk,
18 + parseModelFromJSON,
19 +} from './ReactFlightClient';
20 +
21 +import {
22 + supportsBinaryStreams,
23 + createStringDecoder,
24 + readPartialStringChunk,
25 + readFinalStringChunk,
26 +} from './ReactFlightClientHostConfig';
27 +
28 +export type ReactModelRoot<T> = {|
29 + model: T,
30 +|};
31 +
32 +type Response = ResponseBase & {
33 + fromJSON: (key: string, value: JSONValue) => any,
34 + stringDecoder: StringDecoder,
35 +};
36 +
37 +export function createResponse(): Response {
38 + let response: Response = (createResponseImpl(): any);
39 + response.fromJSON = function(key: string, value: JSONValue) {
40 + return parseModelFromJSON(response, this, key, value);
41 + };
42 + if (supportsBinaryStreams) {
43 + response.stringDecoder = createStringDecoder();
44 + }
45 + return response;
46 +}
47 +
48 +function processFullRow(response: Response, row: string): void {
49 + if (row === '') {
50 + return;
51 + }
52 + let tag = row[0];
53 + switch (tag) {
54 + case 'J': {
55 + let colon = row.indexOf(':', 1);
56 + let id = parseInt(row.substring(1, colon), 16);
57 + let json = row.substring(colon + 1);
58 + let model = JSON.parse(json, response.fromJSON);
59 + resolveModelChunk(response, id, model);
60 + return;
61 + }
62 + case 'E': {
63 + let colon = row.indexOf(':', 1);
64 + let id = parseInt(row.substring(1, colon), 16);
65 + let json = row.substring(colon + 1);
66 + let errorInfo = JSON.parse(json);
67 + resolveErrorChunk(response, id, errorInfo.message, errorInfo.stack);
68 + return;
69 + }
70 + default: {
71 + // Assume this is the root model.
72 + let model = JSON.parse(row, response.fromJSON);
73 + resolveModelChunk(response, 0, model);
74 + return;
75 + }
76 + }
77 +}
78 +
79 +export function processStringChunk(
80 + response: Response,
81 + chunk: string,
82 + offset: number,
83 +): void {
84 + let linebreak = chunk.indexOf('\n', offset);
85 + while (linebreak > -1) {
86 + let fullrow = response.partialRow + chunk.substring(offset, linebreak);
87 + processFullRow(response, fullrow);
88 + response.partialRow = '';
89 + offset = linebreak + 1;
90 + linebreak = chunk.indexOf('\n', offset);
91 + }
92 + response.partialRow += chunk.substring(offset);
93 +}
94 +
95 +export function processBinaryChunk(
96 + response: Response,
97 + chunk: Uint8Array,
98 +): void {
99 + if (!supportsBinaryStreams) {
100 + throw new Error("This environment don't support binary chunks.");
101 + }
102 + let stringDecoder = response.stringDecoder;
103 + let linebreak = chunk.indexOf(10); // newline
104 + while (linebreak > -1) {
105 + let fullrow =
106 + response.partialRow +
107 + readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak));
108 + processFullRow(response, fullrow);
109 + response.partialRow = '';
110 + chunk = chunk.subarray(linebreak + 1);
111 + linebreak = chunk.indexOf(10); // newline
112 + }
113 + response.partialRow += readPartialStringChunk(stringDecoder, chunk);
114 +}
115 +
116 +export {reportGlobalError, close, getModelRoot} from './ReactFlightClient';
packages/react-flight-dom-relay/src/ReactFlightDOMRelayClient.js
+36 -12
@@ -7,24 +7,48 @@
7 * @flow
8 */
9
10 -import type {ReactModelRoot} from 'react-client/src/ReactFlightClient';
10 +import type {Response, JSONValue} from 'react-client/src/ReactFlightClient';
11
12 import {
13 createResponse,
14 getModelRoot,
15 - processStringChunk,
16 - complete,
15 + parseModelFromJSON,
16 + resolveModelChunk,
17 + resolveErrorChunk,
18 + close,
19 } from 'react-client/src/ReactFlightClient';
20
19 -type EncodedData = Array<string>;
20 -
21 -function read<T>(data: EncodedData): ReactModelRoot<T> {
22 - let response = createResponse(data);
23 - for (let i = 0; i < data.length; i++) {
24 - processStringChunk(response, data[i], 0);
21 +function parseModel(response, targetObj, key, value) {
22 + if (typeof value === 'object' && value !== null) {
23 + if (Array.isArray(value)) {
24 + for (let i = 0; i < value.length; i++) {
25 + value[i] = parseModel(response, value, '' + i, value[i]);
26 + }
27 + } else {
28 + for (let innerKey in value) {
29 + value[innerKey] = parseModel(
30 + response,
31 + value,
32 + innerKey,
33 + value[innerKey],
34 + );
35 + }
36 + }
37 }
26 - complete(response);
27 - return getModelRoot(response);
38 + return parseModelFromJSON(response, targetObj, key, value);
39 }
40
30 -export {read};
41 +export {createResponse, getModelRoot, close};
42 +
43 +export function resolveModel(response: Response, id: number, json: JSONValue) {
44 + resolveModelChunk(response, id, parseModel(response, {}, '', json));
45 +}
46 +
47 +export function resolveError(
48 + response: Response,
49 + id: number,
50 + message: string,
51 + stack: string,
52 +) {
53 + resolveErrorChunk(response, id, message, stack);
54 +}
packages/react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js
-2
@@ -7,8 +7,6 @@
7 * @flow
8 */
9
10 -export type Source = Array<string>;
11 -
10 export type StringDecoder = void;
11
12 export const supportsBinaryStreams = false;
packages/react-flight-dom-relay/src/ReactFlightDOMRelayServer.js
+3 -6
@@ -8,16 +8,13 @@
8 */
9
10 import type {ReactModel} from 'react-server/src/ReactFlightServer';
11 +import type {Destination} from './ReactFlightDOMRelayServerHostConfig';
12
13 import {createRequest, startWork} from 'react-server/src/ReactFlightServer';
14
14 -type EncodedData = Array<string>;
15 -
16 -function render(model: ReactModel): EncodedData {
17 - let data: EncodedData = [];
18 - let request = createRequest(model, data);
15 +function render(model: ReactModel, destination: Destination): void {
16 + let request = createRequest(model, destination);
17 startWork(request);
20 - return data;
18 }
19
20 export {render};
packages/react-flight-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js
+89 -11
@@ -7,7 +7,88 @@
7 * @flow
8 */
9
10 -export type Destination = Array<string>;
10 +import type {Request, ReactModel} from 'react-server/src/ReactFlightServer';
11 +
12 +import type {Destination} from 'ReactFlightDOMRelayServerIntegration';
13 +
14 +import {resolveModelToJSON} from 'react-server/src/ReactFlightServer';
15 +
16 +import {emitModel, emitError} from 'ReactFlightDOMRelayServerIntegration';
17 +
18 +export type {Destination} from 'ReactFlightDOMRelayServerIntegration';
19 +
20 +type JSONValue =
21 + | string
22 + | number
23 + | boolean
24 + | null
25 + | {[key: string]: JSONValue}
26 + | Array<JSONValue>;
27 +
28 +export type Chunk =
29 + | {
30 + type: 'json',
31 + id: number,
32 + json: JSONValue,
33 + }
34 + | {
35 + type: 'error',
36 + id: number,
37 + json: {
38 + message: string,
39 + stack: string,
40 + ...
41 + },
42 + };
43 +
44 +export function processErrorChunk(
45 + request: Request,
46 + id: number,
47 + message: string,
48 + stack: string,
49 +): Chunk {
50 + return {
51 + type: 'error',
52 + id: id,
53 + json: {
54 + message,
55 + stack,
56 + },
57 + };
58 +}
59 +
60 +function convertModelToJSON(request: Request, model: ReactModel): JSONValue {
61 + let json = resolveModelToJSON(request, model);
62 + if (typeof json === 'object' && json !== null) {
63 + if (Array.isArray(json)) {
64 + let jsonArray: Array<JSONValue> = [];
65 + for (let i = 0; i < json.length; i++) {
66 + jsonArray[i] = convertModelToJSON(request, json[i]);
67 + }
68 + return jsonArray;
69 + } else {
70 + let jsonObj: {[key: string]: JSONValue} = {};
71 + for (let key in json) {
72 + jsonObj[key] = convertModelToJSON(request, json[key]);
73 + }
74 + return jsonObj;
75 + }
76 + }
77 + return json;
78 +}
79 +
80 +export function processModelChunk(
81 + request: Request,
82 + id: number,
83 + model: ReactModel,
84 +): Chunk {
85 + let json = convertModelToJSON(request, model);
86 + return {
87 + type: 'json',
88 + id: id,
89 + json: json,
90 + };
91 +}
92
93 export function scheduleWork(callback: () => void) {
94 callback();
@@ -17,18 +98,15 @@ export function flushBuffered(destination: Destination) {}
98
99 export function beginWriting(destination: Destination) {}
100
20 -export function writeChunk(
21 - destination: Destination,
22 - buffer: Uint8Array,
23 -): boolean {
24 - destination.push(Buffer.from((buffer: any)).toString('utf8'));
101 +export function writeChunk(destination: Destination, chunk: Chunk): boolean {
102 + if (chunk.type === 'json') {
103 + emitModel(destination, chunk.id, chunk.json);
104 + } else {
105 + emitError(destination, chunk.id, chunk.json.message, chunk.json.stack);
106 + }
107 return true;
108 }
109
110 export function completeWriting(destination: Destination) {}
111
30 -export function close(destination: Destination) {}
31 -
32 -export function convertStringToBuffer(content: string): Uint8Array {
33 - return Buffer.from(content, 'utf8');
34 -}
112 +export {close} from 'ReactFlightDOMRelayServerIntegration';
packages/react-flight-dom-relay/src/__mocks__/ReactFlightDOMRelayServerIntegration.js new
+28
@@ -0,0 +1,28 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +'use strict';
9 +
10 +const ReactFlightDOMRelayServerIntegration = {
11 + emitModel(destination, id, json) {
12 + destination.push({
13 + type: 'json',
14 + id: id,
15 + json: json,
16 + });
17 + },
18 + emitError(destination, id, message, stack) {
19 + destination.push({
20 + type: 'error',
21 + id: id,
22 + json: {message, stack},
23 + });
24 + },
25 + close(destination) {},
26 +};
27 +
28 +module.exports = ReactFlightDOMRelayServerIntegration;
packages/react-flight-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js
+24 -8
@@ -7,9 +7,6 @@
7
8 'use strict';
9
10 -// Polyfills for test environment
11 -global.TextDecoder = require('util').TextDecoder;
12 -
10 let React;
11 let ReactDOMFlightRelayServer;
12 let ReactDOMFlightRelayClient;
@@ -32,11 +29,30 @@ describe('ReactFlightDOMRelay', () => {
29 bar: [<Bar text="a" />, <Bar text="b" />],
30 };
31 }
35 - let data = ReactDOMFlightRelayServer.render({
36 - foo: <Foo />,
37 - });
38 - let root = ReactDOMFlightRelayClient.read(data);
39 - let model = root.model;
32 + let data = [];
33 + ReactDOMFlightRelayServer.render(
34 + {
35 + foo: <Foo />,
36 + },
37 + data,
38 + );
39 +
40 + let response = ReactDOMFlightRelayClient.createResponse();
41 + for (let i = 0; i < data.length; i++) {
42 + let chunk = data[i];
43 + if (chunk.type === 'json') {
44 + ReactDOMFlightRelayClient.resolveModel(response, chunk.id, chunk.json);
45 + } else {
46 + ReactDOMFlightRelayClient.resolveError(
47 + response,
48 + chunk.id,
49 + chunk.json.message,
50 + chunk.json.stack,
51 + );
52 + }
53 + }
54 + let model = ReactDOMFlightRelayClient.getModelRoot(response).model;
55 + ReactDOMFlightRelayClient.close(response);
56 expect(model).toEqual({foo: {bar: ['A', 'B']}});
57 });
58 });
packages/react-flight-dom-webpack/src/ReactFlightDOMClient.js
+8 -8
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {ReactModelRoot} from 'react-client/src/ReactFlightClient';
10 +import type {ReactModelRoot} from 'react-client/src/ReactFlightClientStream';
11
12 import {
13 createResponse,
@@ -15,14 +15,14 @@ import {
15 reportGlobalError,
16 processStringChunk,
17 processBinaryChunk,
18 - complete,
19 -} from 'react-client/src/ReactFlightClient';
18 + close,
19 +} from 'react-client/src/ReactFlightClientStream';
20
21 function startReadingFromStream(response, stream: ReadableStream): void {
22 let reader = stream.getReader();
23 function progress({done, value}) {
24 if (done) {
25 - complete(response);
25 + close(response);
26 return;
27 }
28 let buffer: Uint8Array = (value: any);
@@ -36,7 +36,7 @@ function startReadingFromStream(response, stream: ReadableStream): void {
36 }
37
38 function readFromReadableStream<T>(stream: ReadableStream): ReactModelRoot<T> {
39 - let response = createResponse(stream);
39 + let response = createResponse();
40 startReadingFromStream(response, stream);
41 return getModelRoot(response);
42 }
@@ -44,7 +44,7 @@ function readFromReadableStream<T>(stream: ReadableStream): ReactModelRoot<T> {
44 function readFromFetch<T>(
45 promiseForResponse: Promise<Response>,
46 ): ReactModelRoot<T> {
47 - let response = createResponse(promiseForResponse);
47 + let response = createResponse();
48 promiseForResponse.then(
49 function(r) {
50 startReadingFromStream(response, (r.body: any));
@@ -57,7 +57,7 @@ function readFromFetch<T>(
57 }
58
59 function readFromXHR<T>(request: XMLHttpRequest): ReactModelRoot<T> {
60 - let response = createResponse(request);
60 + let response = createResponse();
61 let processedLength = 0;
62 function progress(e: ProgressEvent): void {
63 let chunk = request.responseText;
@@ -66,7 +66,7 @@ function readFromXHR<T>(request: XMLHttpRequest): ReactModelRoot<T> {
66 }
67 function load(e: ProgressEvent): void {
68 progress(e);
69 - complete(response);
69 + close(response);
70 }
71 function error(e: ProgressEvent): void {
72 reportGlobalError(response, new TypeError('Network error'));
packages/react-noop-renderer/src/ReactNoopFlightClient.js
+2 -2
@@ -24,7 +24,7 @@ const {
24 createResponse,
25 getModelRoot,
26 processStringChunk,
27 - complete,
27 + close,
28 } = ReactFlightClient({
29 supportsBinaryStreams: false,
30 });
@@ -34,7 +34,7 @@ function read<T>(source: Source): ReactModelRoot<T> {
34 for (let i = 0; i < source.length; i++) {
35 processStringChunk(response, source[i], 0);
36 }
37 - complete(response);
37 + close(response);
38 return getModelRoot(response);
39 }
40
packages/react-server/src/ReactFlightServer.js
+29 -102
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {Destination} from './ReactServerStreamConfig';
10 +import type {Destination, Chunk} from './ReactFlightServerConfig';
11
12 import {
13 scheduleWork,
@@ -16,110 +16,52 @@ import {
16 completeWriting,
17 flushBuffered,
18 close,
19 - convertStringToBuffer,
20 -} from './ReactServerStreamConfig';
19 + processModelChunk,
20 + processErrorChunk,
21 +} from './ReactFlightServerConfig';
22 import {renderHostChildrenToString} from './ReactServerFormatConfig';
23 import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
24
24 -/*
25 -
26 -FLIGHT PROTOCOL GRAMMAR
27 -
28 -Response
29 -- JSONData RowSequence
30 -- JSONData
31 -
32 -RowSequence
33 -- Row RowSequence
34 -- Row
35 -
36 -Row
37 -- "J" RowID JSONData
38 -- "H" RowID HTMLData
39 -- "B" RowID BlobData
40 -- "U" RowID URLData
41 -- "E" RowID ErrorData
42 -
43 -RowID
44 -- HexDigits ":"
45 -
46 -HexDigits
47 -- HexDigit HexDigits
48 -- HexDigit
49 -
50 -HexDigit
51 -- 0-F
52 -
53 -URLData
54 -- (UTF8 encoded URL) "\n"
55 -
56 -ErrorData
57 -- (UTF8 encoded JSON: {message: "...", stack: "..."}) "\n"
58 -
59 -JSONData
60 -- (UTF8 encoded JSON) "\n"
61 - - String values that begin with $ are escaped with a "$" prefix.
62 - - References to other rows are encoding as JSONReference strings.
63 -
64 -JSONReference
65 -- "$" HexDigits
66 -
67 -HTMLData
68 -- ByteSize (UTF8 encoded HTML)
69 -
70 -BlobData
71 -- ByteSize (Binary Data)
72 -
73 -ByteSize
74 -- (unsigned 32-bit integer)
75 -*/
76 -
77 -// TODO: Implement HTMLData, BlobData and URLData.
78 -
79 -const stringify = JSON.stringify;
80 -
81 -export type ReactModel =
82 - | React$Element<any>
25 +type ReactJSONValue =
26 | string
27 | boolean
28 | number
29 | null
87 - | Iterable<ReactModel>
30 + | Array<ReactModel>
31 | ReactModelObject;
32
90 -type ReactJSONValue =
33 +export type ReactModel =
34 + | React$Element<any>
35 | string
36 | boolean
37 | number
38 | null
95 - | Array<ReactModel>
39 + | Iterable<ReactModel>
40 | ReactModelObject;
41
98 -type ReactModelObject = {+[key: string]: ReactModel, ...};
42 +type ReactModelObject = {+[key: string]: ReactModel};
43
44 type Segment = {
45 id: number,
46 model: ReactModel,
47 ping: () => void,
104 - ...
48 };
49
107 -type OpaqueRequest = {
50 +export type Request = {
51 destination: Destination,
52 nextChunkId: number,
53 pendingChunks: number,
54 pingedSegments: Array<Segment>,
112 - completedJSONChunks: Array<Uint8Array>,
113 - completedErrorChunks: Array<Uint8Array>,
55 + completedJSONChunks: Array<Chunk>,
56 + completedErrorChunks: Array<Chunk>,
57 flowing: boolean,
58 toJSON: (key: string, value: ReactModel) => ReactJSONValue,
116 - ...
59 };
60
61 export function createRequest(
62 model: ReactModel,
63 destination: Destination,
122 -): OpaqueRequest {
64 +): Request {
65 let pingedSegments = [];
66 let request = {
67 destination,
@@ -152,7 +94,7 @@ function attemptResolveModelComponent(element: React$Element<any>): ReactModel {
94 }
95 }
96
155 -function pingSegment(request: OpaqueRequest, segment: Segment): void {
97 +function pingSegment(request: Request, segment: Segment): void {
98 let pingedSegments = request.pingedSegments;
99 pingedSegments.push(segment);
100 if (pingedSegments.length === 1) {
@@ -160,7 +102,7 @@ function pingSegment(request: OpaqueRequest, segment: Segment): void {
102 }
103 }
104
163 -function createSegment(request: OpaqueRequest, model: ReactModel): Segment {
105 +function createSegment(request: Request, model: ReactModel): Segment {
106 let id = request.nextChunkId++;
107 let segment = {
108 id,
@@ -174,10 +116,6 @@ function serializeIDRef(id: number): string {
116 return '$' + id.toString(16);
117 }
118
177 -function serializeRowHeader(tag: string, id: number) {
178 - return tag + id.toString(16) + ':';
179 -}
180 -
119 function escapeStringValue(value: string): string {
120 if (value[0] === '$') {
121 // We need to escape $ prefixed strings since we use that to encode
@@ -188,8 +126,8 @@ function escapeStringValue(value: string): string {
126 }
127 }
128
191 -function resolveModelToJSON(
192 - request: OpaqueRequest,
129 +export function resolveModelToJSON(
130 + request: Request,
131 value: ReactModel,
132 ): ReactJSONValue {
133 if (typeof value === 'string') {
@@ -224,11 +162,7 @@ function resolveModelToJSON(
162 return value;
163 }
164
227 -function emitErrorChunk(
228 - request: OpaqueRequest,
229 - id: number,
230 - error: mixed,
231 -): void {
165 +function emitErrorChunk(request: Request, id: number, error: mixed): void {
166 // TODO: We should not leak error messages to the client in prod.
167 // Give this an error code instead and log on the server.
168 // We can serialize the error in DEV as a convenience.
@@ -244,12 +178,12 @@ function emitErrorChunk(
178 } catch (x) {
179 message = 'An error occurred but serializing the error message failed.';
180 }
247 - let errorInfo = {message, stack};
248 - let row = serializeRowHeader('E', id) + stringify(errorInfo) + '\n';
249 - request.completedErrorChunks.push(convertStringToBuffer(row));
181 +
182 + let processedChunk = processErrorChunk(request, id, message, stack);
183 + request.completedErrorChunks.push(processedChunk);
184 }
185
252 -function retrySegment(request: OpaqueRequest, segment: Segment): void {
186 +function retrySegment(request: Request, segment: Segment): void {
187 let value = segment.model;
188 try {
189 while (
@@ -263,15 +197,8 @@ function retrySegment(request: OpaqueRequest, segment: Segment): void {
197 segment.model = element;
198 value = attemptResolveModelComponent(element);
199 }
266 - let json = stringify(value, request.toJSON);
267 - let row;
268 - let id = segment.id;
269 - if (id === 0) {
270 - row = json + '\n';
271 - } else {
272 - row = serializeRowHeader('J', id) + json + '\n';
273 - }
274 - request.completedJSONChunks.push(convertStringToBuffer(row));
200 + let processedChunk = processModelChunk(request, segment.id, value);
201 + request.completedJSONChunks.push(processedChunk);
202 } catch (x) {
203 if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
204 // Something suspended again, let's pick it back up later.
@@ -285,7 +212,7 @@ function retrySegment(request: OpaqueRequest, segment: Segment): void {
212 }
213 }
214
288 -function performWork(request: OpaqueRequest): void {
215 +function performWork(request: Request): void {
216 let pingedSegments = request.pingedSegments;
217 request.pingedSegments = [];
218 for (let i = 0; i < pingedSegments.length; i++) {
@@ -298,7 +225,7 @@ function performWork(request: OpaqueRequest): void {
225 }
226
227 let reentrant = false;
301 -function flushCompletedChunks(request: OpaqueRequest): void {
228 +function flushCompletedChunks(request: Request): void {
229 if (reentrant) {
230 return;
231 }
@@ -341,12 +268,12 @@ function flushCompletedChunks(request: OpaqueRequest): void {
268 }
269 }
270
344 -export function startWork(request: OpaqueRequest): void {
271 +export function startWork(request: Request): void {
272 request.flowing = true;
273 scheduleWork(() => performWork(request));
274 }
275
349 -export function startFlowing(request: OpaqueRequest): void {
276 +export function startFlowing(request: Request): void {
277 request.flowing = true;
278 flushCompletedChunks(request);
279 }
packages/react-server/src/ReactFlightServerConfig.js new
+22
@@ -0,0 +1,22 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +/* eslint-disable react-internal/invariant-args */
11 +
12 +import invariant from 'shared/invariant';
13 +
14 +// We expect that our Rollup, Jest, and Flow configurations
15 +// always shim this module with the corresponding host config
16 +// (either provided by a renderer, or a generic shim for npm).
17 +//
18 +// We should never resolve to this file, but it exists to make
19 +// sure that if we *do* accidentally break the configuration,
20 +// the failure isn't silent.
21 +
22 +invariant(false, 'This module must be shimmed by a specific renderer.');
packages/react-server/src/ReactFlightServerConfigStream.js new
+114
@@ -0,0 +1,114 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +// This file is an intermediate layer to translate between Flight
11 +// calls to stream output over a binary stream.
12 +
13 +/*
14 +FLIGHT PROTOCOL GRAMMAR
15 +
16 +Response
17 +- JSONData RowSequence
18 +- JSONData
19 +
20 +RowSequence
21 +- Row RowSequence
22 +- Row
23 +
24 +Row
25 +- "J" RowID JSONData
26 +- "H" RowID HTMLData
27 +- "B" RowID BlobData
28 +- "U" RowID URLData
29 +- "E" RowID ErrorData
30 +
31 +RowID
32 +- HexDigits ":"
33 +
34 +HexDigits
35 +- HexDigit HexDigits
36 +- HexDigit
37 +
38 +HexDigit
39 +- 0-F
40 +
41 +URLData
42 +- (UTF8 encoded URL) "\n"
43 +
44 +ErrorData
45 +- (UTF8 encoded JSON: {message: "...", stack: "..."}) "\n"
46 +
47 +JSONData
48 +- (UTF8 encoded JSON) "\n"
49 + - String values that begin with $ are escaped with a "$" prefix.
50 + - References to other rows are encoding as JSONReference strings.
51 +
52 +JSONReference
53 +- "$" HexDigits
54 +
55 +HTMLData
56 +- ByteSize (UTF8 encoded HTML)
57 +
58 +BlobData
59 +- ByteSize (Binary Data)
60 +
61 +ByteSize
62 +- (unsigned 32-bit integer)
63 +*/
64 +
65 +// TODO: Implement HTMLData, BlobData and URLData.
66 +
67 +import type {Request, ReactModel} from 'react-server/src/ReactFlightServer';
68 +
69 +import {convertStringToBuffer} from './ReactServerStreamConfig';
70 +
71 +export type {Destination} from './ReactServerStreamConfig';
72 +
73 +export type Chunk = Uint8Array;
74 +
75 +const stringify = JSON.stringify;
76 +
77 +function serializeRowHeader(tag: string, id: number) {
78 + return tag + id.toString(16) + ':';
79 +}
80 +
81 +export function processErrorChunk(
82 + request: Request,
83 + id: number,
84 + message: string,
85 + stack: string,
86 +): Chunk {
87 + let errorInfo = {message, stack};
88 + let row = serializeRowHeader('E', id) + stringify(errorInfo) + '\n';
89 + return convertStringToBuffer(row);
90 +}
91 +
92 +export function processModelChunk(
93 + request: Request,
94 + id: number,
95 + model: ReactModel,
96 +): Chunk {
97 + let json = stringify(model, request.toJSON);
98 + let row;
99 + if (id === 0) {
100 + row = json + '\n';
101 + } else {
102 + row = serializeRowHeader('J', id) + json + '\n';
103 + }
104 + return convertStringToBuffer(row);
105 +}
106 +
107 +export {
108 + scheduleWork,
109 + flushBuffered,
110 + beginWriting,
111 + writeChunk,
112 + completeWriting,
113 + close,
114 +} from './ReactServerStreamConfig';
packages/react-server/src/forks/ReactFlightServerConfig.custom.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export * from '../ReactFlightServerConfigStream';
packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export * from '../ReactFlightServerConfigStream';
packages/react-server/src/forks/ReactFlightServerConfig.dom-relay.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export * from 'react-flight-dom-relay/src/ReactFlightDOMRelayServerHostConfig';
packages/react-server/src/forks/ReactFlightServerConfig.dom.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export * from '../ReactFlightServerConfigStream';
packages/react-server/src/forks/ReactServerStreamConfig.dom-relay.js
+1 -1
@@ -7,4 +7,4 @@
7 * @flow
8 */
9
10 -export * from 'react-flight-dom-relay/src/ReactFlightDOMRelayServerHostConfig';
10 +export * from '../ReactServerStreamConfigNode';
scripts/flow/config/flowconfig
+1
@@ -29,6 +29,7 @@
29 ../environment.js
30 ../react-devtools.js
31 ../react-native-host-hooks.js
32 +../react-relay-hooks.js
33
34 [lints]
35 untyped-type-import=error
scripts/flow/createFlowConfigs.js
+1
@@ -51,6 +51,7 @@ function writeConfig(renderer, rendererInfo, isServerSupported) {
51 module.name_mapper='ReactFiberHostConfig$$' -> 'forks/ReactFiberHostConfig.${renderer}'
52 module.name_mapper='ReactServerStreamConfig$$' -> 'forks/ReactServerStreamConfig.${serverRenderer}'
53 module.name_mapper='ReactServerFormatConfig$$' -> 'forks/ReactServerFormatConfig.${serverRenderer}'
54 +module.name_mapper='ReactFlightServerConfig$$' -> 'forks/ReactFlightServerConfig.${serverRenderer}'
55 module.name_mapper='ReactFlightClientHostConfig$$' -> 'forks/ReactFlightClientHostConfig.${serverRenderer}'
56 `.trim(),
57 )
scripts/flow/react-relay-hooks.js new
+32
@@ -0,0 +1,32 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +type JSONValue =
11 + | string
12 + | number
13 + | boolean
14 + | null
15 + | {[key: string]: JSONValue}
16 + | Array<JSONValue>;
17 +
18 +declare module 'ReactFlightDOMRelayServerIntegration' {
19 + declare export opaque type Destination;
20 + declare export function emitModel(
21 + destination: Destination,
22 + id: number,
23 + json: JSONValue,
24 + ): void;
25 + declare export function emitError(
26 + destination: Destination,
27 + id: number,
28 + message: string,
29 + stack: string,
30 + ): void;
31 + declare export function close(destination: Destination): void;
32 +}
scripts/jest/setupHostConfigs.js
+7
@@ -13,6 +13,7 @@ jest.mock('react-reconciler', () => {
13 });
14 const shimServerStreamConfigPath = 'react-server/src/ReactServerStreamConfig';
15 const shimServerFormatConfigPath = 'react-server/src/ReactServerFormatConfig';
16 +const shimFlightServerConfigPath = 'react-server/src/ReactFlightServerConfig';
17 jest.mock('react-server', () => {
18 return config => {
19 jest.mock(shimServerStreamConfigPath, () => config);
@@ -24,6 +25,11 @@ jest.mock('react-server/flight', () => {
25 return config => {
26 jest.mock(shimServerStreamConfigPath, () => config);
27 jest.mock(shimServerFormatConfigPath, () => config);
28 + jest.mock(shimFlightServerConfigPath, () =>
29 + require.requireActual(
30 + 'react-server/src/forks/ReactFlightServerConfig.custom'
31 + )
32 + );
33 return require.requireActual('react-server/flight');
34 };
35 });
@@ -41,6 +47,7 @@ const configPaths = [
47 'react-client/src/ReactFlightClientHostConfig',
48 'react-server/src/ReactServerStreamConfig',
49 'react-server/src/ReactServerFormatConfig',
50 + 'react-server/src/ReactFlightServerConfig',
51 ];
52
53 function mockAllConfigs(rendererInfo) {
scripts/rollup/bundles.js
+5 -1
@@ -203,7 +203,11 @@ const bundles = [
203 moduleType: RENDERER,
204 entry: 'react-flight-dom-relay/server',
205 global: 'ReactFlightDOMRelayServer',
206 - externals: ['react', 'react-dom/server'],
206 + externals: [
207 + 'react',
208 + 'react-dom/server',
209 + 'ReactFlightDOMRelayServerIntegration',
210 + ],
211 },
212
213 /******* React DOM Flight Client Relay *******/
scripts/rollup/forks.js
+28
@@ -354,6 +354,34 @@ const forks = Object.freeze({
354 );
355 },
356
357 + 'react-server/src/ReactFlightServerConfig': (
358 + bundleType,
359 + entry,
360 + dependencies,
361 + moduleType
362 + ) => {
363 + if (dependencies.indexOf('react-server') !== -1) {
364 + return null;
365 + }
366 + if (moduleType !== RENDERER && moduleType !== RECONCILER) {
367 + return null;
368 + }
369 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
370 + for (let rendererInfo of inlinedHostConfigs) {
371 + if (rendererInfo.entryPoints.indexOf(entry) !== -1) {
372 + if (!rendererInfo.isServerSupported) {
373 + return null;
374 + }
375 + return `react-server/src/forks/ReactFlightServerConfig.${rendererInfo.shortName}.js`;
376 + }
377 + }
378 + throw new Error(
379 + 'Expected ReactFlightServerConfig to always be replaced with a shim, but ' +
380 + `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` +
381 + 'Did you mean to add it there to associate it with a specific renderer?'
382 + );
383 + },
384 +
385 'react-client/src/ReactFlightClientHostConfig': (
386 bundleType,
387 entry,