stream: async iteration should work with destroyed stream
Fixes https://github.com/nodejs/node/issues/23730. PR-URL: https://github.com/nodejs/node/pull/23785 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: Matheus Marchini <mat@mmarchini.me>
This commit is contained in:
parent
12c530ccd5
commit
3ec8cec648
@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const finished = require('internal/streams/end-of-stream');
|
||||||
|
|
||||||
const kLastResolve = Symbol('lastResolve');
|
const kLastResolve = Symbol('lastResolve');
|
||||||
const kLastReject = Symbol('lastReject');
|
const kLastReject = Symbol('lastReject');
|
||||||
const kError = Symbol('error');
|
const kError = Symbol('error');
|
||||||
@ -34,30 +36,6 @@ function onReadable(iter) {
|
|||||||
process.nextTick(readAndResolve, iter);
|
process.nextTick(readAndResolve, iter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEnd(iter) {
|
|
||||||
const resolve = iter[kLastResolve];
|
|
||||||
if (resolve !== null) {
|
|
||||||
iter[kLastPromise] = null;
|
|
||||||
iter[kLastResolve] = null;
|
|
||||||
iter[kLastReject] = null;
|
|
||||||
resolve(createIterResult(null, true));
|
|
||||||
}
|
|
||||||
iter[kEnded] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onError(iter, err) {
|
|
||||||
const reject = iter[kLastReject];
|
|
||||||
// reject if we are waiting for data in the Promise
|
|
||||||
// returned by next() and store the error
|
|
||||||
if (reject !== null) {
|
|
||||||
iter[kLastPromise] = null;
|
|
||||||
iter[kLastResolve] = null;
|
|
||||||
iter[kLastReject] = null;
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
iter[kError] = err;
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrapForNext(lastPromise, iter) {
|
function wrapForNext(lastPromise, iter) {
|
||||||
return function(resolve, reject) {
|
return function(resolve, reject) {
|
||||||
lastPromise.then(function() {
|
lastPromise.then(function() {
|
||||||
@ -86,6 +64,22 @@ const ReadableStreamAsyncIteratorPrototype = Object.setPrototypeOf({
|
|||||||
return Promise.resolve(createIterResult(null, true));
|
return Promise.resolve(createIterResult(null, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this[kStream].destroyed) {
|
||||||
|
// We need to defer via nextTick because if .destroy(err) is
|
||||||
|
// called, the error will be emitted via nextTick, and
|
||||||
|
// we cannot guarantee that there is no error lingering around
|
||||||
|
// waiting to be emitted.
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
process.nextTick(() => {
|
||||||
|
if (this[kError]) {
|
||||||
|
reject(this[kError]);
|
||||||
|
} else {
|
||||||
|
resolve(createIterResult(null, true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// if we have multiple next() calls
|
// if we have multiple next() calls
|
||||||
// we will wait for the previous Promise to finish
|
// we will wait for the previous Promise to finish
|
||||||
// this logic is optimized to support for await loops,
|
// this logic is optimized to support for await loops,
|
||||||
@ -155,9 +149,32 @@ const createReadableStreamAsyncIterator = (stream) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
finished(stream, (err) => {
|
||||||
|
if (err) {
|
||||||
|
const reject = iterator[kLastReject];
|
||||||
|
// reject if we are waiting for data in the Promise
|
||||||
|
// returned by next() and store the error
|
||||||
|
if (reject !== null) {
|
||||||
|
iterator[kLastPromise] = null;
|
||||||
|
iterator[kLastResolve] = null;
|
||||||
|
iterator[kLastReject] = null;
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
iterator[kError] = err;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolve = iterator[kLastResolve];
|
||||||
|
if (resolve !== null) {
|
||||||
|
iterator[kLastPromise] = null;
|
||||||
|
iterator[kLastResolve] = null;
|
||||||
|
iterator[kLastReject] = null;
|
||||||
|
resolve(createIterResult(null, true));
|
||||||
|
}
|
||||||
|
iterator[kEnded] = true;
|
||||||
|
});
|
||||||
|
|
||||||
stream.on('readable', onReadable.bind(null, iterator));
|
stream.on('readable', onReadable.bind(null, iterator));
|
||||||
stream.on('end', onEnd.bind(null, iterator));
|
|
||||||
stream.on('error', onError.bind(null, iterator));
|
|
||||||
|
|
||||||
return iterator;
|
return iterator;
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const common = require('../common');
|
const common = require('../common');
|
||||||
const { Readable } = require('stream');
|
const { Readable, PassThrough, pipeline } = require('stream');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
|
||||||
async function tests() {
|
async function tests() {
|
||||||
@ -324,6 +324,44 @@ async function tests() {
|
|||||||
|
|
||||||
assert.strictEqual(data, expected);
|
assert.strictEqual(data, expected);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
await (async function() {
|
||||||
|
console.log('.next() on destroyed stream');
|
||||||
|
const readable = new Readable({
|
||||||
|
read() {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
readable.destroy();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await readable[Symbol.asyncIterator]().next();
|
||||||
|
} catch (e) {
|
||||||
|
assert.strictEqual(e.code, 'ERR_STREAM_PREMATURE_CLOSE');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
await (async function() {
|
||||||
|
console.log('.next() on pipelined stream');
|
||||||
|
const readable = new Readable({
|
||||||
|
read() {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const passthrough = new PassThrough();
|
||||||
|
const err = new Error('kaboom');
|
||||||
|
pipeline(readable, passthrough, common.mustCall((e) => {
|
||||||
|
assert.strictEqual(e, err);
|
||||||
|
}));
|
||||||
|
readable.destroy(err);
|
||||||
|
try {
|
||||||
|
await readable[Symbol.asyncIterator]().next();
|
||||||
|
} catch (e) {
|
||||||
|
assert.strictEqual(e, err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
// to avoid missing some tests if a promise does not resolve
|
// to avoid missing some tests if a promise does not resolve
|
||||||
|
Loading…
x
Reference in New Issue
Block a user