Skip to content

Commit 2fc833c

Browse files
committed
stream: prevent share from eagerly draining source
Wait for buffer space after drop-newest discards an upstream result. This keeps one consumer pull from draining the source or looping indefinitely while a slower consumer keeps the buffer full. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol
1 parent ad7a5b8 commit 2fc833c

2 files changed

Lines changed: 50 additions & 26 deletions

File tree

lib/internal/streams/iter/share.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ class ShareImpl {
181181
}
182182

183183
await self.#pullFromSource(!shouldBuffer);
184+
if (!shouldBuffer) {
185+
await self.#waitForBufferSpaceAfterDrop();
186+
}
184187
}
185188
};
186189

@@ -299,6 +302,17 @@ class ShareImpl {
299302
return true;
300303
}
301304

305+
async #waitForBufferSpaceAfterDrop() {
306+
while (this.#bufferedBytes >= this.#options.budget &&
307+
!this.#cancelled &&
308+
this.#sourceError === undefined &&
309+
!this.#sourceExhausted) {
310+
const { promise, resolve } = PromiseWithResolvers();
311+
ArrayPrototypePush(this.#pullWaiters, resolve);
312+
await promise;
313+
}
314+
}
315+
302316
#pullFromSource(discard = false) {
303317
if (this.#sourceExhausted || this.#cancelled) {
304318
return PromiseResolve();

test/parallel/test-stream-iter-share-from.js

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -170,40 +170,50 @@ async function testShareDropOldest() {
170170
}
171171

172172
async function testShareDropNewest() {
173-
// With drop-newest and a stalled consumer, the async path allows the
174-
// buffer to grow beyond budget (the "drop" applies to the
175-
// backpressure signal, not the buffer contents). Both consumers
176-
// ultimately see all items.
173+
let pulls = 0;
174+
let secondPull;
175+
const secondPullStarted = new Promise((resolve) => {
176+
secondPull = resolve;
177+
});
178+
177179
async function* source() {
178-
for (let i = 0; i < 4; i++) {
180+
for (let i = 0; i < 7; i++) {
181+
pulls++;
182+
if (pulls === 2) secondPull();
179183
const chunk = new Uint8Array(16384);
180184
chunk[0] = i;
181185
yield [chunk];
182186
}
183187
}
184-
const shared = share(source(), { budget: 32768, backpressure: 'drop-newest' });
185-
const fast = shared.pull();
186-
const slow = shared.pull();
188+
const shared = share(source(), {
189+
budget: 16384,
190+
backpressure: 'drop-newest',
191+
});
192+
const fast = shared.pull()[Symbol.asyncIterator]();
193+
const slow = shared.pull()[Symbol.asyncIterator]();
187194

188-
// Fast consumer reads all items
189-
const fastIndices = [];
190-
for await (const batch of fast) {
191-
for (const chunk of batch) {
192-
fastIndices.push(chunk[0]);
193-
}
194-
}
195-
assert.strictEqual(fastIndices.length, 2);
195+
const first = await fast.next();
196+
assert.strictEqual(first.value[0][0], 0);
196197

197-
// Slow consumer also sees all items (buffer grew past budget)
198-
const slowIndices = [];
199-
for await (const batch of slow) {
200-
for (const chunk of batch) {
201-
slowIndices.push(chunk[0]);
202-
}
203-
}
204-
assert.strictEqual(slowIndices.length, 2);
205-
assert.strictEqual(slowIndices[0], 0);
206-
assert.strictEqual(slowIndices[1], 1);
198+
let nextSettled = false;
199+
const next = fast.next().then((result) => {
200+
nextSettled = true;
201+
return result;
202+
});
203+
204+
await secondPullStarted;
205+
await new Promise(setImmediate);
206+
assert.strictEqual(pulls, 2);
207+
assert.strictEqual(nextSettled, false);
208+
209+
const slowResult = await slow.next();
210+
assert.strictEqual(slowResult.value[0][0], 0);
211+
212+
const nextResult = await next;
213+
assert.strictEqual(nextResult.value[0][0], 2);
214+
assert.strictEqual(pulls, 3);
215+
216+
shared.cancel();
207217
}
208218

209219
// =============================================================================

0 commit comments

Comments
 (0)