Skip to content

Commit 995dee6

Browse files
jasnellrichardlau
authored andcommitted
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 <[email protected]> PR-URL: #66483 Reviewed-By: Trivikram Kamat <[email protected]>
1 parent 6eaf913 commit 995dee6

3 files changed

Lines changed: 39 additions & 1 deletion

File tree

‎lib/internal/streams/iter/utils.js‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,10 @@ function toWriterUint8Array(chunk) {
376376
* @returns {boolean}
377377
*/
378378
function getProtocolMethod(value, symbol) {
379-
if (value === null || typeof value !== 'object' || !(symbol in value)) {
379+
// Functions are objects too, and may implement the protocols.
380+
if (value === null ||
381+
(typeof value !== 'object' && typeof value !== 'function') ||
382+
!(symbol in value)) {
380383
return undefined;
381384
}
382385
const method = value[symbol];

‎test/parallel/test-stream-iter-from-async.js‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,12 +507,35 @@ function testFromUndefinedThrows() {
507507
assert.throws(() => from(undefined), { code: 'ERR_INVALID_ARG_TYPE' });
508508
}
509509

510+
async function testFromFunctionWithProtocols() {
511+
// Functions are objects and may implement the protocols.
512+
function asyncSource() {}
513+
asyncSource[Symbol.for('Stream.toAsyncStreamable')] =
514+
async () => 'async-function';
515+
assert.strictEqual(await text(from(asyncSource)), 'async-function');
516+
517+
function syncSource() {}
518+
syncSource[Symbol.for('Stream.toStreamable')] = () => 'sync-function';
519+
assert.strictEqual(await text(from(syncSource)), 'sync-function');
520+
521+
async function* nested() {
522+
yield asyncSource;
523+
yield syncSource;
524+
}
525+
assert.strictEqual(await text(from(nested())),
526+
'async-functionsync-function');
527+
528+
// A function without a protocol is still rejected.
529+
assert.throws(() => from(() => {}), { code: 'ERR_INVALID_ARG_TYPE' });
530+
}
531+
510532
Promise.all([
511533
testFromString(),
512534
testFromAsyncGenerator(),
513535
testFromAsyncIteratorResultShapes(),
514536
testFromSourceErrorDoesNotWaitForReturn(),
515537
testFromBoundsNestedAsyncIterable(),
538+
testFromFunctionWithProtocols(),
516539
testFromDoesNotHoldBackNestedAsyncIterable(),
517540
testFromBoundsPreBatchedAsyncValues(),
518541
testFromSyncIterableAsAsync(),

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,17 @@ function testFromSyncUndefinedThrows() {
239239
assert.throws(() => fromSync(undefined), { code: 'ERR_INVALID_ARG_TYPE' });
240240
}
241241

242+
function testFromSyncFunctionWithToStreamable() {
243+
// Functions are objects and may implement the protocol.
244+
function source() {}
245+
source[Symbol.for('Stream.toStreamable')] = () => 'from-function';
246+
assert.strictEqual(textSync(fromSync(source)), 'from-function');
247+
// ...also when nested inside another source.
248+
assert.strictEqual(textSync(fromSync([source, '!'])), 'from-function!');
249+
// A function without a protocol is still rejected.
250+
assert.throws(() => fromSync(() => {}), { code: 'ERR_INVALID_ARG_TYPE' });
251+
}
252+
242253
Promise.all([
243254
testFromSyncString(),
244255
testFromSyncUint8Array(),
@@ -260,4 +271,5 @@ Promise.all([
260271
testFromSyncPrefersIteratorForThenableIterable(),
261272
testFromSyncRejectsPromise(),
262273
testFromSyncDataView(),
274+
testFromSyncFunctionWithToStreamable(),
263275
]).then(common.mustCall());

0 commit comments

Comments
 (0)