From 5c60c592d676c9deb7980a14ecb57c645c847c39 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Wed, 8 Apr 2026 19:47:53 +0200 Subject: [PATCH] Node.js streams: Apply comments from #92252 --- .../app-render/debug-channel-server.web.ts | 2 +- .../src/server/app-render/stream-ops.node.ts | 60 +++---------------- .../src/server/app-render/stream-ops.web.ts | 7 +-- .../stream-utils/node-web-streams-helper.ts | 60 ------------------- 4 files changed, 12 insertions(+), 117 deletions(-) diff --git a/packages/next/src/server/app-render/debug-channel-server.web.ts b/packages/next/src/server/app-render/debug-channel-server.web.ts index c30480d7a337..c84c581e2248 100644 --- a/packages/next/src/server/app-render/debug-channel-server.web.ts +++ b/packages/next/src/server/app-render/debug-channel-server.web.ts @@ -54,5 +54,5 @@ export function createWebDebugChannel(): DebugChannelPair { * which expects debugChannel to be a Node.js stream with a .write() method. */ export function createNodeDebugChannel(): DebugChannelPair { - throw new Error('not implemented') + throw new Error('Not implemented') } diff --git a/packages/next/src/server/app-render/stream-ops.node.ts b/packages/next/src/server/app-render/stream-ops.node.ts index 554246405494..8d5c119096fe 100644 --- a/packages/next/src/server/app-render/stream-ops.node.ts +++ b/packages/next/src/server/app-render/stream-ops.node.ts @@ -26,7 +26,6 @@ import { streamToString as webStreamToString, createDocumentClosingStream as webCreateDocumentClosingStream, createRuntimePrefetchTransformStream, - CLOSE_TAG, } from '../stream-utils/node-web-streams-helper' import { indexOfUint8Array } from '../stream-utils/uint8array-helpers' import { ENCODED_TAGS } from '../stream-utils/encoded-tags' @@ -210,9 +209,7 @@ function createFlightDataInjectionTransform( const nodeTransform = new Transform({ transform(chunk, _encoding, callback) { this.push(chunk) - if (delayDataUntilFirstHtmlChunk) { - startOrContinuePulling(this) - } + startOrContinuePulling(this) callback() }, flush(callback) { @@ -263,7 +260,7 @@ function createHeadInsertionTransform( if (index !== -1) { if (insertion) { const encodedInsertion = Buffer.from(insertion) - const merged = Buffer.allocUnsafe( + const merged = Buffer.alloc( chunk.length + encodedInsertion.length ) merged.set(chunk.slice(0, index)) @@ -336,6 +333,8 @@ function createMetadataTransform( } iconMarkLength = ENCODED_TAGS.META.ICON_MARK.length + // 47 is `/` – handle self-closing `` (length +2 for `/>`) + // vs non-self-closing `` (length +1 for `>`) if (chunk[iconMarkIndex + iconMarkLength] === 47) { iconMarkLength += 2 } else { @@ -345,7 +344,7 @@ function createMetadataTransform( if (chunkIndex === 0) { closedHeadIndex = indexOfUint8Array(chunk, ENCODED_TAGS.CLOSED.HEAD) if (iconMarkIndex < closedHeadIndex) { - const replaced = Buffer.allocUnsafe(chunk.length - iconMarkLength) + const replaced = Buffer.alloc(chunk.length - iconMarkLength) replaced.set(chunk.subarray(0, iconMarkIndex)) replaced.set( chunk.subarray(iconMarkIndex + iconMarkLength), @@ -356,7 +355,7 @@ function createMetadataTransform( const insertion = await insert() const encodedInsertion = Buffer.from(insertion) const insertionLength = encodedInsertion.length - const replaced = Buffer.allocUnsafe( + const replaced = Buffer.alloc( chunk.length - iconMarkLength + insertionLength ) replaced.set(chunk.subarray(0, iconMarkIndex)) @@ -372,7 +371,7 @@ function createMetadataTransform( const insertion = await insert() const encodedInsertion = Buffer.from(insertion) const insertionLength = encodedInsertion.length - const replaced = Buffer.allocUnsafe( + const replaced = Buffer.alloc( chunk.length - iconMarkLength + insertionLength ) replaced.set(chunk.subarray(0, iconMarkIndex)) @@ -393,36 +392,6 @@ function createMetadataTransform( }) } -// --------------------------------------------------------------------------- -// Deferred suffix – Node.js Transform that appends a suffix string after the -// first HTML chunk, deferring via queueMicrotask so the chunk flushes first. -// --------------------------------------------------------------------------- - -function createDeferredSuffixTransform(suffix: string): Transform { - let flushed = false - const encodedSuffix = Buffer.from(suffix) - - return new Transform({ - transform(chunk, _encoding, callback) { - this.push(chunk) - - if (!flushed) { - flushed = true - queueMicrotask(() => { - this.push(encodedSuffix) - }) - } - callback() - }, - flush(callback) { - if (!flushed) { - this.push(encodedSuffix) - } - callback() - }, - }) -} - // --------------------------------------------------------------------------- // Move suffix – Node.js Transform that strips from its // original position and re-appends it at the very end of the stream, so any @@ -495,9 +464,7 @@ function createHtmlDataDplIdTransform(dplId: string): Transform { const insertionPoint = htmlTagIndex + ENCODED_TAGS.OPENING.HTML.length const encodedAttribute = Buffer.from(` data-dpl-id="${dplId}"`) - const modified = Buffer.allocUnsafe( - chunk.length + encodedAttribute.length - ) + const modified = Buffer.alloc(chunk.length + encodedAttribute.length) modified.set(chunk.subarray(0, insertionPoint)) modified.set(encodedAttribute, insertionPoint) @@ -693,7 +660,6 @@ export async function resumeAndAbort( export async function continueFizzStream( renderStream: AnyStream, { - suffix, inlinedDataStream, isStaticGeneration, allReady, @@ -703,9 +669,6 @@ export async function continueFizzStream( validateRootLayout, }: import('./stream-ops.web').ContinueFizzStreamOptions ): Promise { - // Suffix itself might contain close tags at the end, so we need to split it. - const suffixUnclosed = suffix ? suffix.split(CLOSE_TAG, 1)[0] : null - if (isStaticGeneration) { if (allReady) { await allReady @@ -736,13 +699,6 @@ export async function continueFizzStream( source.pipe(metadata) source = metadata - // Insert suffix content - if (suffixUnclosed != null && suffixUnclosed.length > 0) { - const deferredSuffix = createDeferredSuffixTransform(suffixUnclosed) - source.pipe(deferredSuffix) - source = deferredSuffix - } - // Flight data injection – interleaves RSC data chunks with the HTML stream if (inlinedDataStream) { const flightInjection = createFlightDataInjectionTransform( diff --git a/packages/next/src/server/app-render/stream-ops.web.ts b/packages/next/src/server/app-render/stream-ops.web.ts index 766dce801d24..c0ad8c3a15bc 100644 --- a/packages/next/src/server/app-render/stream-ops.web.ts +++ b/packages/next/src/server/app-render/stream-ops.web.ts @@ -50,7 +50,6 @@ export type ContinueFizzStreamOptions = ContinueStreamSharedOptions & { isStaticGeneration: boolean allReady?: Promise validateRootLayout?: boolean - suffix?: string } export type ContinueStaticPrerenderOptions = ContinueStreamSharedOptions & { @@ -158,7 +157,7 @@ export function continueDynamicHTMLResumeNode( _renderStream: AnyStream, _opts: ContinueDynamicHTMLResumeOptions ): Promise { - throw new Error('not implemented') + throw new Error('Not implemented') } export async function streamToBuffer(stream: AnyStream): Promise { @@ -200,7 +199,7 @@ export function createNodeInlinedDataStream( _nonce: string | undefined, _formState: unknown | null ): AnyStream { - throw new Error('not implemented') + throw new Error('Not implemented') } export function createPendingStream(): AnyStream { @@ -235,7 +234,7 @@ export function renderToNodeFlightStream( _clientModules: FlightClientModules, _opts: FlightRenderOptions ): AnyStream { - throw new Error('not implemented') + throw new Error('Not implemented') } export function renderToWebFlightStream( diff --git a/packages/next/src/server/stream-utils/node-web-streams-helper.ts b/packages/next/src/server/stream-utils/node-web-streams-helper.ts index 5bcd6c812b49..229c14858e32 100644 --- a/packages/next/src/server/stream-utils/node-web-streams-helper.ts +++ b/packages/next/src/server/stream-utils/node-web-streams-helper.ts @@ -666,53 +666,6 @@ export function createInstantTestScriptInsertionTransformStream( }) } -// Suffix after main body content - scripts before , -// but wait for the major chunks to be enqueued. -export function createDeferredSuffixStream( - suffix: string -): TransformStream { - let flushed = false - let pending: DetachedPromise | undefined - - const flush = (controller: TransformStreamDefaultController) => { - const detached = new DetachedPromise() - pending = detached - - scheduleImmediate(() => { - try { - controller.enqueue(encoder.encode(suffix)) - } catch { - // If an error occurs while enqueuing it can't be due to this - // transformers fault. It's likely due to the controller being - // errored due to the stream being cancelled. - } finally { - pending = undefined - detached.resolve() - } - }) - } - - return new TransformStream({ - transform(chunk, controller) { - controller.enqueue(chunk) - - // If we've already flushed, we're done. - if (flushed) return - - // Schedule the flush to happen. - flushed = true - flush(controller) - }, - flush(controller) { - if (pending) return pending.promise - if (flushed) return - - // Flush now. - controller.enqueue(encoder.encode(suffix)) - }, - }) -} - export function createFlightDataInjectionTransformStream( stream: ReadableStream, delayDataUntilFirstHtmlChunk: boolean @@ -996,16 +949,11 @@ export type ContinueStreamOptions = { getServerInsertedHTML: () => Promise getServerInsertedMetadata: () => Promise validateRootLayout?: boolean - /** - * Suffix to inject after the buffered data, but before the close tags. - */ - suffix?: string | undefined } export async function continueFizzStream( renderStream: ReactDOMServerReadableStream, { - suffix, inlinedDataStream, isStaticGeneration, deploymentId, @@ -1014,9 +962,6 @@ export async function continueFizzStream( validateRootLayout, }: ContinueStreamOptions ): Promise> { - // Suffix itself might contain close tags at the end, so we need to split it. - const suffixUnclosed = suffix ? suffix.split(CLOSE_TAG, 1)[0] : null - if (isStaticGeneration) { // If we're generating static HTML we need to wait for it to resolve before continuing. await renderStream.allReady @@ -1036,11 +981,6 @@ export async function continueFizzStream( // Transform metadata createMetadataTransformStream(getServerInsertedMetadata), - // Insert suffix content - suffixUnclosed != null && suffixUnclosed.length > 0 - ? createDeferredSuffixStream(suffixUnclosed) - : null, - // Insert the inlined data (Flight data, form state, etc.) stream into the HTML inlinedDataStream ? createFlightDataInjectionTransformStream(inlinedDataStream, true)