Skip to content

Commit

Permalink
Encode Iterator separately from Iterable in Flight
Browse files Browse the repository at this point in the history
Just giving it a special tag and indirect reference for this edge case.
  • Loading branch information
sebmarkbage committed Apr 17, 2024
1 parent 7909d8e commit a64c6bf
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 1 deletion.
16 changes: 16 additions & 0 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,11 @@ function createFormData(
return formData;
}

function extractIterator(response: Response, model: Array<any>): Iterator<any> {
// $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.
return model[Symbol.iterator]();
}

function createModel(response: Response, model: any): any {
return model;
}
Expand Down Expand Up @@ -918,6 +923,17 @@ function parseModelString(
createFormData,
);
}
case 'i': {
// Iterator
const id = parseInt(value.slice(2), 16);
return getOutlinedModel(
response,
id,
parentObject,
key,
extractIterator,
);
}
case 'I': {
// $Infinity
return Infinity;
Expand Down
18 changes: 18 additions & 0 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,24 @@ describe('ReactFlight', () => {
expect(ReactNoop).toMatchRenderedOutput(<span>ABC</span>);
});

it('can render an iterator as a single shot iterator', async () => {
const iterator = (function* () {
yield 'A';
yield 'B';
yield 'C';
})();

const transport = ReactNoopFlightServer.render(iterator);
const result = await ReactNoopFlightClient.read(transport);

// The iterator should be the same as itself.
expect(result[Symbol.iterator]()).toBe(result);

expect(Array.from(result)).toEqual(['A', 'B', 'C']);
// We've already consumed this iterator.
expect(Array.from(result)).toEqual([]);
});

it('can render undefined', async () => {
function Undefined() {
return undefined;
Expand Down
18 changes: 17 additions & 1 deletion packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,9 @@ export type ReactClientValue =
| bigint
| ReadableStream
| $AsyncIterable<ReactClientValue, ReactClientValue, void>
| $AsyncIterator<ReactClientValue, ReactClientValue, void>
| Iterable<ReactClientValue>
| Iterator<ReactClientValue>
| Array<ReactClientValue>
| Map<ReactClientValue, ReactClientValue>
| Set<ReactClientValue>
Expand Down Expand Up @@ -1458,6 +1460,14 @@ function serializeSet(request: Request, set: Set<ReactClientValue>): string {
return '$W' + id.toString(16);
}

function serializeIterator(
request: Request,
iterator: Iterator<ReactClientValue>,
): string {
const id = outlineModel(request, Array.from(iterator));
return '$i' + id.toString(16);
}

function serializeTypedArray(
request: Request,
tag: string,
Expand Down Expand Up @@ -1911,7 +1921,13 @@ function renderModelDestructive(

const iteratorFn = getIteratorFn(value);
if (iteratorFn) {
return renderFragment(request, task, Array.from((value: any)));
// TODO: Should we serialize the return value as well like we do for AsyncIterables?
const iterator = iteratorFn.call(value);
if (iterator === value) {
// Iterator, not Iterable
return serializeIterator(request, (iterator: any));
}
return renderFragment(request, task, Array.from((iterator: any)));
}

if (enableFlightReadableStream) {
Expand Down

0 comments on commit a64c6bf

Please sign in to comment.