Skip to content

Commit cfd2191

Browse files
jasnelladuh95
authored andcommitted
stream: apply source normalization once at call time
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode PR-URL: #65658 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
1 parent 43c8dcc commit cfd2191

4 files changed

Lines changed: 105 additions & 27 deletions

File tree

‎doc/api/stream_iter.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -685,8 +685,10 @@ added: v24.20.0
685685
* `signal` {AbortSignal} Abort the pipeline.
686686
* Returns: {AsyncIterable} whose chunks fulfill with {Uint8Array\[]}
687687

688-
Create a lazy async pipeline. Data is not read from `source` until the
689-
returned iterable is consumed. Transforms are applied in order.
688+
Create a lazy async pipeline. Source conversion and streamable protocol
689+
dispatch occur when `pull()` is called, but data is not read from `source`
690+
until the returned iterable is consumed. A signal that is already aborted is
691+
thrown synchronously after source conversion. Transforms are applied in order.
690692

691693
```mjs
692694
import { from, pull, text } from 'node:stream/iter';
@@ -754,7 +756,8 @@ added: v24.20.0
754756
* `...transforms` {Function|Object} Zero or more sync transforms.
755757
* Returns: {Iterable} whose chunks return {Uint8Array\[]}
756758

757-
Synchronous version of [`pull()`][]. All transforms must be synchronous.
759+
Synchronous version of [`pull()`][]. Source conversion and streamable protocol
760+
dispatch occur when `pullSync()` is called. All transforms must be synchronous.
758761

759762
## Push streams
760763

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

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,7 @@ async function* createAsyncPipeline(source, transforms, signal) {
802802
* @returns {Iterable<Uint8Array[]>}
803803
*/
804804
function pullSync(source, ...transforms) {
805+
const normalized = fromSync(source);
805806
for (let i = 0; i < transforms.length; i++) {
806807
if (!isTransform(transforms[i])) {
807808
throw new ERR_INVALID_ARG_TYPE(
@@ -812,7 +813,7 @@ function pullSync(source, ...transforms) {
812813
return {
813814
__proto__: null,
814815
*[SymbolIterator]() {
815-
yield* createSyncPipeline(fromSync(source), transforms);
816+
yield* createSyncPipeline(normalized, transforms);
816817
},
817818
};
818819
}
@@ -829,17 +830,9 @@ function pull(source, ...args) {
829830
const signal = options?.signal;
830831
if (signal !== undefined) {
831832
validateAbortSignal(signal, 'options.signal');
832-
// Eagerly check abort at call time per spec
833-
if (signal.aborted) {
834-
return {
835-
__proto__: null,
836-
// eslint-disable-next-line require-yield
837-
async *[SymbolAsyncIterator]() {
838-
throw signal.reason;
839-
},
840-
};
841-
}
842833
}
834+
const normalized = from(source);
835+
signal?.throwIfAborted();
843836

844837
return {
845838
__proto__: null,
@@ -849,7 +842,7 @@ function pull(source, ...args) {
849842
controller.signal : AbortSignal.any([signal, controller.signal]);
850843

851844
async function* pipeline() {
852-
yield* createAsyncPipeline(from(source), transforms, iteratorSignal);
845+
yield* createAsyncPipeline(normalized, transforms, iteratorSignal);
853846
}
854847
const iterator = pipeline();
855848

@@ -884,9 +877,6 @@ function pullWithConsumerCleanup(source, transforms, signal) {
884877
return sourceIterator;
885878
},
886879
};
887-
const pipeline = signal === undefined ?
888-
pull(pipelineSource, ...transforms) :
889-
pull(pipelineSource, ...transforms, { __proto__: null, signal });
890880
let sourceClosed = false;
891881
let abortHandler;
892882

@@ -903,6 +893,21 @@ function pullWithConsumerCleanup(source, transforms, signal) {
903893
}
904894
}
905895

896+
if (signal?.aborted) {
897+
closeSource('throw', signal.reason);
898+
return {
899+
__proto__: null,
900+
// eslint-disable-next-line require-yield
901+
async *[SymbolAsyncIterator]() {
902+
throw signal.reason;
903+
},
904+
};
905+
}
906+
907+
const pipeline = signal === undefined ?
908+
pull(pipelineSource, ...transforms) :
909+
pull(pipelineSource, ...transforms, { __proto__: null, signal });
910+
906911
if (signal !== undefined) {
907912
abortHandler = () => closeSource('throw', signal.reason);
908913
signal.addEventListener('abort', abortHandler,

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

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const {
1111
share,
1212
tap,
1313
text,
14+
toAsyncStreamable,
1415
} = require('stream/iter');
1516

1617
async function testPullIdentity() {
@@ -53,18 +54,54 @@ async function testPullWithAbortSignal() {
5354
yield [new Uint8Array([1])];
5455
}
5556

56-
const result = pull(gen(), { signal: AbortSignal.abort() });
57-
await assert.rejects(
58-
async () => {
59-
// eslint-disable-next-line no-unused-vars
60-
for await (const _ of result) {
61-
assert.fail('Should not reach here');
62-
}
63-
},
57+
assert.throws(
58+
() => pull(gen(), { signal: AbortSignal.abort() }),
6459
{ name: 'AbortError' },
6560
);
6661
}
6762

63+
async function testPullNormalizesSourceAtCallTime() {
64+
let protocolCalls = 0;
65+
let iteratorCalls = 0;
66+
const source = {
67+
[toAsyncStreamable]() {
68+
protocolCalls++;
69+
return {
70+
async *[Symbol.asyncIterator]() {
71+
iteratorCalls++;
72+
yield 'data';
73+
},
74+
};
75+
},
76+
};
77+
78+
const result = pull(source);
79+
assert.strictEqual(protocolCalls, 1);
80+
assert.strictEqual(iteratorCalls, 0);
81+
assert.strictEqual(await text(result), 'data');
82+
assert.strictEqual(protocolCalls, 1);
83+
assert.strictEqual(iteratorCalls, 1);
84+
}
85+
86+
function testPullPreAbortOrdering() {
87+
const reason = new Error('already aborted');
88+
let protocolCalls = 0;
89+
const source = {
90+
[toAsyncStreamable]() {
91+
protocolCalls++;
92+
return from('data');
93+
},
94+
};
95+
const signal = AbortSignal.abort(reason);
96+
97+
assert.throws(() => pull(source, { signal }), (error) => error === reason);
98+
assert.strictEqual(protocolCalls, 1);
99+
assert.throws(
100+
() => pull(null, { signal }),
101+
{ code: 'ERR_INVALID_ARG_TYPE' },
102+
);
103+
}
104+
68105
async function testPullChainedTransforms() {
69106
const enc = new TextEncoder();
70107
const transforms = [
@@ -475,6 +512,8 @@ async function testTransformOptionsNotShared() {
475512
testPullStatelessTransform(),
476513
testPullStatefulTransform(),
477514
testPullWithAbortSignal(),
515+
testPullNormalizesSourceAtCallTime(),
516+
testPullPreAbortOrdering(),
478517
testPullChainedTransforms(),
479518
testPullSourceError(),
480519
testTapCallbackError(),

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,44 @@
33

44
const common = require('../common');
55
const assert = require('assert');
6-
const { pullSync, fromSync, bytesSync, tapSync } = require('stream/iter');
6+
const {
7+
pullSync,
8+
fromSync,
9+
bytesSync,
10+
tapSync,
11+
toStreamable,
12+
} = require('stream/iter');
713

814
function testPullSyncIdentity() {
915
// No transforms - just pass through
1016
const data = bytesSync(pullSync(fromSync('hello')));
1117
assert.deepStrictEqual(data, new TextEncoder().encode('hello'));
1218
}
1319

20+
function testPullSyncNormalizesSourceAtCallTime() {
21+
let protocolCalls = 0;
22+
let iteratorCalls = 0;
23+
const source = {
24+
[toStreamable]() {
25+
protocolCalls++;
26+
return {
27+
*[Symbol.iterator]() {
28+
iteratorCalls++;
29+
yield 'data';
30+
},
31+
};
32+
},
33+
};
34+
35+
const result = pullSync(source);
36+
assert.strictEqual(protocolCalls, 1);
37+
assert.strictEqual(iteratorCalls, 0);
38+
assert.strictEqual(new TextDecoder().decode(bytesSync(result)), 'data');
39+
assert.strictEqual(protocolCalls, 1);
40+
assert.strictEqual(iteratorCalls, 1);
41+
assert.throws(() => pullSync(null), { code: 'ERR_INVALID_ARG_TYPE' });
42+
}
43+
1444
function testPullSyncStatelessTransform() {
1545
const upper = (chunks) => {
1646
if (chunks === null) return null;
@@ -177,6 +207,7 @@ function testPullSyncInvalidTransform() {
177207

178208
Promise.all([
179209
testPullSyncIdentity(),
210+
testPullSyncNormalizesSourceAtCallTime(),
180211
testPullSyncStatelessTransform(),
181212
testPullSyncStatefulTransform(),
182213
testPullSyncChainedTransforms(),

0 commit comments

Comments
 (0)