Skip to content

Commit c0b5d43

Browse files
sebmarkbagekassens
authored andcommitted
[Flight] Support Blobs from Server to Client (facebook#28755)
We currently support Blobs when passing from Client to Server so this adds it in the other direction for parity - when `enableFlightBinary` is enabled. We intentionally only support the `Blob` type to pass-through, not subtype `File`. That's because passing additional meta data like filename might be an accidental leak. You can still pass a `File` through but it'll appear as a `Blob` on the other side. It's also not possible to create a faithful File subclass in all environments without it actually being backed by a file. This implementation isn't great but at least it works. It creates a few indirections. This is because we need to be able to asynchronously emit the buffers but we have to "block" the parent object from resolving while it's loading. Ideally, we should be able to create the Blob on the client early and then stream in it lazily. Because the Blob API doesn't guarantee that the data is available synchronously. Unfortunately, the native APIs doesn't have this. We could implement custom versions of all the data read APIs but then the blobs still wouldn't work with native APIs. So we just have to wait until Blob accepts a stream in the constructor. We should be able to stream each chunk early in the protocol though even though we can't unblock the parent until they've all loaded. I didn't do this yet mostly because of code structure and I'm lazy.
1 parent 87b495f commit c0b5d43

File tree

3 files changed

+85
-0
lines changed

3 files changed

+85
-0
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,15 @@ function parseModelString(
725725
const data = getOutlinedModel(response, id);
726726
return new Set(data);
727727
}
728+
case 'B': {
729+
// Blob
730+
if (enableBinaryFlight) {
731+
const id = parseInt(value.slice(2), 16);
732+
const data = getOutlinedModel(response, id);
733+
return new Blob(data.slice(1), {type: data[0]});
734+
}
735+
return undefined;
736+
}
728737
case 'I': {
729738
// $Infinity
730739
return Infinity;

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

Lines changed: 26 additions & 0 deletions
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';
@@ -14,6 +15,9 @@ global.ReadableStream =
1415
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
1516
global.TextEncoder = require('util').TextEncoder;
1617
global.TextDecoder = require('util').TextDecoder;
18+
if (typeof Blob === 'undefined') {
19+
global.Blob = require('buffer').Blob;
20+
}
1721

1822
// Don't wait before processing work on the server.
1923
// TODO: we can replace this with FlightServer.act().
@@ -326,6 +330,28 @@ describe('ReactFlightDOMEdge', () => {
326330
expect(result).toEqual(buffers);
327331
});
328332

333+
// @gate enableBinaryFlight
334+
it('should be able to serialize a blob', async () => {
335+
const bytes = new Uint8Array([
336+
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
337+
]);
338+
const blob = new Blob([bytes, bytes], {
339+
type: 'application/x-test',
340+
});
341+
const stream = passThrough(
342+
ReactServerDOMServer.renderToReadableStream(blob),
343+
);
344+
const result = await ReactServerDOMClient.createFromReadableStream(stream, {
345+
ssrManifest: {
346+
moduleMap: null,
347+
moduleLoading: null,
348+
},
349+
});
350+
expect(result instanceof Blob).toBe(true);
351+
expect(result.size).toBe(bytes.length * 2);
352+
expect(await result.arrayBuffer()).toEqual(await blob.arrayBuffer());
353+
});
354+
329355
it('warns if passing a this argument to bind() of a server reference', async () => {
330356
const ServerModule = serverExports({
331357
greet: function () {},

packages/react-server/src/ReactFlightServer.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ export type ReactClientValue =
237237
| Array<ReactClientValue>
238238
| Map<ReactClientValue, ReactClientValue>
239239
| Set<ReactClientValue>
240+
| $ArrayBufferView
241+
| ArrayBuffer
240242
| Date
241243
| ReactClientObject
242244
| Promise<ReactClientValue>; // Thenable<ReactClientValue>
@@ -1183,6 +1185,46 @@ function serializeTypedArray(
11831185
return serializeByValueID(bufferId);
11841186
}
11851187

1188+
function serializeBlob(request: Request, blob: Blob): string {
1189+
const id = request.nextChunkId++;
1190+
request.pendingChunks++;
1191+
1192+
const reader = blob.stream().getReader();
1193+
1194+
const model: Array<string | Uint8Array> = [blob.type];
1195+
1196+
function progress(
1197+
entry: {done: false, value: Uint8Array} | {done: true, value: void},
1198+
): Promise<void> | void {
1199+
if (entry.done) {
1200+
const blobId = outlineModel(request, model);
1201+
const blobReference = '$B' + blobId.toString(16);
1202+
const processedChunk = encodeReferenceChunk(request, id, blobReference);
1203+
request.completedRegularChunks.push(processedChunk);
1204+
if (request.destination !== null) {
1205+
flushCompletedChunks(request, request.destination);
1206+
}
1207+
return;
1208+
}
1209+
// TODO: Emit the chunk early and refer to it later.
1210+
model.push(entry.value);
1211+
// $FlowFixMe[incompatible-call]
1212+
return reader.read().then(progress).catch(error);
1213+
}
1214+
1215+
function error(reason: mixed) {
1216+
const digest = logRecoverableError(request, reason);
1217+
emitErrorChunk(request, id, digest, reason);
1218+
if (request.destination !== null) {
1219+
flushCompletedChunks(request, request.destination);
1220+
}
1221+
}
1222+
// $FlowFixMe[incompatible-call]
1223+
reader.read().then(progress).catch(error);
1224+
1225+
return '$' + id.toString(16);
1226+
}
1227+
11861228
function escapeStringValue(value: string): string {
11871229
if (value[0] === '$') {
11881230
// We need to escape $ prefixed strings since we use those to encode
@@ -1559,6 +1601,10 @@ function renderModelDestructive(
15591601
if (value instanceof DataView) {
15601602
return serializeTypedArray(request, 'V', value);
15611603
}
1604+
// TODO: Blob is not available in old Node. Remove the typeof check later.
1605+
if (typeof Blob === 'function' && value instanceof Blob) {
1606+
return serializeBlob(request, value);
1607+
}
15621608
}
15631609

15641610
const iteratorFn = getIteratorFn(value);
@@ -2080,6 +2126,10 @@ function renderConsoleValue(
20802126
if (value instanceof DataView) {
20812127
return serializeTypedArray(request, 'V', value);
20822128
}
2129+
// TODO: Blob is not available in old Node. Remove the typeof check later.
2130+
if (typeof Blob === 'function' && value instanceof Blob) {
2131+
return serializeBlob(request, value);
2132+
}
20832133
}
20842134

20852135
const iteratorFn = getIteratorFn(value);

0 commit comments

Comments
 (0)