Skip to content

Commit

Permalink
Serialize Blobs
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage committed Apr 5, 2024
1 parent fd0da3e commit 6b82d7c
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 0 deletions.
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

0 comments on commit 6b82d7c

Please sign in to comment.