Skip to content

Commit bf426f9

Browse files
authoredApr 21, 2024··
[Flight / Flight Reply] Encode Iterator separately from Iterable (#28854)
For [`AsyncIterable`](#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.
1 parent 3682021 commit bf426f9

File tree

6 files changed

+108
-2
lines changed

6 files changed

+108
-2
lines changed
 

‎packages/react-client/src/ReactFlightClient.js

+16
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,11 @@ function createFormData(
825825
return formData;
826826
}
827827

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+
828833
function createModel(response: Response, model: any): any {
829834
return model;
830835
}
@@ -919,6 +924,17 @@ function parseModelString(
919924
createFormData,
920925
);
921926
}
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+
}
922938
case 'I': {
923939
// $Infinity
924940
return Infinity;

‎packages/react-client/src/ReactFlightReplyClient.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ export type ReactServerValue =
8181
| null
8282
| void
8383
| bigint
84+
| $AsyncIterable<ReactServerValue, ReactServerValue, void>
85+
| $AsyncIterator<ReactServerValue, ReactServerValue, void>
8486
| Iterable<ReactServerValue>
87+
| Iterator<ReactServerValue>
8588
| Array<ReactServerValue>
8689
| Map<ReactServerValue, ReactServerValue>
8790
| Set<ReactServerValue>
@@ -157,6 +160,10 @@ function serializeBlobID(id: number): string {
157160
return '$B' + id.toString(16);
158161
}
159162

163+
function serializeIteratorID(id: number): string {
164+
return '$i' + id.toString(16);
165+
}
166+
160167
function escapeStringValue(value: string): string {
161168
if (value[0] === '$') {
162169
// We need to escape $ prefixed strings since we use those to encode
@@ -448,7 +455,21 @@ export function processReply(
448455

449456
const iteratorFn = getIteratorFn(value);
450457
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));
452473
}
453474

454475
// Verify that this is a simple plain object.

‎packages/react-client/src/__tests__/ReactFlight-test.js

+18
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,24 @@ describe('ReactFlight', () => {
277277
expect(ReactNoop).toMatchRenderedOutput(<span>ABC</span>);
278278
});
279279

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+
280298
it('can render undefined', async () => {
281299
function Undefined() {
282300
return undefined;

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

+29
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,35 @@ describe('ReactFlightDOMReply', () => {
9797
items.push(item);
9898
}
9999
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([]);
100129
});
101130

102131
it('can pass weird numbers as a reply', async () => {

‎packages/react-server/src/ReactFlightReplyServer.js

+6
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,12 @@ function parseModelString(
477477
});
478478
return data;
479479
}
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+
}
480486
case 'I': {
481487
// $Infinity
482488
return Infinity;

‎packages/react-server/src/ReactFlightServer.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,9 @@ export type ReactClientValue =
241241
| bigint
242242
| ReadableStream
243243
| $AsyncIterable<ReactClientValue, ReactClientValue, void>
244+
| $AsyncIterator<ReactClientValue, ReactClientValue, void>
244245
| Iterable<ReactClientValue>
246+
| Iterator<ReactClientValue>
245247
| Array<ReactClientValue>
246248
| Map<ReactClientValue, ReactClientValue>
247249
| Set<ReactClientValue>
@@ -1457,6 +1459,14 @@ function serializeSet(request: Request, set: Set<ReactClientValue>): string {
14571459
return '$W' + id.toString(16);
14581460
}
14591461

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+
14601470
function serializeTypedArray(
14611471
request: Request,
14621472
tag: string,
@@ -1910,7 +1920,13 @@ function renderModelDestructive(
19101920

19111921
const iteratorFn = getIteratorFn(value);
19121922
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)));
19141930
}
19151931

19161932
if (enableFlightReadableStream) {

0 commit comments

Comments
 (0)
Please sign in to comment.