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

[Flight] Serialize weird numbers (#26623)

Sophie Alpert committed Apr 14, 2023 at 09:28 UTC ab2385fa388f3d4ebc0b1a9f5a747c82e4aa82ed
6 files changed +114 -2
packages/react-client/src/ReactFlightClient.js
+16
@@ -559,6 +559,22 @@ export function parseModelString(
559 throw chunk.reason;
560 }
561 }
562 + case 'I': {
563 + // $Infinity
564 + return Infinity;
565 + }
566 + case '-': {
567 + // $-0 or $-Infinity
568 + if (value[2] === '0') {
569 + return -0;
570 + } else {
571 + return -Infinity;
572 + }
573 + }
574 + case 'N': {
575 + // $NaN
576 + return NaN;
577 + }
578 case 'u': {
579 // matches "$undefined"
580 // Special encoding for `undefined` which can't be serialized as JSON otherwise.
packages/react-client/src/ReactFlightReplyClient.js
+23 -1
@@ -71,6 +71,24 @@ function serializeSymbolReference(name: string): string {
71 return '$S' + name;
72 }
73
74 +function serializeNumber(number: number): string | number {
75 + if (Number.isFinite(number)) {
76 + if (number === 0 && 1 / number === -Infinity) {
77 + return '$-0';
78 + } else {
79 + return number;
80 + }
81 + } else {
82 + if (number === Infinity) {
83 + return '$Infinity';
84 + } else if (number === -Infinity) {
85 + return '$-Infinity';
86 + } else {
87 + return '$NaN';
88 + }
89 + }
90 +}
91 +
92 function serializeUndefined(): string {
93 return '$undefined';
94 }
@@ -224,10 +242,14 @@ export function processReply(
242 return escapeStringValue(value);
243 }
244
227 - if (typeof value === 'boolean' || typeof value === 'number') {
245 + if (typeof value === 'boolean') {
246 return value;
247 }
248
249 + if (typeof value === 'number') {
250 + return serializeNumber(value);
251 + }
252 +
253 if (typeof value === 'undefined') {
254 return serializeUndefined();
255 }
packages/react-client/src/__tests__/ReactFlight-test.js
+24
@@ -263,6 +263,30 @@ describe('ReactFlight', () => {
263 expect(ReactNoop).toMatchRenderedOutput(null);
264 });
265
266 + it('can transport weird numbers', async () => {
267 + const nums = [0, -0, Infinity, -Infinity, NaN];
268 + function ComponentClient({prop}) {
269 + expect(prop).not.toBe(nums);
270 + expect(prop).toEqual(nums);
271 + expect(prop.every((p, i) => Object.is(p, nums[i]))).toBe(true);
272 + return `prop: ${prop}`;
273 + }
274 + const Component = clientReference(ComponentClient);
275 +
276 + const model = <Component prop={nums} />;
277 +
278 + const transport = ReactNoopFlightServer.render(model);
279 +
280 + await act(async () => {
281 + ReactNoop.render(await ReactNoopFlightClient.read(transport));
282 + });
283 +
284 + expect(ReactNoop).toMatchRenderedOutput(
285 + // already checked -0 with expects above
286 + 'prop: 0,0,Infinity,-Infinity,NaN',
287 + );
288 + });
289 +
290 it('can transport BigInt', async () => {
291 function ComponentClient({prop}) {
292 return `prop: ${prop} (${typeof prop})`;
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js
+12
@@ -76,6 +76,18 @@ describe('ReactFlightDOMReply', () => {
76 expect(items).toEqual(['A', 'B', 'C']);
77 });
78
79 + it('can pass weird numbers as a reply', async () => {
80 + const nums = [0, -0, Infinity, -Infinity, NaN];
81 + const body = await ReactServerDOMClient.encodeReply(nums);
82 + const nums2 = await ReactServerDOMServer.decodeReply(
83 + body,
84 + webpackServerMap,
85 + );
86 +
87 + expect(nums).toEqual(nums2);
88 + expect(nums.every((n, i) => Object.is(n, nums2[i]))).toBe(true);
89 + });
90 +
91 it('can pass a BigInt as a reply', async () => {
92 const body = await ReactServerDOMClient.encodeReply(90071992547409910000n);
93 const n = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
packages/react-server/src/ReactFlightReplyServer.js
+16
@@ -397,6 +397,22 @@ function parseModelString(
397 key,
398 );
399 }
400 + case 'I': {
401 + // $Infinity
402 + return Infinity;
403 + }
404 + case '-': {
405 + // $-0 or $-Infinity
406 + if (value[2] === '0') {
407 + return -0;
408 + } else {
409 + return -Infinity;
410 + }
411 + }
412 + case 'N': {
413 + // $NaN
414 + return NaN;
415 + }
416 case 'u': {
417 // matches "$undefined"
418 // Special encoding for `undefined` which can't be serialized as JSON otherwise.
packages/react-server/src/ReactFlightServer.js
+23 -1
@@ -549,6 +549,24 @@ function serializeProviderReference(name: string): string {
549 return '$P' + name;
550 }
551
552 +function serializeNumber(number: number): string | number {
553 + if (Number.isFinite(number)) {
554 + if (number === 0 && 1 / number === -Infinity) {
555 + return '$-0';
556 + } else {
557 + return number;
558 + }
559 + } else {
560 + if (number === Infinity) {
561 + return '$Infinity';
562 + } else if (number === -Infinity) {
563 + return '$-Infinity';
564 + } else {
565 + return '$NaN';
566 + }
567 + }
568 +}
569 +
570 function serializeUndefined(): string {
571 return '$undefined';
572 }
@@ -877,10 +895,14 @@ export function resolveModelToJSON(
895 return escapeStringValue(value);
896 }
897
880 - if (typeof value === 'boolean' || typeof value === 'number') {
898 + if (typeof value === 'boolean') {
899 return value;
900 }
901
902 + if (typeof value === 'number') {
903 + return serializeNumber(value);
904 + }
905 +
906 if (typeof value === 'undefined') {
907 return serializeUndefined();
908 }