@samitouri / QOS-React / commits / bf426f9b1d

[Flight / Flight Reply] Encode Iterator separately from Iterable (#28854)

For [`AsyncIterable`](https://github.com/facebook/react/pull/28847) we encode `AsyncIterator` as a separate tag. Previously we encoded `Iterator` as just an Array. This adds a special encoding for this. Technically this is a breaking change. This is kind of an edge case that you'd care about the difference but it becomes more important to treat these correctly for the warnings here #28853.

Sebastian Markbåge committed Apr 21, 2024 at 12:52 UTC bf426f9b1de6ef6adfeff30297beba96464f5efb
6 files changed +108 -2
packages/react-client/src/ReactFlightClient.js
+16
@@ -825,6 +825,11 @@ function createFormData(
825 return formData;
826 }
827
828 +function extractIterator(response: Response, model: Array<any>): Iterator<any> {
829 + // $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.
830 + return model[Symbol.iterator]();
831 +}
832 +
833 function createModel(response: Response, model: any): any {
834 return model;
835 }
@@ -919,6 +924,17 @@ function parseModelString(
924 createFormData,
925 );
926 }
927 + case 'i': {
928 + // Iterator
929 + const id = parseInt(value.slice(2), 16);
930 + return getOutlinedModel(
931 + response,
932 + id,
933 + parentObject,
934 + key,
935 + extractIterator,
936 + );
937 + }
938 case 'I': {
939 // $Infinity
940 return Infinity;
packages/react-client/src/ReactFlightReplyClient.js
+22 -1
@@ -81,7 +81,10 @@ export type ReactServerValue =
81 | null
82 | void
83 | bigint
84 + | $AsyncIterable<ReactServerValue, ReactServerValue, void>
85 + | $AsyncIterator<ReactServerValue, ReactServerValue, void>
86 | Iterable<ReactServerValue>
87 + | Iterator<ReactServerValue>
88 | Array<ReactServerValue>
89 | Map<ReactServerValue, ReactServerValue>
90 | Set<ReactServerValue>
@@ -157,6 +160,10 @@ function serializeBlobID(id: number): string {
160 return '$B' + id.toString(16);
161 }
162
163 +function serializeIteratorID(id: number): string {
164 + return '$i' + id.toString(16);
165 +}
166 +
167 function escapeStringValue(value: string): string {
168 if (value[0] === '$') {
169 // We need to escape $ prefixed strings since we use those to encode
@@ -448,7 +455,21 @@ export function processReply(
455
456 const iteratorFn = getIteratorFn(value);
457 if (iteratorFn) {
451 - return Array.from((value: any));
458 + const iterator = iteratorFn.call(value);
459 + if (iterator === value) {
460 + // Iterator, not Iterable
461 + const partJSON = JSON.stringify(
462 + Array.from((iterator: any)),
463 + resolveToJSON,
464 + );
465 + if (formData === null) {
466 + formData = new FormData();
467 + }
468 + const iteratorId = nextPartId++;
469 + formData.append(formFieldPrefix + iteratorId, partJSON);
470 + return serializeIteratorID(iteratorId);
471 + }
472 + return Array.from((iterator: any));
473 }
474
475 // Verify that this is a simple plain object.
packages/react-client/src/__tests__/ReactFlight-test.js
+18
@@ -277,6 +277,24 @@ describe('ReactFlight', () => {
277 expect(ReactNoop).toMatchRenderedOutput(<span>ABC</span>);
278 });
279
280 + it('can render an iterator as a single shot iterator', async () => {
281 + const iterator = (function* () {
282 + yield 'A';
283 + yield 'B';
284 + yield 'C';
285 + })();
286 +
287 + const transport = ReactNoopFlightServer.render(iterator);
288 + const result = await ReactNoopFlightClient.read(transport);
289 +
290 + // The iterator should be the same as itself.
291 + expect(result[Symbol.iterator]()).toBe(result);
292 +
293 + expect(Array.from(result)).toEqual(['A', 'B', 'C']);
294 + // We've already consumed this iterator.
295 + expect(Array.from(result)).toEqual([]);
296 + });
297 +
298 it('can render undefined', async () => {
299 function Undefined() {
300 return undefined;
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js
+29
@@ -97,6 +97,35 @@ describe('ReactFlightDOMReply', () => {
97 items.push(item);
98 }
99 expect(items).toEqual(['A', 'B', 'C']);
100 +
101 + // Multipass
102 + const items2 = [];
103 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
104 + for (const item of iterable) {
105 + items2.push(item);
106 + }
107 + expect(items2).toEqual(['A', 'B', 'C']);
108 + });
109 +
110 + it('can pass an iterator as a reply', async () => {
111 + const iterator = (function* () {
112 + yield 'A';
113 + yield 'B';
114 + yield 'C';
115 + })();
116 +
117 + const body = await ReactServerDOMClient.encodeReply(iterator);
118 + const result = await ReactServerDOMServer.decodeReply(
119 + body,
120 + webpackServerMap,
121 + );
122 +
123 + // The iterator should be the same as itself.
124 + expect(result[Symbol.iterator]()).toBe(result);
125 +
126 + expect(Array.from(result)).toEqual(['A', 'B', 'C']);
127 + // We've already consumed this iterator.
128 + expect(Array.from(result)).toEqual([]);
129 });
130
131 it('can pass weird numbers as a reply', async () => {
packages/react-server/src/ReactFlightReplyServer.js
+6
@@ -477,6 +477,12 @@ function parseModelString(
477 });
478 return data;
479 }
480 + case 'i': {
481 + // Iterator
482 + const id = parseInt(value.slice(2), 16);
483 + const data = getOutlinedModel(response, id);
484 + return data[Symbol.iterator]();
485 + }
486 case 'I': {
487 // $Infinity
488 return Infinity;
packages/react-server/src/ReactFlightServer.js
+17 -1
@@ -241,7 +241,9 @@ export type ReactClientValue =
241 | bigint
242 | ReadableStream
243 | $AsyncIterable<ReactClientValue, ReactClientValue, void>
244 + | $AsyncIterator<ReactClientValue, ReactClientValue, void>
245 | Iterable<ReactClientValue>
246 + | Iterator<ReactClientValue>
247 | Array<ReactClientValue>
248 | Map<ReactClientValue, ReactClientValue>
249 | Set<ReactClientValue>
@@ -1457,6 +1459,14 @@ function serializeSet(request: Request, set: Set<ReactClientValue>): string {
1459 return '$W' + id.toString(16);
1460 }
1461
1462 +function serializeIterator(
1463 + request: Request,
1464 + iterator: Iterator<ReactClientValue>,
1465 +): string {
1466 + const id = outlineModel(request, Array.from(iterator));
1467 + return '$i' + id.toString(16);
1468 +}
1469 +
1470 function serializeTypedArray(
1471 request: Request,
1472 tag: string,
@@ -1910,7 +1920,13 @@ function renderModelDestructive(
1920
1921 const iteratorFn = getIteratorFn(value);
1922 if (iteratorFn) {
1913 - return renderFragment(request, task, Array.from((value: any)));
1923 + // TODO: Should we serialize the return value as well like we do for AsyncIterables?
1924 + const iterator = iteratorFn.call(value);
1925 + if (iterator === value) {
1926 + // Iterator, not Iterable
1927 + return serializeIterator(request, (iterator: any));
1928 + }
1929 + return renderFragment(request, task, Array.from((iterator: any)));
1930 }
1931
1932 if (enableFlightReadableStream) {