Skip to content

Commit e2dd308

Browse files
authored
[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".
1 parent 59fd09c commit e2dd308

14 files changed

Lines changed: 350 additions & 213 deletions

packages/react-client/src/ReactFlightClient.js

Lines changed: 141 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ import type {LazyComponent} from 'react/src/ReactLazy';
1414
import type {
1515
ModuleReference,
1616
ModuleMetaData,
17+
UninitializedModel,
18+
Response,
1719
} from './ReactFlightClientHostConfig';
1820

1921
import {
2022
resolveModuleReference,
2123
preloadModule,
2224
requireModule,
25+
parseModel,
2326
} from './ReactFlightClientHostConfig';
2427

2528
import {
@@ -33,33 +36,48 @@ export type JSONValue =
3336
| null
3437
| boolean
3538
| string
36-
| {[key: string]: JSONValue}
37-
| Array<JSONValue>;
39+
| {+[key: string]: JSONValue}
40+
| $ReadOnlyArray<JSONValue>;
3841

3942
const PENDING = 0;
40-
const RESOLVED = 1;
41-
const ERRORED = 2;
43+
const RESOLVED_MODEL = 1;
44+
const INITIALIZED = 2;
45+
const ERRORED = 3;
4246

4347
type PendingChunk = {
4448
_status: 0,
4549
_value: null | Array<() => mixed>,
50+
_response: Response,
4651
then(resolve: () => mixed): void,
4752
};
48-
type ResolvedChunk<T> = {
53+
type ResolvedModelChunk = {
4954
_status: 1,
55+
_value: UninitializedModel,
56+
_response: Response,
57+
then(resolve: () => mixed): void,
58+
};
59+
type InitializedChunk<T> = {
60+
_status: 2,
5061
_value: T,
62+
_response: Response,
5163
then(resolve: () => mixed): void,
5264
};
5365
type ErroredChunk = {
54-
_status: 2,
66+
_status: 3,
5567
_value: Error,
68+
_response: Response,
5669
then(resolve: () => mixed): void,
5770
};
58-
type SomeChunk<T> = PendingChunk | ResolvedChunk<T> | ErroredChunk;
71+
type SomeChunk<T> =
72+
| PendingChunk
73+
| ResolvedModelChunk
74+
| InitializedChunk<T>
75+
| ErroredChunk;
5976

60-
function Chunk(status: any, value: any) {
77+
function Chunk(status: any, value: any, response: Response) {
6178
this._status = status;
6279
this._value = value;
80+
this._response = response;
6381
}
6482
Chunk.prototype.then = function<T>(resolve: () => mixed) {
6583
const chunk: SomeChunk<T> = this;
@@ -73,45 +91,40 @@ Chunk.prototype.then = function<T>(resolve: () => mixed) {
7391
}
7492
};
7593

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+
...
8198
};
8299

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;
93113
}
94114
}
95115

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);
107120
}
108121

109-
function createPendingChunk(): PendingChunk {
110-
return new Chunk(PENDING, null);
122+
function createPendingChunk(response: Response): PendingChunk {
123+
return new Chunk(PENDING, null, response);
111124
}
112125

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);
115128
}
116129

117130
function wakeChunk(listeners: null | Array<() => mixed>) {
@@ -135,29 +148,40 @@ function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: Error): void {
135148
wakeChunk(listeners);
136149
}
137150

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);
140156
}
141157

142-
function resolveChunk<T>(chunk: SomeChunk<T>, value: T): void {
158+
function resolveModelChunk<T>(
159+
chunk: SomeChunk<T>,
160+
value: UninitializedModel,
161+
): void {
143162
if (chunk._status !== PENDING) {
144163
// We already resolved. We didn't expect to see this.
145164
return;
146165
}
147166
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;
150169
resolvedChunk._value = value;
151170
wakeChunk(listeners);
152171
}
153172

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+
154181
// Report that any missing chunks in the model is now going to throw this
155182
// 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 => {
161185
// If this chunk was already resolved or errored, it won't
162186
// trigger an error but if it wasn't then we need to
163187
// because we won't be getting any new data to resolve it.
@@ -171,14 +195,7 @@ function readMaybeChunk<T>(maybeChunk: SomeChunk<T> | T): T {
171195
return maybeChunk;
172196
}
173197
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);
182199
}
183200

184201
function createElement(type, key, props): React$Element<any> {
@@ -226,6 +243,7 @@ type UninitializedBlockPayload<Data> = [
226243
mixed,
227244
ModuleMetaData | SomeChunk<ModuleMetaData>,
228245
Data | SomeChunk<Data>,
246+
Response,
229247
];
230248

231249
function initializeBlock<Props, Data>(
@@ -267,83 +285,102 @@ function createLazyBlock<Props, Data>(
267285
return lazyType;
268286
}
269287

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.
291314
return chunk;
292315
}
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);
296319
}
297320
}
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;
311323
}
312324
return value;
313325
}
314326

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,
317354
id: number,
318-
model: M,
355+
model: UninitializedModel,
319356
): void {
320-
const chunks = response.chunks;
357+
const chunks = response._chunks;
321358
const chunk = chunks.get(id);
322359
if (!chunk) {
323-
chunks.set(id, createResolvedChunk(model));
360+
chunks.set(id, createResolvedModelChunk(response, model));
324361
} else {
325-
resolveChunk(chunk, model);
362+
resolveModelChunk(chunk, model);
326363
}
327364
}
328365

329-
export function resolveErrorChunk<T>(
330-
response: Response<T>,
366+
export function resolveError(
367+
response: Response,
331368
id: number,
332369
message: string,
333370
stack: string,
334371
): void {
335372
const error = new Error(message);
336373
error.stack = stack;
337-
const chunks = response.chunks;
374+
const chunks = response._chunks;
338375
const chunk = chunks.get(id);
339376
if (!chunk) {
340-
chunks.set(id, createErrorChunk(error));
377+
chunks.set(id, createErrorChunk(response, error));
341378
} else {
342379
triggerErrorOnChunk(chunk, error);
343380
}
344381
}
345382

346-
export function close<T>(response: Response<T>): void {
383+
export function close(response: Response): void {
347384
// In case there are any remaining unresolved chunks, they won't
348385
// be resolved now. So we need to issue an error to those.
349386
// Ideally we should be able to early bail out if we kept a
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)