Skip to content

Commit 826bf4e

Browse files
authored
[Flight Reply] Encode binary streams as a single collapsed Blob (#28986)
Based on #28893. For other streams we encode each chunk as a separate form field which is a bit bloated. Especially for binary chunks since they also have an indirection. We need some way to encode the chunks as separate anyway. This way the streaming using busboy actually allows each chunk to stream in over the network one at a time. For binary streams the actual chunking is not important. The chunks can be split and recombined in whatever size chunk makes sense. Since we buffer the entire content anyway we can combine the chunks to be consecutive. This PR does that with binary streams and also combine them into a single Blob. That way there's no extra overhead when passing through a binary stream. Ideally, we'd be able to just use the stream from that one Blob but Node.js doesn't return byob streams from Blob. Additionally, we don't actually stream the content of Blobs due to the layering with busboy atm. We could do that for binary streams in particular by replacing the File layering with a stream and resolving each chunk as it comes in. That could be a follow up. If we stop buffering in the future, this set up still allows us to split them and send other form fields in between while blocked since the protocol is still the same.
1 parent e7d213d commit 826bf4e

File tree

1 file changed

+49
-16
lines changed

1 file changed

+49
-16
lines changed

packages/react-client/src/ReactFlightReplyClient.js

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export function processReply(
208208
return '$' + tag + blobId.toString(16);
209209
}
210210

211-
function serializeReadableStream(stream: ReadableStream): string {
211+
function serializeBinaryReader(reader: any): string {
212212
if (formData === null) {
213213
// Upgrade to use FormData to allow us to stream this value.
214214
formData = new FormData();
@@ -218,23 +218,43 @@ export function processReply(
218218
pendingParts++;
219219
const streamId = nextPartId++;
220220

221-
// Detect if this is a BYOB stream. BYOB streams should be able to be read as bytes on the
222-
// receiving side. It also implies that different chunks can be split up or merged as opposed
223-
// to a readable stream that happens to have Uint8Array as the type which might expect it to be
224-
// received in the same slices.
225-
// $FlowFixMe: This is a Node.js extension.
226-
let supportsBYOB: void | boolean = stream.supportsBYOB;
227-
if (supportsBYOB === undefined) {
228-
try {
229-
// $FlowFixMe[extra-arg]: This argument is accepted.
230-
stream.getReader({mode: 'byob'}).releaseLock();
231-
supportsBYOB = true;
232-
} catch (x) {
233-
supportsBYOB = false;
221+
const buffer = [];
222+
223+
function progress(entry: {done: boolean, value: ReactServerValue, ...}) {
224+
if (entry.done) {
225+
const blobId = nextPartId++;
226+
// eslint-disable-next-line react-internal/safe-string-coercion
227+
data.append(formFieldPrefix + blobId, new Blob(buffer));
228+
// eslint-disable-next-line react-internal/safe-string-coercion
229+
data.append(
230+
formFieldPrefix + streamId,
231+
'"$o' + blobId.toString(16) + '"',
232+
);
233+
// eslint-disable-next-line react-internal/safe-string-coercion
234+
data.append(formFieldPrefix + streamId, 'C'); // Close signal
235+
pendingParts--;
236+
if (pendingParts === 0) {
237+
resolve(data);
238+
}
239+
} else {
240+
buffer.push(entry.value);
241+
reader.read(new Uint8Array(1024)).then(progress, reject);
234242
}
235243
}
244+
reader.read(new Uint8Array(1024)).then(progress, reject);
236245

237-
const reader = stream.getReader();
246+
return '$r' + streamId.toString(16);
247+
}
248+
249+
function serializeReader(reader: ReadableStreamReader): string {
250+
if (formData === null) {
251+
// Upgrade to use FormData to allow us to stream this value.
252+
formData = new FormData();
253+
}
254+
const data = formData;
255+
256+
pendingParts++;
257+
const streamId = nextPartId++;
238258

239259
function progress(entry: {done: boolean, value: ReactServerValue, ...}) {
240260
if (entry.done) {
@@ -258,7 +278,20 @@ export function processReply(
258278
}
259279
reader.read().then(progress, reject);
260280

261-
return '$' + (supportsBYOB ? 'r' : 'R') + streamId.toString(16);
281+
return '$R' + streamId.toString(16);
282+
}
283+
284+
function serializeReadableStream(stream: ReadableStream): string {
285+
// Detect if this is a BYOB stream. BYOB streams should be able to be read as bytes on the
286+
// receiving side. For binary streams, we serialize them as plain Blobs.
287+
let binaryReader;
288+
try {
289+
// $FlowFixMe[extra-arg]: This argument is accepted.
290+
binaryReader = stream.getReader({mode: 'byob'});
291+
} catch (x) {
292+
return serializeReader(stream.getReader());
293+
}
294+
return serializeBinaryReader(binaryReader);
262295
}
263296

264297
function serializeAsyncIterable(

0 commit comments

Comments
 (0)