Skip to content

Commit 4847e6c

Browse files
committed
Serialize Map and Set - Server to Client
1 parent db50164 commit 4847e6c

File tree

3 files changed

+124
-25
lines changed

3 files changed

+124
-25
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,24 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
535535
return proxy;
536536
}
537537

538+
function getOutlinedModel(response: Response, id: number): any {
539+
const chunk = getChunk(response, id);
540+
switch (chunk.status) {
541+
case RESOLVED_MODEL:
542+
initializeModelChunk(chunk);
543+
break;
544+
}
545+
// The status might have changed after initialization.
546+
switch (chunk.status) {
547+
case INITIALIZED: {
548+
return chunk.value;
549+
}
550+
// We always encode it first in the stream so it won't be pending.
551+
default:
552+
throw chunk.reason;
553+
}
554+
}
555+
538556
function parseModelString(
539557
response: Response,
540558
parentObject: Object,
@@ -576,22 +594,20 @@ function parseModelString(
576594
case 'F': {
577595
// Server Reference
578596
const id = parseInt(value.slice(2), 16);
579-
const chunk = getChunk(response, id);
580-
switch (chunk.status) {
581-
case RESOLVED_MODEL:
582-
initializeModelChunk(chunk);
583-
break;
584-
}
585-
// The status might have changed after initialization.
586-
switch (chunk.status) {
587-
case INITIALIZED: {
588-
const metadata = chunk.value;
589-
return createServerReferenceProxy(response, metadata);
590-
}
591-
// We always encode it first in the stream so it won't be pending.
592-
default:
593-
throw chunk.reason;
594-
}
597+
const metadata = getOutlinedModel(response, id);
598+
return createServerReferenceProxy(response, metadata);
599+
}
600+
case 'Q': {
601+
// Map
602+
const id = parseInt(value.slice(2), 16);
603+
const data = getOutlinedModel(response, id);
604+
return new Map(data);
605+
}
606+
case 'W': {
607+
// Set
608+
const id = parseInt(value.slice(2), 16);
609+
const data = getOutlinedModel(response, id);
610+
return new Set(data);
595611
}
596612
case 'I': {
597613
// $Infinity

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,67 @@ describe('ReactFlight', () => {
323323
expect(ReactNoop).toMatchRenderedOutput('prop: 2009-02-13T23:31:30.123Z');
324324
});
325325

326+
it('can transport Map', async () => {
327+
function ComponentClient({prop}) {
328+
return `
329+
map: ${prop instanceof Map}
330+
size: ${prop.size}
331+
greet: ${prop.get('hi').greet}
332+
content: ${JSON.stringify(Array.from(prop))}
333+
`;
334+
}
335+
const Component = clientReference(ComponentClient);
336+
337+
const objKey = {obj: 'key'};
338+
const map = new Map([
339+
['hi', {greet: 'world'}],
340+
[objKey, 123],
341+
]);
342+
const model = <Component prop={map} />;
343+
344+
const transport = ReactNoopFlightServer.render(model);
345+
346+
await act(async () => {
347+
ReactNoop.render(await ReactNoopFlightClient.read(transport));
348+
});
349+
350+
expect(ReactNoop).toMatchRenderedOutput(`
351+
map: true
352+
size: 2
353+
greet: world
354+
content: [["hi",{"greet":"world"}],[{"obj":"key"},123]]
355+
`);
356+
});
357+
358+
it('can transport Set', async () => {
359+
function ComponentClient({prop}) {
360+
return `
361+
set: ${prop instanceof Set}
362+
size: ${prop.size}
363+
hi: ${prop.has('hi')}
364+
content: ${JSON.stringify(Array.from(prop))}
365+
`;
366+
}
367+
const Component = clientReference(ComponentClient);
368+
369+
const objKey = {obj: 'key'};
370+
const set = new Set(['hi', objKey]);
371+
const model = <Component prop={set} />;
372+
373+
const transport = ReactNoopFlightServer.render(model);
374+
375+
await act(async () => {
376+
ReactNoop.render(await ReactNoopFlightClient.read(transport));
377+
});
378+
379+
expect(ReactNoop).toMatchRenderedOutput(`
380+
set: true
381+
size: 2
382+
hi: true
383+
content: ["hi",{"obj":"key"}]
384+
`);
385+
});
386+
326387
it('can render a lazy component as a shared component on the server', async () => {
327388
function SharedComponent({text}) {
328389
return (

packages/react-server/src/ReactFlightServer.js

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ export type ReactClientValue =
139139
| void
140140
| Iterable<ReactClientValue>
141141
| Array<ReactClientValue>
142+
| Map<ReactClientValue, ReactClientValue>
143+
| Set<ReactClientValue>
142144
| ReactClientObject
143145
| Promise<ReactClientValue>; // Thenable<ReactClientValue>
144146

@@ -683,6 +685,15 @@ function serializeClientReference(
683685
}
684686
}
685687

688+
function outlineModel(request: Request, value: any): number {
689+
request.pendingChunks++;
690+
const outlinedId = request.nextChunkId++;
691+
// We assume that this object doesn't suspend, but a child might.
692+
const processedChunk = processModelChunk(request, outlinedId, value);
693+
request.completedRegularChunks.push(processedChunk);
694+
return outlinedId;
695+
}
696+
686697
function serializeServerReference(
687698
request: Request,
688699
parent:
@@ -708,15 +719,7 @@ function serializeServerReference(
708719
id: getServerReferenceId(request.bundlerConfig, serverReference),
709720
bound: bound ? Promise.resolve(bound) : null,
710721
};
711-
request.pendingChunks++;
712-
const metadataId = request.nextChunkId++;
713-
// We assume that this object doesn't suspend.
714-
const processedChunk = processModelChunk(
715-
request,
716-
metadataId,
717-
serverReferenceMetadata,
718-
);
719-
request.completedRegularChunks.push(processedChunk);
722+
const metadataId = outlineModel(request, serverReferenceMetadata);
720723
writtenServerReferences.set(serverReference, metadataId);
721724
return serializeServerReferenceID(metadataId);
722725
}
@@ -735,6 +738,19 @@ function serializeLargeTextString(request: Request, text: string): string {
735738
return serializeByValueID(textId);
736739
}
737740

741+
function serializeMap(
742+
request: Request,
743+
map: Map<ReactClientValue, ReactClientValue>,
744+
): string {
745+
const id = outlineModel(request, Array.from(map));
746+
return '$Q' + id.toString(16);
747+
}
748+
749+
function serializeSet(request: Request, set: Set<ReactClientValue>): string {
750+
const id = outlineModel(request, Array.from(set));
751+
return '$W' + id.toString(16);
752+
}
753+
738754
function escapeStringValue(value: string): string {
739755
if (value[0] === '$') {
740756
// We need to escape $ prefixed strings since we use those to encode
@@ -924,6 +940,12 @@ function resolveModelToJSON(
924940
}
925941
return (undefined: any);
926942
}
943+
if (value instanceof Map) {
944+
return serializeMap(request, value);
945+
}
946+
if (value instanceof Set) {
947+
return serializeSet(request, value);
948+
}
927949
if (!isArray(value)) {
928950
const iteratorFn = getIteratorFn(value);
929951
if (iteratorFn) {

0 commit comments

Comments
 (0)