@samitouri / QOS-React-2 / commits / be353d2515

[Flight Reply] Add undefined and Iterable Support (#26365)

These were recently added to ReactClientValue and so needs to be reflected in ReactServerValue too.

Sebastian Markbåge committed Mar 10, 2023 at 12:47 UTC be353d25153e8459013add4b00989784334538cb
3 files changed +100 -5
packages/react-client/src/ReactFlightReplyClient.js
+17 -5
@@ -15,6 +15,7 @@ import {
15 REACT_ELEMENT_TYPE,
16 REACT_LAZY_TYPE,
17 REACT_PROVIDER_TYPE,
18 + getIteratorFn,
19 } from 'shared/ReactSymbols';
20
21 import {
@@ -46,6 +47,7 @@ export type ReactServerValue =
47 | number
48 | symbol
49 | null
50 + | void
51 | Iterable<ReactServerValue>
52 | Array<ReactServerValue>
53 | ReactServerObject
@@ -69,6 +71,10 @@ function serializeSymbolReference(name: string): string {
71 return '$S' + name;
72 }
73
74 +function serializeUndefined(): string {
75 + return '$undefined';
76 +}
77 +
78 function escapeStringValue(value: string): string {
79 if (value[0] === '$') {
80 // We need to escape $ prefixed strings since we use those to encode
@@ -154,6 +160,12 @@ export function processReply(
160 );
161 return serializePromiseID(promiseId);
162 }
163 + if (!isArray(value)) {
164 + const iteratorFn = getIteratorFn(value);
165 + if (iteratorFn) {
166 + return Array.from((value: any));
167 + }
168 + }
169
170 if (__DEV__) {
171 if (value !== null && !isArray(value)) {
@@ -208,14 +220,14 @@ export function processReply(
220 return escapeStringValue(value);
221 }
222
211 - if (
212 - typeof value === 'boolean' ||
213 - typeof value === 'number' ||
214 - typeof value === 'undefined'
215 - ) {
223 + if (typeof value === 'boolean' || typeof value === 'number') {
224 return value;
225 }
226
227 + if (typeof value === 'undefined') {
228 + return serializeUndefined();
229 + }
230 +
231 if (typeof value === 'function') {
232 const metaData = knownServerReferences.get(value);
233 if (metaData !== undefined) {
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js new
+78
@@ -0,0 +1,78 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +// Polyfills for test environment
13 +global.ReadableStream =
14 + require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15 +global.TextEncoder = require('util').TextEncoder;
16 +global.TextDecoder = require('util').TextDecoder;
17 +
18 +// let serverExports;
19 +let webpackServerMap;
20 +let ReactServerDOMServer;
21 +let ReactServerDOMClient;
22 +
23 +describe('ReactFlightDOMReply', () => {
24 + beforeEach(() => {
25 + jest.resetModules();
26 + const WebpackMock = require('./utils/WebpackMock');
27 + // serverExports = WebpackMock.serverExports;
28 + webpackServerMap = WebpackMock.webpackServerMap;
29 + ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
30 + ReactServerDOMClient = require('react-server-dom-webpack/client');
31 + });
32 +
33 + it('can pass undefined as a reply', async () => {
34 + const body = await ReactServerDOMClient.encodeReply(undefined);
35 + const missing = await ReactServerDOMServer.decodeReply(
36 + body,
37 + webpackServerMap,
38 + );
39 + expect(missing).toBe(undefined);
40 +
41 + const body2 = await ReactServerDOMClient.encodeReply({
42 + array: [undefined, null, undefined],
43 + prop: undefined,
44 + });
45 + const object = await ReactServerDOMServer.decodeReply(
46 + body2,
47 + webpackServerMap,
48 + );
49 + expect(object.array.length).toBe(3);
50 + expect(object.array[0]).toBe(undefined);
51 + expect(object.array[1]).toBe(null);
52 + expect(object.array[3]).toBe(undefined);
53 + expect(object.prop).toBe(undefined);
54 + // These should really be true but our deserialization doesn't currently deal with it.
55 + expect('3' in object.array).toBe(false);
56 + expect('prop' in object).toBe(false);
57 + });
58 +
59 + it('can pass an iterable as a reply', async () => {
60 + const body = await ReactServerDOMClient.encodeReply({
61 + [Symbol.iterator]: function* () {
62 + yield 'A';
63 + yield 'B';
64 + yield 'C';
65 + },
66 + });
67 + const iterable = await ReactServerDOMServer.decodeReply(
68 + body,
69 + webpackServerMap,
70 + );
71 + const items = [];
72 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
73 + for (const item of iterable) {
74 + items.push(item);
75 + }
76 + expect(items).toEqual(['A', 'B', 'C']);
77 + });
78 +});
packages/react-server/src/ReactFlightReplyServer.js
+5
@@ -397,6 +397,11 @@ function parseModelString(
397 key,
398 );
399 }
400 + case 'u': {
401 + // matches "$undefined"
402 + // Special encoding for `undefined` which can't be serialized as JSON otherwise.
403 + return undefined;
404 + }
405 default: {
406 // We assume that anything else is a reference ID.
407 const id = parseInt(value.substring(1), 16);