Skip to content

Commit 14505b2

Browse files
committed
fixup: address review comments
Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 6f9a2f3 commit 14505b2

3 files changed

Lines changed: 73 additions & 26 deletions

File tree

lib/internal/streams/readable.js

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
const {
2525
ArrayPrototypeIndexOf,
26+
AsyncIteratorPrototype,
27+
FunctionPrototypeCall,
2628
NumberIsInteger,
2729
NumberIsNaN,
2830
NumberParseInt,
@@ -103,6 +105,7 @@ const FastBuffer = Buffer[SymbolSpecies];
103105

104106
const { StringDecoder } = require('string_decoder');
105107
const from = require('internal/streams/from');
108+
const FixedQueue = require('internal/fixed_queue');
106109

107110
ObjectSetPrototypeOf(Readable.prototype, Stream.prototype);
108111
ObjectSetPrototypeOf(Readable, Stream);
@@ -1394,6 +1397,7 @@ function createAsyncIterator(stream, options) {
13941397
let completed = false;
13951398
let inFlight = false; // An asynchronous request is outstanding
13961399
let queue = null; // Requests received while inFlight
1400+
let draining = false;
13971401
let cleanup;
13981402

13991403
// Used both as the 'readable' listener (where `this === stream`) and
@@ -1450,15 +1454,25 @@ function createAsyncIterator(stream, options) {
14501454
}
14511455

14521456
function drain() {
1453-
while (!inFlight && queue.length > 0) {
1454-
const req = queue.shift();
1455-
if (req.type === 'next') {
1456-
processNext(req.resolve, req.reject);
1457-
} else if (req.type === 'return') {
1458-
processReturn(req.value, req.resolve);
1459-
} else {
1460-
processThrow(req.value, req.reject);
1457+
// Requests settled synchronously call back into drain(); the guard
1458+
// keeps a single loop going instead of recursing once per request.
1459+
if (draining) {
1460+
return;
1461+
}
1462+
draining = true;
1463+
try {
1464+
while (!inFlight && !queue.isEmpty()) {
1465+
const req = queue.shift();
1466+
if (req.type === 'next') {
1467+
processNext(req.resolve, req.reject);
1468+
} else if (req.type === 'return') {
1469+
processReturn(req.value, req.resolve);
1470+
} else {
1471+
processThrow(req.value, req.reject);
1472+
}
14611473
}
1474+
} finally {
1475+
draining = false;
14621476
}
14631477
}
14641478

@@ -1483,8 +1497,11 @@ function createAsyncIterator(stream, options) {
14831497
function pump(resolve, reject) {
14841498
const chunk = stream.destroyed ? null : stream.read();
14851499
if (chunk !== null) {
1486-
if (typeof chunk.then === 'function') {
1487-
PromisePrototypeThen(PromiseResolve(chunk), (value) => {
1500+
// Read `then` only once so that a getter cannot observe (or throw
1501+
// on) a second access.
1502+
const then = chunk.then;
1503+
if (typeof then === 'function') {
1504+
FunctionPrototypeCall(then, chunk, (value) => {
14881505
inFlight = false;
14891506
resolve({ done: false, value });
14901507
if (queue !== null) drain();
@@ -1546,16 +1563,20 @@ function createAsyncIterator(stream, options) {
15461563
}
15471564

15481565
return {
1566+
__proto__: AsyncIteratorPrototype,
15491567
next() {
15501568
if (!inFlight && !completed) {
15511569
if (!started) start();
15521570
// Fast path: a chunk is already buffered.
15531571
const chunk = stream.destroyed ? null : stream.read();
15541572
if (chunk !== null) {
1555-
if (typeof chunk.then === 'function') {
1573+
// Read `then` only once so that a getter cannot observe (or
1574+
// throw on) a second access.
1575+
const then = chunk.then;
1576+
if (typeof then === 'function') {
15561577
inFlight = true;
1557-
return PromisePrototypeThen(
1558-
PromiseResolve(chunk), onChunkFulfilled, onChunkRejected);
1578+
return FunctionPrototypeCall(
1579+
then, chunk, onChunkFulfilled, onChunkRejected);
15591580
}
15601581
return PromiseResolve({ done: false, value: chunk });
15611582
}
@@ -1575,8 +1596,8 @@ function createAsyncIterator(stream, options) {
15751596
}
15761597
return new Promise((resolve, reject) => {
15771598
if (inFlight) {
1578-
queue ??= [];
1579-
queue.push({ type: 'next', value: undefined, resolve, reject });
1599+
queue ??= new FixedQueue();
1600+
queue.push({ __proto__: null, type: 'next', value: undefined, resolve, reject });
15801601
} else {
15811602
resolve({ done: true, value: undefined });
15821603
}
@@ -1585,8 +1606,8 @@ function createAsyncIterator(stream, options) {
15851606
return(value) {
15861607
return new Promise((resolve, reject) => {
15871608
if (inFlight) {
1588-
queue ??= [];
1589-
queue.push({ type: 'return', value, resolve, reject });
1609+
queue ??= new FixedQueue();
1610+
queue.push({ __proto__: null, type: 'return', value, resolve, reject });
15901611
} else {
15911612
processReturn(value, resolve);
15921613
}
@@ -1595,8 +1616,8 @@ function createAsyncIterator(stream, options) {
15951616
throw(err) {
15961617
return new Promise((resolve, reject) => {
15971618
if (inFlight) {
1598-
queue ??= [];
1599-
queue.push({ type: 'throw', value: err, resolve, reject });
1619+
queue ??= new FixedQueue();
1620+
queue.push({ __proto__: null, type: 'throw', value: err, resolve, reject });
16001621
} else {
16011622
processThrow(err, reject);
16021623
}

test/parallel/test-stream-flatMap.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,13 @@ function oneTo5() {
8181
// Both mappers allowed by `concurrency` are now in flight.
8282
ac.abort();
8383
}
84-
await new Promise((resolve, reject) => {
85-
if (signal.aborted) {
86-
reject(signal.reason);
87-
return;
88-
}
89-
signal.addEventListener('abort', () => reject(signal.reason), { once: true });
90-
});
84+
const { promise, reject } = Promise.withResolvers();
85+
if (signal.aborted) {
86+
reject(signal.reason);
87+
}
88+
signal.addEventListener('abort', () => reject(signal.reason), { once: true });
89+
// Promise is expected to reject.
90+
await promise;
9191
}, 2), { signal: ac.signal, concurrency: 2 });
9292
// pump
9393
assert.rejects(async () => {

test/parallel/test-stream-readable-async-iterators.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,5 +963,31 @@ async function tests() {
963963
})().then(common.mustCall());
964964
}
965965

966+
{
967+
// Draining a large number of queued next() calls must not overflow
968+
// the call stack.
969+
// Refs: https://github.com/nodejs/node/pull/64447#discussion_r3566240419
970+
(async () => {
971+
const count = 20_000;
972+
const r = new Readable({ objectMode: true, read() {} });
973+
const it = r[Symbol.asyncIterator]();
974+
975+
const requests = [];
976+
for (let i = 0; i < count; i++) {
977+
requests.push(it.next());
978+
}
979+
for (let i = 0; i < count; i++) {
980+
r.push(i);
981+
}
982+
r.push(null);
983+
984+
const results = await Promise.all(requests);
985+
for (let i = 0; i < count; i++) {
986+
assert.deepStrictEqual(results[i], { done: false, value: i });
987+
}
988+
assert.strictEqual((await it.next()).done, true);
989+
})().then(common.mustCall());
990+
}
991+
966992
// To avoid missing some tests if a promise does not resolve
967993
tests().then(common.mustCall());

0 commit comments

Comments
 (0)