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());