From 6094cc668892e236e8a007a802ea63feca6bfd51 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 16 Oct 2026 23:22:36 +0000 Subject: [PATCH 1/2] Drop TransformStream guards that construction makes unreachable DUP-42, as D-7 decides: unreachable code goes. A TransformStreamDefaultController was created and then attached to its stream in one synchronous step, before any user code could reach it, so the four "Controller is not attached to a stream" throws in its methods could never fire. The controller now takes its stream as a constructor argument, as ReadableStreamDefaultController does, and the init slot goes. Its length stays 0, which the module sets explicitly. The inner streams' start algorithms record their controllers while the TransformStream constructor builds the pair, before transformer.start(), the first user code that can reach it. The two fields are now typed as always set, and the nine checks for an unset one go, among them desiredSize's null arm and enqueue's "TransformStream is not fully initialized" throw. Nothing observable changes. --- src/per_isolate/webstreams/transform.ts | 104 +++++++----------------- 1 file changed, 31 insertions(+), 73 deletions(-) diff --git a/src/per_isolate/webstreams/transform.ts b/src/per_isolate/webstreams/transform.ts index 8243e9a3114..e3a1082636c 100644 --- a/src/per_isolate/webstreams/transform.ts +++ b/src/per_isolate/webstreams/transform.ts @@ -152,11 +152,6 @@ function makeTransformerAlgorithms( // --------------------------------------------------------------------------- -let transformStreamDefaultControllerInit: ( - controller: TransformStreamDefaultController, - stream: TransformStream -) => void; - let transformStreamEnqueue: ( stream: TransformStream, chunk: O @@ -178,7 +173,7 @@ class TransformStreamDefaultController< I = unknown, O = unknown, > implements TransformStreamDefaultControllerType { - #stream: TransformStream | undefined; + #stream: TransformStream; static { assertIsTransformStreamDefaultController = function ( @@ -187,50 +182,31 @@ class TransformStreamDefaultController< if (!isActualObject(self) || !(#stream in self)) throw new TypeError('Illegal invocation'); }; - - transformStreamDefaultControllerInit = (controller, stream) => { - controller.#stream = stream; - }; } - constructor(privateSymbol: symbol) { + constructor(privateSymbol: symbol, stream: TransformStream) { assertPrivateSymbol(privateSymbol); + this.#stream = stream; } get desiredSize(): number | null { assertIsTransformStreamDefaultController(this); - const stream = this.#stream; - if (stream === undefined) { - throw new TypeError('Controller is not attached to a stream'); - } - return transformStreamDesiredSize(stream); + return transformStreamDesiredSize(this.#stream); } enqueue(chunk: O = undefined as O): void { assertIsTransformStreamDefaultController(this); - const stream = this.#stream; - if (stream === undefined) { - throw new TypeError('Controller is not attached to a stream'); - } - transformStreamEnqueue(stream, chunk); + transformStreamEnqueue(this.#stream, chunk); } error(reason: unknown = undefined): void { assertIsTransformStreamDefaultController(this); - const stream = this.#stream; - if (stream === undefined) { - throw new TypeError('Controller is not attached to a stream'); - } - transformStreamError(stream, reason); + transformStreamError(this.#stream, reason); } terminate(): void { assertIsTransformStreamDefaultController(this); - const stream = this.#stream; - if (stream === undefined) { - throw new TypeError('Controller is not attached to a stream'); - } - transformStreamTerminate(stream); + transformStreamTerminate(this.#stream); } } @@ -241,8 +217,10 @@ class TransformStream { // allocated when there are no transformer callbacks to receive it. // @ts-expect-error -- retained for debugging/future use #controller?: TransformStreamDefaultController | undefined; - #readableController: object | undefined; - #writableController: object | undefined; + // Recorded by the inner streams' start algorithms, which run while the + // constructor builds the pair, before any user code can reach it. + #readableController!: object; + #writableController!: object; // Backpressure starts ON: the readable side must pull once before the // sink transforms the first chunk. #backpressure: boolean = true; @@ -257,21 +235,14 @@ class TransformStream { #finishPromise: Promise | undefined; static { - transformStreamDesiredSize = (stream) => { - const readableController = stream.#readableController; - return readableController === undefined - ? null - : readableControllerDesiredSizeGet(readableController); - }; + transformStreamDesiredSize = (stream) => + readableControllerDesiredSizeGet(stream.#readableController); transformStreamEnqueue = ( stream: TransformStream, chunk: O ) => { const readableController = stream.#readableController; - if (readableController === undefined) { - throw new TypeError('TransformStream is not fully initialized'); - } // Spec TransformStreamDefaultControllerEnqueue step 4: // if ReadableStreamDefaultControllerCanCloseOrEnqueue is false // (close requested, or the readable no longer readable), throw a @@ -308,21 +279,15 @@ class TransformStream { stream: TransformStream, reason: unknown ) => { - const readableController = stream.#readableController; - if (readableController !== undefined) { - readableControllerError(readableController, reason); - } + readableControllerError(stream.#readableController, reason); stream.#errorWritableAndUnblockWrite(reason); }; transformStreamTerminate = (stream: TransformStream) => { - const readableController = stream.#readableController; - if (readableController !== undefined) { - try { - readableControllerClose(readableController); - } catch { - // Already closed or errored — nothing to do. - } + try { + readableControllerClose(stream.#readableController); + } catch { + // Already closed or errored — nothing to do. } stream.#errorWritableAndUnblockWrite( new TypeError('The transform stream has been terminated') @@ -366,19 +331,15 @@ class TransformStream { reject(readableInternals.getStoredError(this.#readable)); return; } - const rc = this.#readableController; - if (rc !== undefined) { - try { - readableControllerClose(rc); - } catch { - // Already closed — acceptable per spec. - } + try { + readableControllerClose(this.#readableController); + } catch { + // Already closed — acceptable per spec. } resolve(); }, (r: unknown) => { - const rc = this.#readableController; - if (rc !== undefined) readableControllerError(rc, r); + readableControllerError(this.#readableController, r); reject(r); } ) @@ -406,13 +367,11 @@ class TransformStream { reject(readableInternals.getStoredError(this.#readable)); return; } - const rc = this.#readableController; - if (rc !== undefined) readableControllerError(rc, reason); + readableControllerError(this.#readableController, reason); resolve(); }, (r: unknown) => { - const rc = this.#readableController; - if (rc !== undefined) readableControllerError(rc, r); + readableControllerError(this.#readableController, r); reject(r); } ) @@ -460,10 +419,7 @@ class TransformStream { // later readable.cancel() from invoking the transformer's cancel // callback after the stream has already errored/terminated. this.#clearAlgorithms(); - const writableController = this.#writableController; - if (writableController !== undefined) { - writableControllerError(writableController, reason); - } + writableControllerError(this.#writableController, reason); if (this.#backpressure) { this.#setBackpressure(false); } @@ -558,8 +514,10 @@ class TransformStream { } else { // ---- STANDARD PATH (transformer has algorithms) ---- - controller = new TransformStreamDefaultController(kPrivateSymbol); - transformStreamDefaultControllerInit(controller, this); + controller = new TransformStreamDefaultController( + kPrivateSymbol, + this + ); this.#controller = controller; const algorithms = makeTransformerAlgorithms( @@ -667,7 +625,7 @@ class TransformStream { // for bodies built from this transform; not enforced here. if (expectedLength !== undefined) { readableInternals.setControllerExpectedLength( - this.#readableController as object, + this.#readableController, expectedLength ); } From 7f02098d55d6d7f95b05b3a487efe645ab8c300e Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 16 Oct 2026 23:24:13 +0000 Subject: [PATCH 2/2] Run TransformStream's close, abort and cancel through one finish DUP-24. The sink's close and abort algorithms and the source's cancel algorithm were three copies of one skeleton: return the finish promise if a finish has started; otherwise set it, run flush or cancel, clear the algorithms, and settle the finish when that result settles, rejecting with the ended half's stored error if that half has errored by then. They differed only in the algorithm and in the half they end: the readable for the sink's close and abort, the writable for the source's cancel. #finish(kind, reason) now holds the skeleton once, and the inner sink and source call it. #closeReadable is the spec's ReadableStreamDefaultControllerClose, which returns at once when the readable cannot close. terminate() and the close finish each spelled it as a try/catch around the captured close(). It now checks CanCloseOrEnqueue, which internalsForTransform already exports, rather than catching whatever close() throws: nothing in close() can throw once that check passes. The finish promise is still set before the algorithm runs and the algorithms clear after it, with one reaction on its result, so nothing observable changes. Mutation probes against the three copies found each of their differences pinned, by the transform suite or by WPT's transform-streams. --- src/per_isolate/webstreams/readable.ts | 4 +- src/per_isolate/webstreams/transform.ts | 130 ++++++++---------------- 2 files changed, 47 insertions(+), 87 deletions(-) diff --git a/src/per_isolate/webstreams/readable.ts b/src/per_isolate/webstreams/readable.ts index c98069074e2..e96f40a9d7e 100644 --- a/src/per_isolate/webstreams/readable.ts +++ b/src/per_isolate/webstreams/readable.ts @@ -4995,8 +4995,8 @@ module.exports = { getReadableStreamGetState(stream), getStoredError: (stream: ReadableStream) => getReadableStreamStoredError(stream), - // ReadableStreamDefaultControllerCanCloseOrEnqueue, for the - // TransformStream controller's enqueue pre-check. + // ReadableStreamDefaultControllerCanCloseOrEnqueue, which TransformStream + // checks before it enqueues into its readable and before it closes it. canCloseOrEnqueue: (controller: object) => defaultControllerCanCloseOrEnqueue( controller as ReadableStreamDefaultController diff --git a/src/per_isolate/webstreams/transform.ts b/src/per_isolate/webstreams/transform.ts index e3a1082636c..0864fd4bfb5 100644 --- a/src/per_isolate/webstreams/transform.ts +++ b/src/per_isolate/webstreams/transform.ts @@ -284,11 +284,7 @@ class TransformStream { }; transformStreamTerminate = (stream: TransformStream) => { - try { - readableControllerClose(stream.#readableController); - } catch { - // Already closed or errored — nothing to do. - } + stream.#closeReadable(); stream.#errorWritableAndUnblockWrite( new TypeError('The transform stream has been terminated') ); @@ -313,99 +309,63 @@ class TransformStream { this.#flushAlgorithm = undefined; } - // Spec: TransformStreamDefaultSinkCloseAlgorithm. - #sinkClose(): Promise { - if (this.#finishPromise !== undefined) return this.#finishPromise; - const { promise, resolve, reject } = - PromiseWithResolvers() as PromiseWithResolversType; - this.#finishPromise = promise; - const flushAlgorithm = this.#flushAlgorithm; - const flushResult = - flushAlgorithm !== undefined ? flushAlgorithm() : PromiseResolve(); - this.#clearAlgorithms(); - markPromiseHandled( - PromisePrototypeThen( - flushResult, - () => { - if (readableInternals.getState(this.#readable) === 'errored') { - reject(readableInternals.getStoredError(this.#readable)); - return; - } - try { - readableControllerClose(this.#readableController); - } catch { - // Already closed — acceptable per spec. - } - resolve(); - }, - (r: unknown) => { - readableControllerError(this.#readableController, r); - reject(r); - } - ) - ); - return promise; + // Spec: ReadableStreamDefaultControllerClose, which returns at once when + // the readable can no longer close or enqueue (close requested, closed or + // errored); the captured close() throws then. + #closeReadable(): void { + const readableController = this.#readableController; + if (readableInternals.canCloseOrEnqueue(readableController)) { + readableControllerClose(readableController); + } } - // Spec: TransformStreamDefaultSinkAbortAlgorithm. - #sinkAbort(reason: unknown): Promise { + // Spec: TransformStreamDefaultSinkCloseAlgorithm ('close'), + // TransformStreamDefaultSinkAbortAlgorithm ('abort') and + // TransformStreamDefaultSourceCancelAlgorithm ('cancel'). They differ in + // the algorithm they run, flush or cancel, and in the half they end: the + // sink's close and abort end the readable, the source's cancel the + // writable. Whichever runs first sets the finish promise before its + // algorithm runs, so one started from inside flush() or cancel() returns + // it, as does any started later. The ended half's state is read when the + // algorithm settles (step 7.1.1): an abort, a terminate() or an error that + // reached it by then rejects the finish with its stored error. + #finish(kind: 'close' | 'abort' | 'cancel', reason?: unknown): Promise { if (this.#finishPromise !== undefined) return this.#finishPromise; const { promise, resolve, reject } = PromiseWithResolvers() as PromiseWithResolversType; this.#finishPromise = promise; - const cancelAlgorithm = this.#cancelAlgorithm; - const cancelResult = - cancelAlgorithm !== undefined - ? cancelAlgorithm(reason) - : PromiseResolve(); + const algorithm: ((reason: unknown) => Promise) | undefined = + kind === 'close' ? this.#flushAlgorithm : this.#cancelAlgorithm; + const result = + algorithm !== undefined ? algorithm(reason) : PromiseResolve(); this.#clearAlgorithms(); - markPromiseHandled( - PromisePrototypeThen( - cancelResult, - () => { - if (readableInternals.getState(this.#readable) === 'errored') { - reject(readableInternals.getStoredError(this.#readable)); - return; - } - readableControllerError(this.#readableController, reason); - resolve(); - }, - (r: unknown) => { - readableControllerError(this.#readableController, r); - reject(r); + const endsWritable = kind === 'cancel'; + const internals = endsWritable ? writableInternals : readableInternals; + const ended = endsWritable ? this.#writable : this.#readable; + const errorEnded = endsWritable + ? (e: unknown): void => { + this.#errorWritableAndUnblockWrite(e); } - ) - ); - return promise; - } - - // Spec: TransformStreamDefaultSourceCancelAlgorithm. The writable's state - // is read when the cancel algorithm settles (step 7.1.1): an abort, a - // terminate() or an error that reached it by then rejects the cancel. - #sourceCancel(reason: unknown): Promise { - if (this.#finishPromise !== undefined) return this.#finishPromise; - const { promise, resolve, reject } = - PromiseWithResolvers() as PromiseWithResolversType; - this.#finishPromise = promise; - const cancelAlgorithm = this.#cancelAlgorithm; - const cancelResult = - cancelAlgorithm !== undefined - ? cancelAlgorithm(reason) - : PromiseResolve(); - this.#clearAlgorithms(); + : (e: unknown): void => { + readableControllerError(this.#readableController, e); + }; markPromiseHandled( PromisePrototypeThen( - cancelResult, + result, () => { - if (writableInternals.getState(this.#writable) === 'errored') { - reject(writableInternals.getStoredError(this.#writable)); + if (internals.getState(ended) === 'errored') { + reject(internals.getStoredError(ended)); return; } - this.#errorWritableAndUnblockWrite(reason); + if (kind === 'close') { + this.#closeReadable(); + } else { + errorEnded(reason); + } resolve(); }, (r: unknown) => { - this.#errorWritableAndUnblockWrite(r); + errorEnded(r); reject(r); } ) @@ -590,8 +550,8 @@ class TransformStream { return startPromise; }, write: sinkWrite, - close: () => this.#sinkClose(), - abort: (reason: unknown) => this.#sinkAbort(reason), + close: () => this.#finish('close'), + abort: (reason: unknown) => this.#finish('abort', reason), }, // Converted already: the inner stream reads its own data properties, // and its default highWaterMark (1) is the TransformStream's. @@ -610,7 +570,7 @@ class TransformStream { return startPromise; }, pull: sourcePull, - cancel: (reason: unknown) => this.#sourceCancel(reason), + cancel: (reason: unknown) => this.#finish('cancel', reason), }, { __proto__: null,