@samitouri / QOS-React-2 / commits / e2dd30898e

[Flight] Lazily parse models and allow any value to suspend (#18476)

* Lazily initialize models as they're read intead of eagerly when received This ensures that we don't spend CPU cycles processing models that we're not going to end up rendering. This model will also allow us to suspend during this initialization if data is not yet available to satisfy the model. * Refactoring carefully to ensure bundles still compile to something optimal * Remove generic from Response The root model needs to be cast at one point or another same as othe chunks. So we can parameterize the read instead of the whole Response. * Read roots from the 0 key of the map The special case to read the root isn't worth the field and code. * Store response on each Chunk Instead of storing it on the data tuple which is kind of dynamic, we store it on each Chunk. This uses more memory. Especially compared to just making initializeBlock a closure, but overall is simpler. * Rename private fields to underscores Response objects are exposed. * Encode server components as delayed references This allows us to stream in server components one after another over the wire. It also allows parallelizing their fetches and resuming only the server component instead of the whole parent block. This doesn't yet allow us to suspend deeper while waiting on this content because we don't have "lazy elements".

Sebastian Markbåge committed Apr 3, 2020 at 14:58 UTC e2dd30898e180148ce480cb7311c34fe2ba16484
14 files changed +350 -213
packages/react-client/src/ReactFlightClient.js
+141 -104
@@ -14,12 +14,15 @@ import type {LazyComponent} from 'react/src/ReactLazy';
14 import type {
15 ModuleReference,
16 ModuleMetaData,
17 + UninitializedModel,
18 + Response,
19 } from './ReactFlightClientHostConfig';
20
21 import {
22 resolveModuleReference,
23 preloadModule,
24 requireModule,
25 + parseModel,
26 } from './ReactFlightClientHostConfig';
27
28 import {
@@ -33,33 +36,48 @@ export type JSONValue =
36 | null
37 | boolean
38 | string
36 - | {[key: string]: JSONValue}
37 - | Array<JSONValue>;
39 + | {+[key: string]: JSONValue}
40 + | $ReadOnlyArray<JSONValue>;
41
42 const PENDING = 0;
40 -const RESOLVED = 1;
41 -const ERRORED = 2;
43 +const RESOLVED_MODEL = 1;
44 +const INITIALIZED = 2;
45 +const ERRORED = 3;
46
47 type PendingChunk = {
48 _status: 0,
49 _value: null | Array<() => mixed>,
50 + _response: Response,
51 then(resolve: () => mixed): void,
52 };
48 -type ResolvedChunk<T> = {
53 +type ResolvedModelChunk = {
54 _status: 1,
55 + _value: UninitializedModel,
56 + _response: Response,
57 + then(resolve: () => mixed): void,
58 +};
59 +type InitializedChunk<T> = {
60 + _status: 2,
61 _value: T,
62 + _response: Response,
63 then(resolve: () => mixed): void,
64 };
65 type ErroredChunk = {
54 - _status: 2,
66 + _status: 3,
67 _value: Error,
68 + _response: Response,
69 then(resolve: () => mixed): void,
70 };
58 -type SomeChunk<T> = PendingChunk | ResolvedChunk<T> | ErroredChunk;
71 +type SomeChunk<T> =
72 + | PendingChunk
73 + | ResolvedModelChunk
74 + | InitializedChunk<T>
75 + | ErroredChunk;
76
60 -function Chunk(status: any, value: any) {
77 +function Chunk(status: any, value: any, response: Response) {
78 this._status = status;
79 this._value = value;
80 + this._response = response;
81 }
82 Chunk.prototype.then = function<T>(resolve: () => mixed) {
83 const chunk: SomeChunk<T> = this;
@@ -73,45 +91,40 @@ Chunk.prototype.then = function<T>(resolve: () => mixed) {
91 }
92 };
93
76 -export type Response<T> = {
77 - partialRow: string,
78 - rootChunk: SomeChunk<T>,
79 - chunks: Map<number, SomeChunk<any>>,
80 - readRoot(): T,
94 +export type ResponseBase = {
95 + _chunks: Map<number, SomeChunk<any>>,
96 + readRoot<T>(): T,
97 + ...
98 };
99
83 -function readRoot<T>(): T {
84 - const response: Response<T> = this;
85 - const rootChunk = response.rootChunk;
86 - if (rootChunk._status === RESOLVED) {
87 - return rootChunk._value;
88 - } else if (rootChunk._status === PENDING) {
89 - // eslint-disable-next-line no-throw-literal
90 - throw (rootChunk: Wakeable);
91 - } else {
92 - throw rootChunk._value;
100 +export type {Response};
101 +
102 +function readChunk<T>(chunk: SomeChunk<T>): T {
103 + switch (chunk._status) {
104 + case INITIALIZED:
105 + return chunk._value;
106 + case RESOLVED_MODEL:
107 + return initializeModelChunk(chunk);
108 + case PENDING:
109 + // eslint-disable-next-line no-throw-literal
110 + throw (chunk: Wakeable);
111 + default:
112 + throw chunk._value;
113 }
114 }
115
96 -export function createResponse<T>(): Response<T> {
97 - const rootChunk: SomeChunk<any> = createPendingChunk();
98 - const chunks: Map<number, SomeChunk<any>> = new Map();
99 - chunks.set(0, rootChunk);
100 - const response = {
101 - partialRow: '',
102 - rootChunk,
103 - chunks: chunks,
104 - readRoot: readRoot,
105 - };
106 - return response;
116 +function readRoot<T>(): T {
117 + const response: Response = this;
118 + const chunk = getChunk(response, 0);
119 + return readChunk(chunk);
120 }
121
109 -function createPendingChunk(): PendingChunk {
110 - return new Chunk(PENDING, null);
122 +function createPendingChunk(response: Response): PendingChunk {
123 + return new Chunk(PENDING, null, response);
124 }
125
113 -function createErrorChunk(error: Error): ErroredChunk {
114 - return new Chunk(ERRORED, error);
126 +function createErrorChunk(response: Response, error: Error): ErroredChunk {
127 + return new Chunk(ERRORED, error, response);
128 }
129
130 function wakeChunk(listeners: null | Array<() => mixed>) {
@@ -135,29 +148,40 @@ function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: Error): void {
148 wakeChunk(listeners);
149 }
150
138 -function createResolvedChunk<T>(value: T): ResolvedChunk<T> {
139 - return new Chunk(RESOLVED, value);
151 +function createResolvedModelChunk(
152 + response: Response,
153 + value: UninitializedModel,
154 +): ResolvedModelChunk {
155 + return new Chunk(RESOLVED_MODEL, value, response);
156 }
157
142 -function resolveChunk<T>(chunk: SomeChunk<T>, value: T): void {
158 +function resolveModelChunk<T>(
159 + chunk: SomeChunk<T>,
160 + value: UninitializedModel,
161 +): void {
162 if (chunk._status !== PENDING) {
163 // We already resolved. We didn't expect to see this.
164 return;
165 }
166 const listeners = chunk._value;
148 - const resolvedChunk: ResolvedChunk<T> = (chunk: any);
149 - resolvedChunk._status = RESOLVED;
167 + const resolvedChunk: ResolvedModelChunk = (chunk: any);
168 + resolvedChunk._status = RESOLVED_MODEL;
169 resolvedChunk._value = value;
170 wakeChunk(listeners);
171 }
172
173 +function initializeModelChunk<T>(chunk: ResolvedModelChunk): T {
174 + const value: T = parseModel(chunk._response, chunk._value);
175 + const initializedChunk: InitializedChunk<T> = (chunk: any);
176 + initializedChunk._status = INITIALIZED;
177 + initializedChunk._value = value;
178 + return value;
179 +}
180 +
181 // Report that any missing chunks in the model is now going to throw this
182 // error upon read. Also notify any pending promises.
156 -export function reportGlobalError<T>(
157 - response: Response<T>,
158 - error: Error,
159 -): void {
160 - response.chunks.forEach(chunk => {
183 +export function reportGlobalError(response: Response, error: Error): void {
184 + response._chunks.forEach(chunk => {
185 // If this chunk was already resolved or errored, it won't
186 // trigger an error but if it wasn't then we need to
187 // because we won't be getting any new data to resolve it.
@@ -171,14 +195,7 @@ function readMaybeChunk<T>(maybeChunk: SomeChunk<T> | T): T {
195 return maybeChunk;
196 }
197 const chunk: SomeChunk<T> = (maybeChunk: any);
174 - if (chunk._status === RESOLVED) {
175 - return chunk._value;
176 - } else if (chunk._status === PENDING) {
177 - // eslint-disable-next-line no-throw-literal
178 - throw (chunk: Wakeable);
179 - } else {
180 - throw chunk._value;
181 - }
198 + return readChunk(chunk);
199 }
200
201 function createElement(type, key, props): React$Element<any> {
@@ -226,6 +243,7 @@ type UninitializedBlockPayload<Data> = [
243 mixed,
244 ModuleMetaData | SomeChunk<ModuleMetaData>,
245 Data | SomeChunk<Data>,
246 + Response,
247 ];
248
249 function initializeBlock<Props, Data>(
@@ -267,83 +285,102 @@ function createLazyBlock<Props, Data>(
285 return lazyType;
286 }
287
270 -export function parseModelFromJSON<T>(
271 - response: Response<T>,
272 - targetObj: Object,
273 - key: string,
274 - value: JSONValue,
275 -): mixed {
276 - if (typeof value === 'string') {
277 - if (value[0] === '$') {
278 - if (value === '$') {
279 - return REACT_ELEMENT_TYPE;
280 - } else if (value[1] === '$' || value[1] === '@') {
281 - // This was an escaped string value.
282 - return value.substring(1);
283 - } else {
284 - const id = parseInt(value.substring(1), 16);
285 - const chunks = response.chunks;
286 - let chunk = chunks.get(id);
287 - if (!chunk) {
288 - chunk = createPendingChunk();
289 - chunks.set(id, chunk);
290 - }
288 +function getChunk(response: Response, id: number): SomeChunk<any> {
289 + const chunks = response._chunks;
290 + let chunk = chunks.get(id);
291 + if (!chunk) {
292 + chunk = createPendingChunk(response);
293 + chunks.set(id, chunk);
294 + }
295 + return chunk;
296 +}
297 +
298 +export function parseModelString(
299 + response: Response,
300 + parentObject: Object,
301 + value: string,
302 +): any {
303 + if (value[0] === '$') {
304 + if (value === '$') {
305 + return REACT_ELEMENT_TYPE;
306 + } else if (value[1] === '$' || value[1] === '@') {
307 + // This was an escaped string value.
308 + return value.substring(1);
309 + } else {
310 + const id = parseInt(value.substring(1), 16);
311 + const chunk = getChunk(response, id);
312 + if (parentObject[0] === REACT_BLOCK_TYPE) {
313 + // Block types know how to deal with lazy values.
314 return chunk;
315 }
293 - }
294 - if (value === '@') {
295 - return REACT_BLOCK_TYPE;
316 + // For anything else we must Suspend this block if
317 + // we don't yet have the value.
318 + return readChunk(chunk);
319 }
320 }
298 - if (typeof value === 'object' && value !== null) {
299 - const tuple: [mixed, mixed, mixed, mixed] = (value: any);
300 - switch (tuple[0]) {
301 - case REACT_ELEMENT_TYPE: {
302 - // TODO: Consider having React just directly accept these arrays as elements.
303 - // Or even change the ReactElement type to be an array.
304 - return createElement(tuple[1], tuple[2], tuple[3]);
305 - }
306 - case REACT_BLOCK_TYPE: {
307 - // TODO: Consider having React just directly accept these arrays as blocks.
308 - return createLazyBlock((tuple: any));
309 - }
310 - }
321 + if (value === '@') {
322 + return REACT_BLOCK_TYPE;
323 }
324 return value;
325 }
326
315 -export function resolveModelChunk<T, M>(
316 - response: Response<T>,
327 +export function parseModelTuple(
328 + response: Response,
329 + value: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
330 +): any {
331 + const tuple: [mixed, mixed, mixed, mixed] = (value: any);
332 + if (tuple[0] === REACT_ELEMENT_TYPE) {
333 + // TODO: Consider having React just directly accept these arrays as elements.
334 + // Or even change the ReactElement type to be an array.
335 + return createElement(tuple[1], tuple[2], tuple[3]);
336 + } else if (tuple[0] === REACT_BLOCK_TYPE) {
337 + // TODO: Consider having React just directly accept these arrays as blocks.
338 + return createLazyBlock((tuple: any));
339 + }
340 + return value;
341 +}
342 +
343 +export function createResponse(): ResponseBase {
344 + const chunks: Map<number, SomeChunk<any>> = new Map();
345 + const response = {
346 + _chunks: chunks,
347 + readRoot: readRoot,
348 + };
349 + return response;
350 +}
351 +
352 +export function resolveModel(
353 + response: Response,
354 id: number,
318 - model: M,
355 + model: UninitializedModel,
356 ): void {
320 - const chunks = response.chunks;
357 + const chunks = response._chunks;
358 const chunk = chunks.get(id);
359 if (!chunk) {
323 - chunks.set(id, createResolvedChunk(model));
360 + chunks.set(id, createResolvedModelChunk(response, model));
361 } else {
325 - resolveChunk(chunk, model);
362 + resolveModelChunk(chunk, model);
363 }
364 }
365
329 -export function resolveErrorChunk<T>(
330 - response: Response<T>,
366 +export function resolveError(
367 + response: Response,
368 id: number,
369 message: string,
370 stack: string,
371 ): void {
372 const error = new Error(message);
373 error.stack = stack;
337 - const chunks = response.chunks;
374 + const chunks = response._chunks;
375 const chunk = chunks.get(id);
376 if (!chunk) {
340 - chunks.set(id, createErrorChunk(error));
377 + chunks.set(id, createErrorChunk(response, error));
378 } else {
379 triggerErrorOnChunk(chunk, error);
380 }
381 }
382
346 -export function close<T>(response: Response<T>): void {
383 +export function close(response: Response): void {
384 // In case there are any remaining unresolved chunks, they won't
385 // be resolved now. So we need to issue an error to those.
386 // Ideally we should be able to early bail out if we kept a
packages/react-client/src/ReactFlightClientHostConfigStream.js new
+23
@@ -0,0 +1,23 @@
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 {ResponseBase} from './ReactFlightClient';
11 +import type {StringDecoder} from './ReactFlightClientHostConfig';
12 +
13 +export type Response = ResponseBase & {
14 + _partialRow: string,
15 + _fromJSON: (key: string, value: JSONValue) => any,
16 + _stringDecoder: StringDecoder,
17 +};
18 +
19 +export type UninitializedModel = string;
20 +
21 +export function parseModel<T>(response: Response, json: UninitializedModel): T {
22 + return JSON.parse(json, response._fromJSON);
23 +}
packages/react-client/src/ReactFlightClientStream.js
+51 -41
@@ -7,41 +7,26 @@
7 * @flow
8 */
9
10 -import type {Response as ResponseBase, JSONValue} from './ReactFlightClient';
11 -
12 -import type {StringDecoder} from './ReactFlightClientHostConfig';
10 +import type {Response} from './ReactFlightClientHostConfigStream';
11
12 import {
15 - createResponse as createResponseImpl,
16 - resolveModelChunk,
17 - resolveErrorChunk,
18 - parseModelFromJSON,
13 + resolveModel,
14 + resolveError,
15 + createResponse as createResponseBase,
16 + parseModelString,
17 + parseModelTuple,
18 } from './ReactFlightClient';
19
20 import {
22 - supportsBinaryStreams,
23 - createStringDecoder,
21 readPartialStringChunk,
22 readFinalStringChunk,
23 + supportsBinaryStreams,
24 + createStringDecoder,
25 } from './ReactFlightClientHostConfig';
26
28 -export type Response<T> = ResponseBase<T> & {
29 - fromJSON: (key: string, value: JSONValue) => any,
30 - stringDecoder: StringDecoder,
31 -};
32 -
33 -export function createResponse<T>(): Response<T> {
34 - const response: Response<T> = (createResponseImpl(): any);
35 - response.fromJSON = function(key: string, value: JSONValue) {
36 - return parseModelFromJSON(response, this, key, value);
37 - };
38 - if (supportsBinaryStreams) {
39 - response.stringDecoder = createStringDecoder();
40 - }
41 - return response;
42 -}
27 +export type {Response};
28
44 -function processFullRow<T>(response: Response<T>, row: string): void {
29 +function processFullRow(response: Response, row: string): void {
30 if (row === '') {
31 return;
32 }
@@ -51,8 +36,7 @@ function processFullRow<T>(response: Response<T>, row: string): void {
36 const colon = row.indexOf(':', 1);
37 const id = parseInt(row.substring(1, colon), 16);
38 const json = row.substring(colon + 1);
54 - const model = JSON.parse(json, response.fromJSON);
55 - resolveModelChunk(response, id, model);
39 + resolveModel(response, id, json);
40 return;
41 }
42 case 'E': {
@@ -60,53 +44,79 @@ function processFullRow<T>(response: Response<T>, row: string): void {
44 const id = parseInt(row.substring(1, colon), 16);
45 const json = row.substring(colon + 1);
46 const errorInfo = JSON.parse(json);
63 - resolveErrorChunk(response, id, errorInfo.message, errorInfo.stack);
47 + resolveError(response, id, errorInfo.message, errorInfo.stack);
48 return;
49 }
50 default: {
51 // Assume this is the root model.
68 - const model = JSON.parse(row, response.fromJSON);
69 - resolveModelChunk(response, 0, model);
52 + resolveModel(response, 0, row);
53 return;
54 }
55 }
56 }
57
75 -export function processStringChunk<T>(
76 - response: Response<T>,
58 +export function processStringChunk(
59 + response: Response,
60 chunk: string,
61 offset: number,
62 ): void {
63 let linebreak = chunk.indexOf('\n', offset);
64 while (linebreak > -1) {
82 - const fullrow = response.partialRow + chunk.substring(offset, linebreak);
65 + const fullrow = response._partialRow + chunk.substring(offset, linebreak);
66 processFullRow(response, fullrow);
84 - response.partialRow = '';
67 + response._partialRow = '';
68 offset = linebreak + 1;
69 linebreak = chunk.indexOf('\n', offset);
70 }
88 - response.partialRow += chunk.substring(offset);
71 + response._partialRow += chunk.substring(offset);
72 }
73
91 -export function processBinaryChunk<T>(
92 - response: Response<T>,
74 +export function processBinaryChunk(
75 + response: Response,
76 chunk: Uint8Array,
77 ): void {
78 if (!supportsBinaryStreams) {
79 throw new Error("This environment don't support binary chunks.");
80 }
98 - const stringDecoder = response.stringDecoder;
81 + const stringDecoder = response._stringDecoder;
82 let linebreak = chunk.indexOf(10); // newline
83 while (linebreak > -1) {
84 const fullrow =
102 - response.partialRow +
85 + response._partialRow +
86 readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak));
87 processFullRow(response, fullrow);
105 - response.partialRow = '';
88 + response._partialRow = '';
89 chunk = chunk.subarray(linebreak + 1);
90 linebreak = chunk.indexOf(10); // newline
91 }
109 - response.partialRow += readPartialStringChunk(stringDecoder, chunk);
92 + response._partialRow += readPartialStringChunk(stringDecoder, chunk);
93 +}
94 +
95 +function createFromJSONCallback(response: Response) {
96 + return function(key: string, value: JSONValue) {
97 + if (typeof value === 'string') {
98 + // We can't use .bind here because we need the "this" value.
99 + return parseModelString(response, this, value);
100 + }
101 + if (typeof value === 'object' && value !== null) {
102 + return parseModelTuple(response, value);
103 + }
104 + return value;
105 + };
106 +}
107 +
108 +export function createResponse(): Response {
109 + // NOTE: CHECK THE COMPILER OUTPUT EACH TIME YOU CHANGE THIS.
110 + // It should be inlined to one object literal but minor changes can break it.
111 + const stringDecoder = supportsBinaryStreams ? createStringDecoder() : null;
112 + const response: any = createResponseBase();
113 + response._partialRow = '';
114 + if (supportsBinaryStreams) {
115 + response._stringDecoder = stringDecoder;
116 + }
117 + // Don't inline this call because it causes closure to outline the call above.
118 + response._fromJSON = createFromJSONCallback(response);
119 + return response;
120 }
121
122 export {reportGlobalError, close} from './ReactFlightClient';
packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js
+5
@@ -25,6 +25,7 @@
25
26 declare var $$$hostConfig: any;
27
28 +export type Response = any;
29 export opaque type ModuleMetaData = mixed; // eslint-disable-line no-undef
30 export opaque type ModuleReference<T> = mixed; // eslint-disable-line no-undef
31 export const resolveModuleReference = $$$hostConfig.resolveModuleReference;
@@ -32,6 +33,10 @@ export const preloadModule = $$$hostConfig.preloadModule;
33 export const requireModule = $$$hostConfig.requireModule;
34
35 export opaque type Source = mixed; // eslint-disable-line no-undef
36 +
37 +export type UninitializedModel = string;
38 +export const parseModel = $$$hostConfig.parseModel;
39 +
40 export opaque type StringDecoder = mixed; // eslint-disable-line no-undef
41
42 export const supportsBinaryStreams = $$$hostConfig.supportsBinaryStreams;
packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser.js
+1
@@ -8,4 +8,5 @@
8 */
9
10 export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
11 +export * from 'react-client/src/ReactFlightClientHostConfigStream';
12 export * from 'react-flight-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
packages/react-client/src/forks/ReactFlightClientHostConfig.dom.js
+1
@@ -8,4 +8,5 @@
8 */
9
10 export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
11 +export * from 'react-client/src/ReactFlightClientHostConfigStream';
12 export * from 'react-flight-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
packages/react-flight-dom-relay/src/ReactFlightDOMRelayClient.js
+3 -45
@@ -7,51 +7,9 @@
7 * @flow
8 */
9
10 -import type {Response, JSONValue} from 'react-client/src/ReactFlightClient';
11 -
12 -import {
10 +export {
11 createResponse,
14 - parseModelFromJSON,
15 - resolveModelChunk,
16 - resolveErrorChunk,
12 + resolveModel,
13 + resolveError,
14 close,
15 } from 'react-client/src/ReactFlightClient';
19 -
20 -function parseModel<T>(response: Response<T>, targetObj, key, value) {
21 - if (typeof value === 'object' && value !== null) {
22 - if (Array.isArray(value)) {
23 - for (let i = 0; i < value.length; i++) {
24 - (value: any)[i] = parseModel(response, value, '' + i, value[i]);
25 - }
26 - } else {
27 - for (const innerKey in value) {
28 - (value: any)[innerKey] = parseModel(
29 - response,
30 - value,
31 - innerKey,
32 - value[innerKey],
33 - );
34 - }
35 - }
36 - }
37 - return parseModelFromJSON(response, targetObj, key, value);
38 -}
39 -
40 -export {createResponse, close};
41 -
42 -export function resolveModel<T>(
43 - response: Response<T>,
44 - id: number,
45 - json: JSONValue,
46 -) {
47 - resolveModelChunk(response, id, parseModel(response, {}, '', json));
48 -}
49 -
50 -export function resolveError<T>(
51 - response: Response<T>,
52 - id: number,
53 - message: string,
54 - stack: string,
55 -) {
56 - resolveErrorChunk(response, id, message, stack);
57 -}
packages/react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js
+40
@@ -7,6 +7,13 @@
7 * @flow
8 */
9
10 +import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient';
11 +
12 +import {
13 + parseModelString,
14 + parseModelTuple,
15 +} from 'react-client/src/ReactFlightClient';
16 +
17 export {
18 resolveModuleReference,
19 preloadModule,
@@ -17,3 +24,36 @@ export type {
24 ModuleReference,
25 ModuleMetaData,
26 } from 'ReactFlightDOMRelayClientIntegration';
27 +
28 +export opaque type UninitializedModel = JSONValue;
29 +
30 +export type Response = ResponseBase;
31 +
32 +function parseModelRecursively(response: Response, parentObj, value) {
33 + if (typeof value === 'string') {
34 + return parseModelString(response, parentObj, value);
35 + }
36 + if (typeof value === 'object' && value !== null) {
37 + if (Array.isArray(value)) {
38 + for (let i = 0; i < value.length; i++) {
39 + (value: any)[i] = parseModelRecursively(response, value, value[i]);
40 + }
41 + return parseModelTuple(response, value);
42 + } else {
43 + for (const innerKey in value) {
44 + (value: any)[innerKey] = parseModelRecursively(
45 + response,
46 + value,
47 + value[innerKey],
48 + );
49 + }
50 + }
51 + }
52 + return value;
53 +}
54 +
55 +const dummy = {};
56 +
57 +export function parseModel<T>(response: Response, json: UninitializedModel): T {
58 + return (parseModelRecursively(response, dummy, json): any);
59 +}
packages/react-flight-dom-webpack/src/ReactFlightDOMClient.js
+9 -11
@@ -17,8 +17,8 @@ import {
17 close,
18 } from 'react-client/src/ReactFlightClientStream';
19
20 -function startReadingFromStream<T>(
21 - response: FlightResponse<T>,
20 +function startReadingFromStream(
21 + response: FlightResponse,
22 stream: ReadableStream,
23 ): void {
24 const reader = stream.getReader();
@@ -37,18 +37,16 @@ function startReadingFromStream<T>(
37 reader.read().then(progress, error);
38 }
39
40 -function createFromReadableStream<T>(
41 - stream: ReadableStream,
42 -): FlightResponse<T> {
43 - const response: FlightResponse<T> = createResponse();
40 +function createFromReadableStream(stream: ReadableStream): FlightResponse {
41 + const response: FlightResponse = createResponse();
42 startReadingFromStream(response, stream);
43 return response;
44 }
45
48 -function createFromFetch<T>(
46 +function createFromFetch(
47 promiseForResponse: Promise<Response>,
50 -): FlightResponse<T> {
51 - const response: FlightResponse<T> = createResponse();
48 +): FlightResponse {
49 + const response: FlightResponse = createResponse();
50 promiseForResponse.then(
51 function(r) {
52 startReadingFromStream(response, (r.body: any));
@@ -60,8 +58,8 @@ function createFromFetch<T>(
58 return response;
59 }
60
63 -function createFromXHR<T>(request: XMLHttpRequest): FlightResponse<T> {
64 - const response: FlightResponse<T> = createResponse();
61 +function createFromXHR(request: XMLHttpRequest): FlightResponse {
62 + const response: FlightResponse = createResponse();
63 let processedLength = 0;
64 function progress(e: ProgressEvent): void {
65 const chunk = request.responseText;
packages/react-flight-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+29 -4
@@ -286,7 +286,7 @@ describe('ReactFlightDOM', () => {
286 function Text({children}) {
287 return children;
288 }
289 - function makeDelayedText() {
289 + function makeDelayedTextBlock() {
290 let error, _resolve, _reject;
291 let promise = new Promise((resolve, reject) => {
292 _resolve = () => {
@@ -315,11 +315,36 @@ describe('ReactFlightDOM', () => {
315 return [loadBlock(), _resolve, _reject];
316 }
317
318 + function makeDelayedText() {
319 + let error, _resolve, _reject;
320 + let promise = new Promise((resolve, reject) => {
321 + _resolve = () => {
322 + promise = null;
323 + resolve();
324 + };
325 + _reject = e => {
326 + error = e;
327 + promise = null;
328 + reject(e);
329 + };
330 + });
331 + function DelayedText({children}, data) {
332 + if (promise) {
333 + throw promise;
334 + }
335 + if (error) {
336 + throw error;
337 + }
338 + return <Text>{children}</Text>;
339 + }
340 + return [DelayedText, _resolve, _reject];
341 + }
342 +
343 const [FriendsModel, resolveFriendsModel] = makeDelayedText();
344 const [NameModel, resolveNameModel] = makeDelayedText();
320 - const [PostsModel, resolvePostsModel] = makeDelayedText();
321 - const [PhotosModel, resolvePhotosModel] = makeDelayedText();
322 - const [GamesModel, , rejectGamesModel] = makeDelayedText();
345 + const [PostsModel, resolvePostsModel] = makeDelayedTextBlock();
346 + const [PhotosModel, resolvePhotosModel] = makeDelayedTextBlock();
347 + const [GamesModel, , rejectGamesModel] = makeDelayedTextBlock();
348 function ProfileMore() {
349 return {
350 avatar: <Text>:avatar:</Text>,
packages/react-noop-renderer/src/ReactNoopFlightClient.js
+3
@@ -29,6 +29,9 @@ const {createResponse, processStringChunk, close} = ReactFlightClient({
29 requireModule(idx: string) {
30 return readModule(idx);
31 },
32 + parseModel(response: Response, json) {
33 + return JSON.parse(json, response._fromJSON);
34 + },
35 });
36
37 function read<T>(source: Source): T {
packages/react-server/src/ReactFlightServer.js
+31 -3
@@ -237,9 +237,23 @@ export function resolveModelToJSON(
237 value.$$typeof === REACT_ELEMENT_TYPE
238 ) {
239 // TODO: Concatenate keys of parents onto children.
240 - // TODO: Allow elements to suspend independently and serialize as references to future elements.
240 const element: React$Element<any> = (value: any);
242 - value = attemptResolveElement(element);
241 + try {
242 + // Attempt to render the server component.
243 + value = attemptResolveElement(element);
244 + } catch (x) {
245 + if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
246 + // Something suspended, we'll need to create a new segment and resolve it later.
247 + request.pendingChunks++;
248 + const newSegment = createSegment(request, () => value);
249 + const ping = newSegment.ping;
250 + x.then(ping, ping);
251 + return serializeIDRef(newSegment.id);
252 + } else {
253 + // Something errored. Don't bother encoding anything up to here.
254 + throw x;
255 + }
256 + }
257 }
258
259 return value;
@@ -268,8 +282,22 @@ function emitErrorChunk(request: Request, id: number, error: mixed): void {
282
283 function retrySegment(request: Request, segment: Segment): void {
284 const query = segment.query;
285 + let value;
286 try {
272 - const value = query();
287 + value = query();
288 + while (
289 + typeof value === 'object' &&
290 + value !== null &&
291 + value.$$typeof === REACT_ELEMENT_TYPE
292 + ) {
293 + // TODO: Concatenate keys of parents onto children.
294 + const element: React$Element<any> = (value: any);
295 + // Attempt to render the server component.
296 + // Doing this here lets us reuse this same segment if the next component
297 + // also suspends.
298 + segment.query = () => value;
299 + value = attemptResolveElement(element);
300 + }
301 const processedChunk = processModelChunk(request, segment.id, value);
302 request.completedJSONChunks.push(processedChunk);
303 } catch (x) {
scripts/eslint/index.js
+5 -4
@@ -68,10 +68,11 @@ function runESLint({onlyChanged}) {
68 if (typeof onlyChanged !== 'boolean') {
69 throw new Error('Pass options.onlyChanged as a boolean.');
70 }
71 - const {errorCount, warningCount, output} = runESLintOnFilesWithOptions(
72 - allPaths,
73 - onlyChanged
74 - );
71 + const {
72 + errorCount,
73 + warningCount,
74 + output,
75 + } = runESLintOnFilesWithOptions(allPaths, onlyChanged, {fix: true});
76 console.log(output);
77 return errorCount === 0 && warningCount === 0;
78 }
scripts/shared/inlinedHostConfigs.js
+8 -1
@@ -27,6 +27,7 @@ module.exports = [
27 'react-flight-dom-webpack/server.node',
28 'react-flight-dom-webpack/server-runtime',
29 'react-flight-dom-webpack/src/ReactFlightDOMServerNode.js', // react-flight-dom-webpack/server.browser
30 + 'react-client/src/ReactFlightClientStream.js', // We can only type check this in streaming configurations.
31 'react-interactions',
32 ],
33 isFlowTyped: true,
@@ -51,6 +52,7 @@ module.exports = [
52 'react-flight-dom-webpack/server.browser',
53 'react-flight-dom-webpack/server-runtime',
54 'react-flight-dom-webpack/src/ReactFlightDOMServerBrowser.js', // react-flight-dom-webpack/server.browser
55 + 'react-client/src/ReactFlightClientStream.js', // We can only type check this in streaming configurations.
56 ],
57 isFlowTyped: true,
58 isServerSupported: true,
@@ -103,7 +105,12 @@ module.exports = [
105 'react-server/flight',
106 'react-server/flight-server-runtime',
107 ],
106 - paths: [],
108 + paths: [
109 + 'react-client/flight',
110 + 'react-server/flight',
111 + 'react-server/flight-server-runtime',
112 + 'react-client/src/ReactFlightClientStream.js', // We can only type check this in streaming configurations.
113 + ],
114 isFlowTyped: true,
115 isServerSupported: true,
116 },