From f21d60412c583b36dc8116711783e9f355e0b8d6 Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:48:50 -0700 Subject: [PATCH] stream: prefer sync iterator in fromSync When an input provides Symbol.iterator, fromSync() should consume the synchronous iterator instead of rejecting the input because it also exposes an asynchronous or promise-like protocol. Continue rejecting async-only iterables and promise/thenable-only inputs, but allow sync iterables that also define Symbol.asyncIterator or then(). Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 --- lib/internal/streams/iter/from.js | 13 +++++++--- test/parallel/test-stream-iter-from-sync.js | 28 ++++++++++++++++++++- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/lib/internal/streams/iter/from.js b/lib/internal/streams/iter/from.js index 1efe83e9a04162..3277760f5f0a9d 100644 --- a/lib/internal/streams/iter/from.js +++ b/lib/internal/streams/iter/from.js @@ -484,15 +484,20 @@ function fromSync(input) { return fromSync(input[toStreamable]()); } - // Reject explicit async inputs - if (isAsyncIterable(input)) { + const isIterable = isSyncIterable(input); + + // Reject explicit async-only inputs + if (!isIterable && isAsyncIterable(input)) { throw new ERR_INVALID_ARG_TYPE( 'input', 'a synchronous input (not AsyncIterable)', input, ); } - if (typeof input === 'object' && input !== null && typeof input.then === 'function') { + if (!isIterable && + typeof input === 'object' && + input !== null && + typeof input.then === 'function') { throw new ERR_INVALID_ARG_TYPE( 'input', 'a synchronous input (not Promise)', @@ -501,7 +506,7 @@ function fromSync(input) { } // Must be a SyncStreamable - if (!isSyncIterable(input)) { + if (!isIterable) { throw new ERR_INVALID_ARG_TYPE( 'input', ['string', 'ArrayBuffer', 'ArrayBufferView', 'Iterable', 'toStreamable'], diff --git a/test/parallel/test-stream-iter-from-sync.js b/test/parallel/test-stream-iter-from-sync.js index d3a5cf671e10d8..50d85dc94a98b7 100644 --- a/test/parallel/test-stream-iter-from-sync.js +++ b/test/parallel/test-stream-iter-from-sync.js @@ -3,7 +3,7 @@ const common = require('../common'); const assert = require('assert'); -const { fromSync } = require('stream/iter'); +const { fromSync, textSync } = require('stream/iter'); function testFromSyncString() { // String input should be UTF-8 encoded @@ -186,6 +186,30 @@ function testFromSyncRejectsAsyncIterable() { assert.throws(() => fromSync(gen()), { code: 'ERR_INVALID_ARG_TYPE' }); } +function testFromSyncPrefersIteratorForDualIterable() { + const input = { + *[Symbol.iterator]() { + yield new TextEncoder().encode('sync'); + }, + async *[Symbol.asyncIterator]() { + yield new TextEncoder().encode('async'); + }, + }; + + assert.strictEqual(textSync(fromSync(input)), 'sync'); +} + +function testFromSyncPrefersIteratorForThenableIterable() { + const input = { + then() {}, + *[Symbol.iterator]() { + yield new TextEncoder().encode('sync'); + }, + }; + + assert.strictEqual(textSync(fromSync(input)), 'sync'); +} + // Promise rejected function testFromSyncRejectsPromise() { assert.throws(() => fromSync(Promise.resolve('hello')), @@ -232,6 +256,8 @@ Promise.all([ testFromSyncTopLevelProtocolOverIterator(), testFromSyncIgnoresAsyncStreamable(), testFromSyncRejectsAsyncIterable(), + testFromSyncPrefersIteratorForDualIterable(), + testFromSyncPrefersIteratorForThenableIterable(), testFromSyncRejectsPromise(), testFromSyncDataView(), ]).then(common.mustCall());