Skip to content

Commit

Permalink
[Flight Reply] Add undefined and Iterable Support (#26365)
Browse files Browse the repository at this point in the history
These were recently added to ReactClientValue and so needs to be
reflected in ReactServerValue too.
  • Loading branch information
sebmarkbage authored Mar 10, 2023
1 parent ef8bdbe commit be353d2
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 5 deletions.
22 changes: 17 additions & 5 deletions packages/react-client/src/ReactFlightReplyClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
REACT_ELEMENT_TYPE,
REACT_LAZY_TYPE,
REACT_PROVIDER_TYPE,
getIteratorFn,
} from 'shared/ReactSymbols';

import {
Expand Down Expand Up @@ -46,6 +47,7 @@ export type ReactServerValue =
| number
| symbol
| null
| void
| Iterable<ReactServerValue>
| Array<ReactServerValue>
| ReactServerObject
Expand All @@ -69,6 +71,10 @@ function serializeSymbolReference(name: string): string {
return '$S' + name;
}

function serializeUndefined(): string {
return '$undefined';
}

function escapeStringValue(value: string): string {
if (value[0] === '$') {
// We need to escape $ prefixed strings since we use those to encode
Expand Down Expand Up @@ -154,6 +160,12 @@ export function processReply(
);
return serializePromiseID(promiseId);
}
if (!isArray(value)) {
const iteratorFn = getIteratorFn(value);
if (iteratorFn) {
return Array.from((value: any));
}
}

if (__DEV__) {
if (value !== null && !isArray(value)) {
Expand Down Expand Up @@ -208,14 +220,14 @@ export function processReply(
return escapeStringValue(value);
}

if (
typeof value === 'boolean' ||
typeof value === 'number' ||
typeof value === 'undefined'
) {
if (typeof value === 'boolean' || typeof value === 'number') {
return value;
}

if (typeof value === 'undefined') {
return serializeUndefined();
}

if (typeof value === 'function') {
const metaData = knownServerReferences.get(value);
if (metaData !== undefined) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/

'use strict';

// Polyfills for test environment
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;

// let serverExports;
let webpackServerMap;
let ReactServerDOMServer;
let ReactServerDOMClient;

describe('ReactFlightDOMReply', () => {
beforeEach(() => {
jest.resetModules();
const WebpackMock = require('./utils/WebpackMock');
// serverExports = WebpackMock.serverExports;
webpackServerMap = WebpackMock.webpackServerMap;
ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
ReactServerDOMClient = require('react-server-dom-webpack/client');
});

it('can pass undefined as a reply', async () => {
const body = await ReactServerDOMClient.encodeReply(undefined);
const missing = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);
expect(missing).toBe(undefined);

const body2 = await ReactServerDOMClient.encodeReply({
array: [undefined, null, undefined],
prop: undefined,
});
const object = await ReactServerDOMServer.decodeReply(
body2,
webpackServerMap,
);
expect(object.array.length).toBe(3);
expect(object.array[0]).toBe(undefined);
expect(object.array[1]).toBe(null);
expect(object.array[3]).toBe(undefined);
expect(object.prop).toBe(undefined);
// These should really be true but our deserialization doesn't currently deal with it.
expect('3' in object.array).toBe(false);
expect('prop' in object).toBe(false);
});

it('can pass an iterable as a reply', async () => {
const body = await ReactServerDOMClient.encodeReply({
[Symbol.iterator]: function* () {
yield 'A';
yield 'B';
yield 'C';
},
});
const iterable = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);
const items = [];
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const item of iterable) {
items.push(item);
}
expect(items).toEqual(['A', 'B', 'C']);
});
});
5 changes: 5 additions & 0 deletions packages/react-server/src/ReactFlightReplyServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,11 @@ function parseModelString(
key,
);
}
case 'u': {
// matches "$undefined"
// Special encoding for `undefined` which can't be serialized as JSON otherwise.
return undefined;
}
default: {
// We assume that anything else is a reference ID.
const id = parseInt(value.substring(1), 16);
Expand Down

0 comments on commit be353d2

Please sign in to comment.