Skip to content

Commit ec15267

Browse files
authored
[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.
1 parent 6bac4f2 commit ec15267

File tree

4 files changed

+154
-43
lines changed

4 files changed

+154
-43
lines changed

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,47 @@ describe('ReactFlightDOMBrowser', () => {
11301130
expect(result).toBe('Hello world');
11311131
});
11321132

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+
11331174
it('supports Float hints before the first await in server components in Fiber', async () => {
11341175
function Component() {
11351176
return <p>hello world</p>;

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,23 @@ describe('ReactFlightDOMReplyEdge', () => {
8585
expect(new Uint8Array(result[0])).toEqual(new Uint8Array(buffers[0]));
8686
});
8787

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+
88105
// @gate enableBinaryFlight
89106
it('should be able to serialize a blob', async () => {
90107
const bytes = new Uint8Array([

packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,16 @@ const url = require('url');
1111
const Module = require('module');
1212

1313
let webpackModuleIdx = 0;
14+
let webpackChunkIdx = 0;
1415
const webpackServerModules = {};
1516
const webpackClientModules = {};
1617
const webpackErroredModules = {};
1718
const webpackServerMap = {};
1819
const webpackClientMap = {};
20+
const webpackChunkMap = {};
21+
global.__webpack_chunk_load__ = function (id) {
22+
return webpackChunkMap[id];
23+
};
1924
global.__webpack_require__ = function (id) {
2025
if (webpackErroredModules[id]) {
2126
throw webpackErroredModules[id];
@@ -117,13 +122,20 @@ exports.clientExports = function clientExports(
117122
};
118123

119124
// 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) {
121126
const idx = '' + webpackModuleIdx++;
122127
webpackServerModules[idx] = moduleExports;
123128
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+
}
124136
webpackServerMap[path] = {
125137
id: idx,
126-
chunks: [],
138+
chunks: chunks,
127139
name: '*',
128140
};
129141
// We only add this if this test is testing ESM compat.

packages/react-server/src/ReactFlightReplyServer.js

Lines changed: 82 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,14 @@ function loadServerReference<T>(
327327
}
328328
}
329329
promise.then(
330-
createModelResolver(parentChunk, parentObject, key),
330+
createModelResolver(
331+
parentChunk,
332+
parentObject,
333+
key,
334+
false,
335+
response,
336+
createModel,
337+
),
331338
createModelReject(parentChunk),
332339
);
333340
// We need a placeholder value that will be replaced later.
@@ -406,19 +413,24 @@ function createModelResolver<T>(
406413
chunk: SomeChunk<T>,
407414
parentObject: Object,
408415
key: string,
416+
cyclic: boolean,
417+
response: Response,
418+
map: (response: Response, model: any) => T,
409419
): (value: any) => void {
410420
let blocked;
411421
if (initializingChunkBlockedModel) {
412422
blocked = initializingChunkBlockedModel;
413-
blocked.deps++;
423+
if (!cyclic) {
424+
blocked.deps++;
425+
}
414426
} else {
415427
blocked = initializingChunkBlockedModel = {
416-
deps: 1,
428+
deps: cyclic ? 0 : 1,
417429
value: (null: any),
418430
};
419431
}
420432
return value => {
421-
parentObject[key] = value;
433+
parentObject[key] = map(response, value);
422434

423435
// If this is the root object for a model reference, where `blocked.value`
424436
// is a stale `null`, the resolved value can be used directly.
@@ -446,16 +458,61 @@ function createModelReject<T>(chunk: SomeChunk<T>): (error: mixed) => void {
446458
return (error: mixed) => triggerErrorOnChunk(chunk, error);
447459
}
448460

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 {
450468
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;
453473
}
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;
457495
}
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;
459516
}
460517

461518
function parseTypedArray(
@@ -481,10 +538,17 @@ function parseTypedArray(
481538
});
482539

483540
// 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.
485542
const parentChunk = initializingChunk;
486543
promise.then(
487-
createModelResolver(parentChunk, parentObject, parentKey),
544+
createModelResolver(
545+
parentChunk,
546+
parentObject,
547+
parentKey,
548+
false,
549+
response,
550+
createModel,
551+
),
488552
createModelReject(parentChunk),
489553
);
490554
return null;
@@ -728,7 +792,7 @@ function parseModelString(
728792
const id = parseInt(value.slice(2), 16);
729793
// TODO: Just encode this in the reference inline instead of as a model.
730794
const metaData: {id: ServerReferenceId, bound: Thenable<Array<any>>} =
731-
getOutlinedModel(response, id);
795+
getOutlinedModel(response, id, obj, key, createModel);
732796
return loadServerReference(
733797
response,
734798
metaData.id,
@@ -745,14 +809,12 @@ function parseModelString(
745809
case 'Q': {
746810
// Map
747811
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);
750813
}
751814
case 'W': {
752815
// Set
753816
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);
756818
}
757819
case 'K': {
758820
// FormData
@@ -774,8 +836,7 @@ function parseModelString(
774836
case 'i': {
775837
// Iterator
776838
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);
779840
}
780841
case 'I': {
781842
// $Infinity
@@ -873,27 +934,7 @@ function parseModelString(
873934

874935
// We assume that anything else is a reference ID.
875936
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);
897938
}
898939
return value;
899940
}

0 commit comments

Comments
 (0)