[Flight Reply] Resolve outlined models async in Reply just like in Flight Client (#28988)
This is the same change as #28780 but for the Flight Reply receiver. While it's not possible to create an "async module" reference in this case - resolving a server reference can still be async if loading it requires loading chunks like in a new server instance. Since extracting a typed array from a Blob is async, that's also a case where a dependency can be async.
Sebastian Markbåge committed
May 7, 2024 at 22:19 UTC
ec15267a001086deb4ab5412d3f8b7e13573d6a5
4 files changed
+154
-43
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+41
@@ -1130,6 +1130,47 @@ describe('ReactFlightDOMBrowser', () => {
1130
expect(result).toBe('Hello world');
1131
});
1132
1133
+ it('can pass an async server exports that resolves later to an outline object like a Map', async () => {
1134
+ let resolve;
1135
+ const chunkPromise = new Promise(r => (resolve = r));
1136
+
1137
+ function action() {}
1138
+ const serverModule = serverExports(
1139
+ {
1140
+ action: action,
1141
+ },
1142
+ chunkPromise,
1143
+ );
1144
+
1145
+ // Send the action to the client
1146
+ const stream = ReactServerDOMServer.renderToReadableStream(
1147
+ {action: serverModule.action},
1148
+ webpackMap,
1149
+ );
1150
+ const response =
1151
+ await ReactServerDOMClient.createFromReadableStream(stream);
1152
+
1153
+ // Pass the action back to the server inside a Map
1154
+
1155
+ const map = new Map();
1156
+ map.set('action', response.action);
1157
+
1158
+ const body = await ReactServerDOMClient.encodeReply(map);
1159
+ const resultPromise = ReactServerDOMServer.decodeReply(
1160
+ body,
1161
+ webpackServerMap,
1162
+ );
1163
+
1164
+ // We couldn't yet resolve the server reference because we haven't loaded
1165
+ // its chunk yet in the new server instance. We now resolve it which loads
1166
+ // it asynchronously.
1167
+ await resolve();
1168
+
1169
+ const result = await resultPromise;
1170
+ expect(result instanceof Map).toBe(true);
1171
+ expect(result.get('action')).toBe(action);
1172
+ });
1173
+
1174
it('supports Float hints before the first await in server components in Fiber', async () => {
1175
function Component() {
1176
return <p>hello world</p>;
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js
+17
@@ -85,6 +85,23 @@ describe('ReactFlightDOMReplyEdge', () => {
85
expect(new Uint8Array(result[0])).toEqual(new Uint8Array(buffers[0]));
86
});
87
88
+ // @gate enableBinaryFlight
89
+ it('should be able to serialize a typed array inside a Map', async () => {
90
+ const array = new Uint8Array([
91
+ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
92
+ ]);
93
+ const map = new Map();
94
+ map.set('array', array);
95
+
96
+ const body = await ReactServerDOMClient.encodeReply(map);
97
+ const result = await ReactServerDOMServer.decodeReply(
98
+ body,
99
+ webpackServerMap,
100
+ );
101
+
102
+ expect(result.get('array')).toEqual(array);
103
+ });
104
+
105
// @gate enableBinaryFlight
106
it('should be able to serialize a blob', async () => {
107
const bytes = new Uint8Array([
packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js
+14
-2
@@ -11,11 +11,16 @@ const url = require('url');
11
const Module = require('module');
12
13
let webpackModuleIdx = 0;
14
+let webpackChunkIdx = 0;
15
const webpackServerModules = {};
16
const webpackClientModules = {};
17
const webpackErroredModules = {};
18
const webpackServerMap = {};
19
const webpackClientMap = {};
20
+const webpackChunkMap = {};
21
+global.__webpack_chunk_load__ = function (id) {
22
+ return webpackChunkMap[id];
23
+};
24
global.__webpack_require__ = function (id) {
25
if (webpackErroredModules[id]) {
26
throw webpackErroredModules[id];
@@ -117,13 +122,20 @@ exports.clientExports = function clientExports(
122
};
123
124
// This tests server to server references. There's another case of client to server references.
120
-exports.serverExports = function serverExports(moduleExports) {
125
+exports.serverExports = function serverExports(moduleExports, blockOnChunk) {
126
const idx = '' + webpackModuleIdx++;
127
webpackServerModules[idx] = moduleExports;
128
const path = url.pathToFileURL(idx).href;
129
+
130
+ const chunks = [];
131
+ if (blockOnChunk) {
132
+ const chunkId = webpackChunkIdx++;
133
+ webpackChunkMap[chunkId] = blockOnChunk;
134
+ chunks.push(chunkId);
135
+ }
136
webpackServerMap[path] = {
137
id: idx,
126
- chunks: [],
138
+ chunks: chunks,
139
name: '*',
140
};
141
// We only add this if this test is testing ESM compat.
packages/react-server/src/ReactFlightReplyServer.js
+82
-41
@@ -327,7 +327,14 @@ function loadServerReference<T>(
327
}
328
}
329
promise.then(
330
- createModelResolver(parentChunk, parentObject, key),
330
+ createModelResolver(
331
+ parentChunk,
332
+ parentObject,
333
+ key,
334
+ false,
335
+ response,
336
+ createModel,
337
+ ),
338
createModelReject(parentChunk),
339
);
340
// We need a placeholder value that will be replaced later.
@@ -406,19 +413,24 @@ function createModelResolver<T>(
413
chunk: SomeChunk<T>,
414
parentObject: Object,
415
key: string,
416
+ cyclic: boolean,
417
+ response: Response,
418
+ map: (response: Response, model: any) => T,
419
): (value: any) => void {
420
let blocked;
421
if (initializingChunkBlockedModel) {
422
blocked = initializingChunkBlockedModel;
413
- blocked.deps++;
423
+ if (!cyclic) {
424
+ blocked.deps++;
425
+ }
426
} else {
427
blocked = initializingChunkBlockedModel = {
416
- deps: 1,
428
+ deps: cyclic ? 0 : 1,
429
value: (null: any),
430
};
431
}
432
return value => {
421
- parentObject[key] = value;
433
+ parentObject[key] = map(response, value);
434
435
// If this is the root object for a model reference, where `blocked.value`
436
// is a stale `null`, the resolved value can be used directly.
@@ -446,16 +458,61 @@ function createModelReject<T>(chunk: SomeChunk<T>): (error: mixed) => void {
458
return (error: mixed) => triggerErrorOnChunk(chunk, error);
459
}
460
449
-function getOutlinedModel(response: Response, id: number): any {
461
+function getOutlinedModel<T>(
462
+ response: Response,
463
+ id: number,
464
+ parentObject: Object,
465
+ key: string,
466
+ map: (response: Response, model: any) => T,
467
+): T {
468
const chunk = getChunk(response, id);
451
- if (chunk.status === RESOLVED_MODEL) {
452
- initializeModelChunk(chunk);
469
+ switch (chunk.status) {
470
+ case RESOLVED_MODEL:
471
+ initializeModelChunk(chunk);
472
+ break;
473
}
454
- if (chunk.status !== INITIALIZED) {
455
- // We know that this is emitted earlier so otherwise it's an error.
456
- throw chunk.reason;
474
+ // The status might have changed after initialization.
475
+ switch (chunk.status) {
476
+ case INITIALIZED:
477
+ return map(response, chunk.value);
478
+ case PENDING:
479
+ case BLOCKED:
480
+ const parentChunk = initializingChunk;
481
+ chunk.then(
482
+ createModelResolver(
483
+ parentChunk,
484
+ parentObject,
485
+ key,
486
+ false,
487
+ response,
488
+ map,
489
+ ),
490
+ createModelReject(parentChunk),
491
+ );
492
+ return (null: any);
493
+ default:
494
+ throw chunk.reason;
495
}
458
- return chunk.value;
496
+}
497
+
498
+function createMap(
499
+ response: Response,
500
+ model: Array<[any, any]>,
501
+): Map<any, any> {
502
+ return new Map(model);
503
+}
504
+
505
+function createSet(response: Response, model: Array<any>): Set<any> {
506
+ return new Set(model);
507
+}
508
+
509
+function extractIterator(response: Response, model: Array<any>): Iterator<any> {
510
+ // $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.
511
+ return model[Symbol.iterator]();
512
+}
513
+
514
+function createModel(response: Response, model: any): any {
515
+ return model;
516
}
517
518
function parseTypedArray(
@@ -481,10 +538,17 @@ function parseTypedArray(
538
});
539
540
// Since loading the buffer is an async operation we'll be blocking the parent
484
- // chunk. TODO: This is not safe if the parent chunk needs a mapper like Map.
541
+ // chunk.
542
const parentChunk = initializingChunk;
543
promise.then(
487
- createModelResolver(parentChunk, parentObject, parentKey),
544
+ createModelResolver(
545
+ parentChunk,
546
+ parentObject,
547
+ parentKey,
548
+ false,
549
+ response,
550
+ createModel,
551
+ ),
552
createModelReject(parentChunk),
553
);
554
return null;
@@ -728,7 +792,7 @@ function parseModelString(
792
const id = parseInt(value.slice(2), 16);
793
// TODO: Just encode this in the reference inline instead of as a model.
794
const metaData: {id: ServerReferenceId, bound: Thenable<Array<any>>} =
731
- getOutlinedModel(response, id);
795
+ getOutlinedModel(response, id, obj, key, createModel);
796
return loadServerReference(
797
response,
798
metaData.id,
@@ -745,14 +809,12 @@ function parseModelString(
809
case 'Q': {
810
// Map
811
const id = parseInt(value.slice(2), 16);
748
- const data = getOutlinedModel(response, id);
749
- return new Map(data);
812
+ return getOutlinedModel(response, id, obj, key, createMap);
813
}
814
case 'W': {
815
// Set
816
const id = parseInt(value.slice(2), 16);
754
- const data = getOutlinedModel(response, id);
755
- return new Set(data);
817
+ return getOutlinedModel(response, id, obj, key, createSet);
818
}
819
case 'K': {
820
// FormData
@@ -774,8 +836,7 @@ function parseModelString(
836
case 'i': {
837
// Iterator
838
const id = parseInt(value.slice(2), 16);
777
- const data = getOutlinedModel(response, id);
778
- return data[Symbol.iterator]();
839
+ return getOutlinedModel(response, id, obj, key, extractIterator);
840
}
841
case 'I': {
842
// $Infinity
@@ -873,27 +934,7 @@ function parseModelString(
934
935
// We assume that anything else is a reference ID.
936
const id = parseInt(value.slice(1), 16);
876
- const chunk = getChunk(response, id);
877
- switch (chunk.status) {
878
- case RESOLVED_MODEL:
879
- initializeModelChunk(chunk);
880
- break;
881
- }
882
- // The status might have changed after initialization.
883
- switch (chunk.status) {
884
- case INITIALIZED:
885
- return chunk.value;
886
- case PENDING:
887
- case BLOCKED:
888
- const parentChunk = initializingChunk;
889
- chunk.then(
890
- createModelResolver(parentChunk, obj, key),
891
- createModelReject(parentChunk),
892
- );
893
- return null;
894
- default:
895
- throw chunk.reason;
896
- }
937
+ return getOutlinedModel(response, id, obj, key, createModel);
938
}
939
return value;
940
}