@samitouri / QOS-React-1 / commits / ec15267a00

[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 }