1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and 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 {Thenable} from 'shared/ReactTypes';
11
+
12
+// The server acts as a Client of itself when resolving Server References.
13
+// That's why we import the Client configuration from the Server.
14
+// Everything is aliased as their Server equivalence for clarity.
15
+import type {
16
+ ServerReferenceId,
17
+ ServerManifest,
18
+ ClientReference as ServerReference,
19
+} from 'react-client/src/ReactFlightClientHostConfig';
20
+
21
+import {
22
+ resolveServerReference,
23
+ preloadModule,
24
+ requireModule,
25
+} from 'react-client/src/ReactFlightClientHostConfig';
26
+
27
+export type JSONValue =
28
+ | number
29
+ | null
30
+ | boolean
31
+ | string
32
+ | {+[key: string]: JSONValue}
33
+ | $ReadOnlyArray<JSONValue>;
34
+
35
+const PENDING = 'pending';
36
+const BLOCKED = 'blocked';
37
+const RESOLVED_MODEL = 'resolved_model';
38
+const INITIALIZED = 'fulfilled';
39
+const ERRORED = 'rejected';
40
+
41
+type PendingChunk<T> = {
42
+ status: 'pending',
43
+ value: null | Array<(T) => mixed>,
44
+ reason: null | Array<(mixed) => mixed>,
45
+ _response: Response,
46
+ then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
47
+};
48
+type BlockedChunk<T> = {
49
+ status: 'blocked',
50
+ value: null | Array<(T) => mixed>,
51
+ reason: null | Array<(mixed) => mixed>,
52
+ _response: Response,
53
+ then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
54
+};
55
+type ResolvedModelChunk<T> = {
56
+ status: 'resolved_model',
57
+ value: string,
58
+ reason: null,
59
+ _response: Response,
60
+ then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
61
+};
62
+type InitializedChunk<T> = {
63
+ status: 'fulfilled',
64
+ value: T,
65
+ reason: null,
66
+ _response: Response,
67
+ then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
68
+};
69
+type ErroredChunk<T> = {
70
+ status: 'rejected',
71
+ value: null,
72
+ reason: mixed,
73
+ _response: Response,
74
+ then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
75
+};
76
+type SomeChunk<T> =
77
+ | PendingChunk<T>
78
+ | BlockedChunk<T>
79
+ | ResolvedModelChunk<T>
80
+ | InitializedChunk<T>
81
+ | ErroredChunk<T>;
82
+
83
+// $FlowFixMe[missing-this-annot]
84
+function Chunk(status: any, value: any, reason: any, response: Response) {
85
+ this.status = status;
86
+ this.value = value;
87
+ this.reason = reason;
88
+ this._response = response;
89
+}
90
+// We subclass Promise.prototype so that we get other methods like .catch
91
+Chunk.prototype = (Object.create(Promise.prototype): any);
92
+// TODO: This doesn't return a new Promise chain unlike the real .then
93
+Chunk.prototype.then = function <T>(
94
+ this: SomeChunk<T>,
95
+ resolve: (value: T) => mixed,
96
+ reject: (reason: mixed) => mixed,
97
+) {
98
+ const chunk: SomeChunk<T> = this;
99
+ // If we have resolved content, we try to initialize it first which
100
+ // might put us back into one of the other states.
101
+ switch (chunk.status) {
102
+ case RESOLVED_MODEL:
103
+ initializeModelChunk(chunk);
104
+ break;
105
+ }
106
+ // The status might have changed after initialization.
107
+ switch (chunk.status) {
108
+ case INITIALIZED:
109
+ resolve(chunk.value);
110
+ break;
111
+ case PENDING:
112
+ case BLOCKED:
113
+ if (resolve) {
114
+ if (chunk.value === null) {
115
+ chunk.value = ([]: Array<(T) => mixed>);
116
+ }
117
+ chunk.value.push(resolve);
118
+ }
119
+ if (reject) {
120
+ if (chunk.reason === null) {
121
+ chunk.reason = ([]: Array<(mixed) => mixed>);
122
+ }
123
+ chunk.reason.push(reject);
124
+ }
125
+ break;
126
+ default:
127
+ reject(chunk.reason);
128
+ break;
129
+ }
130
+};
131
+
132
+export type Response = {
133
+ _bundlerConfig: ServerManifest,
134
+ _chunks: Map<number, SomeChunk<any>>,
135
+ _fromJSON: (key: string, value: JSONValue) => any,
136
+};
137
+
138
+export function getRoot<T>(response: Response): Thenable<T> {
139
+ const chunk = getChunk(response, 0);
140
+ return (chunk: any);
141
+}
142
+
143
+function createPendingChunk<T>(response: Response): PendingChunk<T> {
144
+ // $FlowFixMe Flow doesn't support functions as constructors
145
+ return new Chunk(PENDING, null, null, response);
146
+}
147
+
148
+function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void {
149
+ for (let i = 0; i < listeners.length; i++) {
150
+ const listener = listeners[i];
151
+ listener(value);
152
+ }
153
+}
154
+
155
+function wakeChunkIfInitialized<T>(
156
+ chunk: SomeChunk<T>,
157
+ resolveListeners: Array<(T) => mixed>,
158
+ rejectListeners: null | Array<(mixed) => mixed>,
159
+): void {
160
+ switch (chunk.status) {
161
+ case INITIALIZED:
162
+ wakeChunk(resolveListeners, chunk.value);
163
+ break;
164
+ case PENDING:
165
+ case BLOCKED:
166
+ chunk.value = resolveListeners;
167
+ chunk.reason = rejectListeners;
168
+ break;
169
+ case ERRORED:
170
+ if (rejectListeners) {
171
+ wakeChunk(rejectListeners, chunk.reason);
172
+ }
173
+ break;
174
+ }
175
+}
176
+
177
+function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: mixed): void {
178
+ if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
179
+ // We already resolved. We didn't expect to see this.
180
+ return;
181
+ }
182
+ const listeners = chunk.reason;
183
+ const erroredChunk: ErroredChunk<T> = (chunk: any);
184
+ erroredChunk.status = ERRORED;
185
+ erroredChunk.reason = error;
186
+ if (listeners !== null) {
187
+ wakeChunk(listeners, error);
188
+ }
189
+}
190
+
191
+function createResolvedModelChunk<T>(
192
+ response: Response,
193
+ value: string,
194
+): ResolvedModelChunk<T> {
195
+ // $FlowFixMe Flow doesn't support functions as constructors
196
+ return new Chunk(RESOLVED_MODEL, value, null, response);
197
+}
198
+
199
+function resolveModelChunk<T>(chunk: SomeChunk<T>, value: string): void {
200
+ if (chunk.status !== PENDING) {
201
+ // We already resolved. We didn't expect to see this.
202
+ return;
203
+ }
204
+ const resolveListeners = chunk.value;
205
+ const rejectListeners = chunk.reason;
206
+ const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
207
+ resolvedChunk.status = RESOLVED_MODEL;
208
+ resolvedChunk.value = value;
209
+ if (resolveListeners !== null) {
210
+ // This is unfortunate that we're reading this eagerly if
211
+ // we already have listeners attached since they might no
212
+ // longer be rendered or might not be the highest pri.
213
+ initializeModelChunk(resolvedChunk);
214
+ // The status might have changed after initialization.
215
+ wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
216
+ }
217
+}
218
+
219
+function bindArgs(fn: any, args: any) {
220
+ return fn.bind.apply(fn, [null].concat(args));
221
+}
222
+
223
+function loadServerReference<T>(
224
+ response: Response,
225
+ id: ServerReferenceId,
226
+ bound: null | Thenable<Array<any>>,
227
+ parentChunk: SomeChunk<T>,
228
+ parentObject: Object,
229
+ key: string,
230
+): T {
231
+ const serverReference: ServerReference<T> =
232
+ resolveServerReference<$FlowFixMe>(response._bundlerConfig, id);
233
+ // We expect most servers to not really need this because you'd just have all
234
+ // the relevant modules already loaded but it allows for lazy loading of code
235
+ // if needed.
236
+ const preloadPromise = preloadModule(serverReference);
237
+ let promise: Promise<T>;
238
+ if (bound) {
239
+ promise = Promise.all([(bound: any), preloadPromise]).then(
240
+ ([args]: Array<any>) => bindArgs(requireModule(serverReference), args),
241
+ );
242
+ } else {
243
+ if (preloadPromise) {
244
+ promise = Promise.resolve(preloadPromise).then(() =>
245
+ requireModule(serverReference),
246
+ );
247
+ } else {
248
+ // Synchronously available
249
+ return requireModule(serverReference);
250
+ }
251
+ }
252
+ promise.then(
253
+ createModelResolver(parentChunk, parentObject, key),
254
+ createModelReject(parentChunk),
255
+ );
256
+ // We need a placeholder value that will be replaced later.
257
+ return (null: any);
258
+}
259
+
260
+let initializingChunk: ResolvedModelChunk<any> = (null: any);
261
+let initializingChunkBlockedModel: null | {deps: number, value: any} = null;
262
+function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
263
+ const prevChunk = initializingChunk;
264
+ const prevBlocked = initializingChunkBlockedModel;
265
+ initializingChunk = chunk;
266
+ initializingChunkBlockedModel = null;
267
+ try {
268
+ const value: T = JSON.parse(chunk.value, chunk._response._fromJSON);
269
+ if (
270
+ initializingChunkBlockedModel !== null &&
271
+ initializingChunkBlockedModel.deps > 0
272
+ ) {
273
+ initializingChunkBlockedModel.value = value;
274
+ // We discovered new dependencies on modules that are not yet resolved.
275
+ // We have to go the BLOCKED state until they're resolved.
276
+ const blockedChunk: BlockedChunk<T> = (chunk: any);
277
+ blockedChunk.status = BLOCKED;
278
+ blockedChunk.value = null;
279
+ blockedChunk.reason = null;
280
+ } else {
281
+ const initializedChunk: InitializedChunk<T> = (chunk: any);
282
+ initializedChunk.status = INITIALIZED;
283
+ initializedChunk.value = value;
284
+ }
285
+ } catch (error) {
286
+ const erroredChunk: ErroredChunk<T> = (chunk: any);
287
+ erroredChunk.status = ERRORED;
288
+ erroredChunk.reason = error;
289
+ } finally {
290
+ initializingChunk = prevChunk;
291
+ initializingChunkBlockedModel = prevBlocked;
292
+ }
293
+}
294
+
295
+// Report that any missing chunks in the model is now going to throw this
296
+// error upon read. Also notify any pending promises.
297
+export function reportGlobalError(response: Response, error: Error): void {
298
+ response._chunks.forEach(chunk => {
299
+ // If this chunk was already resolved or errored, it won't
300
+ // trigger an error but if it wasn't then we need to
301
+ // because we won't be getting any new data to resolve it.
302
+ if (chunk.status === PENDING) {
303
+ triggerErrorOnChunk(chunk, error);
304
+ }
305
+ });
306
+}
307
+
308
+function getChunk(response: Response, id: number): SomeChunk<any> {
309
+ const chunks = response._chunks;
310
+ let chunk = chunks.get(id);
311
+ if (!chunk) {
312
+ chunk = createPendingChunk(response);
313
+ chunks.set(id, chunk);
314
+ }
315
+ return chunk;
316
+}
317
+
318
+function createModelResolver<T>(
319
+ chunk: SomeChunk<T>,
320
+ parentObject: Object,
321
+ key: string,
322
+): (value: any) => void {
323
+ let blocked;
324
+ if (initializingChunkBlockedModel) {
325
+ blocked = initializingChunkBlockedModel;
326
+ blocked.deps++;
327
+ } else {
328
+ blocked = initializingChunkBlockedModel = {
329
+ deps: 1,
330
+ value: null,
331
+ };
332
+ }
333
+ return value => {
334
+ parentObject[key] = value;
335
+ blocked.deps--;
336
+ if (blocked.deps === 0) {
337
+ if (chunk.status !== BLOCKED) {
338
+ return;
339
+ }
340
+ const resolveListeners = chunk.value;
341
+ const initializedChunk: InitializedChunk<T> = (chunk: any);
342
+ initializedChunk.status = INITIALIZED;
343
+ initializedChunk.value = blocked.value;
344
+ if (resolveListeners !== null) {
345
+ wakeChunk(resolveListeners, blocked.value);
346
+ }
347
+ }
348
+ };
349
+}
350
+
351
+function createModelReject<T>(chunk: SomeChunk<T>): (error: mixed) => void {
352
+ return (error: mixed) => triggerErrorOnChunk(chunk, error);
353
+}
354
+
355
+function parseModelString(
356
+ response: Response,
357
+ parentObject: Object,
358
+ key: string,
359
+ value: string,
360
+): any {
361
+ if (value[0] === '$') {
362
+ switch (value[1]) {
363
+ case '$': {
364
+ // This was an escaped string value.
365
+ return value.substring(1);
366
+ }
367
+ case '@': {
368
+ // Promise
369
+ const id = parseInt(value.substring(2), 16);
370
+ const chunk = getChunk(response, id);
371
+ return chunk;
372
+ }
373
+ case 'S': {
374
+ // Symbol
375
+ return Symbol.for(value.substring(2));
376
+ }
377
+ case 'F': {
378
+ // Server Reference
379
+ const id = parseInt(value.substring(2), 16);
380
+ const chunk = getChunk(response, id);
381
+ if (chunk.status === RESOLVED_MODEL) {
382
+ initializeModelChunk(chunk);
383
+ }
384
+ if (chunk.status !== INITIALIZED) {
385
+ // We know that this is emitted earlier so otherwise it's an error.
386
+ throw chunk.reason;
387
+ }
388
+ // TODO: Just encode this in the reference inline instead of as a model.
389
+ const metaData: {id: ServerReferenceId, bound: Thenable<Array<any>>} =
390
+ chunk.value;
391
+ return loadServerReference(
392
+ response,
393
+ metaData.id,
394
+ metaData.bound,
395
+ initializingChunk,
396
+ parentObject,
397
+ key,
398
+ );
399
+ }
400
+ default: {
401
+ // We assume that anything else is a reference ID.
402
+ const id = parseInt(value.substring(1), 16);
403
+ const chunk = getChunk(response, id);
404
+ switch (chunk.status) {
405
+ case RESOLVED_MODEL:
406
+ initializeModelChunk(chunk);
407
+ break;
408
+ }
409
+ // The status might have changed after initialization.
410
+ switch (chunk.status) {
411
+ case INITIALIZED:
412
+ return chunk.value;
413
+ case PENDING:
414
+ case BLOCKED:
415
+ const parentChunk = initializingChunk;
416
+ chunk.then(
417
+ createModelResolver(parentChunk, parentObject, key),
418
+ createModelReject(parentChunk),
419
+ );
420
+ return null;
421
+ default:
422
+ throw chunk.reason;
423
+ }
424
+ }
425
+ }
426
+ }
427
+ return value;
428
+}
429
+
430
+export function createResponse(bundlerConfig: ServerManifest): Response {
431
+ const chunks: Map<number, SomeChunk<any>> = new Map();
432
+ const response: Response = {
433
+ _bundlerConfig: bundlerConfig,
434
+ _chunks: chunks,
435
+ _fromJSON: function (this: any, key: string, value: JSONValue) {
436
+ if (typeof value === 'string') {
437
+ // We can't use .bind here because we need the "this" value.
438
+ return parseModelString(response, this, key, value);
439
+ }
440
+ return value;
441
+ },
442
+ };
443
+ return response;
444
+}
445
+
446
+export function resolveField(
447
+ response: Response,
448
+ id: number,
449
+ model: string,
450
+): void {
451
+ const chunks = response._chunks;
452
+ const chunk = chunks.get(id);
453
+ if (!chunk) {
454
+ chunks.set(id, createResolvedModelChunk(response, model));
455
+ } else {
456
+ resolveModelChunk(chunk, model);
457
+ }
458
+}
459
+
460
+export function resolveFile(response: Response, id: number, file: File): void {
461
+ throw new Error('Not implemented.');
462
+}
463
+
464
+export opaque type FileHandle = {};
465
+
466
+export function resolveFileInfo(
467
+ response: Response,
468
+ id: number,
469
+ filename: string,
470
+ mime: string,
471
+): FileHandle {
472
+ throw new Error('Not implemented.');
473
+}
474
+
475
+export function resolveFileChunk(
476
+ response: Response,
477
+ handle: FileHandle,
478
+ chunk: Uint8Array,
479
+): void {
480
+ throw new Error('Not implemented.');
481
+}
482
+
483
+export function resolveFileComplete(
484
+ response: Response,
485
+ handle: FileHandle,
486
+): void {
487
+ throw new Error('Not implemented.');
488
+}
489
+
490
+export function close(response: Response): void {
491
+ // In case there are any remaining unresolved chunks, they won't
492
+ // be resolved now. So we need to issue an error to those.
493
+ // Ideally we should be able to early bail out if we kept a
494
+ // ref count of pending chunks.
495
+ reportGlobalError(response, new Error('Connection closed.'));
496
+}