diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index c0cec0db366cf..8a35e97b1679a 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -737,6 +737,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; diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index 8304d9927d372..ada7bb35cae27 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @emails react-core + * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment */ 'use strict'; @@ -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(). @@ -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 () {}, diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index a8c648881fe2c..4c8fa75fec9e9 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -239,6 +239,8 @@ export type ReactClientValue = | Array | Map | Set + | $ArrayBufferView + | ArrayBuffer | Date | ReactClientObject | Promise; // Thenable @@ -1229,6 +1231,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 = [blob.type]; + + function progress( + entry: {done: false, value: Uint8Array} | {done: true, value: void}, + ): Promise | 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 @@ -1606,6 +1648,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); @@ -2146,6 +2192,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);