From e5e3f54dc1b895d4e9ee3883243c6d5d70390333 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 13 Jun 2023 21:21:48 -0400 Subject: [PATCH] Add BinaryChunk type This lets us have different environments operate on different types, such as passing the raw typed array when possible. --- .eslintrc.js | 1 + .../ReactDOMLegacyServerStreamConfig.js | 15 ++++++- .../src/ReactServerStreamConfigFB.js | 11 +++++ .../react-server/src/ReactFlightServer.js | 6 +-- .../src/ReactServerStreamConfigBrowser.js | 40 +++++++++++++++---- .../src/ReactServerStreamConfigBun.js | 18 +++++++-- .../src/ReactServerStreamConfigEdge.js | 40 +++++++++++++++---- .../src/ReactServerStreamConfigNode.js | 26 ++++++++++-- .../forks/ReactServerStreamConfig.custom.js | 3 ++ 9 files changed, 132 insertions(+), 28 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 00e3094b06347..9c616ec6cf5c9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -454,6 +454,7 @@ module.exports = { $PropertyType: 'readonly', $ReadOnly: 'readonly', $ReadOnlyArray: 'readonly', + $ArrayBufferView: 'readonly', $Shape: 'readonly', AnimationFrameID: 'readonly', // For Flow type annotation. Only `BigInt` is valid at runtime. 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..1a76f38ca8b0f 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 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. @@ -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/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 3aab4f6111772..e477e036664d1 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -7,7 +7,7 @@ * @flow */ -import type {Chunk, Destination} from './ReactServerStreamConfig'; +import type {Chunk, BinaryChunk, Destination} from './ReactServerStreamConfig'; import { scheduleWork, @@ -172,7 +172,7 @@ export type Request = { pingedTasks: Array, completedImportChunks: Array, completedHintChunks: Array, - completedRegularChunks: Array, + completedRegularChunks: Array, completedErrorChunks: Array, writtenSymbols: Map, writtenClientReferences: Map, @@ -231,7 +231,7 @@ export function createRequest( pingedTasks: pingedTasks, completedImportChunks: ([]: Array), completedHintChunks: ([]: Array), - completedRegularChunks: ([]: Array), + completedRegularChunks: ([]: Array), completedErrorChunks: ([]: Array), writtenSymbols: new Map(), writtenClientReferences: new Map(), 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;