Repository: nodejs/node (JavaScript)

Issue:
Implement TransformStream cleanup using "transformer.cancel"
The Streams specification has been updated with support for a hook to cleanup a TransformStream on error / abort. See https://github.com/whatwg/streams/pull/1283. WPTs are available: https://github.com/web-platform-tests/wpt/pull/40453


Discussion: I am trying to implement this, i am coming across some weird behaviour for the test https://github.com/web-platform-tests/wpt/blob/1267d50de7f8adf06dbddaba00b4d9651ae146a4/streams/transform-streams/errors.any.js#L266 it seems after calling controller.error the writable stream should also error? if so cancelPromise should also throw because writable has entered errored state? or is node's behaviour wrong here?



Fix (patch):
diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js
index a5f95596e3082a..a3b22afd2f2ea0 100644
--- a/lib/internal/webstreams/readablestream.js
+++ b/lib/internal/webstreams/readablestream.js
@@ -14,6 +14,7 @@ const {
   ObjectCreate,
   ObjectDefineProperties,
   ObjectSetPrototypeOf,
+  Promise,
   PromisePrototypeThen,
   PromiseResolve,
   PromiseReject,
@@ -2440,7 +2441,7 @@ function setupReadableStreamDefaultController(
   const startResult = startAlgorithm();
 
   PromisePrototypeThen(
-    PromiseResolve(startResult),
+    new Promise((r) => r(startResult)),
     () => {
       controller[kState].started = true;
       assert(!controller[kState].pulling);
@@ -3239,7 +3240,7 @@ function setupReadableByteStreamController(
   const startResult = startAlgorithm();
 
   PromisePrototypeThen(
-    PromiseResolve(startResult),
+    new Promise((r) => r(startResult)),
     () => {
       controller[kState].started = true;
       assert(!controller[kState].pulling);
diff --git a/lib/internal/webstreams/transformstream.js b/lib/internal/webstreams/transformstream.js
index 4d47856a6a12de..3dc85b79a8273e 100644
--- a/lib/internal/webstreams/transformstream.js
+++ b/lib/internal/webstreams/transformstream.js
@@ -6,7 +6,6 @@ const {
   ObjectDefineProperties,
   ObjectSetPrototypeOf,
   PromisePrototypeThen,
-  PromiseResolve,
   SymbolToStringTag,
   Symbol,
 } = primordials;
@@ -47,6 +46,7 @@ const {
   nonOpFlush,
   kType,
   kState,
+  nonOpCancel,
 } = require('internal/webstreams/util');
 
 const {
@@ -384,8 +384,7 @@ function initializeTransformStream(
       return transformStreamDefaultSourcePullAlgorithm(stream);
     },
     cancel(reason) {
-      transformStreamErrorWritableAndUnblockWrite(stream, reason);
-      return PromiseResolve();
+      return transformStreamDefaultSourceCancelAlgorithm(stream, reason);
     },
   }, {
     highWaterMark: readableHighWaterMark,
@@ -427,6 +426,10 @@ function transformStreamErrorWritableAndUnblockWrite(stream, error) {
   writableStreamDefaultControllerErrorIfNeeded(
     writable[kState].controller,
     error);
+  transformStreamUnblockWrite(stream);
+}
+
+function transformStreamUnblockWrite(stream) {
   if (stream[kState].backpressure)
     transformStreamSetBackpressure(stream, false);
 }
@@ -443,13 +446,15 @@ function setupTransformStreamDefaultController(
   stream,
   controller,
   transformAlgorithm,
-  flushAlgorithm) {
+  flushAlgorithm,
+  cancelAlgorithm) {
   assert(isTransformStream(stream));
   assert(stream[kState].controller === undefined);
   controller[kState] = {
     stream,
     transformAlgorithm,
     flushAlgorithm,
+    cancelAlgorithm,
   };
   stream[kState].controller = controller;
 }
@@ -460,21 +465,26 @@ function setupTransformStreamDefaultControllerFromTransformer(
   const controller = new TransformStreamDefaultController(kSkipThrow);
   const transform = transformer?.transform || defaultTransformAlgorithm;
   const flush = transformer?.flush || nonOpFlush;
+  const cancel = transformer?.cancel || nonOpCancel;
   const transformAlgorithm =
     FunctionPrototypeBind(transform, transformer);
   const flushAlgorithm =
     FunctionPrototypeBind(flush, transformer);
+  const cancelAlgorithm =
+    FunctionPrototypeBind(cancel, transformer);
 
   setupTransformStreamDefaultController(
     stream,
     controller,
     transformAlgorithm,
-    flushAlgorithm);
+    flushAlgorithm,
+    cancelAlgorithm);
 }
 
 function transformStreamDefaultControllerClearAlgorithms(controller) {
   controller[kState].transformAlgorithm = undefined;
   controller[kState].flushAlgorithm = undefined;
+  controller[kState].cancelAlgorithm = undefined;
 }
 
 function transformStreamDefaultControllerEnqueue(controller, chunk) {
@@ -563,7 +573,40 @@ function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) {
 }
 
 async function transformStreamDefaultSinkAbortAlgorithm(stream, reason) {
-  transformStreamError(stream, reason);
+  const {
+    controller,
+    readable,
+  } = stream[kState];
+
+  if (controller[kState].finishPromise !== undefined) {
+    return controller[kState].finishPromise;
+  }
+
+  const { promise, resolve, reject } = createDeferredPromise();
+  controller[kState].finishPromise = promise;
+  const cancelPromise = ensureIsPromise(
+    controller[kState].cancelAlgorithm,
+    controller,
+    reason);
+  transformStreamDefaultControllerClearAlgorithms(controller);
+
+  PromisePrototypeThen(
+    cancelPromise,
+    () => {
+      if (readable[kState].state === 'errored')
+        reject(readable[kState].storedError);
+      else {
+        readableStreamDefaultControllerError(readable[kState].controller, reason);
+        resolve();
+      }
+    },
+    (error) => {
+      readableStreamDefaultControllerError(readable[kState].controller, error);
+      reject(error);
+    },
+  );
+
+  return controller[kState].finishPromise;
 }
 
 function transformStreamDefaultSinkCloseAlgorithm(stream) {
@@ -572,23 +615,32 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
     controller,
   } = stream[kState];
 
+  if (controller[kState].finishPromise !== undefined) {
+    return controller[kState].finishPromise;
+  }
+  const { promise, resolve, reject } = createDeferredPromise();
+  controller[kState].finishPromise = promise;
   const flushPromise =
     ensureIsPromise(
       controller[kState].flushAlgorithm,
       controller,
       controller);
   transformStreamDefaultControllerClearAlgorithms(controller);
-  return PromisePrototypeThen(
+  PromisePrototypeThen(
     flushPromise,
     () => {
       if (readable[kState].state === 'errored')
-        throw readable[kState].storedError;
-      readableStreamDefaultControllerClose(readable[kState].controller);
+        reject(readable[kState].storedError);
+      else {
+        readableStreamDefaultControllerClose(readable[kState].controller);
+        resolve();
+      }
     },
     (error) => {
-      transformStreamError(stream, error);
-      throw readable[kState].storedError;
+      readableStreamDefaultControllerError(readable[kState].controller, error);
+      reject(error);
     });
+  return controller[kState].finishPromise;
 }
 
 function transformStreamDefaultSourcePullAlgorithm(stream) {
@@ -598,6 +650,48 @@ function transformStreamDefaultSourcePullAlgorithm(stream) {
   return stream[kState].backpressureChange.promise;
 }
 
+function transformStreamDefaultSourceCancelAlgorithm(stream, reason) {
+  const {
+    controller,
+    writable,
+  } = stream[kState];
+
+  if (controller[kState].finishPromise !== undefined) {
+    return controller[kState].finishPromise;
+  }
+
+  const { promise, resolve, reject } = createDeferredPromise();
+  controller[kState].finishPromise = promise;
+  const cancelPromise = ensureIsPromise(
+    controller[kState].cancelAlgorithm,
+    controller,
+    reason);
+  transformStreamDefaultControllerClearAlgorithms(controller);
+
+  PromisePrototypeThen(cancelPromise,
+                       () => {
+                         if (writable[kState].state === 'errored')
+                           reject(writable[kState].storedError);
+                         else {
+                           writableStreamDefaultControllerErrorIfNeeded(
+                             writable[kState].controller,
+                             reason);
+                           transformStreamUnblockWrite(stream);
+                           resolve();
+                         }
+                       },
+                       (error) => {
+                         writableStreamDefaultControllerErrorIfNeeded(
+                           writable[kState].controller,
+                           error);
+                         transformStreamUnblockWrite(stream);
+                         reject(error);
+                       },
+  );
+
+  return controller[kState].finishPromise;
+}
+
 module.exports = {
   TransformStream,
   TransformStreamDefaultController,
diff --git a/lib/internal/webstreams/writablestream.js b/lib/internal/webstreams/writablestream.js
index 2115aba36e927b..6dd7bc65566db6 100644
--- a/lib/internal/webstreams/writablestream.js
+++ b/lib/internal/webstreams/writablestream.js
@@ -7,6 +7,7 @@ const {
   FunctionPrototypeCall,
   ObjectDefineProperties,
   ObjectSetPrototypeOf,
+  Promise,
   PromisePrototypeThen,
   PromiseResolve,
   PromiseReject,
@@ -1295,7 +1296,7 @@ function setupWritableStreamDefaultController(
   const startResult = startAlgorithm();
 
   PromisePrototypeThen(
-    PromiseResolve(startResult),
+    new Promise((r) => r(startResult)),
     () => {
       assert(stream[kState].state === 'writable' ||
              stream[kState].state === 'erroring');