diff --git a/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js b/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js index d1b68be1b350e..ed7e32f65d5c6 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js @@ -54,6 +54,12 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk { return content; } +export function clonePrecomputedChunk( + chunk: PrecomputedChunk, +): PrecomputedChunk { + return chunk; +} + export function closeWithError(destination: Destination, error: mixed): void { // $FlowFixMe: This is an Error object or the destination accepts other types. destination.destroy(error); diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index b6a5727314cbe..91d241e4e7929 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -37,6 +37,7 @@ import { writeChunkAndReturn, stringToChunk, stringToPrecomputedChunk, + clonePrecomputedChunk, } from 'react-server/src/ReactServerStreamConfig'; import { @@ -2443,7 +2444,10 @@ export function writeCompletedBoundaryInstruction( if (!responseState.sentCompleteBoundaryFunction) { responseState.sentCompleteBoundaryFunction = true; responseState.sentStyleInsertionFunction = true; - writeChunk(destination, completeBoundaryWithStylesScript1FullBoth); + writeChunk( + destination, + clonePrecomputedChunk(completeBoundaryWithStylesScript1FullBoth), + ); } else if (!responseState.sentStyleInsertionFunction) { responseState.sentStyleInsertionFunction = true; writeChunk(destination, completeBoundaryWithStylesScript1FullPartial); diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index 522620c6f5a50..586512bc963f4 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -45,6 +45,9 @@ const ReactNoopFlightServer = ReactFlightServer({ stringToPrecomputedChunk(content: string): string { return content; }, + clonePrecomputedChunk(chunk: string): string { + return chunk; + }, isModuleReference(reference: Object): boolean { return reference.$$typeof === Symbol.for('react.module.reference'); }, diff --git a/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js b/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js index a1874ce362386..bc5e97680018a 100644 --- a/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js +++ b/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js @@ -59,6 +59,12 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk { return content; } +export function clonePrecomputedChunk( + chunk: PrecomputedChunk, +): PrecomputedChunk { + return chunk; +} + export function closeWithError(destination: Destination, error: mixed): void { destination.done = true; destination.fatal = true; diff --git a/packages/react-server/src/ReactServerStreamConfigBrowser.js b/packages/react-server/src/ReactServerStreamConfigBrowser.js index 082e0edbaf46c..2d9346efe54db 100644 --- a/packages/react-server/src/ReactServerStreamConfigBrowser.js +++ b/packages/react-server/src/ReactServerStreamConfigBrowser.js @@ -46,6 +46,15 @@ export function writeChunk( } if (chunk.length > VIEW_SIZE) { + if (__DEV__) { + if (precomputedChunkSet.has(chunk)) { + console.error( + 'A large precomputed chunk was passed to writeChunk without being copied.' + + ' Large chunks get enqueued directly and are not copied. This is incompatible with precomputed chunks because you cannot enqueue the same precomputed chunk twice.' + + ' Use "cloneChunk" to make a copy of this large precomputed chunk before writing it. This is a bug in React.', + ); + } + } // this chunk may overflow a single view which implies it was not // one that is cached by the streaming renderer. We will enqueu // it directly and expect it is not re-used @@ -117,8 +126,24 @@ export function stringToChunk(content: string): Chunk { return textEncoder.encode(content); } +const precomputedChunkSet: Set = __DEV__ ? new Set() : (null: any); + export function stringToPrecomputedChunk(content: string): PrecomputedChunk { - return textEncoder.encode(content); + const precomputedChunk = textEncoder.encode(content); + + if (__DEV__) { + precomputedChunkSet.add(precomputedChunk); + } + + return precomputedChunk; +} + +export function clonePrecomputedChunk( + precomputedChunk: PrecomputedChunk, +): PrecomputedChunk { + return precomputedChunk.length > VIEW_SIZE + ? precomputedChunk.slice() + : precomputedChunk; } export function closeWithError(destination: Destination, error: mixed): void { diff --git a/packages/react-server/src/ReactServerStreamConfigBun.js b/packages/react-server/src/ReactServerStreamConfigBun.js index c50ce77fa9a7b..fd90c17a3d1e0 100644 --- a/packages/react-server/src/ReactServerStreamConfigBun.js +++ b/packages/react-server/src/ReactServerStreamConfigBun.js @@ -64,6 +64,12 @@ export function stringToPrecomputedChunk(content: string): PrecomputedChunk { return content; } +export function clonePrecomputedChunk( + chunk: PrecomputedChunk, +): PrecomputedChunk { + return chunk; +} + 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 5790682d301c9..458c84e076d87 100644 --- a/packages/react-server/src/ReactServerStreamConfigNode.js +++ b/packages/react-server/src/ReactServerStreamConfigNode.js @@ -95,6 +95,15 @@ function writeViewChunk(destination: Destination, chunk: PrecomputedChunk) { return; } if (chunk.byteLength > VIEW_SIZE) { + if (__DEV__) { + if (precomputedChunkSet && precomputedChunkSet.has(chunk)) { + console.error( + 'A large precomputed chunk was passed to writeChunk without being copied.' + + ' Large chunks get enqueued directly and are not copied. This is incompatible with precomputed chunks because you cannot enqueue the same precomputed chunk twice.' + + ' Use "cloneChunk" to make a copy of this large precomputed chunk before writing it. This is a bug in React.', + ); + } + } // this chunk may overflow a single view which implies it was not // one that is cached by the streaming renderer. We will enqueu // it directly and expect it is not re-used @@ -185,8 +194,26 @@ export function stringToChunk(content: string): Chunk { return content; } +const precomputedChunkSet = __DEV__ ? new Set() : null; + export function stringToPrecomputedChunk(content: string): PrecomputedChunk { - return textEncoder.encode(content); + const precomputedChunk = textEncoder.encode(content); + + if (__DEV__) { + if (precomputedChunkSet) { + precomputedChunkSet.add(precomputedChunk); + } + } + + return precomputedChunk; +} + +export function clonePrecomputedChunk( + precomputedChunk: PrecomputedChunk, +): PrecomputedChunk { + return precomputedChunk.length > VIEW_SIZE + ? precomputedChunk.slice() + : precomputedChunk; } export function closeWithError(destination: Destination, error: mixed): void { diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.custom.js b/packages/react-server/src/forks/ReactServerStreamConfig.custom.js index 8a5fd3173c96b..0e7bafab961aa 100644 --- a/packages/react-server/src/forks/ReactServerStreamConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerStreamConfig.custom.js @@ -41,3 +41,4 @@ export const close = $$$hostConfig.close; export const closeWithError = $$$hostConfig.closeWithError; export const stringToChunk = $$$hostConfig.stringToChunk; export const stringToPrecomputedChunk = $$$hostConfig.stringToPrecomputedChunk; +export const clonePrecomputedChunk = $$$hostConfig.clonePrecomputedChunk;