Skip to content

Commit 90a7f4d

Browse files
committed
Resolve outlined models async in Reply just like in Flight Client
1 parent 0a0a3af commit 90a7f4d

File tree

4 files changed

+162
-44
lines changed

4 files changed

+162
-44
lines changed

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

+41
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

+17
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,23 @@ describe('ReactFlightDOMReplyEdge', () => {
9191
expect(result).toEqual(buffers);
9292
});
9393

94+
// @gate enableBinaryFlight
95+
it('should be able to serialize a typed array inside a Map', async () => {
96+
const array = new Uint8Array([
97+
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
98+
]);
99+
const map = new Map();
100+
map.set('array', array);
101+
102+
const body = await ReactServerDOMClient.encodeReply(map);
103+
const result = await ReactServerDOMServer.decodeReply(
104+
body,
105+
webpackServerMap,
106+
);
107+
108+
expect(result.get('array')).toEqual(array);
109+
});
110+
94111
// @gate enableBinaryFlight
95112
it('should be able to serialize a blob', async () => {
96113
const bytes = new Uint8Array([

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

+14-2
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

+90-42
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,14 @@ function loadServerReference<T>(
255255
}
256256
}
257257
promise.then(
258-
createModelResolver(parentChunk, parentObject, key),
258+
createModelResolver(
259+
parentChunk,
260+
parentObject,
261+
key,
262+
false,
263+
response,
264+
createModel,
265+
),
259266
createModelReject(parentChunk),
260267
);
261268
// We need a placeholder value that will be replaced later.
@@ -334,19 +341,31 @@ function createModelResolver<T>(
334341
chunk: SomeChunk<T>,
335342
parentObject: Object,
336343
key: string,
344+
cyclic: boolean,
345+
response: Response,
346+
map: (response: Response, model: any) => T,
337347
): (value: any) => void {
338348
let blocked;
339349
if (initializingChunkBlockedModel) {
340350
blocked = initializingChunkBlockedModel;
341-
blocked.deps++;
351+
if (!cyclic) {
352+
blocked.deps++;
353+
}
342354
} else {
343355
blocked = initializingChunkBlockedModel = {
344-
deps: 1,
345-
value: null,
356+
deps: cyclic ? 0 : 1,
357+
value: (null: any),
346358
};
347359
}
348360
return value => {
349-
parentObject[key] = value;
361+
parentObject[key] = map(response, value);
362+
363+
// If this is the root object for a model reference, where `blocked.value`
364+
// is a stale `null`, the resolved value can be used directly.
365+
if (key === '' && blocked.value === null) {
366+
blocked.value = parentObject[key];
367+
}
368+
350369
blocked.deps--;
351370
if (blocked.deps === 0) {
352371
if (chunk.status !== BLOCKED) {
@@ -367,16 +386,61 @@ function createModelReject<T>(chunk: SomeChunk<T>): (error: mixed) => void {
367386
return (error: mixed) => triggerErrorOnChunk(chunk, error);
368387
}
369388

370-
function getOutlinedModel(response: Response, id: number): any {
389+
function getOutlinedModel<T>(
390+
response: Response,
391+
id: number,
392+
parentObject: Object,
393+
key: string,
394+
map: (response: Response, model: any) => T,
395+
): T {
371396
const chunk = getChunk(response, id);
372-
if (chunk.status === RESOLVED_MODEL) {
373-
initializeModelChunk(chunk);
397+
switch (chunk.status) {
398+
case RESOLVED_MODEL:
399+
initializeModelChunk(chunk);
400+
break;
374401
}
375-
if (chunk.status !== INITIALIZED) {
376-
// We know that this is emitted earlier so otherwise it's an error.
377-
throw chunk.reason;
402+
// The status might have changed after initialization.
403+
switch (chunk.status) {
404+
case INITIALIZED:
405+
return map(response, chunk.value);
406+
case PENDING:
407+
case BLOCKED:
408+
const parentChunk = initializingChunk;
409+
chunk.then(
410+
createModelResolver(
411+
parentChunk,
412+
parentObject,
413+
key,
414+
false,
415+
response,
416+
map,
417+
),
418+
createModelReject(parentChunk),
419+
);
420+
return (null: any);
421+
default:
422+
throw chunk.reason;
378423
}
379-
return chunk.value;
424+
}
425+
426+
function createMap(
427+
response: Response,
428+
model: Array<[any, any]>,
429+
): Map<any, any> {
430+
return new Map(model);
431+
}
432+
433+
function createSet(response: Response, model: Array<any>): Set<any> {
434+
return new Set(model);
435+
}
436+
437+
function extractIterator(response: Response, model: Array<any>): Iterator<any> {
438+
// $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.
439+
return model[Symbol.iterator]();
440+
}
441+
442+
function createModel(response: Response, model: any): any {
443+
return model;
380444
}
381445

382446
function parseTypedArray(
@@ -402,10 +466,17 @@ function parseTypedArray(
402466
});
403467

404468
// Since loading the buffer is an async operation we'll be blocking the parent
405-
// chunk. TODO: This is not safe if the parent chunk needs a mapper like Map.
469+
// chunk.
406470
const parentChunk = initializingChunk;
407471
promise.then(
408-
createModelResolver(parentChunk, parentObject, parentKey),
472+
createModelResolver(
473+
parentChunk,
474+
parentObject,
475+
parentKey,
476+
false,
477+
response,
478+
createModel,
479+
),
409480
createModelReject(parentChunk),
410481
);
411482
return null;
@@ -434,7 +505,7 @@ function parseModelString(
434505
const id = parseInt(value.slice(2), 16);
435506
// TODO: Just encode this in the reference inline instead of as a model.
436507
const metaData: {id: ServerReferenceId, bound: Thenable<Array<any>>} =
437-
getOutlinedModel(response, id);
508+
getOutlinedModel(response, id, obj, key, createModel);
438509
return loadServerReference(
439510
response,
440511
metaData.id,
@@ -451,14 +522,12 @@ function parseModelString(
451522
case 'Q': {
452523
// Map
453524
const id = parseInt(value.slice(2), 16);
454-
const data = getOutlinedModel(response, id);
455-
return new Map(data);
525+
return getOutlinedModel(response, id, obj, key, createMap);
456526
}
457527
case 'W': {
458528
// Set
459529
const id = parseInt(value.slice(2), 16);
460-
const data = getOutlinedModel(response, id);
461-
return new Set(data);
530+
return getOutlinedModel(response, id, obj, key, createSet);
462531
}
463532
case 'K': {
464533
// FormData
@@ -480,8 +549,7 @@ function parseModelString(
480549
case 'i': {
481550
// Iterator
482551
const id = parseInt(value.slice(2), 16);
483-
const data = getOutlinedModel(response, id);
484-
return data[Symbol.iterator]();
552+
return getOutlinedModel(response, id, obj, key, extractIterator);
485553
}
486554
case 'I': {
487555
// $Infinity
@@ -563,27 +631,7 @@ function parseModelString(
563631

564632
// We assume that anything else is a reference ID.
565633
const id = parseInt(value.slice(1), 16);
566-
const chunk = getChunk(response, id);
567-
switch (chunk.status) {
568-
case RESOLVED_MODEL:
569-
initializeModelChunk(chunk);
570-
break;
571-
}
572-
// The status might have changed after initialization.
573-
switch (chunk.status) {
574-
case INITIALIZED:
575-
return chunk.value;
576-
case PENDING:
577-
case BLOCKED:
578-
const parentChunk = initializingChunk;
579-
chunk.then(
580-
createModelResolver(parentChunk, obj, key),
581-
createModelReject(parentChunk),
582-
);
583-
return null;
584-
default:
585-
throw chunk.reason;
586-
}
634+
return getOutlinedModel(response, id, obj, key, createModel);
587635
}
588636
return value;
589637
}

0 commit comments

Comments
 (0)