7
* @flow
8
*/
9
10
-import type {Wakeable} from 'shared/ReactTypes';
10
+import type {Thenable} from 'shared/ReactTypes';
11
import type {LazyComponent} from 'react/src/ReactLazy';
12
13
import type {
37
| {+[key: string]: JSONValue}
38
| $ReadOnlyArray<JSONValue>;
39
40
-const PENDING = 0;
41
-const RESOLVED_MODEL = 1;
42
-const RESOLVED_MODULE = 2;
43
-const INITIALIZED = 3;
44
-const ERRORED = 4;
45
-
46
-type PendingChunk = {
47
- _status: 0,
48
- _value: null | Array<() => mixed>,
40
+const PENDING = 'pending';
41
+const BLOCKED = 'blocked';
42
+const RESOLVED_MODEL = 'resolved_model';
43
+const RESOLVED_MODULE = 'resolved_module';
44
+const INITIALIZED = 'fulfilled';
45
+const ERRORED = 'rejected';
46
+
47
+type PendingChunk<T> = {
48
+ status: 'pending',
49
+ value: null | Array<(T) => mixed>,
50
+ reason: null | Array<(mixed) => mixed>,
51
+ _response: Response,
52
+ then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
53
+};
54
+type BlockedChunk<T> = {
55
+ status: 'blocked',
56
+ value: null | Array<(T) => mixed>,
57
+ reason: null | Array<(mixed) => mixed>,
58
_response: Response,
50
- then(resolve: () => mixed): void,
59
+ then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
60
};
52
-type ResolvedModelChunk = {
53
- _status: 1,
54
- _value: UninitializedModel,
61
+type ResolvedModelChunk<T> = {
62
+ status: 'resolved_model',
63
+ value: UninitializedModel,
64
+ reason: null,
65
_response: Response,
56
- then(resolve: () => mixed): void,
66
+ then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
67
};
68
type ResolvedModuleChunk<T> = {
59
- _status: 2,
60
- _value: ModuleReference<T>,
69
+ status: 'resolved_module',
70
+ value: ModuleReference<T>,
71
+ reason: null,
72
_response: Response,
62
- then(resolve: () => mixed): void,
73
+ then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
74
};
75
type InitializedChunk<T> = {
65
- _status: 3,
66
- _value: T,
76
+ status: 'fulfilled',
77
+ value: T,
78
+ reason: null,
79
_response: Response,
68
- then(resolve: () => mixed): void,
80
+ then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
81
};
70
-type ErroredChunk = {
71
- _status: 4,
72
- _value: Error,
82
+type ErroredChunk<T> = {
83
+ status: 'rejected',
84
+ value: null,
85
+ reason: mixed,
86
_response: Response,
74
- then(resolve: () => mixed): void,
87
+ then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
88
};
89
type SomeChunk<T> =
77
- | PendingChunk
78
- | ResolvedModelChunk
90
+ | PendingChunk<T>
91
+ | BlockedChunk<T>
92
+ | ResolvedModelChunk<T>
93
| ResolvedModuleChunk<T>
94
| InitializedChunk<T>
81
- | ErroredChunk;
95
+ | ErroredChunk<T>;
96
83
-function Chunk(status: any, value: any, response: Response) {
84
- this._status = status;
85
- this._value = value;
97
+function Chunk(status: any, value: any, reason: any, response: Response) {
98
+ this.status = status;
99
+ this.value = value;
100
+ this.reason = reason;
101
this._response = response;
102
}
88
-Chunk.prototype.then = function<T>(resolve: () => mixed) {
103
+// We subclass Promise.prototype so that we get other methods like .catch
104
+Chunk.prototype = (Object.create(Promise.prototype): any);
105
+// TODO: This doesn't return a new Promise chain unlike the real .then
106
+Chunk.prototype.then = function<T>(
107
+ resolve: (value: T) => mixed,
108
+ reject: (reason: mixed) => mixed,
109
+) {
110
const chunk: SomeChunk<T> = this;
90
- if (chunk._status === PENDING) {
91
- if (chunk._value === null) {
92
- chunk._value = [];
93
- }
94
- chunk._value.push(resolve);
95
- } else {
96
- resolve();
111
+ // If we have resolved content, we try to initialize it first which
112
+ // might put us back into one of the other states.
113
+ switch (chunk.status) {
114
+ case RESOLVED_MODEL:
115
+ initializeModelChunk(chunk);
116
+ break;
117
+ case RESOLVED_MODULE:
118
+ initializeModuleChunk(chunk);
119
+ break;
120
+ }
121
+ // The status might have changed after initialization.
122
+ switch (chunk.status) {
123
+ case INITIALIZED:
124
+ resolve(chunk.value);
125
+ break;
126
+ case PENDING:
127
+ case BLOCKED:
128
+ if (resolve) {
129
+ if (chunk.value === null) {
130
+ chunk.value = [];
131
+ }
132
+ chunk.value.push(resolve);
133
+ }
134
+ if (reject) {
135
+ if (chunk.reason === null) {
136
+ chunk.reason = [];
137
+ }
138
+ chunk.reason.push(reject);
139
+ }
140
+ break;
141
+ default:
142
+ reject(chunk.reason);
143
+ break;
144
}
145
};
146
154
export type {Response};
155
156
function readChunk<T>(chunk: SomeChunk<T>): T {
110
- switch (chunk._status) {
111
- case INITIALIZED:
112
- return chunk._value;
157
+ // If we have resolved content, we try to initialize it first which
158
+ // might put us back into one of the other states.
159
+ switch (chunk.status) {
160
case RESOLVED_MODEL:
114
- return initializeModelChunk(chunk);
161
+ initializeModelChunk(chunk);
162
+ break;
163
case RESOLVED_MODULE:
116
- return initializeModuleChunk(chunk);
164
+ initializeModuleChunk(chunk);
165
+ break;
166
+ }
167
+ // The status might have changed after initialization.
168
+ switch (chunk.status) {
169
+ case INITIALIZED:
170
+ return chunk.value;
171
case PENDING:
172
+ case BLOCKED:
173
// eslint-disable-next-line no-throw-literal
119
- throw (chunk: Wakeable);
174
+ throw ((chunk: any): Thenable<T>);
175
default:
121
- throw chunk._value;
176
+ throw chunk.reason;
177
}
178
}
179
183
return readChunk(chunk);
184
}
185
131
-function createPendingChunk(response: Response): PendingChunk {
186
+function createPendingChunk<T>(response: Response): PendingChunk<T> {
187
+ // $FlowFixMe Flow doesn't support functions as constructors
188
+ return new Chunk(PENDING, null, null, response);
189
+}
190
+
191
+function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
192
// $FlowFixMe Flow doesn't support functions as constructors
133
- return new Chunk(PENDING, null, response);
193
+ return new Chunk(BLOCKED, null, null, response);
194
}
195
136
-function createErrorChunk(response: Response, error: Error): ErroredChunk {
196
+function createErrorChunk<T>(
197
+ response: Response,
198
+ error: Error,
199
+): ErroredChunk<T> {
200
// $FlowFixMe Flow doesn't support functions as constructors
138
- return new Chunk(ERRORED, error, response);
201
+ return new Chunk(ERRORED, null, error, response);
202
}
203
204
function createInitializedChunk<T>(
206
value: T,
207
): InitializedChunk<T> {
208
// $FlowFixMe Flow doesn't support functions as constructors
146
- return new Chunk(INITIALIZED, value, response);
209
+ return new Chunk(INITIALIZED, value, null, response);
210
}
211
149
-function wakeChunk(listeners: null | Array<() => mixed>) {
150
- if (listeners !== null) {
151
- for (let i = 0; i < listeners.length; i++) {
152
- const listener = listeners[i];
153
- listener();
154
- }
212
+function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void {
213
+ for (let i = 0; i < listeners.length; i++) {
214
+ const listener = listeners[i];
215
+ listener(value);
216
}
217
}
218
158
-function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: Error): void {
159
- if (chunk._status !== PENDING) {
219
+function wakeChunkIfInitialized<T>(
220
+ chunk: SomeChunk<T>,
221
+ resolveListeners: Array<(T) => mixed>,
222
+ rejectListeners: null | Array<(mixed) => mixed>,
223
+): void {
224
+ switch (chunk.status) {
225
+ case INITIALIZED:
226
+ wakeChunk(resolveListeners, chunk.value);
227
+ break;
228
+ case PENDING:
229
+ case BLOCKED:
230
+ chunk.value = resolveListeners;
231
+ chunk.reason = rejectListeners;
232
+ break;
233
+ case ERRORED:
234
+ if (rejectListeners) {
235
+ wakeChunk(rejectListeners, chunk.reason);
236
+ }
237
+ break;
238
+ }
239
+}
240
+
241
+function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: mixed): void {
242
+ if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
243
// We already resolved. We didn't expect to see this.
244
return;
245
}
163
- const listeners = chunk._value;
164
- const erroredChunk: ErroredChunk = (chunk: any);
165
- erroredChunk._status = ERRORED;
166
- erroredChunk._value = error;
167
- wakeChunk(listeners);
246
+ const listeners = chunk.reason;
247
+ const erroredChunk: ErroredChunk<T> = (chunk: any);
248
+ erroredChunk.status = ERRORED;
249
+ erroredChunk.reason = error;
250
+ if (listeners !== null) {
251
+ wakeChunk(listeners, error);
252
+ }
253
}
254
170
-function createResolvedModelChunk(
255
+function createResolvedModelChunk<T>(
256
response: Response,
257
value: UninitializedModel,
173
-): ResolvedModelChunk {
258
+): ResolvedModelChunk<T> {
259
// $FlowFixMe Flow doesn't support functions as constructors
175
- return new Chunk(RESOLVED_MODEL, value, response);
260
+ return new Chunk(RESOLVED_MODEL, value, null, response);
261
}
262
263
function createResolvedModuleChunk<T>(
265
value: ModuleReference<T>,
266
): ResolvedModuleChunk<T> {
267
// $FlowFixMe Flow doesn't support functions as constructors
183
- return new Chunk(RESOLVED_MODULE, value, response);
268
+ return new Chunk(RESOLVED_MODULE, value, null, response);
269
}
270
271
function resolveModelChunk<T>(
272
chunk: SomeChunk<T>,
273
value: UninitializedModel,
274
): void {
190
- if (chunk._status !== PENDING) {
275
+ if (chunk.status !== PENDING) {
276
// We already resolved. We didn't expect to see this.
277
return;
278
}
194
- const listeners = chunk._value;
195
- const resolvedChunk: ResolvedModelChunk = (chunk: any);
196
- resolvedChunk._status = RESOLVED_MODEL;
197
- resolvedChunk._value = value;
198
- wakeChunk(listeners);
279
+ const resolveListeners = chunk.value;
280
+ const rejectListeners = chunk.reason;
281
+ const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
282
+ resolvedChunk.status = RESOLVED_MODEL;
283
+ resolvedChunk.value = value;
284
+ if (resolveListeners !== null) {
285
+ // This is unfortunate that we're reading this eagerly if
286
+ // we already have listeners attached since they might no
287
+ // longer be rendered or might not be the highest pri.
288
+ initializeModelChunk(resolvedChunk);
289
+ // The status might have changed after initialization.
290
+ wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
291
+ }
292
}
293
294
function resolveModuleChunk<T>(
295
chunk: SomeChunk<T>,
296
value: ModuleReference<T>,
297
): void {
205
- if (chunk._status !== PENDING) {
298
+ if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
299
// We already resolved. We didn't expect to see this.
300
return;
301
}
209
- const listeners = chunk._value;
302
+ const resolveListeners = chunk.value;
303
+ const rejectListeners = chunk.reason;
304
const resolvedChunk: ResolvedModuleChunk<T> = (chunk: any);
211
- resolvedChunk._status = RESOLVED_MODULE;
212
- resolvedChunk._value = value;
213
- wakeChunk(listeners);
305
+ resolvedChunk.status = RESOLVED_MODULE;
306
+ resolvedChunk.value = value;
307
+ if (resolveListeners !== null) {
308
+ initializeModuleChunk(resolvedChunk);
309
+ wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
310
+ }
311
}
312
216
-function initializeModelChunk<T>(chunk: ResolvedModelChunk): T {
217
- const value: T = parseModel(chunk._response, chunk._value);
218
- const initializedChunk: InitializedChunk<T> = (chunk: any);
219
- initializedChunk._status = INITIALIZED;
220
- initializedChunk._value = value;
221
- return value;
313
+let initializingChunk: ResolvedModelChunk<any> = (null: any);
314
+let initializingChunkBlockedModel: null | {deps: number, value: any} = null;
315
+function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
316
+ const prevChunk = initializingChunk;
317
+ const prevBlocked = initializingChunkBlockedModel;
318
+ initializingChunk = chunk;
319
+ initializingChunkBlockedModel = null;
320
+ try {
321
+ const value: T = parseModel(chunk._response, chunk.value);
322
+ if (
323
+ initializingChunkBlockedModel !== null &&
324
+ initializingChunkBlockedModel.deps > 0
325
+ ) {
326
+ initializingChunkBlockedModel.value = value;
327
+ // We discovered new dependencies on modules that are not yet resolved.
328
+ // We have to go the BLOCKED state until they're resolved.
329
+ const blockedChunk: BlockedChunk<T> = (chunk: any);
330
+ blockedChunk.status = BLOCKED;
331
+ blockedChunk.value = null;
332
+ blockedChunk.reason = null;
333
+ } else {
334
+ const initializedChunk: InitializedChunk<T> = (chunk: any);
335
+ initializedChunk.status = INITIALIZED;
336
+ initializedChunk.value = value;
337
+ }
338
+ } catch (error) {
339
+ const erroredChunk: ErroredChunk<T> = (chunk: any);
340
+ erroredChunk.status = ERRORED;
341
+ erroredChunk.reason = error;
342
+ } finally {
343
+ initializingChunk = prevChunk;
344
+ initializingChunkBlockedModel = prevBlocked;
345
+ }
346
}
347
224
-function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): T {
225
- const value: T = requireModule(chunk._value);
226
- const initializedChunk: InitializedChunk<T> = (chunk: any);
227
- initializedChunk._status = INITIALIZED;
228
- initializedChunk._value = value;
229
- return value;
348
+function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): void {
349
+ try {
350
+ const value: T = requireModule(chunk.value);
351
+ const initializedChunk: InitializedChunk<T> = (chunk: any);
352
+ initializedChunk.status = INITIALIZED;
353
+ initializedChunk.value = value;
354
+ } catch (error) {
355
+ const erroredChunk: ErroredChunk<T> = (chunk: any);
356
+ erroredChunk.status = ERRORED;
357
+ erroredChunk.reason = error;
358
+ }
359
}
360
361
// Report that any missing chunks in the model is now going to throw this
365
// If this chunk was already resolved or errored, it won't
366
// trigger an error but if it wasn't then we need to
367
// because we won't be getting any new data to resolve it.
239
- triggerErrorOnChunk(chunk, error);
368
+ if (chunk.status === PENDING) {
369
+ triggerErrorOnChunk(chunk, error);
370
+ }
371
});
372
}
373
433
return chunk;
434
}
435
436
+function createModelResolver<T>(
437
+ chunk: SomeChunk<T>,
438
+ parentObject: Object,
439
+ key: string,
440
+) {
441
+ let blocked;
442
+ if (initializingChunkBlockedModel) {
443
+ blocked = initializingChunkBlockedModel;
444
+ blocked.deps++;
445
+ } else {
446
+ blocked = initializingChunkBlockedModel = {
447
+ deps: 1,
448
+ value: null,
449
+ };
450
+ }
451
+ return value => {
452
+ parentObject[key] = value;
453
+ blocked.deps--;
454
+ if (blocked.deps === 0) {
455
+ if (chunk.status !== BLOCKED) {
456
+ return;
457
+ }
458
+ const resolveListeners = chunk.value;
459
+ const initializedChunk: InitializedChunk<T> = (chunk: any);
460
+ initializedChunk.status = INITIALIZED;
461
+ initializedChunk.value = blocked.value;
462
+ if (resolveListeners !== null) {
463
+ wakeChunk(resolveListeners, blocked.value);
464
+ }
465
+ }
466
+ };
467
+}
468
+
469
+function createModelReject<T>(chunk: SomeChunk<T>) {
470
+ return error => triggerErrorOnChunk(chunk, error);
471
+}
472
+
473
export function parseModelString(
474
response: Response,
475
parentObject: Object,
476
+ key: string,
477
value: string,
478
): any {
479
switch (value[0]) {
486
} else {
487
const id = parseInt(value.substring(1), 16);
488
const chunk = getChunk(response, id);
320
- return readChunk(chunk);
489
+ switch (chunk.status) {
490
+ case RESOLVED_MODEL:
491
+ initializeModelChunk(chunk);
492
+ break;
493
+ case RESOLVED_MODULE:
494
+ initializeModuleChunk(chunk);
495
+ break;
496
+ }
497
+ // The status might have changed after initialization.
498
+ switch (chunk.status) {
499
+ case INITIALIZED:
500
+ return chunk.value;
501
+ case PENDING:
502
+ case BLOCKED:
503
+ const parentChunk = initializingChunk;
504
+ chunk.then(
505
+ createModelResolver(parentChunk, parentObject, key),
506
+ createModelReject(parentChunk),
507
+ );
508
+ return null;
509
+ default:
510
+ throw chunk.reason;
511
+ }
512
}
513
}
514
case '@': {
591
// TODO: Add an option to encode modules that are lazy loaded.
592
// For now we preload all modules as early as possible since it's likely
593
// that we'll need them.
403
- preloadModule(moduleReference);
404
-
405
- if (!chunk) {
406
- chunks.set(id, createResolvedModuleChunk(response, moduleReference));
594
+ const promise = preloadModule(moduleReference);
595
+ if (promise) {
596
+ let blockedChunk: BlockedChunk<any>;
597
+ if (!chunk) {
598
+ // Technically, we should just treat promise as the chunk in this
599
+ // case. Because it'll just behave as any other promise.
600
+ blockedChunk = createBlockedChunk(response);
601
+ chunks.set(id, blockedChunk);
602
+ } else {
603
+ // This can't actually happen because we don't have any forward
604
+ // references to modules.
605
+ blockedChunk = (chunk: any);
606
+ blockedChunk.status = BLOCKED;
607
+ }
608
+ promise.then(
609
+ () => resolveModuleChunk(blockedChunk, moduleReference),
610
+ error => triggerErrorOnChunk(blockedChunk, error),
611
+ );
612
} else {
408
- resolveModuleChunk(chunk, moduleReference);
613
+ if (!chunk) {
614
+ chunks.set(id, createResolvedModuleChunk(response, moduleReference));
615
+ } else {
616
+ // This can't actually happen because we don't have any forward
617
+ // references to modules.
618
+ resolveModuleChunk(chunk, moduleReference);
619
+ }
620
}
621
}
622