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

[Flight] Add Serialization of Typed Arrays / ArrayBuffer / DataView (#26954)

This uses the same mechanism as [large strings](https://github.com/facebook/react/pull/26932) to encode chunks of length based binary data in the RSC payload behind a flag. I introduce a new BinaryChunk type that's specific to each stream and ways to convert into it. That's because we sometimes need all chunks to be Uint8Array for the output, even if the source is another array buffer view, and sometimes we need to clone it before transferring. Each type of typed array is its own row tag. This lets us ensure that the instance is directly in the right format in the cached entry instead of creating a wrapper at each reference. Ideally this is also how Map/Set should work but those are lazy which complicates that approach a bit. We assume both server and client use little-endian for now. If we want to support other modes, we'd convert it to/from little-endian so that the transfer protocol is always little-endian. That way the common clients can be the fastest possible. So far this only implements Server to Client. Still need to implement Client to Server for parity. NOTE: This is the first time we make RSC effectively a binary format. This is not compatible with existing SSR techniques which serialize the stream as unicode in the HTML. To be compatible, those implementations would have to use base64 or something like that. Which is what we'll do when we move this technique to be built-in to Fizz.

Sebastian Markbåge committed Jun 29, 2023 at 13:16 UTC d9c333199ed19798484e49eef992735321c32cb9
25 files changed +517 -66
.eslintrc.js
+3
@@ -454,11 +454,14 @@ module.exports = {
454 $PropertyType: 'readonly',
455 $ReadOnly: 'readonly',
456 $ReadOnlyArray: 'readonly',
457 + $ArrayBufferView: 'readonly',
458 $Shape: 'readonly',
459 AnimationFrameID: 'readonly',
460 // For Flow type annotation. Only `BigInt` is valid at runtime.
461 bigint: 'readonly',
462 BigInt: 'readonly',
463 + BigInt64Array: 'readonly',
464 + BigUint64Array: 'readonly',
465 Class: 'readonly',
466 ClientRect: 'readonly',
467 CopyInspectedElementPath: 'readonly',
packages/react-client/src/ReactFlightClient.js
+143 -11
@@ -21,6 +21,8 @@ import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
21
22 import type {CallServerCallback} from './ReactFlightReplyClient';
23
24 +import {enableBinaryFlight} from 'shared/ReactFeatureFlags';
25 +
26 import {
27 resolveClientReference,
28 preloadModule,
@@ -297,6 +299,14 @@ function createInitializedTextChunk(
299 return new Chunk(INITIALIZED, value, null, response);
300 }
301
302 +function createInitializedBufferChunk(
303 + response: Response,
304 + value: $ArrayBufferView | ArrayBuffer,
305 +): InitializedChunk<Uint8Array> {
306 + // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
307 + return new Chunk(INITIALIZED, value, null, response);
308 +}
309 +
310 function resolveModelChunk<T>(
311 chunk: SomeChunk<T>,
312 value: UninitializedModel,
@@ -738,6 +748,16 @@ function resolveText(response: Response, id: number, text: string): void {
748 chunks.set(id, createInitializedTextChunk(response, text));
749 }
750
751 +function resolveBuffer(
752 + response: Response,
753 + id: number,
754 + buffer: $ArrayBufferView | ArrayBuffer,
755 +): void {
756 + const chunks = response._chunks;
757 + // We assume that we always reference buffers after they've been emitted.
758 + chunks.set(id, createInitializedBufferChunk(response, buffer));
759 +}
760 +
761 function resolveModule(
762 response: Response,
763 id: number,
@@ -856,24 +876,120 @@ function resolveHint(
876 dispatchHint(code, hintModel);
877 }
878
879 +function mergeBuffer(
880 + buffer: Array<Uint8Array>,
881 + lastChunk: Uint8Array,
882 +): Uint8Array {
883 + const l = buffer.length;
884 + // Count the bytes we'll need
885 + let byteLength = lastChunk.length;
886 + for (let i = 0; i < l; i++) {
887 + byteLength += buffer[i].byteLength;
888 + }
889 + // Allocate enough contiguous space
890 + const result = new Uint8Array(byteLength);
891 + let offset = 0;
892 + // Copy all the buffers into it.
893 + for (let i = 0; i < l; i++) {
894 + const chunk = buffer[i];
895 + result.set(chunk, offset);
896 + offset += chunk.byteLength;
897 + }
898 + result.set(lastChunk, offset);
899 + return result;
900 +}
901 +
902 +function resolveTypedArray(
903 + response: Response,
904 + id: number,
905 + buffer: Array<Uint8Array>,
906 + lastChunk: Uint8Array,
907 + constructor: any,
908 + bytesPerElement: number,
909 +): void {
910 + // If the view fits into one original buffer, we just reuse that buffer instead of
911 + // copying it out to a separate copy. This means that it's not always possible to
912 + // transfer these values to other threads without copying first since they may
913 + // share array buffer. For this to work, it must also have bytes aligned to a
914 + // multiple of a size of the type.
915 + const chunk =
916 + buffer.length === 0 && lastChunk.byteOffset % bytesPerElement === 0
917 + ? lastChunk
918 + : mergeBuffer(buffer, lastChunk);
919 + // TODO: The transfer protocol of RSC is little-endian. If the client isn't little-endian
920 + // we should convert it instead. In practice big endian isn't really Web compatible so it's
921 + // somewhat safe to assume that browsers aren't going to run it, but maybe there's some SSR
922 + // server that's affected.
923 + const view: $ArrayBufferView = new constructor(
924 + chunk.buffer,
925 + chunk.byteOffset,
926 + chunk.byteLength / bytesPerElement,
927 + );
928 + resolveBuffer(response, id, view);
929 +}
930 +
931 function processFullRow(
932 response: Response,
933 id: number,
934 tag: number,
935 buffer: Array<Uint8Array>,
864 - lastChunk: string | Uint8Array,
936 + chunk: Uint8Array,
937 ): void {
866 - let row = '';
938 + if (enableBinaryFlight) {
939 + switch (tag) {
940 + case 65 /* "A" */:
941 + // We must always clone to extract it into a separate buffer instead of just a view.
942 + resolveBuffer(response, id, mergeBuffer(buffer, chunk).buffer);
943 + return;
944 + case 67 /* "C" */:
945 + resolveTypedArray(response, id, buffer, chunk, Int8Array, 1);
946 + return;
947 + case 99 /* "c" */:
948 + resolveBuffer(
949 + response,
950 + id,
951 + buffer.length === 0 ? chunk : mergeBuffer(buffer, chunk),
952 + );
953 + return;
954 + case 85 /* "U" */:
955 + resolveTypedArray(response, id, buffer, chunk, Uint8ClampedArray, 1);
956 + return;
957 + case 83 /* "S" */:
958 + resolveTypedArray(response, id, buffer, chunk, Int16Array, 2);
959 + return;
960 + case 115 /* "s" */:
961 + resolveTypedArray(response, id, buffer, chunk, Uint16Array, 2);
962 + return;
963 + case 76 /* "L" */:
964 + resolveTypedArray(response, id, buffer, chunk, Int32Array, 4);
965 + return;
966 + case 108 /* "l" */:
967 + resolveTypedArray(response, id, buffer, chunk, Uint32Array, 4);
968 + return;
969 + case 70 /* "F" */:
970 + resolveTypedArray(response, id, buffer, chunk, Float32Array, 4);
971 + return;
972 + case 68 /* "D" */:
973 + resolveTypedArray(response, id, buffer, chunk, Float64Array, 8);
974 + return;
975 + case 78 /* "N" */:
976 + resolveTypedArray(response, id, buffer, chunk, BigInt64Array, 8);
977 + return;
978 + case 109 /* "m" */:
979 + resolveTypedArray(response, id, buffer, chunk, BigUint64Array, 8);
980 + return;
981 + case 86 /* "V" */:
982 + resolveTypedArray(response, id, buffer, chunk, DataView, 1);
983 + return;
984 + }
985 + }
986 +
987 const stringDecoder = response._stringDecoder;
988 + let row = '';
989 for (let i = 0; i < buffer.length; i++) {
869 - const chunk = buffer[i];
870 - row += readPartialStringChunk(stringDecoder, chunk);
871 - }
872 - if (typeof lastChunk === 'string') {
873 - row += lastChunk;
874 - } else {
875 - row += readFinalStringChunk(stringDecoder, lastChunk);
990 + row += readPartialStringChunk(stringDecoder, buffer[i]);
991 }
992 + row += readFinalStringChunk(stringDecoder, chunk);
993 switch (tag) {
994 case 73 /* "I" */: {
995 resolveModule(response, id, row);
@@ -903,7 +1019,7 @@ function processFullRow(
1019 resolveText(response, id, row);
1020 return;
1021 }
906 - default: {
1022 + default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
1023 // We assume anything else is JSON.
1024 resolveModel(response, id, row);
1025 return;
@@ -937,7 +1053,23 @@ export function processBinaryChunk(
1053 }
1054 case ROW_TAG: {
1055 const resolvedRowTag = chunk[i];
940 - if (resolvedRowTag === 84 /* "T" */) {
1056 + if (
1057 + resolvedRowTag === 84 /* "T" */ ||
1058 + (enableBinaryFlight &&
1059 + (resolvedRowTag === 65 /* "A" */ ||
1060 + resolvedRowTag === 67 /* "C" */ ||
1061 + resolvedRowTag === 99 /* "c" */ ||
1062 + resolvedRowTag === 85 /* "U" */ ||
1063 + resolvedRowTag === 83 /* "S" */ ||
1064 + resolvedRowTag === 115 /* "s" */ ||
1065 + resolvedRowTag === 76 /* "L" */ ||
1066 + resolvedRowTag === 108 /* "l" */ ||
1067 + resolvedRowTag === 70 /* "F" */ ||
1068 + resolvedRowTag === 68 /* "D" */ ||
1069 + resolvedRowTag === 78 /* "N" */ ||
1070 + resolvedRowTag === 109 /* "m" */ ||
1071 + resolvedRowTag === 86)) /* "V" */
1072 + ) {
1073 rowTag = resolvedRowTag;
1074 rowState = ROW_LENGTH;
1075 i++;
packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js
+13 -2
@@ -14,6 +14,7 @@ export interface Destination {
14
15 export opaque type PrecomputedChunk = string;
16 export opaque type Chunk = string;
17 +export opaque type BinaryChunk = string;
18
19 export function scheduleWork(callback: () => void) {
20 callback();
@@ -25,14 +26,14 @@ export function beginWriting(destination: Destination) {}
26
27 export function writeChunk(
28 destination: Destination,
28 - chunk: Chunk | PrecomputedChunk,
29 + chunk: Chunk | PrecomputedChunk | BinaryChunk,
30 ): void {
31 writeChunkAndReturn(destination, chunk);
32 }
33
34 export function writeChunkAndReturn(
35 destination: Destination,
35 - chunk: Chunk | PrecomputedChunk,
36 + chunk: Chunk | PrecomputedChunk | BinaryChunk,
37 ): boolean {
38 return destination.push(chunk);
39 }
@@ -51,6 +52,12 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
52 return content;
53 }
54
55 +export function typedArrayToBinaryChunk(
56 + content: $ArrayBufferView,
57 +): BinaryChunk {
58 + throw new Error('Not implemented.');
59 +}
60 +
61 export function clonePrecomputedChunk(
62 chunk: PrecomputedChunk,
63 ): PrecomputedChunk {
@@ -61,6 +68,10 @@ export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
68 throw new Error('Not implemented.');
69 }
70
71 +export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number {
72 + throw new Error('Not implemented.');
73 +}
74 +
75 export function closeWithError(destination: Destination, error: mixed): void {
76 // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
77 destination.destroy(error);
packages/react-server-dom-fb/src/ReactServerStreamConfigFB.js
+13 -2
@@ -16,6 +16,7 @@ export type Destination = {
16
17 export opaque type PrecomputedChunk = string;
18 export opaque type Chunk = string;
19 +export opaque type BinaryChunk = string;
20
21 export function scheduleWork(callback: () => void) {
22 // We don't schedule work in this model, and instead expect performWork to always be called repeatedly.
@@ -30,14 +31,14 @@ export function beginWriting(destination: Destination) {}
31
32 export function writeChunk(
33 destination: Destination,
33 - chunk: Chunk | PrecomputedChunk,
34 + chunk: Chunk | PrecomputedChunk | BinaryChunk,
35 ): void {
36 destination.buffer += chunk;
37 }
38
39 export function writeChunkAndReturn(
40 destination: Destination,
40 - chunk: Chunk | PrecomputedChunk,
41 + chunk: Chunk | PrecomputedChunk | BinaryChunk,
42 ): boolean {
43 destination.buffer += chunk;
44 return true;
@@ -57,6 +58,12 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
58 return content;
59 }
60
61 +export function typedArrayToBinaryChunk(
62 + content: $ArrayBufferView,
63 +): BinaryChunk {
64 + throw new Error('Not implemented.');
65 +}
66 +
67 export function clonePrecomputedChunk(
68 chunk: PrecomputedChunk,
69 ): PrecomputedChunk {
@@ -67,6 +74,10 @@ export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
74 throw new Error('Not implemented.');
75 }
76
77 +export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number {
78 + throw new Error('Not implemented.');
79 +}
80 +
81 export function closeWithError(destination: Destination, error: mixed): void {
82 destination.done = true;
83 destination.fatal = true;
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+27
@@ -153,4 +153,31 @@ describe('ReactFlightDOMEdge', () => {
153 expect(result.text).toBe(testString);
154 expect(result.text2).toBe(testString2);
155 });
156 +
157 + // @gate enableBinaryFlight
158 + it('should be able to serialize any kind of typed array', async () => {
159 + const buffer = new Uint8Array([
160 + 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
161 + ]).buffer;
162 + const buffers = [
163 + buffer,
164 + new Int8Array(buffer, 1),
165 + new Uint8Array(buffer, 2),
166 + new Uint8ClampedArray(buffer, 2),
167 + new Int16Array(buffer, 2),
168 + new Uint16Array(buffer, 2),
169 + new Int32Array(buffer, 4),
170 + new Uint32Array(buffer, 4),
171 + new Float32Array(buffer, 4),
172 + new Float64Array(buffer, 0),
173 + new BigInt64Array(buffer, 0),
174 + new BigUint64Array(buffer, 0),
175 + new DataView(buffer, 3),
176 + ];
177 + const stream = passThrough(
178 + ReactServerDOMServer.renderToReadableStream(buffers),
179 + );
180 + const result = await ReactServerDOMClient.createFromReadableStream(stream);
181 + expect(result).toEqual(buffers);
182 + });
183 });
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+28
@@ -131,4 +131,32 @@ describe('ReactFlightDOMNode', () => {
131 // Should still match the result when parsed
132 expect(result.text).toBe(testString);
133 });
134 +
135 + // @gate enableBinaryFlight
136 + it('should be able to serialize any kind of typed array', async () => {
137 + const buffer = new Uint8Array([
138 + 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
139 + ]).buffer;
140 + const buffers = [
141 + buffer,
142 + new Int8Array(buffer, 1),
143 + new Uint8Array(buffer, 2),
144 + new Uint8ClampedArray(buffer, 2),
145 + new Int16Array(buffer, 2),
146 + new Uint16Array(buffer, 2),
147 + new Int32Array(buffer, 4),
148 + new Uint32Array(buffer, 4),
149 + new Float32Array(buffer, 4),
150 + new Float64Array(buffer, 0),
151 + new BigInt64Array(buffer, 0),
152 + new BigUint64Array(buffer, 0),
153 + new DataView(buffer, 3),
154 + ];
155 + const stream = ReactServerDOMServer.renderToPipeableStream(buffers);
156 + const readable = new Stream.PassThrough();
157 + const promise = ReactServerDOMClient.createFromNodeStream(readable);
158 + stream.pipe(readable);
159 + const result = await promise;
160 + expect(result).toEqual(buffers);
161 + });
162 });
packages/react-server/src/ReactFlightServer.js
+92 -5
@@ -7,7 +7,9 @@
7 * @flow
8 */
9
10 -import type {Chunk, Destination} from './ReactServerStreamConfig';
10 +import type {Chunk, BinaryChunk, Destination} from './ReactServerStreamConfig';
11 +
12 +import {enableBinaryFlight} from 'shared/ReactFeatureFlags';
13
14 import {
15 scheduleWork,
@@ -15,7 +17,9 @@ import {
17 beginWriting,
18 writeChunkAndReturn,
19 stringToChunk,
20 + typedArrayToBinaryChunk,
21 byteLengthOfChunk,
22 + byteLengthOfBinaryChunk,
23 completeWriting,
24 close,
25 closeWithError,
@@ -176,7 +180,7 @@ export type Request = {
180 pingedTasks: Array<Task>,
181 completedImportChunks: Array<Chunk>,
182 completedHintChunks: Array<Chunk>,
179 - completedRegularChunks: Array<Chunk>,
183 + completedRegularChunks: Array<Chunk | BinaryChunk>,
184 completedErrorChunks: Array<Chunk>,
185 writtenSymbols: Map<symbol, number>,
186 writtenClientReferences: Map<ClientReferenceKey, number>,
@@ -235,7 +239,7 @@ export function createRequest(
239 pingedTasks: pingedTasks,
240 completedImportChunks: ([]: Array<Chunk>),
241 completedHintChunks: ([]: Array<Chunk>),
238 - completedRegularChunks: ([]: Array<Chunk>),
242 + completedRegularChunks: ([]: Array<Chunk | BinaryChunk>),
243 completedErrorChunks: ([]: Array<Chunk>),
244 writtenSymbols: new Map(),
245 writtenClientReferences: new Map(),
@@ -733,7 +737,6 @@ function serializeLargeTextString(request: Request, text: string): string {
737 const headerChunk = processTextHeader(
738 request,
739 textId,
736 - text,
740 byteLengthOfChunk(textChunk),
741 );
742 request.completedRegularChunks.push(headerChunk, textChunk);
@@ -753,6 +756,25 @@ function serializeSet(request: Request, set: Set<ReactClientValue>): string {
756 return '$W' + id.toString(16);
757 }
758
759 +function serializeTypedArray(
760 + request: Request,
761 + tag: string,
762 + typedArray: $ArrayBufferView,
763 +): string {
764 + request.pendingChunks += 2;
765 + const bufferId = request.nextChunkId++;
766 + // TODO: Convert to little endian if that's not the server default.
767 + const binaryChunk = typedArrayToBinaryChunk(typedArray);
768 + const headerChunk = processBufferHeader(
769 + request,
770 + tag,
771 + bufferId,
772 + byteLengthOfBinaryChunk(binaryChunk),
773 + );
774 + request.completedRegularChunks.push(headerChunk, binaryChunk);
775 + return serializeByValueID(bufferId);
776 +}
777 +
778 function escapeStringValue(value: string): string {
779 if (value[0] === '$') {
780 // We need to escape $ prefixed strings since we use those to encode
@@ -942,12 +964,68 @@ function resolveModelToJSON(
964 }
965 return (undefined: any);
966 }
967 +
968 if (value instanceof Map) {
969 return serializeMap(request, value);
970 }
971 if (value instanceof Set) {
972 return serializeSet(request, value);
973 }
974 +
975 + if (enableBinaryFlight) {
976 + if (value instanceof ArrayBuffer) {
977 + return serializeTypedArray(request, 'A', new Uint8Array(value));
978 + }
979 + if (value instanceof Int8Array) {
980 + // char
981 + return serializeTypedArray(request, 'C', value);
982 + }
983 + if (value instanceof Uint8Array) {
984 + // unsigned char
985 + return serializeTypedArray(request, 'c', value);
986 + }
987 + if (value instanceof Uint8ClampedArray) {
988 + // unsigned clamped char
989 + return serializeTypedArray(request, 'U', value);
990 + }
991 + if (value instanceof Int16Array) {
992 + // sort
993 + return serializeTypedArray(request, 'S', value);
994 + }
995 + if (value instanceof Uint16Array) {
996 + // unsigned short
997 + return serializeTypedArray(request, 's', value);
998 + }
999 + if (value instanceof Int32Array) {
1000 + // long
1001 + return serializeTypedArray(request, 'L', value);
1002 + }
1003 + if (value instanceof Uint32Array) {
1004 + // unsigned long
1005 + return serializeTypedArray(request, 'l', value);
1006 + }
1007 + if (value instanceof Float32Array) {
1008 + // float
1009 + return serializeTypedArray(request, 'F', value);
1010 + }
1011 + if (value instanceof Float64Array) {
1012 + // double
1013 + return serializeTypedArray(request, 'D', value);
1014 + }
1015 + if (value instanceof BigInt64Array) {
1016 + // number
1017 + return serializeTypedArray(request, 'N', value);
1018 + }
1019 + if (value instanceof BigUint64Array) {
1020 + // unsigned number
1021 + // We use "m" instead of "n" since JSON can start with "null"
1022 + return serializeTypedArray(request, 'm', value);
1023 + }
1024 + if (value instanceof DataView) {
1025 + return serializeTypedArray(request, 'V', value);
1026 + }
1027 + }
1028 +
1029 if (!isArray(value)) {
1030 const iteratorFn = getIteratorFn(value);
1031 if (iteratorFn) {
@@ -1593,9 +1671,18 @@ function processHintChunk(
1671 function processTextHeader(
1672 request: Request,
1673 id: number,
1596 - text: string,
1674 binaryLength: number,
1675 ): Chunk {
1676 const row = id.toString(16) + ':T' + binaryLength.toString(16) + ',';
1677 return stringToChunk(row);
1678 }
1679 +
1680 +function processBufferHeader(
1681 + request: Request,
1682 + tag: string,
1683 + id: number,
1684 + binaryLength: number,
1685 +): Chunk {
1686 + const row = id.toString(16) + ':' + tag + binaryLength.toString(16) + ',';
1687 + return stringToChunk(row);
1688 +}
packages/react-server/src/ReactServerStreamConfigBrowser.js
+32 -8
@@ -11,6 +11,7 @@ export type Destination = ReadableStreamController;
11
12 export type PrecomputedChunk = Uint8Array;
13 export opaque type Chunk = Uint8Array;
14 +export type BinaryChunk = Uint8Array;
15
16 export function scheduleWork(callback: () => void) {
17 callback();
@@ -32,13 +33,13 @@ export function beginWriting(destination: Destination) {
33
34 export function writeChunk(
35 destination: Destination,
35 - chunk: PrecomputedChunk | Chunk,
36 + chunk: PrecomputedChunk | Chunk | BinaryChunk,
37 ): void {
37 - if (chunk.length === 0) {
38 + if (chunk.byteLength === 0) {
39 return;
40 }
41
41 - if (chunk.length > VIEW_SIZE) {
42 + if (chunk.byteLength > VIEW_SIZE) {
43 if (__DEV__) {
44 if (precomputedChunkSet.has(chunk)) {
45 console.error(
@@ -68,7 +69,7 @@ export function writeChunk(
69
70 let bytesToWrite = chunk;
71 const allowableBytes = ((currentView: any): Uint8Array).length - writtenBytes;
71 - if (allowableBytes < bytesToWrite.length) {
72 + if (allowableBytes < bytesToWrite.byteLength) {
73 // this chunk would overflow the current view. We enqueue a full view
74 // and start a new view with the remaining chunk
75 if (allowableBytes === 0) {
@@ -89,12 +90,12 @@ export function writeChunk(
90 writtenBytes = 0;
91 }
92 ((currentView: any): Uint8Array).set(bytesToWrite, writtenBytes);
92 - writtenBytes += bytesToWrite.length;
93 + writtenBytes += bytesToWrite.byteLength;
94 }
95
96 export function writeChunkAndReturn(
97 destination: Destination,
97 - chunk: PrecomputedChunk | Chunk,
98 + chunk: PrecomputedChunk | Chunk | BinaryChunk,
99 ): boolean {
100 writeChunk(destination, chunk);
101 // in web streams there is no backpressure so we can alwas write more
@@ -119,7 +120,9 @@ export function stringToChunk(content: string): Chunk {
120 return textEncoder.encode(content);
121 }
122
122 -const precomputedChunkSet: Set<Chunk> = __DEV__ ? new Set() : (null: any);
123 +const precomputedChunkSet: Set<Chunk | BinaryChunk> = __DEV__
124 + ? new Set()
125 + : (null: any);
126
127 export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
128 const precomputedChunk = textEncoder.encode(content);
@@ -131,10 +134,27 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
134 return precomputedChunk;
135 }
136
137 +export function typedArrayToBinaryChunk(
138 + content: $ArrayBufferView,
139 +): BinaryChunk {
140 + // Convert any non-Uint8Array array to Uint8Array. We could avoid this for Uint8Arrays.
141 + // If we passed through this straight to enqueue we wouldn't have to convert it but since
142 + // we need to copy the buffer in that case, we need to convert it to copy it.
143 + // When we copy it into another array using set() it needs to be a Uint8Array.
144 + const buffer = new Uint8Array(
145 + content.buffer,
146 + content.byteOffset,
147 + content.byteLength,
148 + );
149 + // We clone large chunks so that we can transfer them when we write them.
150 + // Others get copied into the target buffer.
151 + return content.byteLength > VIEW_SIZE ? buffer.slice() : buffer;
152 +}
153 +
154 export function clonePrecomputedChunk(
155 precomputedChunk: PrecomputedChunk,
156 ): PrecomputedChunk {
137 - return precomputedChunk.length > VIEW_SIZE
157 + return precomputedChunk.byteLength > VIEW_SIZE
158 ? precomputedChunk.slice()
159 : precomputedChunk;
160 }
@@ -143,6 +163,10 @@ export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
163 return chunk.byteLength;
164 }
165
166 +export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number {
167 + return chunk.byteLength;
168 +}
169 +
170 export function closeWithError(destination: Destination, error: mixed): void {
171 // $FlowFixMe[method-unbinding]
172 if (typeof destination.error === 'function') {
packages/react-server/src/ReactServerStreamConfigBun.js
+15 -3
@@ -9,13 +9,14 @@
9
10 type BunReadableStreamController = ReadableStreamController & {
11 end(): mixed,
12 - write(data: Chunk): void,
12 + write(data: Chunk | BinaryChunk): void,
13 error(error: Error): void,
14 };
15 export type Destination = BunReadableStreamController;
16
17 export type PrecomputedChunk = string;
18 export opaque type Chunk = string;
19 +export type BinaryChunk = $ArrayBufferView;
20
21 export function scheduleWork(callback: () => void) {
22 callback();
@@ -30,7 +31,7 @@ export function beginWriting(destination: Destination) {}
31
32 export function writeChunk(
33 destination: Destination,
33 - chunk: PrecomputedChunk | Chunk,
34 + chunk: PrecomputedChunk | Chunk | BinaryChunk,
35 ): void {
36 if (chunk.length === 0) {
37 return;
@@ -41,7 +42,7 @@ export function writeChunk(
42
43 export function writeChunkAndReturn(
44 destination: Destination,
44 - chunk: PrecomputedChunk | Chunk,
45 + chunk: PrecomputedChunk | Chunk | BinaryChunk,
46 ): boolean {
47 return !!destination.write(chunk);
48 }
@@ -60,6 +61,13 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
61 return content;
62 }
63
64 +export function typedArrayToBinaryChunk(
65 + content: $ArrayBufferView,
66 +): BinaryChunk {
67 + // TODO: Does this needs to be cloned if it's transferred in enqueue()?
68 + return content;
69 +}
70 +
71 export function clonePrecomputedChunk(
72 chunk: PrecomputedChunk,
73 ): PrecomputedChunk {
@@ -70,6 +78,10 @@ export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
78 return Buffer.byteLength(chunk, 'utf8');
79 }
80
81 +export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number {
82 + return chunk.byteLength;
83 +}
84 +
85 export function closeWithError(destination: Destination, error: mixed): void {
86 if (typeof destination.error === 'function') {
87 // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
packages/react-server/src/ReactServerStreamConfigEdge.js
+32 -8
@@ -11,6 +11,7 @@ export type Destination = ReadableStreamController;
11
12 export type PrecomputedChunk = Uint8Array;
13 export opaque type Chunk = Uint8Array;
14 +export type BinaryChunk = Uint8Array;
15
16 export function scheduleWork(callback: () => void) {
17 setTimeout(callback, 0);
@@ -32,13 +33,13 @@ export function beginWriting(destination: Destination) {
33
34 export function writeChunk(
35 destination: Destination,
35 - chunk: PrecomputedChunk | Chunk,
36 + chunk: PrecomputedChunk | Chunk | BinaryChunk,
37 ): void {
37 - if (chunk.length === 0) {
38 + if (chunk.byteLength === 0) {
39 return;
40 }
41
41 - if (chunk.length > VIEW_SIZE) {
42 + if (chunk.byteLength > VIEW_SIZE) {
43 if (__DEV__) {
44 if (precomputedChunkSet.has(chunk)) {
45 console.error(
@@ -68,7 +69,7 @@ export function writeChunk(
69
70 let bytesToWrite = chunk;
71 const allowableBytes = ((currentView: any): Uint8Array).length - writtenBytes;
71 - if (allowableBytes < bytesToWrite.length) {
72 + if (allowableBytes < bytesToWrite.byteLength) {
73 // this chunk would overflow the current view. We enqueue a full view
74 // and start a new view with the remaining chunk
75 if (allowableBytes === 0) {
@@ -89,12 +90,12 @@ export function writeChunk(
90 writtenBytes = 0;
91 }
92 ((currentView: any): Uint8Array).set(bytesToWrite, writtenBytes);
92 - writtenBytes += bytesToWrite.length;
93 + writtenBytes += bytesToWrite.byteLength;
94 }
95
96 export function writeChunkAndReturn(
97 destination: Destination,
97 - chunk: PrecomputedChunk | Chunk,
98 + chunk: PrecomputedChunk | Chunk | BinaryChunk,
99 ): boolean {
100 writeChunk(destination, chunk);
101 // in web streams there is no backpressure so we can alwas write more
@@ -119,7 +120,9 @@ export function stringToChunk(content: string): Chunk {
120 return textEncoder.encode(content);
121 }
122
122 -const precomputedChunkSet: Set<Chunk> = __DEV__ ? new Set() : (null: any);
123 +const precomputedChunkSet: Set<Chunk | BinaryChunk> = __DEV__
124 + ? new Set()
125 + : (null: any);
126
127 export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
128 const precomputedChunk = textEncoder.encode(content);
@@ -131,10 +134,27 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
134 return precomputedChunk;
135 }
136
137 +export function typedArrayToBinaryChunk(
138 + content: $ArrayBufferView,
139 +): BinaryChunk {
140 + // Convert any non-Uint8Array array to Uint8Array. We could avoid this for Uint8Arrays.
141 + // If we passed through this straight to enqueue we wouldn't have to convert it but since
142 + // we need to copy the buffer in that case, we need to convert it to copy it.
143 + // When we copy it into another array using set() it needs to be a Uint8Array.
144 + const buffer = new Uint8Array(
145 + content.buffer,
146 + content.byteOffset,
147 + content.byteLength,
148 + );
149 + // We clone large chunks so that we can transfer them when we write them.
150 + // Others get copied into the target buffer.
151 + return content.byteLength > VIEW_SIZE ? buffer.slice() : buffer;
152 +}
153 +
154 export function clonePrecomputedChunk(
155 precomputedChunk: PrecomputedChunk,
156 ): PrecomputedChunk {
137 - return precomputedChunk.length > VIEW_SIZE
157 + return precomputedChunk.byteLength > VIEW_SIZE
158 ? precomputedChunk.slice()
159 : precomputedChunk;
160 }
@@ -143,6 +163,10 @@ export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
163 return chunk.byteLength;
164 }
165
166 +export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number {
167 + return chunk.byteLength;
168 +}
169 +
170 export function closeWithError(destination: Destination, error: mixed): void {
171 // $FlowFixMe[method-unbinding]
172 if (typeof destination.error === 'function') {
packages/react-server/src/ReactServerStreamConfigNode.js
+22 -4
@@ -19,6 +19,7 @@ export type Destination = Writable & MightBeFlushable;
19
20 export type PrecomputedChunk = Uint8Array;
21 export opaque type Chunk = string;
22 +export type BinaryChunk = Uint8Array;
23
24 export function scheduleWork(callback: () => void) {
25 setImmediate(callback);
@@ -89,7 +90,10 @@ function writeStringChunk(destination: Destination, stringChunk: string) {
90 }
91 }
92
92 -function writeViewChunk(destination: Destination, chunk: PrecomputedChunk) {
93 +function writeViewChunk(
94 + destination: Destination,
95 + chunk: PrecomputedChunk | BinaryChunk,
96 +) {
97 if (chunk.byteLength === 0) {
98 return;
99 }
@@ -152,16 +156,19 @@ function writeViewChunk(destination: Destination, chunk: PrecomputedChunk) {
156
157 export function writeChunk(
158 destination: Destination,
155 - chunk: PrecomputedChunk | Chunk,
159 + chunk: PrecomputedChunk | Chunk | BinaryChunk,
160 ): void {
161 if (typeof chunk === 'string') {
162 writeStringChunk(destination, chunk);
163 } else {
160 - writeViewChunk(destination, ((chunk: any): PrecomputedChunk));
164 + writeViewChunk(destination, ((chunk: any): PrecomputedChunk | BinaryChunk));
165 }
166 }
167
164 -function writeToDestination(destination: Destination, view: Uint8Array) {
168 +function writeToDestination(
169 + destination: Destination,
170 + view: string | Uint8Array,
171 +) {
172 const currentHasCapacity = destination.write(view);
173 destinationHasCapacity = destinationHasCapacity && currentHasCapacity;
174 }
@@ -207,6 +214,13 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
214 return precomputedChunk;
215 }
216
217 +export function typedArrayToBinaryChunk(
218 + content: $ArrayBufferView,
219 +): BinaryChunk {
220 + // Convert any non-Uint8Array array to Uint8Array. We could avoid this for Uint8Arrays.
221 + return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
222 +}
223 +
224 export function clonePrecomputedChunk(
225 precomputedChunk: PrecomputedChunk,
226 ): PrecomputedChunk {
@@ -221,6 +235,10 @@ export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
235 : chunk.byteLength;
236 }
237
238 +export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number {
239 + return chunk.byteLength;
240 +}
241 +
242 export function closeWithError(destination: Destination, error: mixed): void {
243 // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
244 destination.destroy(error);
packages/react-server/src/forks/ReactServerStreamConfig.custom.js
+3
@@ -28,6 +28,7 @@ export opaque type Destination = mixed; // eslint-disable-line no-undef
28
29 export opaque type PrecomputedChunk = mixed; // eslint-disable-line no-undef
30 export opaque type Chunk = mixed; // eslint-disable-line no-undef
31 +export opaque type BinaryChunk = mixed; // eslint-disable-line no-undef
32
33 export const scheduleWork = $$$config.scheduleWork;
34 export const beginWriting = $$$config.beginWriting;
@@ -39,5 +40,7 @@ export const close = $$$config.close;
40 export const closeWithError = $$$config.closeWithError;
41 export const stringToChunk = $$$config.stringToChunk;
42 export const stringToPrecomputedChunk = $$$config.stringToPrecomputedChunk;
43 +export const typedArrayToBinaryChunk = $$$config.typedArrayToBinaryChunk;
44 export const clonePrecomputedChunk = $$$config.clonePrecomputedChunk;
45 export const byteLengthOfChunk = $$$config.byteLengthOfChunk;
46 +export const byteLengthOfBinaryChunk = $$$config.byteLengthOfBinaryChunk;
packages/shared/ReactFeatureFlags.js
+2
@@ -84,6 +84,8 @@ export const enableFetchInstrumentation = true;
84
85 export const enableFormActions = __EXPERIMENTAL__;
86
87 +export const enableBinaryFlight = __EXPERIMENTAL__;
88 +
89 export const enableTransitionTracing = false;
90
91 // No known bugs, but needs performance testing
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -36,6 +36,7 @@ export const enableLegacyCache = false;
36 export const enableCacheElement = true;
37 export const enableFetchInstrumentation = false;
38 export const enableFormActions = true; // Doesn't affect Native
39 +export const enableBinaryFlight = true;
40 export const enableSchedulerDebugging = false;
41 export const debugRenderPhaseSideEffectsForStrictMode = true;
42 export const disableJavaScriptURLs = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -24,6 +24,7 @@ export const enableLegacyCache = false;
24 export const enableCacheElement = false;
25 export const enableFetchInstrumentation = false;
26 export const enableFormActions = true; // Doesn't affect Native
27 +export const enableBinaryFlight = true;
28 export const disableJavaScriptURLs = false;
29 export const disableCommentsAsDOMContainers = true;
30 export const disableInputAttributeSyncing = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -24,6 +24,7 @@ export const enableLegacyCache = __EXPERIMENTAL__;
24 export const enableCacheElement = __EXPERIMENTAL__;
25 export const enableFetchInstrumentation = true;
26 export const enableFormActions = true; // Doesn't affect Test Renderer
27 +export const enableBinaryFlight = true;
28 export const disableJavaScriptURLs = false;
29 export const disableCommentsAsDOMContainers = true;
30 export const disableInputAttributeSyncing = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -24,6 +24,7 @@ export const enableLegacyCache = false;
24 export const enableCacheElement = true;
25 export const enableFetchInstrumentation = false;
26 export const enableFormActions = true; // Doesn't affect Test Renderer
27 +export const enableBinaryFlight = true;
28 export const disableJavaScriptURLs = false;
29 export const disableCommentsAsDOMContainers = true;
30 export const disableInputAttributeSyncing = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -24,6 +24,7 @@ export const enableLegacyCache = true;
24 export const enableCacheElement = true;
25 export const enableFetchInstrumentation = false;
26 export const enableFormActions = true; // Doesn't affect Test Renderer
27 +export const enableBinaryFlight = true;
28 export const enableSchedulerDebugging = false;
29 export const disableJavaScriptURLs = false;
30 export const disableCommentsAsDOMContainers = true;
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -75,6 +75,8 @@ export const enableFetchInstrumentation = false;
75
76 export const enableFormActions = false;
77
78 +export const enableBinaryFlight = true;
79 +
80 export const disableJavaScriptURLs = true;
81
82 // TODO: www currently relies on this feature. It's disabled in open source.
scripts/rollup/validate/eslintrc.cjs.js
+14 -4
@@ -14,7 +14,21 @@ module.exports = {
14 Symbol: 'readonly',
15 WeakMap: 'readonly',
16 WeakSet: 'readonly',
17 +
18 + Int8Array: 'readonly',
19 + Uint8Array: 'readonly',
20 + Uint8ClampedArray: 'readonly',
21 + Int16Array: 'readonly',
22 Uint16Array: 'readonly',
23 + Int32Array: 'readonly',
24 + Uint32Array: 'readonly',
25 + Float32Array: 'readonly',
26 + Float64Array: 'readonly',
27 + BigInt64Array: 'readonly',
28 + BigUint64Array: 'readonly',
29 + DataView: 'readonly',
30 + ArrayBuffer: 'readonly',
31 +
32 Reflect: 'readonly',
33 globalThis: 'readonly',
34 // Vendor specific
@@ -28,15 +42,11 @@ module.exports = {
42 trustedTypes: 'readonly',
43
44 // Scheduler profiling
31 - Int32Array: 'readonly',
32 - ArrayBuffer: 'readonly',
33 -
45 TaskController: 'readonly',
46 reportError: 'readonly',
47 AggregateError: 'readonly',
48
49 // Flight
39 - Uint8Array: 'readonly',
50 Promise: 'readonly',
51
52 // Temp
scripts/rollup/validate/eslintrc.cjs2015.js
+14 -4
@@ -14,7 +14,21 @@ module.exports = {
14 Symbol: 'readonly',
15 WeakMap: 'readonly',
16 WeakSet: 'readonly',
17 +
18 + Int8Array: 'readonly',
19 + Uint8Array: 'readonly',
20 + Uint8ClampedArray: 'readonly',
21 + Int16Array: 'readonly',
22 Uint16Array: 'readonly',
23 + Int32Array: 'readonly',
24 + Uint32Array: 'readonly',
25 + Float32Array: 'readonly',
26 + Float64Array: 'readonly',
27 + BigInt64Array: 'readonly',
28 + BigUint64Array: 'readonly',
29 + DataView: 'readonly',
30 + ArrayBuffer: 'readonly',
31 +
32 Reflect: 'readonly',
33 globalThis: 'readonly',
34 // Vendor specific
@@ -28,15 +42,11 @@ module.exports = {
42 trustedTypes: 'readonly',
43
44 // Scheduler profiling
31 - Int32Array: 'readonly',
32 - ArrayBuffer: 'readonly',
33 -
45 TaskController: 'readonly',
46 reportError: 'readonly',
47 AggregateError: 'readonly',
48
49 // Flight
39 - Uint8Array: 'readonly',
50 Promise: 'readonly',
51
52 // Temp
scripts/rollup/validate/eslintrc.esm.js
+14 -4
@@ -14,7 +14,21 @@ module.exports = {
14 Symbol: 'readonly',
15 WeakMap: 'readonly',
16 WeakSet: 'readonly',
17 +
18 + Int8Array: 'readonly',
19 + Uint8Array: 'readonly',
20 + Uint8ClampedArray: 'readonly',
21 + Int16Array: 'readonly',
22 Uint16Array: 'readonly',
23 + Int32Array: 'readonly',
24 + Uint32Array: 'readonly',
25 + Float32Array: 'readonly',
26 + Float64Array: 'readonly',
27 + BigInt64Array: 'readonly',
28 + BigUint64Array: 'readonly',
29 + DataView: 'readonly',
30 + ArrayBuffer: 'readonly',
31 +
32 Reflect: 'readonly',
33 globalThis: 'readonly',
34 // Vendor specific
@@ -28,15 +42,11 @@ module.exports = {
42 trustedTypes: 'readonly',
43
44 // Scheduler profiling
31 - Int32Array: 'readonly',
32 - ArrayBuffer: 'readonly',
33 -
45 TaskController: 'readonly',
46 reportError: 'readonly',
47 AggregateError: 'readonly',
48
49 // Flight
39 - Uint8Array: 'readonly',
50 Promise: 'readonly',
51
52 // Temp
scripts/rollup/validate/eslintrc.fb.js
+14 -4
@@ -14,7 +14,21 @@ module.exports = {
14 Proxy: 'readonly',
15 WeakMap: 'readonly',
16 WeakSet: 'readonly',
17 +
18 + Int8Array: 'readonly',
19 + Uint8Array: 'readonly',
20 + Uint8ClampedArray: 'readonly',
21 + Int16Array: 'readonly',
22 Uint16Array: 'readonly',
23 + Int32Array: 'readonly',
24 + Uint32Array: 'readonly',
25 + Float32Array: 'readonly',
26 + Float64Array: 'readonly',
27 + BigInt64Array: 'readonly',
28 + BigUint64Array: 'readonly',
29 + DataView: 'readonly',
30 + ArrayBuffer: 'readonly',
31 +
32 Reflect: 'readonly',
33 globalThis: 'readonly',
34 // Vendor specific
@@ -29,15 +43,11 @@ module.exports = {
43 trustedTypes: 'readonly',
44
45 // Scheduler profiling
32 - Int32Array: 'readonly',
33 - ArrayBuffer: 'readonly',
34 -
46 TaskController: 'readonly',
47 reportError: 'readonly',
48 AggregateError: 'readonly',
49
50 // Flight
40 - Uint8Array: 'readonly',
51 Promise: 'readonly',
52
53 // Temp
scripts/rollup/validate/eslintrc.rn.js
+15 -3
@@ -14,6 +14,21 @@ module.exports = {
14 Proxy: 'readonly',
15 WeakMap: 'readonly',
16 WeakSet: 'readonly',
17 +
18 + Int8Array: 'readonly',
19 + Uint8Array: 'readonly',
20 + Uint8ClampedArray: 'readonly',
21 + Int16Array: 'readonly',
22 + Uint16Array: 'readonly',
23 + Int32Array: 'readonly',
24 + Uint32Array: 'readonly',
25 + Float32Array: 'readonly',
26 + Float64Array: 'readonly',
27 + BigInt64Array: 'readonly',
28 + BigUint64Array: 'readonly',
29 + DataView: 'readonly',
30 + ArrayBuffer: 'readonly',
31 +
32 Reflect: 'readonly',
33 globalThis: 'readonly',
34 // Vendor specific
@@ -29,9 +44,6 @@ module.exports = {
44 // RN supports this
45 setImmediate: 'readonly',
46 // Scheduler profiling
32 - Int32Array: 'readonly',
33 - ArrayBuffer: 'readonly',
34 -
47 TaskController: 'readonly',
48 reportError: 'readonly',
49 AggregateError: 'readonly',
scripts/rollup/validate/eslintrc.umd.js
+14 -4
@@ -13,7 +13,21 @@ module.exports = {
13 Proxy: 'readonly',
14 WeakMap: 'readonly',
15 WeakSet: 'readonly',
16 +
17 + Int8Array: 'readonly',
18 + Uint8Array: 'readonly',
19 + Uint8ClampedArray: 'readonly',
20 + Int16Array: 'readonly',
21 Uint16Array: 'readonly',
22 + Int32Array: 'readonly',
23 + Uint32Array: 'readonly',
24 + Float32Array: 'readonly',
25 + Float64Array: 'readonly',
26 + BigInt64Array: 'readonly',
27 + BigUint64Array: 'readonly',
28 + DataView: 'readonly',
29 + ArrayBuffer: 'readonly',
30 +
31 Reflect: 'readonly',
32 globalThis: 'readonly',
33 // Vendor specific
@@ -33,15 +47,11 @@ module.exports = {
47 trustedTypes: 'readonly',
48
49 // Scheduler profiling
36 - Int32Array: 'readonly',
37 - ArrayBuffer: 'readonly',
38 -
50 TaskController: 'readonly',
51 reportError: 'readonly',
52 AggregateError: 'readonly',
53
54 // Flight
44 - Uint8Array: 'readonly',
55 Promise: 'readonly',
56
57 // Temp