Skip to content

Commit 17e920c

Browse files
authored
[Flight Reply] Encode Typed Arrays and Blobs (#28819)
With the enableBinaryFlight flag on we should encode typed arrays and blobs in the Reply direction too for parity. It's already possible to pass Blobs inside FormData but you should be able to pass them inside objects too. We encode typed arrays as blobs and then unwrap them automatically to the right typed array type. Unlike the other protocol, I encode the type as a reference tag instead of row tag. Therefore I need to rename the tags to avoid conflicts with other tags in references. We are running out of characters though.
1 parent fd35655 commit 17e920c

File tree

5 files changed

+302
-46
lines changed

5 files changed

+302
-46
lines changed

packages/react-client/src/ReactFlightClient.js

+10-10
Original file line numberDiff line numberDiff line change
@@ -1262,10 +1262,10 @@ function processFullRow(
12621262
// We must always clone to extract it into a separate buffer instead of just a view.
12631263
resolveBuffer(response, id, mergeBuffer(buffer, chunk).buffer);
12641264
return;
1265-
case 67 /* "C" */:
1265+
case 79 /* "O" */:
12661266
resolveTypedArray(response, id, buffer, chunk, Int8Array, 1);
12671267
return;
1268-
case 99 /* "c" */:
1268+
case 111 /* "o" */:
12691269
resolveBuffer(
12701270
response,
12711271
id,
@@ -1287,13 +1287,13 @@ function processFullRow(
12871287
case 108 /* "l" */:
12881288
resolveTypedArray(response, id, buffer, chunk, Uint32Array, 4);
12891289
return;
1290-
case 70 /* "F" */:
1290+
case 71 /* "G" */:
12911291
resolveTypedArray(response, id, buffer, chunk, Float32Array, 4);
12921292
return;
1293-
case 100 /* "d" */:
1293+
case 103 /* "g" */:
12941294
resolveTypedArray(response, id, buffer, chunk, Float64Array, 8);
12951295
return;
1296-
case 78 /* "N" */:
1296+
case 77 /* "M" */:
12971297
resolveTypedArray(response, id, buffer, chunk, BigInt64Array, 8);
12981298
return;
12991299
case 109 /* "m" */:
@@ -1417,16 +1417,16 @@ export function processBinaryChunk(
14171417
resolvedRowTag === 84 /* "T" */ ||
14181418
(enableBinaryFlight &&
14191419
(resolvedRowTag === 65 /* "A" */ ||
1420-
resolvedRowTag === 67 /* "C" */ ||
1421-
resolvedRowTag === 99 /* "c" */ ||
1420+
resolvedRowTag === 79 /* "O" */ ||
1421+
resolvedRowTag === 111 /* "o" */ ||
14221422
resolvedRowTag === 85 /* "U" */ ||
14231423
resolvedRowTag === 83 /* "S" */ ||
14241424
resolvedRowTag === 115 /* "s" */ ||
14251425
resolvedRowTag === 76 /* "L" */ ||
14261426
resolvedRowTag === 108 /* "l" */ ||
1427-
resolvedRowTag === 70 /* "F" */ ||
1428-
resolvedRowTag === 100 /* "d" */ ||
1429-
resolvedRowTag === 78 /* "N" */ ||
1427+
resolvedRowTag === 71 /* "G" */ ||
1428+
resolvedRowTag === 103 /* "g" */ ||
1429+
resolvedRowTag === 77 /* "M" */ ||
14301430
resolvedRowTag === 109 /* "m" */ ||
14311431
resolvedRowTag === 86)) /* "V" */
14321432
) {

packages/react-client/src/ReactFlightReplyClient.js

+85-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import type {
1717
import type {LazyComponent} from 'react/src/ReactLazy';
1818
import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';
1919

20-
import {enableRenderableContext} from 'shared/ReactFeatureFlags';
20+
import {
21+
enableRenderableContext,
22+
enableBinaryFlight,
23+
} from 'shared/ReactFeatureFlags';
2124

2225
import {
2326
REACT_ELEMENT_TYPE,
@@ -150,6 +153,10 @@ function serializeSetID(id: number): string {
150153
return '$W' + id.toString(16);
151154
}
152155

156+
function serializeBlobID(id: number): string {
157+
return '$B' + id.toString(16);
158+
}
159+
153160
function escapeStringValue(value: string): string {
154161
if (value[0] === '$') {
155162
// We need to escape $ prefixed strings since we use those to encode
@@ -171,6 +178,19 @@ export function processReply(
171178
let pendingParts = 0;
172179
let formData: null | FormData = null;
173180

181+
function serializeTypedArray(
182+
tag: string,
183+
typedArray: ArrayBuffer | $ArrayBufferView,
184+
): string {
185+
const blob = new Blob([typedArray]);
186+
const blobId = nextPartId++;
187+
if (formData === null) {
188+
formData = new FormData();
189+
}
190+
formData.append(formFieldPrefix + blobId, blob);
191+
return '$' + tag + blobId.toString(16);
192+
}
193+
174194
function resolveToJSON(
175195
this:
176196
| {+[key: string | number]: ReactServerValue}
@@ -362,6 +382,70 @@ export function processReply(
362382
formData.append(formFieldPrefix + setId, partJSON);
363383
return serializeSetID(setId);
364384
}
385+
386+
if (enableBinaryFlight) {
387+
if (value instanceof ArrayBuffer) {
388+
return serializeTypedArray('A', value);
389+
}
390+
if (value instanceof Int8Array) {
391+
// char
392+
return serializeTypedArray('O', value);
393+
}
394+
if (value instanceof Uint8Array) {
395+
// unsigned char
396+
return serializeTypedArray('o', value);
397+
}
398+
if (value instanceof Uint8ClampedArray) {
399+
// unsigned clamped char
400+
return serializeTypedArray('U', value);
401+
}
402+
if (value instanceof Int16Array) {
403+
// sort
404+
return serializeTypedArray('S', value);
405+
}
406+
if (value instanceof Uint16Array) {
407+
// unsigned short
408+
return serializeTypedArray('s', value);
409+
}
410+
if (value instanceof Int32Array) {
411+
// long
412+
return serializeTypedArray('L', value);
413+
}
414+
if (value instanceof Uint32Array) {
415+
// unsigned long
416+
return serializeTypedArray('l', value);
417+
}
418+
if (value instanceof Float32Array) {
419+
// float
420+
return serializeTypedArray('G', value);
421+
}
422+
if (value instanceof Float64Array) {
423+
// double
424+
return serializeTypedArray('g', value);
425+
}
426+
if (value instanceof BigInt64Array) {
427+
// number
428+
return serializeTypedArray('M', value);
429+
}
430+
if (value instanceof BigUint64Array) {
431+
// unsigned number
432+
// We use "m" instead of "n" since JSON can start with "null"
433+
return serializeTypedArray('m', value);
434+
}
435+
if (value instanceof DataView) {
436+
return serializeTypedArray('V', value);
437+
}
438+
// TODO: Blob is not available in old Node/browsers. Remove the typeof check later.
439+
if (typeof Blob === 'function' && value instanceof Blob) {
440+
if (formData === null) {
441+
formData = new FormData();
442+
}
443+
const blobId = nextPartId++;
444+
formData.append(formFieldPrefix + blobId, value);
445+
return serializeBlobID(blobId);
446+
}
447+
}
448+
365449
const iteratorFn = getIteratorFn(value);
366450
if (iteratorFn) {
367451
return Array.from((value: any));

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js

+93
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*
77
* @emails react-core
8+
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
89
*/
910

1011
'use strict';
@@ -15,6 +16,13 @@ global.ReadableStream =
1516
global.TextEncoder = require('util').TextEncoder;
1617
global.TextDecoder = require('util').TextDecoder;
1718

19+
if (typeof Blob === 'undefined') {
20+
global.Blob = require('buffer').Blob;
21+
}
22+
if (typeof File === 'undefined') {
23+
global.File = require('buffer').File;
24+
}
25+
1826
// let serverExports;
1927
let webpackServerMap;
2028
let ReactServerDOMServer;
@@ -36,6 +44,13 @@ describe('ReactFlightDOMReplyEdge', () => {
3644
ReactServerDOMClient = require('react-server-dom-webpack/client.edge');
3745
});
3846

47+
if (typeof FormData === 'undefined') {
48+
// We can't test if we don't have a native FormData implementation because the JSDOM one
49+
// is missing the arrayBuffer() method.
50+
it('cannot test', () => {});
51+
return;
52+
}
53+
3954
it('can encode a reply', async () => {
4055
const body = await ReactServerDOMClient.encodeReply({some: 'object'});
4156
const decoded = await ReactServerDOMServer.decodeReply(
@@ -45,4 +60,82 @@ describe('ReactFlightDOMReplyEdge', () => {
4560

4661
expect(decoded).toEqual({some: 'object'});
4762
});
63+
64+
// @gate enableBinaryFlight
65+
it('should be able to serialize any kind of typed array', async () => {
66+
const buffer = new Uint8Array([
67+
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
68+
]).buffer;
69+
const buffers = [
70+
buffer,
71+
new Int8Array(buffer, 1),
72+
new Uint8Array(buffer, 2),
73+
new Uint8ClampedArray(buffer, 2),
74+
new Int16Array(buffer, 2),
75+
new Uint16Array(buffer, 2),
76+
new Int32Array(buffer, 4),
77+
new Uint32Array(buffer, 4),
78+
new Float32Array(buffer, 4),
79+
new Float64Array(buffer, 0),
80+
new BigInt64Array(buffer, 0),
81+
new BigUint64Array(buffer, 0),
82+
new DataView(buffer, 3),
83+
];
84+
85+
const body = await ReactServerDOMClient.encodeReply(buffers);
86+
const result = await ReactServerDOMServer.decodeReply(
87+
body,
88+
webpackServerMap,
89+
);
90+
91+
expect(result).toEqual(buffers);
92+
});
93+
94+
// @gate enableBinaryFlight
95+
it('should be able to serialize a blob', async () => {
96+
const bytes = new Uint8Array([
97+
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
98+
]);
99+
const blob = new Blob([bytes, bytes], {
100+
type: 'application/x-test',
101+
});
102+
const body = await ReactServerDOMClient.encodeReply(blob);
103+
const result = await ReactServerDOMServer.decodeReply(
104+
body,
105+
webpackServerMap,
106+
);
107+
expect(result instanceof Blob).toBe(true);
108+
expect(result.size).toBe(bytes.length * 2);
109+
expect(await result.arrayBuffer()).toEqual(await blob.arrayBuffer());
110+
});
111+
112+
it('can transport FormData (blobs)', async () => {
113+
const bytes = new Uint8Array([
114+
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
115+
]);
116+
const blob = new Blob([bytes, bytes], {
117+
type: 'application/x-test',
118+
});
119+
120+
const formData = new FormData();
121+
formData.append('hi', 'world');
122+
formData.append('file', blob, 'filename.test');
123+
124+
expect(formData.get('file') instanceof File).toBe(true);
125+
expect(formData.get('file').name).toBe('filename.test');
126+
127+
const body = await ReactServerDOMClient.encodeReply(formData);
128+
const result = await ReactServerDOMServer.decodeReply(
129+
body,
130+
webpackServerMap,
131+
);
132+
133+
expect(result instanceof FormData).toBe(true);
134+
expect(result.get('hi')).toBe('world');
135+
const resultBlob = result.get('file');
136+
expect(resultBlob instanceof Blob).toBe(true);
137+
expect(resultBlob.name).toBe('filename.test'); // In this direction we allow file name to pass through but not other direction.
138+
expect(resultBlob.size).toBe(bytes.length * 2);
139+
expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
140+
});
48141
});

0 commit comments

Comments
 (0)