mirror of
https://github.com/nodejs/node.git
synced 2026-10-14 10:26:26 -04:00
stream: simpler and faster Readable async iterator
Reimplement as an async generator instead of a custom iterator class. PR-URL: https://github.com/nodejs/node/pull/34035 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
1 parent
bf0d82c102
commit
08e8997d54
7 files changed
+125
-269
No files matched your search
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const Readable = require('stream').Readable;
|
||||
|
||||
const bench = common.createBenchmark(main, {
|
||||
n: [1e5],
|
||||
sync: ['yes', 'no'],
|
||||
});
|
||||
|
||||
async function main({ n, sync }) {
|
||||
sync = sync === 'yes';
|
||||
|
||||
const s = new Readable({
|
||||
objectMode: true,
|
||||
read() {
|
||||
if (sync) {
|
||||
this.push(1);
|
||||
} else {
|
||||
process.nextTick(() => {
|
||||
this.push(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bench.start();
|
||||
|
||||
let x = 0;
|
||||
for await (const chunk of s) {
|
||||
x += chunk;
|
||||
if (x > n) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Side effect to ensure V8 does not optimize away the
|
||||
// loop as a noop.
|
||||
console.log(x);
|
||||
|
||||
bench.end(n);
|
||||
}
|
||||
+61
-5
@@ -28,6 +28,7 @@ const {
|
||||
ObjectDefineProperties,
|
||||
ObjectKeys,
|
||||
ObjectSetPrototypeOf,
|
||||
Promise,
|
||||
Set,
|
||||
SymbolAsyncIterator,
|
||||
Symbol
|
||||
@@ -60,11 +61,11 @@ const kPaused = Symbol('kPaused');
|
||||
|
||||
// Lazy loaded to improve the startup performance.
|
||||
let StringDecoder;
|
||||
let createReadableStreamAsyncIterator;
|
||||
let from;
|
||||
|
||||
ObjectSetPrototypeOf(Readable.prototype, Stream.prototype);
|
||||
ObjectSetPrototypeOf(Readable, Stream);
|
||||
function nop() {}
|
||||
|
||||
const { errorOrDestroy } = destroyImpl;
|
||||
|
||||
@@ -1055,13 +1056,68 @@ Readable.prototype.wrap = function(stream) {
|
||||
};
|
||||
|
||||
Readable.prototype[SymbolAsyncIterator] = function() {
|
||||
if (createReadableStreamAsyncIterator === undefined) {
|
||||
createReadableStreamAsyncIterator =
|
||||
require('internal/streams/async_iterator');
|
||||
let stream = this;
|
||||
|
||||
if (typeof stream.read !== 'function') {
|
||||
// v1 stream
|
||||
const src = stream;
|
||||
stream = new Readable({
|
||||
objectMode: true,
|
||||
destroy(err, callback) {
|
||||
destroyImpl.destroyer(src, err);
|
||||
callback();
|
||||
}
|
||||
}).wrap(src);
|
||||
}
|
||||
return createReadableStreamAsyncIterator(this);
|
||||
|
||||
const iter = createAsyncIterator(stream);
|
||||
iter.stream = stream;
|
||||
return iter;
|
||||
};
|
||||
|
||||
async function* createAsyncIterator(stream) {
|
||||
let callback = nop;
|
||||
|
||||
function next(resolve) {
|
||||
if (this === stream) {
|
||||
callback();
|
||||
callback = nop;
|
||||
} else {
|
||||
callback = resolve;
|
||||
}
|
||||
}
|
||||
|
||||
stream
|
||||
.on('readable', next)
|
||||
.on('error', next)
|
||||
.on('end', next)
|
||||
.on('close', next);
|
||||
|
||||
try {
|
||||
const state = stream._readableState;
|
||||
while (true) {
|
||||
const chunk = stream.read();
|
||||
if (chunk !== null) {
|
||||
yield chunk;
|
||||
} else if (state.errored) {
|
||||
throw state.errored;
|
||||
} else if (state.ended) {
|
||||
break;
|
||||
} else if (state.closed) {
|
||||
// TODO(ronag): ERR_PREMATURE_CLOSE?
|
||||
break;
|
||||
} else {
|
||||
await new Promise(next);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
destroyImpl.destroyer(stream, err);
|
||||
throw err;
|
||||
} finally {
|
||||
destroyImpl.destroyer(stream, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Making it explicit these properties are not enumerable
|
||||
// because otherwise some prototype manipulation in
|
||||
// userland will fail.
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
ObjectCreate,
|
||||
ObjectGetPrototypeOf,
|
||||
ObjectSetPrototypeOf,
|
||||
Promise,
|
||||
PromiseReject,
|
||||
PromiseResolve,
|
||||
Symbol,
|
||||
} = primordials;
|
||||
|
||||
const finished = require('internal/streams/end-of-stream');
|
||||
const destroyImpl = require('internal/streams/destroy');
|
||||
|
||||
const kLastResolve = Symbol('lastResolve');
|
||||
const kLastReject = Symbol('lastReject');
|
||||
const kError = Symbol('error');
|
||||
const kEnded = Symbol('ended');
|
||||
const kLastPromise = Symbol('lastPromise');
|
||||
const kHandlePromise = Symbol('handlePromise');
|
||||
const kStream = Symbol('stream');
|
||||
|
||||
let Readable;
|
||||
|
||||
function createIterResult(value, done) {
|
||||
return { value, done };
|
||||
}
|
||||
|
||||
function readAndResolve(iter) {
|
||||
const resolve = iter[kLastResolve];
|
||||
if (resolve !== null) {
|
||||
const data = iter[kStream].read();
|
||||
// We defer if data is null. We can be expecting either 'end' or 'error'.
|
||||
if (data !== null) {
|
||||
iter[kLastPromise] = null;
|
||||
iter[kLastResolve] = null;
|
||||
iter[kLastReject] = null;
|
||||
resolve(createIterResult(data, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onReadable(iter) {
|
||||
// We wait for the next tick, because it might
|
||||
// emit an error with `process.nextTick()`.
|
||||
process.nextTick(readAndResolve, iter);
|
||||
}
|
||||
|
||||
function wrapForNext(lastPromise, iter) {
|
||||
return (resolve, reject) => {
|
||||
lastPromise.then(() => {
|
||||
if (iter[kEnded]) {
|
||||
resolve(createIterResult(undefined, true));
|
||||
return;
|
||||
}
|
||||
|
||||
iter[kHandlePromise](resolve, reject);
|
||||
}, reject);
|
||||
};
|
||||
}
|
||||
|
||||
const AsyncIteratorPrototype = ObjectGetPrototypeOf(
|
||||
ObjectGetPrototypeOf(async function* () {}).prototype);
|
||||
|
||||
function finish(self, err) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const stream = self[kStream];
|
||||
|
||||
finished(stream, (err) => {
|
||||
if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(createIterResult(undefined, true));
|
||||
}
|
||||
});
|
||||
destroyImpl.destroyer(stream, err);
|
||||
});
|
||||
}
|
||||
|
||||
const ReadableStreamAsyncIteratorPrototype = ObjectSetPrototypeOf({
|
||||
get stream() {
|
||||
return this[kStream];
|
||||
},
|
||||
|
||||
next() {
|
||||
// If we have detected an error in the meanwhile
|
||||
// reject straight away.
|
||||
const error = this[kError];
|
||||
if (error !== null) {
|
||||
return PromiseReject(error);
|
||||
}
|
||||
|
||||
if (this[kEnded]) {
|
||||
return PromiseResolve(createIterResult(undefined, true));
|
||||
}
|
||||
|
||||
if (this[kStream].destroyed) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this[kError]) {
|
||||
reject(this[kError]);
|
||||
} else if (this[kEnded]) {
|
||||
resolve(createIterResult(undefined, true));
|
||||
} else {
|
||||
finished(this[kStream], (err) => {
|
||||
if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(createIterResult(undefined, true));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If we have multiple next() calls we will wait for the previous Promise to
|
||||
// finish. This logic is optimized to support for await loops, where next()
|
||||
// is only called once at a time.
|
||||
const lastPromise = this[kLastPromise];
|
||||
let promise;
|
||||
|
||||
if (lastPromise) {
|
||||
promise = new Promise(wrapForNext(lastPromise, this));
|
||||
} else {
|
||||
// Fast path needed to support multiple this.push()
|
||||
// without triggering the next() queue.
|
||||
const data = this[kStream].read();
|
||||
if (data !== null) {
|
||||
return PromiseResolve(createIterResult(data, false));
|
||||
}
|
||||
|
||||
promise = new Promise(this[kHandlePromise]);
|
||||
}
|
||||
|
||||
this[kLastPromise] = promise;
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
return() {
|
||||
return finish(this);
|
||||
},
|
||||
|
||||
throw(err) {
|
||||
return finish(this, err);
|
||||
},
|
||||
}, AsyncIteratorPrototype);
|
||||
|
||||
const createReadableStreamAsyncIterator = (stream) => {
|
||||
if (typeof stream.read !== 'function') {
|
||||
// v1 stream
|
||||
|
||||
if (!Readable) {
|
||||
Readable = require('_stream_readable');
|
||||
}
|
||||
|
||||
const src = stream;
|
||||
stream = new Readable({ objectMode: true }).wrap(src);
|
||||
finished(stream, (err) => destroyImpl.destroyer(src, err));
|
||||
}
|
||||
|
||||
const iterator = ObjectCreate(ReadableStreamAsyncIteratorPrototype, {
|
||||
[kStream]: { value: stream, writable: true },
|
||||
[kLastResolve]: { value: null, writable: true },
|
||||
[kLastReject]: { value: null, writable: true },
|
||||
[kError]: { value: null, writable: true },
|
||||
[kEnded]: {
|
||||
value: stream.readableEnded || stream._readableState.endEmitted,
|
||||
writable: true
|
||||
},
|
||||
// The function passed to new Promise is cached so we avoid allocating a new
|
||||
// closure at every run.
|
||||
[kHandlePromise]: {
|
||||
value: (resolve, reject) => {
|
||||
const data = iterator[kStream].read();
|
||||
if (data) {
|
||||
iterator[kLastPromise] = null;
|
||||
iterator[kLastResolve] = null;
|
||||
iterator[kLastReject] = null;
|
||||
resolve(createIterResult(data, false));
|
||||
} else {
|
||||
iterator[kLastResolve] = resolve;
|
||||
iterator[kLastReject] = reject;
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
});
|
||||
iterator[kLastPromise] = null;
|
||||
|
||||
finished(stream, { writable: false }, (err) => {
|
||||
if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
|
||||
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(undefined, true));
|
||||
}
|
||||
iterator[kEnded] = true;
|
||||
});
|
||||
|
||||
stream.on('readable', onReadable.bind(null, iterator));
|
||||
|
||||
return iterator;
|
||||
};
|
||||
|
||||
module.exports = createReadableStreamAsyncIterator;
|
||||
@@ -23,7 +23,7 @@ const {
|
||||
|
||||
let EE;
|
||||
let PassThrough;
|
||||
let createReadableStreamAsyncIterator;
|
||||
let Readable;
|
||||
|
||||
function destroyer(stream, reading, writing, callback) {
|
||||
callback = once(callback);
|
||||
@@ -113,11 +113,11 @@ function makeAsyncIterable(val) {
|
||||
}
|
||||
|
||||
async function* fromReadable(val) {
|
||||
if (!createReadableStreamAsyncIterator) {
|
||||
createReadableStreamAsyncIterator =
|
||||
require('internal/streams/async_iterator');
|
||||
if (!Readable) {
|
||||
Readable = require('_stream_readable');
|
||||
}
|
||||
yield* createReadableStreamAsyncIterator(val);
|
||||
|
||||
yield* Readable.prototype[SymbolAsyncIterator].call(val);
|
||||
}
|
||||
|
||||
async function pump(iterable, writable, finish) {
|
||||
|
||||
@@ -230,7 +230,6 @@
|
||||
'lib/internal/worker/js_transferable.js',
|
||||
'lib/internal/watchdog.js',
|
||||
'lib/internal/streams/lazy_transform.js',
|
||||
'lib/internal/streams/async_iterator.js',
|
||||
'lib/internal/streams/buffer_list.js',
|
||||
'lib/internal/streams/duplexpair.js',
|
||||
'lib/internal/streams/from.js',
|
||||
|
||||
@@ -75,6 +75,7 @@ async function testMutualDestroy() {
|
||||
break;
|
||||
}
|
||||
assert.deepStrictEqual(iteratedLines, expectedLines);
|
||||
break;
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(iteratedLines, expectedLines);
|
||||
|
||||
@@ -11,17 +11,6 @@ const {
|
||||
const assert = require('assert');
|
||||
|
||||
async function tests() {
|
||||
{
|
||||
const AsyncIteratorPrototype = Object.getPrototypeOf(
|
||||
Object.getPrototypeOf(async function* () {}).prototype);
|
||||
const rs = new Readable({
|
||||
read() {}
|
||||
});
|
||||
assert.strictEqual(
|
||||
Object.getPrototypeOf(Object.getPrototypeOf(rs[Symbol.asyncIterator]())),
|
||||
AsyncIteratorPrototype);
|
||||
}
|
||||
|
||||
{
|
||||
// v1 stream
|
||||
|
||||
@@ -53,7 +42,9 @@ async function tests() {
|
||||
});
|
||||
|
||||
const iter = Readable.prototype[Symbol.asyncIterator].call(stream);
|
||||
iter.next().catch(common.mustCall((err) => {
|
||||
await iter.next();
|
||||
await iter.next();
|
||||
await iter.next().catch(common.mustCall((err) => {
|
||||
assert.strictEqual(err.message, 'asd');
|
||||
}));
|
||||
}
|
||||
@@ -189,12 +180,19 @@ async function tests() {
|
||||
resolved.forEach(common.mustCall(
|
||||
(item, i) => assert.strictEqual(item.value, 'hello-' + i), max));
|
||||
|
||||
errors.forEach((promise) => {
|
||||
errors.slice(0, 1).forEach((promise) => {
|
||||
promise.catch(common.mustCall((err) => {
|
||||
assert.strictEqual(err.message, 'kaboom');
|
||||
}));
|
||||
});
|
||||
|
||||
errors.slice(1).forEach((promise) => {
|
||||
promise.then(common.mustCall(({ done, value }) => {
|
||||
assert.strictEqual(done, true);
|
||||
assert.strictEqual(value, undefined);
|
||||
}));
|
||||
});
|
||||
|
||||
readable.destroy(new Error('kaboom'));
|
||||
}
|
||||
|
||||
@@ -284,28 +282,6 @@ async function tests() {
|
||||
assert.strictEqual(received, 1);
|
||||
}
|
||||
|
||||
{
|
||||
// Iterator throw.
|
||||
|
||||
const readable = new Readable({
|
||||
objectMode: true,
|
||||
read() {
|
||||
this.push('hello');
|
||||
}
|
||||
});
|
||||
|
||||
readable.on('error', common.mustCall((err) => {
|
||||
assert.strictEqual(err.message, 'kaboom');
|
||||
}));
|
||||
|
||||
const it = readable[Symbol.asyncIterator]();
|
||||
it.throw(new Error('kaboom')).catch(common.mustCall((err) => {
|
||||
assert.strictEqual(err.message, 'kaboom');
|
||||
}));
|
||||
|
||||
assert.strictEqual(readable.destroyed, true);
|
||||
}
|
||||
|
||||
{
|
||||
console.log('destroyed by throw');
|
||||
const readable = new Readable({
|
||||
@@ -577,12 +553,15 @@ async function tests() {
|
||||
assert.strictEqual(e, err);
|
||||
})(), (async () => {
|
||||
let e;
|
||||
let x;
|
||||
try {
|
||||
await d;
|
||||
x = await d;
|
||||
} catch (_e) {
|
||||
e = _e;
|
||||
}
|
||||
assert.strictEqual(e, err);
|
||||
assert.strictEqual(e, undefined);
|
||||
assert.strictEqual(x.done, true);
|
||||
assert.strictEqual(x.value, undefined);
|
||||
})()]);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user