Skip to content

Commit 91b65a4

Browse files
authored
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 PR-URL: #64294 Fixes: #64292 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 8e1ab55 commit 91b65a4

2 files changed

Lines changed: 36 additions & 5 deletions

File tree

lib/internal/streams/iter/from.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -484,15 +484,20 @@ function fromSync(input) {
484484
return fromSync(input[toStreamable]());
485485
}
486486

487-
// Reject explicit async inputs
488-
if (isAsyncIterable(input)) {
487+
const isIterable = isSyncIterable(input);
488+
489+
// Reject explicit async-only inputs
490+
if (!isIterable && isAsyncIterable(input)) {
489491
throw new ERR_INVALID_ARG_TYPE(
490492
'input',
491493
'a synchronous input (not AsyncIterable)',
492494
input,
493495
);
494496
}
495-
if (typeof input === 'object' && input !== null && typeof input.then === 'function') {
497+
if (!isIterable &&
498+
typeof input === 'object' &&
499+
input !== null &&
500+
typeof input.then === 'function') {
496501
throw new ERR_INVALID_ARG_TYPE(
497502
'input',
498503
'a synchronous input (not Promise)',
@@ -501,7 +506,7 @@ function fromSync(input) {
501506
}
502507

503508
// Must be a SyncStreamable
504-
if (!isSyncIterable(input)) {
509+
if (!isIterable) {
505510
throw new ERR_INVALID_ARG_TYPE(
506511
'input',
507512
['string', 'ArrayBuffer', 'ArrayBufferView', 'Iterable', 'toStreamable'],

test/parallel/test-stream-iter-from-sync.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
const common = require('../common');
55
const assert = require('assert');
6-
const { fromSync } = require('stream/iter');
6+
const { fromSync, textSync } = require('stream/iter');
77

88
function testFromSyncString() {
99
// String input should be UTF-8 encoded
@@ -186,6 +186,30 @@ function testFromSyncRejectsAsyncIterable() {
186186
assert.throws(() => fromSync(gen()), { code: 'ERR_INVALID_ARG_TYPE' });
187187
}
188188

189+
function testFromSyncPrefersIteratorForDualIterable() {
190+
const input = {
191+
*[Symbol.iterator]() {
192+
yield new TextEncoder().encode('sync');
193+
},
194+
async *[Symbol.asyncIterator]() {
195+
yield new TextEncoder().encode('async');
196+
},
197+
};
198+
199+
assert.strictEqual(textSync(fromSync(input)), 'sync');
200+
}
201+
202+
function testFromSyncPrefersIteratorForThenableIterable() {
203+
const input = {
204+
then() {},
205+
*[Symbol.iterator]() {
206+
yield new TextEncoder().encode('sync');
207+
},
208+
};
209+
210+
assert.strictEqual(textSync(fromSync(input)), 'sync');
211+
}
212+
189213
// Promise rejected
190214
function testFromSyncRejectsPromise() {
191215
assert.throws(() => fromSync(Promise.resolve('hello')),
@@ -232,6 +256,8 @@ Promise.all([
232256
testFromSyncTopLevelProtocolOverIterator(),
233257
testFromSyncIgnoresAsyncStreamable(),
234258
testFromSyncRejectsAsyncIterable(),
259+
testFromSyncPrefersIteratorForDualIterable(),
260+
testFromSyncPrefersIteratorForThenableIterable(),
235261
testFromSyncRejectsPromise(),
236262
testFromSyncDataView(),
237263
]).then(common.mustCall());

0 commit comments

Comments
 (0)