Repository: nodejs/node
Pull request: lib: convert signals to array before validation

Fixes: https://github.com/nodejs/node/issues/54674

Changed files:
filename: lib/internal/abort_controller.js
patch: @@ -39,6 +39,10 @@ const {
     ERR_INVALID_THIS,
   },
 } = require('internal/errors');
+const {
+  converters,
+  createSequenceConverter,
+} = require('internal/webidl');
 
 const {
   validateAbortSignal,
@@ -225,15 +229,19 @@ class AbortSignal extends EventTarget {
    * @returns {AbortSignal}
    */
   static any(signals) {
-    validateAbortSignalArray(signals, 'signals');
+    const signalsArray = createSequenceConverter(
+      converters.any,
+    )(signals);
+
+    validateAbortSignalArray(signalsArray, 'signals');
     const resultSignal = new AbortSignal(kDontThrowSymbol, { composite: true });
-    if (!signals.length) {
+    if (!signalsArray.length) {
       return resultSignal;
     }
     const resultSignalWeakRef = new WeakRef(resultSignal);
     resultSignal[kSourceSignals] = new SafeSet();
-    for (let i = 0; i < signals.length; i++) {
-      const signal = signals[i];
+    for (let i = 0; i < signalsArray.length; i++) {
+      const signal = signalsArray[i];
       if (signal.aborted) {
         abortSignal(resultSignal, signal.reason);
         return resultSignal;

filename: lib/internal/crypto/webidl.js
patch: @@ -22,7 +22,6 @@ const {
   ObjectPrototypeIsPrototypeOf,
   SafeArrayIterator,
   String,
-  SymbolIterator,
   TypedArrayPrototypeGetBuffer,
   TypedArrayPrototypeGetSymbolToStringTag,
   globalThis: {
@@ -33,6 +32,7 @@ const {
 const {
   makeException,
   createEnumConverter,
+  createSequenceConverter,
 } = require('internal/webidl');
 
 const {
@@ -293,39 +293,6 @@ function createDictionaryConverter(name, dictionaries) {
   };
 }
 
-function createSequenceConverter(converter) {
-  return function(V, opts = kEmptyObject) {
-    if (type(V) !== 'Object') {
-      throw makeException(
-        'can not be converted to sequence.',
-        opts);
-    }
-    const iter = V?.[SymbolIterator]?.();
-    if (iter === undefined) {
-      throw makeException(
-        'can not be converted to sequence.',
-        opts);
-    }
-    const array = [];
-    while (true) {
-      const res = iter?.next?.();
-      if (res === undefined) {
-        throw makeException(
-          'can not be converted to sequence.',
-          opts);
-      }
-      if (res.done === true) break;
-      const val = converter(res.value, {
-        __proto__: null,
-        ...opts,
-        context: `${opts.context}, index ${array.length}`,
-      });
-      ArrayPrototypePush(array, val);
-    }
-    return array;
-  };
-}
-
 function createInterfaceConverter(name, prototype) {
   return (V, opts) => {
     if (!ObjectPrototypeIsPrototypeOf(prototype, V)) {

filename: lib/internal/webidl.js
patch: @@ -1,6 +1,7 @@
 'use strict';
 
 const {
+  ArrayPrototypePush,
   MathAbs,
   MathMax,
   MathMin,
@@ -13,6 +14,7 @@ const {
   ObjectAssign,
   SafeSet,
   String,
+  SymbolIterator,
   TypeError,
 } = primordials;
 
@@ -25,6 +27,15 @@ const { kEmptyObject } = require('internal/util');
 
 const converters = { __proto__: null };
 
+/**
+ * @see https://webidl.spec.whatwg.org/#es-any
+ * @param {any} V
+ * @returns {any}
+ */
+converters.any = (V) => {
+  return V;
+};
+
 // https://webidl.spec.whatwg.org/#abstract-opdef-integerpart
 const integerPart = MathTrunc;
 
@@ -209,10 +220,76 @@ function createEnumConverter(name, values) {
   };
 }
 
+// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values
+function type(V) {
+  if (V === null)
+    return 'Null';
+
+  switch (typeof V) {
+    case 'undefined':
+      return 'Undefined';
+    case 'boolean':
+      return 'Boolean';
+    case 'number':
+      return 'Number';
+    case 'string':
+      return 'String';
+    case 'symbol':
+      return 'Symbol';
+    case 'bigint':
+      return 'BigInt';
+    case 'object': // Fall through
+    case 'function': // Fall through
+    default:
+      // Per ES spec, typeof returns an implemention-defined value that is not
+      // any of the existing ones for uncallable non-standard exotic objects.
+      // Yet Type() which the Web IDL spec depends on returns Object for such
+      // cases. So treat the default case as an object.
+      return 'Object';
+  }
+}
+
+// https://webidl.spec.whatwg.org/#es-sequence
+function createSequenceConverter(converter) {
+  return function(V, opts = kEmptyObject) {
+    if (type(V) !== 'Object') {
+      throw makeException(
+        'can not be converted to sequence.',
+        opts);
+    }
+    const iter = V?.[SymbolIterator]?.();
+    if (iter === undefined) {
+      throw makeException(
+        'can not be converted to sequence.',
+        opts);
+    }
+    const array = [];
+    while (true) {
+      const res = iter?.next?.();
+      if (res === undefined) {
+        throw makeException(
+          'can not be converted to sequence.',
+          opts);
+      }
+      if (res.done === true) break;
+      const val = converter(res.value, {
+        __proto__: null,
+        ...opts,
+        context: `${opts.context}, index ${array.length}`,
+      });
+      ArrayPrototypePush(array, val);
+    };
+    return array;
+  };
+}
+
+
 module.exports = {
+  type,
   converters,
   convertToInt,
   createEnumConverter,
+  createSequenceConverter,
   evenRound,
   makeException,
 };

filename: test/parallel/test-abortsignal-any.mjs
patch: @@ -101,4 +101,21 @@ describe('AbortSignal.any()', { concurrency: !process.env.TEST_PARALLEL }, () =>
     controller.abort();
     assert.strictEqual(result, '01234');
   });
+
+  it('must accept WebIDL sequence', () => {
+    const controller = new AbortController();
+    const iterable = {
+      *[Symbol.iterator]() {
+        yield controller.signal;
+        yield new AbortController().signal;
+        yield new AbortController().signal;
+        yield new AbortController().signal;
+      },
+    };
+    const signal = AbortSignal.any(iterable);
+    let result = 0;
+    signal.addEventListener('abort', () => result += 1);
+    controller.abort();
+    assert.strictEqual(result, 1);
+  });
 });