Files
node/test/parallel/test-stream-iter-share-from.js
T
James M Snell 64c115072e stream, quic: update iterable streams backpressure
Signed-off-by: James M Snell <jasnell@gmail.com>
Assisted-by: Opencode/Opus
PR-URL: https://github.com/nodejs/node/pull/64464
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
2026-07-23 05:19:45 -07:00

247 lines
7.5 KiB
JavaScript

// Flags: --experimental-stream-iter
'use strict';
const common = require('../common');
const assert = require('assert');
const {
from,
fromSync,
share,
Share,
SyncShare,
text,
textSync,
} = require('stream/iter');
// =============================================================================
// Share.from
// =============================================================================
async function testShareFrom() {
const shared = Share.from(from('share-from'));
const consumer = shared.pull();
const data = await text(consumer);
assert.strictEqual(data, 'share-from');
}
function testShareFromRejectsNonStreamable() {
assert.throws(
() => Share.from(12345),
{ code: 'ERR_INVALID_ARG_TYPE' },
);
}
// =============================================================================
// SyncShare.fromSync
// =============================================================================
async function testSyncShareFromSync() {
const shared = SyncShare.fromSync(fromSync('sync-share-from'));
const consumer = shared.pull();
const data = textSync(consumer);
assert.strictEqual(data, 'sync-share-from');
}
function testSyncShareFromRejectsNonStreamable() {
assert.throws(
() => SyncShare.fromSync(12345),
{ code: 'ERR_INVALID_ARG_TYPE' },
);
}
// =============================================================================
// Protocol validation
// =============================================================================
function testShareProtocolReturnsNull() {
const obj = {
[Symbol.for('Stream.shareProtocol')]() { return null; },
};
assert.throws(
() => Share.from(obj),
{ code: 'ERR_INVALID_RETURN_VALUE' },
);
}
function testShareProtocolReturnsNonObject() {
const obj = {
[Symbol.for('Stream.shareProtocol')]() { return 42; },
};
assert.throws(
() => Share.from(obj),
{ code: 'ERR_INVALID_RETURN_VALUE' },
);
}
function testSyncShareProtocolReturnsNull() {
const obj = {
[Symbol.for('Stream.shareSyncProtocol')]() { return null; },
};
assert.throws(
() => SyncShare.fromSync(obj),
{ code: 'ERR_INVALID_RETURN_VALUE' },
);
}
function testSyncShareProtocolReturnsNonObject() {
const obj = {
[Symbol.for('Stream.shareSyncProtocol')]() { return 'bad'; },
};
assert.throws(
() => SyncShare.fromSync(obj),
{ code: 'ERR_INVALID_RETURN_VALUE' },
);
}
// =============================================================================
// Unbounded backpressure: two consumers, slow consumer blocks the source
// =============================================================================
async function testShareBlockBackpressure() {
// A source that yields 5 items. With two consumers and budget: 16384,
// the fast consumer drives the source forward. The slow consumer holds back
// trimming, causing the buffer to fill. 'unbounded' mode should stall the
// source pull until the slow consumer catches up.
const enc = new TextEncoder();
async function* source() {
for (let i = 0; i < 5; i++) {
yield [enc.encode(`item${i}`)];
}
}
const shared = share(source(), { budget: 16384, backpressure: 'unbounded' });
const fast = shared.pull();
const slow = shared.pull();
// Both consumers should ultimately receive all 5 items
const [fastData, slowData] = await Promise.all([
text(fast),
text(slow),
]);
assert.strictEqual(fastData, 'item0item1item2item3item4');
assert.strictEqual(slowData, 'item0item1item2item3item4');
}
// =============================================================================
// Drop backpressure modes: use a fast + stalled consumer to trigger drops
// =============================================================================
async function testShareDropOldest() {
// Two consumers, fast reads eagerly then slow reads. With drop-oldest,
// the slow consumer's cursor is advanced past dropped items, so it
// misses old data and only sees recent items.
async function* source() {
for (let i = 0; i < 4; i++) {
const chunk = new Uint8Array(16384);
chunk[0] = i; // Tag first byte with index
yield [chunk];
}
}
const shared = share(source(), { budget: 32768, backpressure: 'drop-oldest' });
const fast = shared.pull();
const slow = shared.pull();
// Fast consumer reads all items
const fastIndices = [];
for await (const batch of fast) {
for (const chunk of batch) {
fastIndices.push(chunk[0]);
}
}
assert.strictEqual(fastIndices.length, 4);
// Slow consumer reads after fast is done — old items were dropped
const slowIndices = [];
for await (const batch of slow) {
for (const chunk of batch) {
slowIndices.push(chunk[0]);
}
}
// The slow consumer should see fewer items than were produced
assert.ok(slowIndices.length < 4,
`Expected < 4 items after drop-oldest, got ${slowIndices.length}`);
assert.ok(slowIndices.length > 0,
'Expected at least some items after drop-oldest');
// The last item should always be present (most recent items kept)
assert.strictEqual(slowIndices[slowIndices.length - 1], 3);
}
async function testShareDropNewest() {
// With drop-newest and a stalled consumer, the async path allows the
// buffer to grow beyond budget (the "drop" applies to the
// backpressure signal, not the buffer contents). Both consumers
// ultimately see all items.
async function* source() {
for (let i = 0; i < 4; i++) {
const chunk = new Uint8Array(16384);
chunk[0] = i;
yield [chunk];
}
}
const shared = share(source(), { budget: 32768, backpressure: 'drop-newest' });
const fast = shared.pull();
const slow = shared.pull();
// Fast consumer reads all items
const fastIndices = [];
for await (const batch of fast) {
for (const chunk of batch) {
fastIndices.push(chunk[0]);
}
}
assert.strictEqual(fastIndices.length, 2);
// Slow consumer also sees all items (buffer grew past budget)
const slowIndices = [];
for await (const batch of slow) {
for (const chunk of batch) {
slowIndices.push(chunk[0]);
}
}
assert.strictEqual(slowIndices.length, 2);
assert.strictEqual(slowIndices[0], 0);
assert.strictEqual(slowIndices[1], 1);
}
// =============================================================================
// Strict backpressure: should throw when buffer overflows
// =============================================================================
async function testShareStrictBackpressure() {
async function* source() {
for (let i = 0; i < 10; i++) {
yield [new Uint8Array(16384)];
}
}
const shared = share(source(), { budget: 32768, backpressure: 'strict' });
const fast = shared.pull();
// Create a second consumer that never reads — this prevents buffer trimming
shared.pull();
// The fast consumer's pulls will eventually cause the buffer to exceed
// the budget (since the slow consumer prevents trimming),
// triggering an ERR_OUT_OF_RANGE error.
await assert.rejects(async () => {
// eslint-disable-next-line no-unused-vars
for await (const _ of fast) { /* consume */ }
}, { code: 'ERR_OUT_OF_RANGE' });
}
Promise.all([
testShareFrom(),
testShareFromRejectsNonStreamable(),
testSyncShareFromSync(),
testSyncShareFromRejectsNonStreamable(),
testShareProtocolReturnsNull(),
testShareProtocolReturnsNonObject(),
testSyncShareProtocolReturnsNull(),
testSyncShareProtocolReturnsNonObject(),
testShareBlockBackpressure(),
testShareDropOldest(),
testShareDropNewest(),
testShareStrictBackpressure(),
]).then(common.mustCall());