Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Flight] Support Blobs from Server to Client #28755

Merged
merged 1 commit into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,15 @@ function parseModelString(
const data = getOutlinedModel(response, id);
return new Set(data);
}
case 'B': {
// Blob
if (enableBinaryFlight) {
const id = parseInt(value.slice(2), 16);
const data = getOutlinedModel(response, id);
return new Blob(data.slice(1), {type: data[0]});
}
return undefined;
}
case 'I': {
// $Infinity
return Infinity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
*/

'use strict';
Expand All @@ -14,6 +15,9 @@ global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
if (typeof Blob === 'undefined') {
global.Blob = require('buffer').Blob;
}

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

// @gate enableBinaryFlight
it('should be able to serialize a blob', async () => {
const bytes = new Uint8Array([
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
]);
const blob = new Blob([bytes, bytes], {
type: 'application/x-test',
});
const stream = passThrough(
ReactServerDOMServer.renderToReadableStream(blob),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
expect(result instanceof Blob).toBe(true);
expect(result.size).toBe(bytes.length * 2);
expect(await result.arrayBuffer()).toEqual(await blob.arrayBuffer());
});

it('warns if passing a this argument to bind() of a server reference', async () => {
const ServerModule = serverExports({
greet: function () {},
Expand Down
50 changes: 50 additions & 0 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ export type ReactClientValue =
| Array<ReactClientValue>
| Map<ReactClientValue, ReactClientValue>
| Set<ReactClientValue>
| $ArrayBufferView
| ArrayBuffer
| Date
| ReactClientObject
| Promise<ReactClientValue>; // Thenable<ReactClientValue>
Expand Down Expand Up @@ -1183,6 +1185,46 @@ function serializeTypedArray(
return serializeByValueID(bufferId);
}

function serializeBlob(request: Request, blob: Blob): string {
const id = request.nextChunkId++;
request.pendingChunks++;

const reader = blob.stream().getReader();

const model: Array<string | Uint8Array> = [blob.type];

function progress(
entry: {done: false, value: Uint8Array} | {done: true, value: void},
): Promise<void> | void {
if (entry.done) {
const blobId = outlineModel(request, model);
const blobReference = '$B' + blobId.toString(16);
const processedChunk = encodeReferenceChunk(request, id, blobReference);
request.completedRegularChunks.push(processedChunk);
if (request.destination !== null) {
flushCompletedChunks(request, request.destination);
}
return;
}
// TODO: Emit the chunk early and refer to it later.
model.push(entry.value);
// $FlowFixMe[incompatible-call]
return reader.read().then(progress).catch(error);
}

function error(reason: mixed) {
const digest = logRecoverableError(request, reason);
emitErrorChunk(request, id, digest, reason);
if (request.destination !== null) {
flushCompletedChunks(request, request.destination);
}
}
// $FlowFixMe[incompatible-call]
reader.read().then(progress).catch(error);

return '$' + id.toString(16);
}

function escapeStringValue(value: string): string {
if (value[0] === '$') {
// We need to escape $ prefixed strings since we use those to encode
Expand Down Expand Up @@ -1559,6 +1601,10 @@ function renderModelDestructive(
if (value instanceof DataView) {
return serializeTypedArray(request, 'V', value);
}
// TODO: Blob is not available in old Node. Remove the typeof check later.
if (typeof Blob === 'function' && value instanceof Blob) {
return serializeBlob(request, value);
}
}

const iteratorFn = getIteratorFn(value);
Expand Down Expand Up @@ -2080,6 +2126,10 @@ function renderConsoleValue(
if (value instanceof DataView) {
return serializeTypedArray(request, 'V', value);
}
// TODO: Blob is not available in old Node. Remove the typeof check later.
if (typeof Blob === 'function' && value instanceof Blob) {
return serializeBlob(request, value);
}
}

const iteratorFn = getIteratorFn(value);
Expand Down