mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
The iterators of push(), share(), shareSync(), broadcast() consumers
and from() normalization created a `{ __proto__: null, done, value }`
literal for every result. V8 creates such literals in dictionary mode,
which makes them several times more expensive to create and read than
ordinary objects.
Create them with an IterResult constructor whose prototype is a single
frozen, null-prototype object instead. Results have fast properties
and a single shape, and still have no %Object.prototype% in their
prototype chain, so a polluted Object.prototype.then still cannot turn
a result into a thenable. Creating and reading a result is about 5x
faster in a microbenchmark.
benchmark/streams/iter-throughput-share-sync.js improves by 7% to 42%
(more with more consumers) and iter-throughput-share.js by 4-6%;
pipeTo() with small chunks is about 2.5% faster.
This is observable: results are no longer null-prototype objects, so
deepStrictEqual() comparisons against `{ __proto__: null, ... }` no
longer match, and util.inspect() prints them as
`IterResult { done, value }` (the prototype has a non-enumerable
`constructor` for that purpose). The five tests that compared results
that way now compare their own properties, and a new test covers the
result contract and the prototype pollution case.
Assisted-by: OpenCode
Signed-off-by: James M Snell <jasnell@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66648
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
221 lines
6.0 KiB
JavaScript
221 lines
6.0 KiB
JavaScript
// Flags: --experimental-stream-iter
|
|
'use strict';
|
|
|
|
// Coverage tests for share.js: protocol happy path, dispose, throw,
|
|
// non-Error source throws.
|
|
|
|
const common = require('../common');
|
|
const assert = require('assert');
|
|
const {
|
|
Share,
|
|
SyncShare,
|
|
dump,
|
|
from,
|
|
fromSync,
|
|
share,
|
|
shareProtocol,
|
|
shareSync,
|
|
shareSyncProtocol,
|
|
text,
|
|
textSync,
|
|
} = require('stream/iter');
|
|
|
|
// Share.from — protocol symbol happy path (returns valid share object)
|
|
async function testShareProtocolHappyPath() {
|
|
const obj = {
|
|
[shareProtocol](options) {
|
|
return share(from('protocol-data'), options);
|
|
},
|
|
};
|
|
const shared = Share.from(obj);
|
|
const result = await text(shared.pull());
|
|
assert.strictEqual(result, 'protocol-data');
|
|
}
|
|
|
|
// SyncShare.fromSync — protocol symbol happy path
|
|
async function testSyncShareProtocolHappyPath() {
|
|
const obj = {
|
|
[shareSyncProtocol](options) {
|
|
return shareSync(fromSync('sync-protocol'), options);
|
|
},
|
|
};
|
|
const shared = SyncShare.fromSync(obj);
|
|
const result = textSync(shared.pull());
|
|
assert.strictEqual(result, 'sync-protocol');
|
|
}
|
|
|
|
// Async share — Symbol.dispose cancels
|
|
async function testShareDispose() {
|
|
const shared = share(from('dispose-test'));
|
|
const consumer = shared.pull();
|
|
shared[Symbol.dispose]();
|
|
assert.strictEqual(shared.consumerCount, 0);
|
|
// Consumer should yield nothing (cancelled before read)
|
|
const result = await text(consumer);
|
|
assert.strictEqual(result, '');
|
|
}
|
|
|
|
// Sync share — Symbol.dispose cancels
|
|
async function testSyncShareDispose() {
|
|
const shared = shareSync(fromSync('sync-dispose'));
|
|
const consumer = shared.pull();
|
|
shared[Symbol.dispose]();
|
|
assert.strictEqual(shared.consumerCount, 0);
|
|
const result = textSync(consumer);
|
|
assert.strictEqual(result, '');
|
|
}
|
|
|
|
// Async consumer iterator throw()
|
|
async function testAsyncIteratorThrow() {
|
|
const shared = share(from('throw-test'));
|
|
const consumer = shared.pull();
|
|
const iter = consumer[Symbol.asyncIterator]();
|
|
const first = await iter.next();
|
|
assert.strictEqual(first.done, false);
|
|
const result = await iter.throw(new Error('test-throw'));
|
|
assert.strictEqual(result.done, true);
|
|
assert.strictEqual(shared.consumerCount, 0);
|
|
}
|
|
|
|
// Sync consumer iterator throw()
|
|
async function testSyncIteratorThrow() {
|
|
const shared = shareSync(fromSync('throw-sync'));
|
|
const consumer = shared.pull();
|
|
const iter = consumer[Symbol.iterator]();
|
|
const first = iter.next();
|
|
assert.strictEqual(first.done, false);
|
|
const result = iter.throw(new Error('test-throw'));
|
|
assert.strictEqual(result.done, true);
|
|
assert.strictEqual(shared.consumerCount, 0);
|
|
}
|
|
|
|
async function testCompletedSyncConsumerStaysCompleted() {
|
|
const reason = undefined;
|
|
const source = {
|
|
__proto__: null,
|
|
[Symbol.iterator]() {
|
|
return {
|
|
__proto__: null,
|
|
next() { throw reason; },
|
|
};
|
|
},
|
|
};
|
|
const shared = shareSync(source);
|
|
const completed = shared.pull()[Symbol.iterator]();
|
|
const active = shared.pull()[Symbol.iterator]();
|
|
|
|
completed.return();
|
|
let caught = false;
|
|
try {
|
|
active.next();
|
|
} catch (error) {
|
|
caught = true;
|
|
assert.strictEqual(error, reason);
|
|
}
|
|
assert.strictEqual(caught, true);
|
|
assert.deepStrictEqual({ ...completed.next() }, {
|
|
done: true,
|
|
value: undefined,
|
|
});
|
|
}
|
|
|
|
async function testSyncCancelIgnoresCleanupError() {
|
|
const reason = null;
|
|
const source = {
|
|
__proto__: null,
|
|
[Symbol.iterator]() {
|
|
let done = false;
|
|
return {
|
|
__proto__: null,
|
|
next() {
|
|
if (done) return { done: true, value: undefined };
|
|
done = true;
|
|
return { done: false, value: [Buffer.from('data')] };
|
|
},
|
|
get return() { throw new Error('cleanup failed'); },
|
|
};
|
|
},
|
|
};
|
|
const shared = shareSync(source);
|
|
const iterator = shared.pull()[Symbol.iterator]();
|
|
|
|
iterator.next();
|
|
shared.cancel(reason);
|
|
|
|
assert.strictEqual(shared.consumerCount, 0);
|
|
assert.throws(() => iterator.next(), (error) => error === reason);
|
|
}
|
|
|
|
async function testAsyncCancelIgnoresCleanupGetterError() {
|
|
const reason = null;
|
|
const source = {
|
|
__proto__: null,
|
|
[Symbol.asyncIterator]() {
|
|
let done = false;
|
|
return {
|
|
__proto__: null,
|
|
next() {
|
|
if (done) return Promise.resolve({ done: true, value: undefined });
|
|
done = true;
|
|
return Promise.resolve({
|
|
done: false,
|
|
value: [Buffer.from('data')],
|
|
});
|
|
},
|
|
get return() { throw new Error('cleanup failed'); },
|
|
};
|
|
},
|
|
};
|
|
const shared = share(source);
|
|
const iterator = shared.pull()[Symbol.asyncIterator]();
|
|
|
|
await iterator.next();
|
|
shared.cancel(reason);
|
|
|
|
assert.strictEqual(shared.consumerCount, 0);
|
|
await assert.rejects(iterator.next(), (error) => error === reason);
|
|
}
|
|
|
|
// Async source preserves a non-Error thrown value.
|
|
async function testShareSourceThrowsNonError() {
|
|
const reason = 'not an error';
|
|
async function* source() {
|
|
yield [new TextEncoder().encode('ok')];
|
|
throw reason;
|
|
}
|
|
const shared = share(source());
|
|
const consumer = shared.pull();
|
|
await assert.rejects(async () => {
|
|
await dump(consumer);
|
|
}, (error) => error === reason);
|
|
}
|
|
|
|
// Sync source preserves a non-Error thrown value.
|
|
async function testSyncShareSourceThrowsNonError() {
|
|
const reason = 42;
|
|
function* source() {
|
|
yield [new TextEncoder().encode('ok')];
|
|
throw reason;
|
|
}
|
|
const shared = shareSync(source());
|
|
const consumer = shared.pull();
|
|
assert.throws(() => {
|
|
// eslint-disable-next-line no-unused-vars
|
|
for (const batch of consumer) { /* consume */ }
|
|
}, (error) => error === reason);
|
|
}
|
|
|
|
Promise.all([
|
|
testShareProtocolHappyPath(),
|
|
testSyncShareProtocolHappyPath(),
|
|
testShareDispose(),
|
|
testSyncShareDispose(),
|
|
testAsyncIteratorThrow(),
|
|
testSyncIteratorThrow(),
|
|
testCompletedSyncConsumerStaysCompleted(),
|
|
testSyncCancelIgnoresCleanupError(),
|
|
testAsyncCancelIgnoresCleanupGetterError(),
|
|
testShareSourceThrowsNonError(),
|
|
testSyncShareSourceThrowsNonError(),
|
|
]).then(common.mustCall());
|