Skip to content

Commit 26f6448

Browse files
committed
Serialize Typed Arrays, ArrayBuffer and DataView
1 parent bc380ba commit 26f6448

File tree

4 files changed

+287
-13
lines changed

4 files changed

+287
-13
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 143 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
2121

2222
import type {CallServerCallback} from './ReactFlightReplyClient';
2323

24+
import {enableBinaryFlight} from 'shared/ReactFeatureFlags';
25+
2426
import {
2527
resolveClientReference,
2628
preloadModule,
@@ -297,6 +299,14 @@ function createInitializedTextChunk(
297299
return new Chunk(INITIALIZED, value, null, response);
298300
}
299301

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+
300310
function resolveModelChunk<T>(
301311
chunk: SomeChunk<T>,
302312
value: UninitializedModel,
@@ -738,6 +748,16 @@ function resolveText(response: Response, id: number, text: string): void {
738748
chunks.set(id, createInitializedTextChunk(response, text));
739749
}
740750

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+
741761
function resolveModule(
742762
response: Response,
743763
id: number,
@@ -856,24 +876,120 @@ function resolveHint(
856876
dispatchHint(code, hintModel);
857877
}
858878

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+
859931
function processFullRow(
860932
response: Response,
861933
id: number,
862934
tag: number,
863935
buffer: Array<Uint8Array>,
864-
lastChunk: string | Uint8Array,
936+
chunk: Uint8Array,
865937
): 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+
867987
const stringDecoder = response._stringDecoder;
988+
let row = '';
868989
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]);
876991
}
992+
row += readFinalStringChunk(stringDecoder, chunk);
877993
switch (tag) {
878994
case 73 /* "I" */: {
879995
resolveModule(response, id, row);
@@ -903,7 +1019,7 @@ function processFullRow(
9031019
resolveText(response, id, row);
9041020
return;
9051021
}
906-
default: {
1022+
default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
9071023
// We assume anything else is JSON.
9081024
resolveModel(response, id, row);
9091025
return;
@@ -937,7 +1053,23 @@ export function processBinaryChunk(
9371053
}
9381054
case ROW_TAG: {
9391055
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+
) {
9411073
rowTag = resolvedRowTag;
9421074
rowState = ROW_LENGTH;
9431075
i++;

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,31 @@ describe('ReactFlightDOMEdge', () => {
153153
expect(result.text).toBe(testString);
154154
expect(result.text2).toBe(testString2);
155155
});
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+
});
156183
});

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,32 @@ describe('ReactFlightDOMNode', () => {
131131
// Should still match the result when parsed
132132
expect(result.text).toBe(testString);
133133
});
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+
});
134162
});

0 commit comments

Comments
 (0)