diff --git a/.eslintrc.js b/.eslintrc.js index 00e3094b06347..941c2e3b23ca8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -454,11 +454,14 @@ module.exports = { $PropertyType: 'readonly', $ReadOnly: 'readonly', $ReadOnlyArray: 'readonly', + $ArrayBufferView: 'readonly', $Shape: 'readonly', AnimationFrameID: 'readonly', // For Flow type annotation. Only `BigInt` is valid at runtime. bigint: 'readonly', BigInt: 'readonly', + BigInt64Array: 'readonly', + BigUint64Array: 'readonly', Class: 'readonly', ClientRect: 'readonly', CopyInspectedElementPath: 'readonly', diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 0143c0129ce11..522a11e6d9da8 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -21,6 +21,8 @@ import type {HintModel} from 'react-server/src/ReactFlightServerConfig'; import type {CallServerCallback} from './ReactFlightReplyClient'; +import {enableBinaryFlight} from 'shared/ReactFeatureFlags'; + import { resolveClientReference, preloadModule, @@ -297,6 +299,14 @@ function createInitializedTextChunk( return new Chunk(INITIALIZED, value, null, response); } +function createInitializedBufferChunk( + response: Response, + value: $ArrayBufferView | ArrayBuffer, +): InitializedChunk { + // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors + return new Chunk(INITIALIZED, value, null, response); +} + function resolveModelChunk( chunk: SomeChunk, value: UninitializedModel, @@ -738,6 +748,16 @@ function resolveText(response: Response, id: number, text: string): void { chunks.set(id, createInitializedTextChunk(response, text)); } +function resolveBuffer( + response: Response, + id: number, + buffer: $ArrayBufferView | ArrayBuffer, +): void { + const chunks = response._chunks; + // We assume that we always reference buffers after they've been emitted. + chunks.set(id, createInitializedBufferChunk(response, buffer)); +} + function resolveModule( response: Response, id: number, @@ -856,24 +876,120 @@ function resolveHint( dispatchHint(code, hintModel); } +function mergeBuffer( + buffer: Array, + lastChunk: Uint8Array, +): Uint8Array { + const l = buffer.length; + // Count the bytes we'll need + let byteLength = lastChunk.length; + for (let i = 0; i < l; i++) { + byteLength += buffer[i].byteLength; + } + // Allocate enough contiguous space + const result = new Uint8Array(byteLength); + let offset = 0; + // Copy all the buffers into it. + for (let i = 0; i < l; i++) { + const chunk = buffer[i]; + result.set(chunk, offset); + offset += chunk.byteLength; + } + result.set(lastChunk, offset); + return result; +} + +function resolveTypedArray( + response: Response, + id: number, + buffer: Array, + lastChunk: Uint8Array, + constructor: any, + bytesPerElement: number, +): void { + // If the view fits into one original buffer, we just reuse that buffer instead of + // copying it out to a separate copy. This means that it's not always possible to + // transfer these values to other threads without copying first since they may + // share array buffer. For this to work, it must also have bytes aligned to a + // multiple of a size of the type. + const chunk = + buffer.length === 0 && lastChunk.byteOffset % bytesPerElement === 0 + ? lastChunk + : mergeBuffer(buffer, lastChunk); + // TODO: The transfer protocol of RSC is little-endian. If the client isn't little-endian + // we should convert it instead. In practice big endian isn't really Web compatible so it's + // somewhat safe to assume that browsers aren't going to run it, but maybe there's some SSR + // server that's affected. + const view: $ArrayBufferView = new constructor( + chunk.buffer, + chunk.byteOffset, + chunk.byteLength / bytesPerElement, + ); + resolveBuffer(response, id, view); +} + function processFullRow( response: Response, id: number, tag: number, buffer: Array, - lastChunk: string | Uint8Array, + chunk: Uint8Array, ): void { - let row = ''; + if (enableBinaryFlight) { + switch (tag) { + case 65 /* "A" */: + // We must always clone to extract it into a separate buffer instead of just a view. + resolveBuffer(response, id, mergeBuffer(buffer, chunk).buffer); + return; + case 67 /* "C" */: + resolveTypedArray(response, id, buffer, chunk, Int8Array, 1); + return; + case 99 /* "c" */: + resolveBuffer( + response, + id, + buffer.length === 0 ? chunk : mergeBuffer(buffer, chunk), + ); + return; + case 85 /* "U" */: + resolveTypedArray(response, id, buffer, chunk, Uint8ClampedArray, 1); + return; + case 83 /* "S" */: + resolveTypedArray(response, id, buffer, chunk, Int16Array, 2); + return; + case 115 /* "s" */: + resolveTypedArray(response, id, buffer, chunk, Uint16Array, 2); + return; + case 76 /* "L" */: + resolveTypedArray(response, id, buffer, chunk, Int32Array, 4); + return; + case 108 /* "l" */: + resolveTypedArray(response, id, buffer, chunk, Uint32Array, 4); + return; + case 70 /* "F" */: + resolveTypedArray(response, id, buffer, chunk, Float32Array, 4); + return; + case 68 /* "D" */: + resolveTypedArray(response, id, buffer, chunk, Float64Array, 8); + return; + case 78 /* "N" */: + resolveTypedArray(response, id, buffer, chunk, BigInt64Array, 8); + return; + case 109 /* "m" */: + resolveTypedArray(response, id, buffer, chunk, BigUint64Array, 8); + return; + case 86 /* "V" */: + resolveTypedArray(response, id, buffer, chunk, DataView, 1); + return; + } + } + const stringDecoder = response._stringDecoder; + let row = ''; for (let i = 0; i < buffer.length; i++) { - const chunk = buffer[i]; - row += readPartialStringChunk(stringDecoder, chunk); - } - if (typeof lastChunk === 'string') { - row += lastChunk; - } else { - row += readFinalStringChunk(stringDecoder, lastChunk); + row += readPartialStringChunk(stringDecoder, buffer[i]); } + row += readFinalStringChunk(stringDecoder, chunk); switch (tag) { case 73 /* "I" */: { resolveModule(response, id, row); @@ -903,7 +1019,7 @@ function processFullRow( resolveText(response, id, row); return; } - default: { + default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ { // We assume anything else is JSON. resolveModel(response, id, row); return; @@ -937,7 +1053,23 @@ export function processBinaryChunk( } case ROW_TAG: { const resolvedRowTag = chunk[i]; - if (resolvedRowTag === 84 /* "T" */) { + if ( + resolvedRowTag === 84 /* "T" */ || + (enableBinaryFlight && + (resolvedRowTag === 65 /* "A" */ || + resolvedRowTag === 67 /* "C" */ || + resolvedRowTag === 99 /* "c" */ || + resolvedRowTag === 85 /* "U" */ || + resolvedRowTag === 83 /* "S" */ || + resolvedRowTag === 115 /* "s" */ || + resolvedRowTag === 76 /* "L" */ || + resolvedRowTag === 108 /* "l" */ || + resolvedRowTag === 70 /* "F" */ || + resolvedRowTag === 68 /* "D" */ || + resolvedRowTag === 78 /* "N" */ || + resolvedRowTag === 109 /* "m" */ || + resolvedRowTag === 86)) /* "V" */ + ) { rowTag = resolvedRowTag; rowState = ROW_LENGTH; i++; diff --git a/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js b/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js index 5d055026492f3..c682865cabafd 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js @@ -14,6 +14,7 @@ export interface Destination { export opaque type PrecomputedChunk = string; export opaque type Chunk = string; +export opaque type BinaryChunk = string; export function scheduleWork(callback: () => void) { callback(); @@ -25,14 +26,14 @@ export function beginWriting(destination: Destination) {} export function writeChunk( destination: Destination, - chunk: Chunk | PrecomputedChunk, + chunk: Chunk | PrecomputedChunk | BinaryChunk, ): void { writeChunkAndReturn(destination, chunk); } export function writeChunkAndReturn( destination: Destination, - chunk: Chunk | PrecomputedChunk, + chunk: Chunk | PrecomputedChunk | BinaryChunk, ): boolean { return destination.push(chunk); } @@ -51,6 +52,12 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk { return content; } +export function typedArrayToBinaryChunk( + content: $ArrayBufferView, +): BinaryChunk { + throw new Error('Not implemented.'); +} + export function clonePrecomputedChunk( chunk: PrecomputedChunk, ): PrecomputedChunk { @@ -61,6 +68,10 @@ export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number { throw new Error('Not implemented.'); } +export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number { + throw new Error('Not implemented.'); +} + export function closeWithError(destination: Destination, error: mixed): void { // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types. destination.destroy(error); diff --git a/packages/react-server-dom-fb/src/ReactServerStreamConfigFB.js b/packages/react-server-dom-fb/src/ReactServerStreamConfigFB.js index 71f1949d2a17b..8321bdc62e551 100644 --- a/packages/react-server-dom-fb/src/ReactServerStreamConfigFB.js +++ b/packages/react-server-dom-fb/src/ReactServerStreamConfigFB.js @@ -16,6 +16,7 @@ export type Destination = { export opaque type PrecomputedChunk = string; export opaque type Chunk = string; +export opaque type BinaryChunk = string; export function scheduleWork(callback: () => void) { // We don't schedule work in this model, and instead expect performWork to always be called repeatedly. @@ -30,14 +31,14 @@ export function beginWriting(destination: Destination) {} export function writeChunk( destination: Destination, - chunk: Chunk | PrecomputedChunk, + chunk: Chunk | PrecomputedChunk | BinaryChunk, ): void { destination.buffer += chunk; } export function writeChunkAndReturn( destination: Destination, - chunk: Chunk | PrecomputedChunk, + chunk: Chunk | PrecomputedChunk | BinaryChunk, ): boolean { destination.buffer += chunk; return true; @@ -57,6 +58,12 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk { return content; } +export function typedArrayToBinaryChunk( + content: $ArrayBufferView, +): BinaryChunk { + throw new Error('Not implemented.'); +} + export function clonePrecomputedChunk( chunk: PrecomputedChunk, ): PrecomputedChunk { @@ -67,6 +74,10 @@ export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number { throw new Error('Not implemented.'); } +export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number { + throw new Error('Not implemented.'); +} + export function closeWithError(destination: Destination, error: mixed): void { destination.done = true; destination.fatal = true; 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 a08925af7bf9f..d728623671422 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -153,4 +153,31 @@ describe('ReactFlightDOMEdge', () => { expect(result.text).toBe(testString); expect(result.text2).toBe(testString2); }); + + // @gate enableBinaryFlight + it('should be able to serialize any kind of typed array', async () => { + const buffer = new Uint8Array([ + 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, + ]).buffer; + const buffers = [ + buffer, + new Int8Array(buffer, 1), + new Uint8Array(buffer, 2), + new Uint8ClampedArray(buffer, 2), + new Int16Array(buffer, 2), + new Uint16Array(buffer, 2), + new Int32Array(buffer, 4), + new Uint32Array(buffer, 4), + new Float32Array(buffer, 4), + new Float64Array(buffer, 0), + new BigInt64Array(buffer, 0), + new BigUint64Array(buffer, 0), + new DataView(buffer, 3), + ]; + const stream = passThrough( + ReactServerDOMServer.renderToReadableStream(buffers), + ); + const result = await ReactServerDOMClient.createFromReadableStream(stream); + expect(result).toEqual(buffers); + }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index 4eed4c562152c..cb7f7e84e5ff3 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -131,4 +131,32 @@ describe('ReactFlightDOMNode', () => { // Should still match the result when parsed expect(result.text).toBe(testString); }); + + // @gate enableBinaryFlight + it('should be able to serialize any kind of typed array', async () => { + const buffer = new Uint8Array([ + 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, + ]).buffer; + const buffers = [ + buffer, + new Int8Array(buffer, 1), + new Uint8Array(buffer, 2), + new Uint8ClampedArray(buffer, 2), + new Int16Array(buffer, 2), + new Uint16Array(buffer, 2), + new Int32Array(buffer, 4), + new Uint32Array(buffer, 4), + new Float32Array(buffer, 4), + new Float64Array(buffer, 0), + new BigInt64Array(buffer, 0), + new BigUint64Array(buffer, 0), + new DataView(buffer, 3), + ]; + const stream = ReactServerDOMServer.renderToPipeableStream(buffers); + const readable = new Stream.PassThrough(); + const promise = ReactServerDOMClient.createFromNodeStream(readable); + stream.pipe(readable); + const result = await promise; + expect(result).toEqual(buffers); + }); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index fa7ef0e12f57e..e8e18d758dd3d 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -7,7 +7,9 @@ * @flow */ -import type {Chunk, Destination} from './ReactServerStreamConfig'; +import type {Chunk, BinaryChunk, Destination} from './ReactServerStreamConfig'; + +import {enableBinaryFlight} from 'shared/ReactFeatureFlags'; import { scheduleWork, @@ -15,7 +17,9 @@ import { beginWriting, writeChunkAndReturn, stringToChunk, + typedArrayToBinaryChunk, byteLengthOfChunk, + byteLengthOfBinaryChunk, completeWriting, close, closeWithError, @@ -176,7 +180,7 @@ export type Request = { pingedTasks: Array, completedImportChunks: Array, completedHintChunks: Array, - completedRegularChunks: Array, + completedRegularChunks: Array, completedErrorChunks: Array, writtenSymbols: Map, writtenClientReferences: Map, @@ -235,7 +239,7 @@ export function createRequest( pingedTasks: pingedTasks, completedImportChunks: ([]: Array), completedHintChunks: ([]: Array), - completedRegularChunks: ([]: Array), + completedRegularChunks: ([]: Array), completedErrorChunks: ([]: Array), writtenSymbols: new Map(), writtenClientReferences: new Map(), @@ -733,7 +737,6 @@ function serializeLargeTextString(request: Request, text: string): string { const headerChunk = processTextHeader( request, textId, - text, byteLengthOfChunk(textChunk), ); request.completedRegularChunks.push(headerChunk, textChunk); @@ -753,6 +756,25 @@ function serializeSet(request: Request, set: Set): string { return '$W' + id.toString(16); } +function serializeTypedArray( + request: Request, + tag: string, + typedArray: $ArrayBufferView, +): string { + request.pendingChunks += 2; + const bufferId = request.nextChunkId++; + // TODO: Convert to little endian if that's not the server default. + const binaryChunk = typedArrayToBinaryChunk(typedArray); + const headerChunk = processBufferHeader( + request, + tag, + bufferId, + byteLengthOfBinaryChunk(binaryChunk), + ); + request.completedRegularChunks.push(headerChunk, binaryChunk); + return serializeByValueID(bufferId); +} + function escapeStringValue(value: string): string { if (value[0] === '$') { // We need to escape $ prefixed strings since we use those to encode @@ -942,12 +964,68 @@ function resolveModelToJSON( } return (undefined: any); } + if (value instanceof Map) { return serializeMap(request, value); } if (value instanceof Set) { return serializeSet(request, value); } + + if (enableBinaryFlight) { + if (value instanceof ArrayBuffer) { + return serializeTypedArray(request, 'A', new Uint8Array(value)); + } + if (value instanceof Int8Array) { + // char + return serializeTypedArray(request, 'C', value); + } + if (value instanceof Uint8Array) { + // unsigned char + return serializeTypedArray(request, 'c', value); + } + if (value instanceof Uint8ClampedArray) { + // unsigned clamped char + return serializeTypedArray(request, 'U', value); + } + if (value instanceof Int16Array) { + // sort + return serializeTypedArray(request, 'S', value); + } + if (value instanceof Uint16Array) { + // unsigned short + return serializeTypedArray(request, 's', value); + } + if (value instanceof Int32Array) { + // long + return serializeTypedArray(request, 'L', value); + } + if (value instanceof Uint32Array) { + // unsigned long + return serializeTypedArray(request, 'l', value); + } + if (value instanceof Float32Array) { + // float + return serializeTypedArray(request, 'F', value); + } + if (value instanceof Float64Array) { + // double + return serializeTypedArray(request, 'D', value); + } + if (value instanceof BigInt64Array) { + // number + return serializeTypedArray(request, 'N', value); + } + if (value instanceof BigUint64Array) { + // unsigned number + // We use "m" instead of "n" since JSON can start with "null" + return serializeTypedArray(request, 'm', value); + } + if (value instanceof DataView) { + return serializeTypedArray(request, 'V', value); + } + } + if (!isArray(value)) { const iteratorFn = getIteratorFn(value); if (iteratorFn) { @@ -1593,9 +1671,18 @@ function processHintChunk( function processTextHeader( request: Request, id: number, - text: string, binaryLength: number, ): Chunk { const row = id.toString(16) + ':T' + binaryLength.toString(16) + ','; return stringToChunk(row); } + +function processBufferHeader( + request: Request, + tag: string, + id: number, + binaryLength: number, +): Chunk { + const row = id.toString(16) + ':' + tag + binaryLength.toString(16) + ','; + return stringToChunk(row); +} diff --git a/packages/react-server/src/ReactServerStreamConfigBrowser.js b/packages/react-server/src/ReactServerStreamConfigBrowser.js index 44b5e5af839b8..c6f2ba8d7cf0a 100644 --- a/packages/react-server/src/ReactServerStreamConfigBrowser.js +++ b/packages/react-server/src/ReactServerStreamConfigBrowser.js @@ -11,6 +11,7 @@ export type Destination = ReadableStreamController; export type PrecomputedChunk = Uint8Array; export opaque type Chunk = Uint8Array; +export type BinaryChunk = Uint8Array; export function scheduleWork(callback: () => void) { callback(); @@ -32,13 +33,13 @@ export function beginWriting(destination: Destination) { export function writeChunk( destination: Destination, - chunk: PrecomputedChunk | Chunk, + chunk: PrecomputedChunk | Chunk | BinaryChunk, ): void { - if (chunk.length === 0) { + if (chunk.byteLength === 0) { return; } - if (chunk.length > VIEW_SIZE) { + if (chunk.byteLength > VIEW_SIZE) { if (__DEV__) { if (precomputedChunkSet.has(chunk)) { console.error( @@ -68,7 +69,7 @@ export function writeChunk( let bytesToWrite = chunk; const allowableBytes = ((currentView: any): Uint8Array).length - writtenBytes; - if (allowableBytes < bytesToWrite.length) { + if (allowableBytes < bytesToWrite.byteLength) { // this chunk would overflow the current view. We enqueue a full view // and start a new view with the remaining chunk if (allowableBytes === 0) { @@ -89,12 +90,12 @@ export function writeChunk( writtenBytes = 0; } ((currentView: any): Uint8Array).set(bytesToWrite, writtenBytes); - writtenBytes += bytesToWrite.length; + writtenBytes += bytesToWrite.byteLength; } export function writeChunkAndReturn( destination: Destination, - chunk: PrecomputedChunk | Chunk, + chunk: PrecomputedChunk | Chunk | BinaryChunk, ): boolean { writeChunk(destination, chunk); // in web streams there is no backpressure so we can alwas write more @@ -119,7 +120,9 @@ export function stringToChunk(content: string): Chunk { return textEncoder.encode(content); } -const precomputedChunkSet: Set = __DEV__ ? new Set() : (null: any); +const precomputedChunkSet: Set = __DEV__ + ? new Set() + : (null: any); export function stringToPrecomputedChunk(content: string): PrecomputedChunk { const precomputedChunk = textEncoder.encode(content); @@ -131,10 +134,27 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk { return precomputedChunk; } +export function typedArrayToBinaryChunk( + content: $ArrayBufferView, +): BinaryChunk { + // Convert any non-Uint8Array array to Uint8Array. We could avoid this for Uint8Arrays. + // If we passed through this straight to enqueue we wouldn't have to convert it but since + // we need to copy the buffer in that case, we need to convert it to copy it. + // When we copy it into another array using set() it needs to be a Uint8Array. + const buffer = new Uint8Array( + content.buffer, + content.byteOffset, + content.byteLength, + ); + // We clone large chunks so that we can transfer them when we write them. + // Others get copied into the target buffer. + return content.byteLength > VIEW_SIZE ? buffer.slice() : buffer; +} + export function clonePrecomputedChunk( precomputedChunk: PrecomputedChunk, ): PrecomputedChunk { - return precomputedChunk.length > VIEW_SIZE + return precomputedChunk.byteLength > VIEW_SIZE ? precomputedChunk.slice() : precomputedChunk; } @@ -143,6 +163,10 @@ export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number { return chunk.byteLength; } +export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number { + return chunk.byteLength; +} + export function closeWithError(destination: Destination, error: mixed): void { // $FlowFixMe[method-unbinding] if (typeof destination.error === 'function') { diff --git a/packages/react-server/src/ReactServerStreamConfigBun.js b/packages/react-server/src/ReactServerStreamConfigBun.js index ac245209d53d0..27317f0925cd4 100644 --- a/packages/react-server/src/ReactServerStreamConfigBun.js +++ b/packages/react-server/src/ReactServerStreamConfigBun.js @@ -9,13 +9,14 @@ type BunReadableStreamController = ReadableStreamController & { end(): mixed, - write(data: Chunk): void, + write(data: Chunk | BinaryChunk): void, error(error: Error): void, }; export type Destination = BunReadableStreamController; export type PrecomputedChunk = string; export opaque type Chunk = string; +export type BinaryChunk = $ArrayBufferView; export function scheduleWork(callback: () => void) { callback(); @@ -30,7 +31,7 @@ export function beginWriting(destination: Destination) {} export function writeChunk( destination: Destination, - chunk: PrecomputedChunk | Chunk, + chunk: PrecomputedChunk | Chunk | BinaryChunk, ): void { if (chunk.length === 0) { return; @@ -41,7 +42,7 @@ export function writeChunk( export function writeChunkAndReturn( destination: Destination, - chunk: PrecomputedChunk | Chunk, + chunk: PrecomputedChunk | Chunk | BinaryChunk, ): boolean { return !!destination.write(chunk); } @@ -60,6 +61,13 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk { return content; } +export function typedArrayToBinaryChunk( + content: $ArrayBufferView, +): BinaryChunk { + // TODO: Does this needs to be cloned if it's transferred in enqueue()? + return content; +} + export function clonePrecomputedChunk( chunk: PrecomputedChunk, ): PrecomputedChunk { @@ -70,6 +78,10 @@ export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number { return Buffer.byteLength(chunk, 'utf8'); } +export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number { + return chunk.byteLength; +} + export function closeWithError(destination: Destination, error: mixed): void { if (typeof destination.error === 'function') { // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types. diff --git a/packages/react-server/src/ReactServerStreamConfigEdge.js b/packages/react-server/src/ReactServerStreamConfigEdge.js index 00b0f4077d5b9..b665a13706edc 100644 --- a/packages/react-server/src/ReactServerStreamConfigEdge.js +++ b/packages/react-server/src/ReactServerStreamConfigEdge.js @@ -11,6 +11,7 @@ export type Destination = ReadableStreamController; export type PrecomputedChunk = Uint8Array; export opaque type Chunk = Uint8Array; +export type BinaryChunk = Uint8Array; export function scheduleWork(callback: () => void) { setTimeout(callback, 0); @@ -32,13 +33,13 @@ export function beginWriting(destination: Destination) { export function writeChunk( destination: Destination, - chunk: PrecomputedChunk | Chunk, + chunk: PrecomputedChunk | Chunk | BinaryChunk, ): void { - if (chunk.length === 0) { + if (chunk.byteLength === 0) { return; } - if (chunk.length > VIEW_SIZE) { + if (chunk.byteLength > VIEW_SIZE) { if (__DEV__) { if (precomputedChunkSet.has(chunk)) { console.error( @@ -68,7 +69,7 @@ export function writeChunk( let bytesToWrite = chunk; const allowableBytes = ((currentView: any): Uint8Array).length - writtenBytes; - if (allowableBytes < bytesToWrite.length) { + if (allowableBytes < bytesToWrite.byteLength) { // this chunk would overflow the current view. We enqueue a full view // and start a new view with the remaining chunk if (allowableBytes === 0) { @@ -89,12 +90,12 @@ export function writeChunk( writtenBytes = 0; } ((currentView: any): Uint8Array).set(bytesToWrite, writtenBytes); - writtenBytes += bytesToWrite.length; + writtenBytes += bytesToWrite.byteLength; } export function writeChunkAndReturn( destination: Destination, - chunk: PrecomputedChunk | Chunk, + chunk: PrecomputedChunk | Chunk | BinaryChunk, ): boolean { writeChunk(destination, chunk); // in web streams there is no backpressure so we can alwas write more @@ -119,7 +120,9 @@ export function stringToChunk(content: string): Chunk { return textEncoder.encode(content); } -const precomputedChunkSet: Set = __DEV__ ? new Set() : (null: any); +const precomputedChunkSet: Set = __DEV__ + ? new Set() + : (null: any); export function stringToPrecomputedChunk(content: string): PrecomputedChunk { const precomputedChunk = textEncoder.encode(content); @@ -131,10 +134,27 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk { return precomputedChunk; } +export function typedArrayToBinaryChunk( + content: $ArrayBufferView, +): BinaryChunk { + // Convert any non-Uint8Array array to Uint8Array. We could avoid this for Uint8Arrays. + // If we passed through this straight to enqueue we wouldn't have to convert it but since + // we need to copy the buffer in that case, we need to convert it to copy it. + // When we copy it into another array using set() it needs to be a Uint8Array. + const buffer = new Uint8Array( + content.buffer, + content.byteOffset, + content.byteLength, + ); + // We clone large chunks so that we can transfer them when we write them. + // Others get copied into the target buffer. + return content.byteLength > VIEW_SIZE ? buffer.slice() : buffer; +} + export function clonePrecomputedChunk( precomputedChunk: PrecomputedChunk, ): PrecomputedChunk { - return precomputedChunk.length > VIEW_SIZE + return precomputedChunk.byteLength > VIEW_SIZE ? precomputedChunk.slice() : precomputedChunk; } @@ -143,6 +163,10 @@ export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number { return chunk.byteLength; } +export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number { + return chunk.byteLength; +} + export function closeWithError(destination: Destination, error: mixed): void { // $FlowFixMe[method-unbinding] if (typeof destination.error === 'function') { diff --git a/packages/react-server/src/ReactServerStreamConfigNode.js b/packages/react-server/src/ReactServerStreamConfigNode.js index 12814e36e0b47..d6784e3c77b3a 100644 --- a/packages/react-server/src/ReactServerStreamConfigNode.js +++ b/packages/react-server/src/ReactServerStreamConfigNode.js @@ -19,6 +19,7 @@ export type Destination = Writable & MightBeFlushable; export type PrecomputedChunk = Uint8Array; export opaque type Chunk = string; +export type BinaryChunk = Uint8Array; export function scheduleWork(callback: () => void) { setImmediate(callback); @@ -89,7 +90,10 @@ function writeStringChunk(destination: Destination, stringChunk: string) { } } -function writeViewChunk(destination: Destination, chunk: PrecomputedChunk) { +function writeViewChunk( + destination: Destination, + chunk: PrecomputedChunk | BinaryChunk, +) { if (chunk.byteLength === 0) { return; } @@ -152,16 +156,19 @@ function writeViewChunk(destination: Destination, chunk: PrecomputedChunk) { export function writeChunk( destination: Destination, - chunk: PrecomputedChunk | Chunk, + chunk: PrecomputedChunk | Chunk | BinaryChunk, ): void { if (typeof chunk === 'string') { writeStringChunk(destination, chunk); } else { - writeViewChunk(destination, ((chunk: any): PrecomputedChunk)); + writeViewChunk(destination, ((chunk: any): PrecomputedChunk | BinaryChunk)); } } -function writeToDestination(destination: Destination, view: Uint8Array) { +function writeToDestination( + destination: Destination, + view: string | Uint8Array, +) { const currentHasCapacity = destination.write(view); destinationHasCapacity = destinationHasCapacity && currentHasCapacity; } @@ -207,6 +214,13 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk { return precomputedChunk; } +export function typedArrayToBinaryChunk( + content: $ArrayBufferView, +): BinaryChunk { + // Convert any non-Uint8Array array to Uint8Array. We could avoid this for Uint8Arrays. + return new Uint8Array(content.buffer, content.byteOffset, content.byteLength); +} + export function clonePrecomputedChunk( precomputedChunk: PrecomputedChunk, ): PrecomputedChunk { @@ -221,6 +235,10 @@ export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number { : chunk.byteLength; } +export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number { + return chunk.byteLength; +} + export function closeWithError(destination: Destination, error: mixed): void { // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types. destination.destroy(error); diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.custom.js b/packages/react-server/src/forks/ReactServerStreamConfig.custom.js index 913bb56d67e64..23bd4c35ddfa5 100644 --- a/packages/react-server/src/forks/ReactServerStreamConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerStreamConfig.custom.js @@ -28,6 +28,7 @@ export opaque type Destination = mixed; // eslint-disable-line no-undef export opaque type PrecomputedChunk = mixed; // eslint-disable-line no-undef export opaque type Chunk = mixed; // eslint-disable-line no-undef +export opaque type BinaryChunk = mixed; // eslint-disable-line no-undef export const scheduleWork = $$$config.scheduleWork; export const beginWriting = $$$config.beginWriting; @@ -39,5 +40,7 @@ export const close = $$$config.close; export const closeWithError = $$$config.closeWithError; export const stringToChunk = $$$config.stringToChunk; export const stringToPrecomputedChunk = $$$config.stringToPrecomputedChunk; +export const typedArrayToBinaryChunk = $$$config.typedArrayToBinaryChunk; export const clonePrecomputedChunk = $$$config.clonePrecomputedChunk; export const byteLengthOfChunk = $$$config.byteLengthOfChunk; +export const byteLengthOfBinaryChunk = $$$config.byteLengthOfBinaryChunk; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 86aaaa9403268..3813798d09b3e 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -84,6 +84,8 @@ export const enableFetchInstrumentation = true; export const enableFormActions = __EXPERIMENTAL__; +export const enableBinaryFlight = __EXPERIMENTAL__; + export const enableTransitionTracing = false; // No known bugs, but needs performance testing diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 85f9ef17c329d..70909c55a0309 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -36,6 +36,7 @@ export const enableLegacyCache = false; export const enableCacheElement = true; export const enableFetchInstrumentation = false; export const enableFormActions = true; // Doesn't affect Native +export const enableBinaryFlight = true; export const enableSchedulerDebugging = false; export const debugRenderPhaseSideEffectsForStrictMode = true; export const disableJavaScriptURLs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 1ec92ff86e116..675f937d58d49 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -24,6 +24,7 @@ export const enableLegacyCache = false; export const enableCacheElement = false; export const enableFetchInstrumentation = false; export const enableFormActions = true; // Doesn't affect Native +export const enableBinaryFlight = true; export const disableJavaScriptURLs = false; export const disableCommentsAsDOMContainers = true; export const disableInputAttributeSyncing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index b1e9fc806358a..e83c4dab44b9d 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -24,6 +24,7 @@ export const enableLegacyCache = __EXPERIMENTAL__; export const enableCacheElement = __EXPERIMENTAL__; export const enableFetchInstrumentation = true; export const enableFormActions = true; // Doesn't affect Test Renderer +export const enableBinaryFlight = true; export const disableJavaScriptURLs = false; export const disableCommentsAsDOMContainers = true; export const disableInputAttributeSyncing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 049854054cfeb..9ddc9004125f5 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -24,6 +24,7 @@ export const enableLegacyCache = false; export const enableCacheElement = true; export const enableFetchInstrumentation = false; export const enableFormActions = true; // Doesn't affect Test Renderer +export const enableBinaryFlight = true; export const disableJavaScriptURLs = false; export const disableCommentsAsDOMContainers = true; export const disableInputAttributeSyncing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index a702ec223d234..bdfc1acd77eed 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -24,6 +24,7 @@ export const enableLegacyCache = true; export const enableCacheElement = true; export const enableFetchInstrumentation = false; export const enableFormActions = true; // Doesn't affect Test Renderer +export const enableBinaryFlight = true; export const enableSchedulerDebugging = false; export const disableJavaScriptURLs = false; export const disableCommentsAsDOMContainers = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 48ca992b9b302..6321770d7946a 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -75,6 +75,8 @@ export const enableFetchInstrumentation = false; export const enableFormActions = false; +export const enableBinaryFlight = true; + export const disableJavaScriptURLs = true; // TODO: www currently relies on this feature. It's disabled in open source. diff --git a/scripts/rollup/validate/eslintrc.cjs.js b/scripts/rollup/validate/eslintrc.cjs.js index e8c2943b93349..313f9639fb984 100644 --- a/scripts/rollup/validate/eslintrc.cjs.js +++ b/scripts/rollup/validate/eslintrc.cjs.js @@ -14,7 +14,21 @@ module.exports = { Symbol: 'readonly', WeakMap: 'readonly', WeakSet: 'readonly', + + Int8Array: 'readonly', + Uint8Array: 'readonly', + Uint8ClampedArray: 'readonly', + Int16Array: 'readonly', Uint16Array: 'readonly', + Int32Array: 'readonly', + Uint32Array: 'readonly', + Float32Array: 'readonly', + Float64Array: 'readonly', + BigInt64Array: 'readonly', + BigUint64Array: 'readonly', + DataView: 'readonly', + ArrayBuffer: 'readonly', + Reflect: 'readonly', globalThis: 'readonly', // Vendor specific @@ -28,15 +42,11 @@ module.exports = { trustedTypes: 'readonly', // Scheduler profiling - Int32Array: 'readonly', - ArrayBuffer: 'readonly', - TaskController: 'readonly', reportError: 'readonly', AggregateError: 'readonly', // Flight - Uint8Array: 'readonly', Promise: 'readonly', // Temp diff --git a/scripts/rollup/validate/eslintrc.cjs2015.js b/scripts/rollup/validate/eslintrc.cjs2015.js index e5b79f84c3e09..dc35c98311863 100644 --- a/scripts/rollup/validate/eslintrc.cjs2015.js +++ b/scripts/rollup/validate/eslintrc.cjs2015.js @@ -14,7 +14,21 @@ module.exports = { Symbol: 'readonly', WeakMap: 'readonly', WeakSet: 'readonly', + + Int8Array: 'readonly', + Uint8Array: 'readonly', + Uint8ClampedArray: 'readonly', + Int16Array: 'readonly', Uint16Array: 'readonly', + Int32Array: 'readonly', + Uint32Array: 'readonly', + Float32Array: 'readonly', + Float64Array: 'readonly', + BigInt64Array: 'readonly', + BigUint64Array: 'readonly', + DataView: 'readonly', + ArrayBuffer: 'readonly', + Reflect: 'readonly', globalThis: 'readonly', // Vendor specific @@ -28,15 +42,11 @@ module.exports = { trustedTypes: 'readonly', // Scheduler profiling - Int32Array: 'readonly', - ArrayBuffer: 'readonly', - TaskController: 'readonly', reportError: 'readonly', AggregateError: 'readonly', // Flight - Uint8Array: 'readonly', Promise: 'readonly', // Temp diff --git a/scripts/rollup/validate/eslintrc.esm.js b/scripts/rollup/validate/eslintrc.esm.js index a9bf9ab5a0bcc..ae1846c08d7ba 100644 --- a/scripts/rollup/validate/eslintrc.esm.js +++ b/scripts/rollup/validate/eslintrc.esm.js @@ -14,7 +14,21 @@ module.exports = { Symbol: 'readonly', WeakMap: 'readonly', WeakSet: 'readonly', + + Int8Array: 'readonly', + Uint8Array: 'readonly', + Uint8ClampedArray: 'readonly', + Int16Array: 'readonly', Uint16Array: 'readonly', + Int32Array: 'readonly', + Uint32Array: 'readonly', + Float32Array: 'readonly', + Float64Array: 'readonly', + BigInt64Array: 'readonly', + BigUint64Array: 'readonly', + DataView: 'readonly', + ArrayBuffer: 'readonly', + Reflect: 'readonly', globalThis: 'readonly', // Vendor specific @@ -28,15 +42,11 @@ module.exports = { trustedTypes: 'readonly', // Scheduler profiling - Int32Array: 'readonly', - ArrayBuffer: 'readonly', - TaskController: 'readonly', reportError: 'readonly', AggregateError: 'readonly', // Flight - Uint8Array: 'readonly', Promise: 'readonly', // Temp diff --git a/scripts/rollup/validate/eslintrc.fb.js b/scripts/rollup/validate/eslintrc.fb.js index 122e7e3fd4c4d..94483e5fe075a 100644 --- a/scripts/rollup/validate/eslintrc.fb.js +++ b/scripts/rollup/validate/eslintrc.fb.js @@ -14,7 +14,21 @@ module.exports = { Proxy: 'readonly', WeakMap: 'readonly', WeakSet: 'readonly', + + Int8Array: 'readonly', + Uint8Array: 'readonly', + Uint8ClampedArray: 'readonly', + Int16Array: 'readonly', Uint16Array: 'readonly', + Int32Array: 'readonly', + Uint32Array: 'readonly', + Float32Array: 'readonly', + Float64Array: 'readonly', + BigInt64Array: 'readonly', + BigUint64Array: 'readonly', + DataView: 'readonly', + ArrayBuffer: 'readonly', + Reflect: 'readonly', globalThis: 'readonly', // Vendor specific @@ -29,15 +43,11 @@ module.exports = { trustedTypes: 'readonly', // Scheduler profiling - Int32Array: 'readonly', - ArrayBuffer: 'readonly', - TaskController: 'readonly', reportError: 'readonly', AggregateError: 'readonly', // Flight - Uint8Array: 'readonly', Promise: 'readonly', // Temp diff --git a/scripts/rollup/validate/eslintrc.rn.js b/scripts/rollup/validate/eslintrc.rn.js index 000c1ae92a7a3..9038701285521 100644 --- a/scripts/rollup/validate/eslintrc.rn.js +++ b/scripts/rollup/validate/eslintrc.rn.js @@ -14,6 +14,21 @@ module.exports = { Proxy: 'readonly', WeakMap: 'readonly', WeakSet: 'readonly', + + Int8Array: 'readonly', + Uint8Array: 'readonly', + Uint8ClampedArray: 'readonly', + Int16Array: 'readonly', + Uint16Array: 'readonly', + Int32Array: 'readonly', + Uint32Array: 'readonly', + Float32Array: 'readonly', + Float64Array: 'readonly', + BigInt64Array: 'readonly', + BigUint64Array: 'readonly', + DataView: 'readonly', + ArrayBuffer: 'readonly', + Reflect: 'readonly', globalThis: 'readonly', // Vendor specific @@ -29,9 +44,6 @@ module.exports = { // RN supports this setImmediate: 'readonly', // Scheduler profiling - Int32Array: 'readonly', - ArrayBuffer: 'readonly', - TaskController: 'readonly', reportError: 'readonly', AggregateError: 'readonly', diff --git a/scripts/rollup/validate/eslintrc.umd.js b/scripts/rollup/validate/eslintrc.umd.js index 17eae00040551..020bfe0bb6b0c 100644 --- a/scripts/rollup/validate/eslintrc.umd.js +++ b/scripts/rollup/validate/eslintrc.umd.js @@ -13,7 +13,21 @@ module.exports = { Proxy: 'readonly', WeakMap: 'readonly', WeakSet: 'readonly', + + Int8Array: 'readonly', + Uint8Array: 'readonly', + Uint8ClampedArray: 'readonly', + Int16Array: 'readonly', Uint16Array: 'readonly', + Int32Array: 'readonly', + Uint32Array: 'readonly', + Float32Array: 'readonly', + Float64Array: 'readonly', + BigInt64Array: 'readonly', + BigUint64Array: 'readonly', + DataView: 'readonly', + ArrayBuffer: 'readonly', + Reflect: 'readonly', globalThis: 'readonly', // Vendor specific @@ -33,15 +47,11 @@ module.exports = { trustedTypes: 'readonly', // Scheduler profiling - Int32Array: 'readonly', - ArrayBuffer: 'readonly', - TaskController: 'readonly', reportError: 'readonly', AggregateError: 'readonly', // Flight - Uint8Array: 'readonly', Promise: 'readonly', // Temp