Commit 7c0a5179c4f for nodejs

commit 7c0a5179c4fa34ecfad94b495f0628536bd7fda7
Author: James M Snell <jasnell@gmail.com>
Date:   Sat Oct 3 14:53:30 2026 +0000

    stream: honor stream/iter protocols on function objects

    The protocol lookup used by from(), fromSync(), and the
    Broadcast/Share helpers only considered values with typeof 'object',
    so a function implementing, e.g., Symbol.for('Stream.toStreamable') was
    rejected with ERR_INVALID_ARG_TYPE. Functions are objects and the spec
    does not exclude them; the iteration protocol checks already accept
    them.

    Assisted-by: OpenCode
    Signed-off-by: James M Snell <jasnell@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/66483
    Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>

diff --git a/lib/internal/streams/iter/utils.js b/lib/internal/streams/iter/utils.js
index 34698dbdf0d..f84d663e917 100644
--- a/lib/internal/streams/iter/utils.js
+++ b/lib/internal/streams/iter/utils.js
@@ -376,7 +376,10 @@ function toWriterUint8Array(chunk) {
  * @returns {boolean}
  */
 function getProtocolMethod(value, symbol) {
-  if (value === null || typeof value !== 'object' || !(symbol in value)) {
+  // Functions are objects too, and may implement the protocols.
+  if (value === null ||
+      (typeof value !== 'object' && typeof value !== 'function') ||
+      !(symbol in value)) {
     return undefined;
   }
   const method = value[symbol];
diff --git a/test/parallel/test-stream-iter-from-async.js b/test/parallel/test-stream-iter-from-async.js
index d04d7df9aad..1b58fd6603c 100644
--- a/test/parallel/test-stream-iter-from-async.js
+++ b/test/parallel/test-stream-iter-from-async.js
@@ -507,12 +507,35 @@ function testFromUndefinedThrows() {
   assert.throws(() => from(undefined), { code: 'ERR_INVALID_ARG_TYPE' });
 }

+async function testFromFunctionWithProtocols() {
+  // Functions are objects and may implement the protocols.
+  function asyncSource() {}
+  asyncSource[Symbol.for('Stream.toAsyncStreamable')] =
+    async () => 'async-function';
+  assert.strictEqual(await text(from(asyncSource)), 'async-function');
+
+  function syncSource() {}
+  syncSource[Symbol.for('Stream.toStreamable')] = () => 'sync-function';
+  assert.strictEqual(await text(from(syncSource)), 'sync-function');
+
+  async function* nested() {
+    yield asyncSource;
+    yield syncSource;
+  }
+  assert.strictEqual(await text(from(nested())),
+                     'async-functionsync-function');
+
+  // A function without a protocol is still rejected.
+  assert.throws(() => from(() => {}), { code: 'ERR_INVALID_ARG_TYPE' });
+}
+
 Promise.all([
   testFromString(),
   testFromAsyncGenerator(),
   testFromAsyncIteratorResultShapes(),
   testFromSourceErrorDoesNotWaitForReturn(),
   testFromBoundsNestedAsyncIterable(),
+  testFromFunctionWithProtocols(),
   testFromDoesNotHoldBackNestedAsyncIterable(),
   testFromBoundsPreBatchedAsyncValues(),
   testFromSyncIterableAsAsync(),
diff --git a/test/parallel/test-stream-iter-from-sync.js b/test/parallel/test-stream-iter-from-sync.js
index 50d85dc94a9..af7acfd4a54 100644
--- a/test/parallel/test-stream-iter-from-sync.js
+++ b/test/parallel/test-stream-iter-from-sync.js
@@ -239,6 +239,17 @@ function testFromSyncUndefinedThrows() {
   assert.throws(() => fromSync(undefined), { code: 'ERR_INVALID_ARG_TYPE' });
 }

+function testFromSyncFunctionWithToStreamable() {
+  // Functions are objects and may implement the protocol.
+  function source() {}
+  source[Symbol.for('Stream.toStreamable')] = () => 'from-function';
+  assert.strictEqual(textSync(fromSync(source)), 'from-function');
+  // ...also when nested inside another source.
+  assert.strictEqual(textSync(fromSync([source, '!'])), 'from-function!');
+  // A function without a protocol is still rejected.
+  assert.throws(() => fromSync(() => {}), { code: 'ERR_INVALID_ARG_TYPE' });
+}
+
 Promise.all([
   testFromSyncString(),
   testFromSyncUint8Array(),
@@ -260,4 +271,5 @@ Promise.all([
   testFromSyncPrefersIteratorForThenableIterable(),
   testFromSyncRejectsPromise(),
   testFromSyncDataView(),
+  testFromSyncFunctionWithToStreamable(),
 ]).then(common.mustCall());