Skip to content

Commit dfdc0e0

Browse files
committed
Serialize Typed Arrays, ArrayBuffer and DataView
1 parent e5e3f54 commit dfdc0e0

File tree

5 files changed

+287
-13
lines changed

5 files changed

+287
-13
lines changed

.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,8 @@ module.exports = {
460460
// For Flow type annotation. Only `BigInt` is valid at runtime.
461461
bigint: 'readonly',
462462
BigInt: 'readonly',
463+
BigInt64Array: 'readonly',
464+
BigUint64Array: 'readonly',
463465
Class: 'readonly',
464466
ClientRect: 'readonly',
465467
CopyInspectedElementPath: 'readonly',

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,
@@ -296,6 +298,14 @@ function createInitializedTextChunk(
296298
return new Chunk(INITIALIZED, value, null, response);
297299
}
298300

301+
function createInitializedBufferChunk(
302+
response: Response,
303+
value: $ArrayBufferView | ArrayBuffer,
304+
): InitializedChunk<Uint8Array> {
305+
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
306+
return new Chunk(INITIALIZED, value, null, response);
307+
}
308+
299309
function resolveModelChunk<T>(
300310
chunk: SomeChunk<T>,
301311
value: UninitializedModel,
@@ -719,6 +729,16 @@ function resolveText(response: Response, id: number, text: string): void {
719729
chunks.set(id, createInitializedTextChunk(response, text));
720730
}
721731

732+
function resolveBuffer(
733+
response: Response,
734+
id: number,
735+
buffer: $ArrayBufferView | ArrayBuffer,
736+
): void {
737+
const chunks = response._chunks;
738+
// We assume that we always reference buffers after they've been emitted.
739+
chunks.set(id, createInitializedBufferChunk(response, buffer));
740+
}
741+
722742
function resolveModule(
723743
response: Response,
724744
id: number,
@@ -837,24 +857,120 @@ function resolveHint(
837857
dispatchHint(code, hintModel);
838858
}
839859

860+
function mergeBuffer(
861+
buffer: Array<Uint8Array>,
862+
lastChunk: Uint8Array,
863+
): Uint8Array {
864+
const l = buffer.length;
865+
// Count the bytes we'll need
866+
let byteLength = lastChunk.length;
867+
for (let i = 0; i < l; i++) {
868+
byteLength += buffer[i].byteLength;
869+
}
870+
// Allocate enough contiguous space
871+
const result = new Uint8Array(byteLength);
872+
let offset = 0;
873+
// Copy all the buffers into it.
874+
for (let i = 0; i < l; i++) {
875+
const chunk = buffer[i];
876+
result.set(chunk, offset);
877+
offset += chunk.byteLength;
878+
}
879+
result.set(lastChunk, offset);
880+
return result;
881+
}
882+
883+
function resolveTypedArray(
884+
response: Response,
885+
id: number,
886+
buffer: Array<Uint8Array>,
887+
lastChunk: Uint8Array,
888+
constructor: any,
889+
bytesPerElement: number,
890+
): void {
891+
// If the view fits into one original buffer, we just reuse that buffer instead of
892+
// copying it out to a separate copy. This means that it's not always possible to
893+
// transfer these values to other threads without copying first since they may
894+
// share array buffer. For this to work, it must also have bytes aligned to a
895+
// multiple of a size of the type.
896+
const chunk =
897+
buffer.length === 0 && lastChunk.byteOffset % bytesPerElement === 0
898+
? lastChunk
899+
: mergeBuffer(buffer, lastChunk);
900+
// TODO: The transfer protocol of RSC is little-endian. If the client isn't little-endian
901+
// we should convert it instead. In practice big endian isn't really Web compatible so it's
902+
// somewhat safe to assume that browsers aren't going to run it, but maybe there's some SSR
903+
// server that's affected.
904+
const view: $ArrayBufferView = new constructor(
905+
chunk.buffer,
906+
chunk.byteOffset,
907+
chunk.byteLength / bytesPerElement,
908+
);
909+
resolveBuffer(response, id, view);
910+
}
911+
840912
function processFullRow(
841913
response: Response,
842914
id: number,
843915
tag: number,
844916
buffer: Array<Uint8Array>,
845-
lastChunk: string | Uint8Array,
917+
chunk: Uint8Array,
846918
): void {
847-
let row = '';
919+
if (enableBinaryFlight) {
920+
switch (tag) {
921+
case 65 /* "A" */:
922+
// We must always clone to extract it into a separate buffer instead of just a view.
923+
resolveBuffer(response, id, mergeBuffer(buffer, chunk).buffer);
924+
return;
925+
case 67 /* "C" */:
926+
resolveTypedArray(response, id, buffer, chunk, Int8Array, 1);
927+
return;
928+
case 99 /* "c" */:
929+
resolveBuffer(
930+
response,
931+
id,
932+
buffer.length === 0 ? chunk : mergeBuffer(buffer, chunk),
933+
);
934+
return;
935+
case 85 /* "U" */:
936+
resolveTypedArray(response, id, buffer, chunk, Uint8ClampedArray, 1);
937+
return;
938+
case 83 /* "S" */:
939+
resolveTypedArray(response, id, buffer, chunk, Int16Array, 2);
940+
return;
941+
case 115 /* "s" */:
942+
resolveTypedArray(response, id, buffer, chunk, Uint16Array, 2);
943+
return;
944+
case 76 /* "L" */:
945+
resolveTypedArray(response, id, buffer, chunk, Int32Array, 4);
946+
return;
947+
case 108 /* "l" */:
948+
resolveTypedArray(response, id, buffer, chunk, Uint32Array, 4);
949+
return;
950+
case 70 /* "F" */:
951+
resolveTypedArray(response, id, buffer, chunk, Float32Array, 4);
952+
return;
953+
case 68 /* "D" */:
954+
resolveTypedArray(response, id, buffer, chunk, Float64Array, 8);
955+
return;
956+
case 78 /* "N" */:
957+
resolveTypedArray(response, id, buffer, chunk, BigInt64Array, 8);
958+
return;
959+
case 109 /* "m" */:
960+
resolveTypedArray(response, id, buffer, chunk, BigUint64Array, 8);
961+
return;
962+
case 86 /* "V" */:
963+
resolveTypedArray(response, id, buffer, chunk, DataView, 1);
964+
return;
965+
}
966+
}
967+
848968
const stringDecoder = response._stringDecoder;
969+
let row = '';
849970
for (let i = 0; i < buffer.length; i++) {
850-
const chunk = buffer[i];
851-
row += readPartialStringChunk(stringDecoder, chunk);
852-
}
853-
if (typeof lastChunk === 'string') {
854-
row += lastChunk;
855-
} else {
856-
row += readFinalStringChunk(stringDecoder, lastChunk);
971+
row += readPartialStringChunk(stringDecoder, buffer[i]);
857972
}
973+
row += readFinalStringChunk(stringDecoder, chunk);
858974
switch (tag) {
859975
case 73 /* "I" */: {
860976
resolveModule(response, id, row);
@@ -884,7 +1000,7 @@ function processFullRow(
8841000
resolveText(response, id, row);
8851001
return;
8861002
}
887-
default: {
1003+
default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
8881004
// We assume anything else is JSON.
8891005
resolveModel(response, id, row);
8901006
return;
@@ -918,7 +1034,23 @@ export function processBinaryChunk(
9181034
}
9191035
case ROW_TAG: {
9201036
const resolvedRowTag = chunk[i];
921-
if (resolvedRowTag === 84 /* "T" */) {
1037+
if (
1038+
resolvedRowTag === 84 /* "T" */ ||
1039+
(enableBinaryFlight &&
1040+
(resolvedRowTag === 65 /* "A" */ ||
1041+
resolvedRowTag === 67 /* "C" */ ||
1042+
resolvedRowTag === 99 /* "c" */ ||
1043+
resolvedRowTag === 85 /* "U" */ ||
1044+
resolvedRowTag === 83 /* "S" */ ||
1045+
resolvedRowTag === 115 /* "s" */ ||
1046+
resolvedRowTag === 76 /* "L" */ ||
1047+
resolvedRowTag === 108 /* "l" */ ||
1048+
resolvedRowTag === 70 /* "F" */ ||
1049+
resolvedRowTag === 68 /* "D" */ ||
1050+
resolvedRowTag === 78 /* "N" */ ||
1051+
resolvedRowTag === 109 /* "m" */ ||
1052+
resolvedRowTag === 86)) /* "V" */
1053+
) {
9221054
rowTag = resolvedRowTag;
9231055
rowState = ROW_LENGTH;
9241056
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)