Commit dc51c79

Browse files
trivikraduh95
authored andcommitted
stream: drain pending writes before broadcast end
Keep processing writes that were queued before end(). Signal the broadcast end only after those writes enter the shared buffer. Resolve end() after all consumers reach end-of-stream. This prevents backpressured writes from remaining pending forever. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65334Fixes: #65333 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5fbf6c5 commit dc51c79

4 files changed

Lines changed: 258 additions & 50 deletions

File tree

β€Ždoc/api/stream_iter.mdβ€Ž

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,14 +431,16 @@ the write. Use [`ondrain()`][] to wait for capacity rather than polling.
431431
the pending `end()` call; it does not fail the writer itself.
432432
* Returns: {Promise} Fulfills with the total number of bytes written.
433433

434-
Signal that no more data will be written.
434+
Signals that no more data will be written and waits for buffered data to drain.
435435

436436
#### `writer.endSync()`
437437

438-
* Returns: {number} Total bytes written, or `-1` if the writer is not open.
438+
* Returns: {number} Total bytes written, or `-1` if ending cannot complete
439+
synchronously.
439440

440-
Synchronous variant of `writer.end()`. Returns `-1` if the writer is already
441-
closed or errored. Can be used as a try-fallback pattern:
441+
Synchronous variant of `writer.end()`. A return value of `-1` means closing has
442+
started but requires asynchronous draining. Use the try-fallback pattern to
443+
await completion:
442444

443445
```cjs
444446
constresult=writer.endSync();

β€Žlib/internal/streams/iter/broadcast.jsβ€Ž

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const {
1414
PromiseReject,
1515
PromiseResolve,
1616
PromiseWithResolvers,
17+
SafePromisePrototypeFinally,
18+
SafePromiseRace,
1719
SafeSet,
1820
Symbol,
1921
SymbolAsyncDispose,
@@ -77,6 +79,22 @@ const kEnd = Symbol('kEnd');
7779
constkAbort=Symbol('kAbort');
7880
constkCanWrite=Symbol('kCanWrite');
7981
constkOnBufferDrained=Symbol('kOnBufferDrained');
82+
constkOnEndDrained=Symbol('kOnEndDrained');
83+
constkPendingWriteRemoved=Symbol('kPendingWriteRemoved');
84+
85+
functionraceEndWithSignal(promise,signal){
86+
if(!signal)returnpromise;
87+
88+
const{promise: aborted, reject }=PromiseWithResolvers();
89+
constonAbort=()=>reject(signal.reason);
90+
signal.addEventListener('abort',onAbort,{__proto__: null,once: true});
91+
if(signal.aborted)onAbort();
92+
93+
returnSafePromisePrototypeFinally(
94+
SafePromiseRace([promise,aborted]),
95+
()=>signal.removeEventListener('abort',onAbort),
96+
);
97+
}
8098

8199
// =============================================================================
82100
// Broadcast Implementation
@@ -100,6 +118,7 @@ class BroadcastImpl {
100118
constructor(options){
101119
this.#options =options;
102120
this[kOnBufferDrained]=null;
121+
this[kOnEndDrained]=null;
103122
}
104123

105124
setWriter(writer){
@@ -183,6 +202,7 @@ class BroadcastImpl {
183202
if(self.#deleteConsumer(state)){
184203
self.#tryTrimBuffer();
185204
}
205+
self.#notifyEndDrained();
186206
}
187207

188208
return{
@@ -358,10 +378,11 @@ class BroadcastImpl {
358378
}
359379
}
360380
}
381+
this.#notifyEndDrained();
361382
}
362383

363384
[kAbort](reason){
364-
if(this.#ended ||this.#error !==undefined)return;
385+
if(this.#error !==undefined)return;
365386
this.#error =reason;
366387
this.#ended =true;
367388

@@ -396,6 +417,12 @@ class BroadcastImpl {
396417

397418
// Private methods
398419

420+
#notifyEndDrained(){
421+
if(this.#ended &&this.#consumers.size===0){
422+
this[kOnEndDrained]?.();
423+
}
424+
}
425+
399426
#recomputeMinCursor(){
400427
const{ minCursor, minCursorConsumers }=getMinCursor(
401428
this.#consumers,this.#bufferStart +this.#buffer.length);
@@ -516,8 +543,9 @@ let getBroadcastPendingWrites;
516543
classBroadcastWriter{
517544
#broadcast;
518545
#totalBytes =0;
519-
#closed;
520-
#aborted =false;
546+
#state ='open';
547+
#error;
548+
#pendingEnd;
521549
#pendingWrites =newRingBuffer();
522550
#pendingDrains =[];
523551

@@ -532,8 +560,11 @@ class BroadcastWriter {
532560

533561
this.#broadcast[kOnBufferDrained]=()=>{
534562
this.#resolvePendingWrites();
535-
this.#resolvePendingDrains(true);
563+
if(this.#state ==='open'){
564+
this.#resolvePendingDrains(true);
565+
}
536566
};
567+
this.#broadcast[kOnEndDrained]=()=>this.#endDrained();
537568
}
538569

539570
// The drainable protocol works with Stream.ondrain to provide a notification
@@ -547,20 +578,12 @@ class BroadcastWriter {
547578
returnpromise;
548579
}
549580

550-
#isClosed(){
551-
returnthis.#closed !==undefined;
552-
}
553-
554-
#isClosedOrAborted(){
555-
returnthis.#isClosed()||this.#aborted;
556-
}
557-
558581
getcanWrite(){
559-
returnthis.#isClosedOrAborted() ? null :this.#broadcast[kCanWrite]();
582+
returnthis.#state ==='open' ?this.#broadcast[kCanWrite]() : null;
560583
}
561584

562585
#canUseWriteFastPath(signal){
563-
return!signal&&!this.#isClosed()&&!this.#aborted&&
586+
return!signal&&this.#state ==='open'&&
564587
this.#broadcast[kCanWrite]();
565588
}
566589

@@ -592,13 +615,15 @@ class BroadcastWriter {
592615
}
593616

594617
async #writevSlow(chunks,signal){
595-
// Check for pre-aborted
596-
signal?.throwIfAborted();
597-
598-
if(this.#isClosedOrAborted()){
618+
if(this.#state ==='errored'){
619+
throwthis.#error;
620+
}
621+
if(this.#state !=='open'){
599622
thrownewERR_INVALID_STATE.TypeError('Writer is closed');
600623
}
601624

625+
signal?.throwIfAborted();
626+
602627
constconverted=convertChunks(chunks);
603628

604629
if(this.#broadcast[kWrite](converted)){
@@ -624,7 +649,7 @@ class BroadcastWriter {
624649
}
625650

626651
writeSync(chunk){
627-
if(this.#isClosedOrAborted())returnfalse;
652+
if(this.#state !=='open')returnfalse;
628653
if(!this.#broadcast[kCanWrite]())returnfalse;
629654
constconverted=
630655
toUint8Array(chunk);
@@ -637,7 +662,7 @@ class BroadcastWriter {
637662

638663
writevSync(chunks){
639664
validateArray(chunks,'chunks');
640-
if(this.#isClosedOrAborted())returnfalse;
665+
if(this.#state !=='open')returnfalse;
641666
if(!this.#broadcast[kCanWrite]())returnfalse;
642667
constconverted=convertChunks(chunks);
643668
if(this.#broadcast[kWrite](converted)){
@@ -651,34 +676,43 @@ class BroadcastWriter {
651676

652677
end(options){
653678
constsignal=getWriterSignal(options);
679+
if(this.#state ==='errored')returnPromiseReject(this.#error);
680+
if(this.#state ==='closed')returnPromiseResolve(this.#totalBytes);
654681
if(signal?.aborted)returnPromiseReject(signal.reason);
655682

656-
if(this.#isClosed())returnthis.#closed;
657-
this.#closed =PromiseResolve(this.#totalBytes);
658-
this.#broadcast[kEnd]();
659-
this.#resolvePendingDrains(false);
660-
returnthis.#closed;
683+
constendPromise=this.#getEndPromise();
684+
if(this.#state ==='open'){
685+
this.#state ='closing';
686+
this.#resolvePendingDrains(false);
687+
this.#finishEndIfReady();
688+
}
689+
690+
returnraceEndWithSignal(endPromise,signal);
661691
}
662692

663693
endSync(){
664-
if(this.#closed)returnthis.#totalBytes;
665-
this.#closed =PromiseResolve(this.#totalBytes);
666-
this.#broadcast[kEnd]();
694+
if(this.#state ==='closed')returnthis.#totalBytes;
695+
if(this.#state ==='errored'||this.#state ==='closing')return-1;
696+
697+
this.#state ='closing';
667698
this.#resolvePendingDrains(false);
668-
returnthis.#totalBytes;
699+
this.#finishEndIfReady();
700+
returnthis.#state ==='closed' ? this.#totalBytes : -1;
669701
}
670702

671703
fail(reason){
672-
if(this.#isClosedOrAborted())return;
673-
this.#aborted =true;
674-
this.#closed =PromiseResolve(this.#totalBytes);
704+
if(this.#state ==='errored'||this.#state ==='closed')return;
705+
this.#state ='errored';
675706
consterror=reason??newERR_INVALID_STATE.TypeError('Failed');
707+
this.#error =error;
676708
this.#rejectPendingWrites(error);
677709
this.#rejectPendingDrains(error);
710+
this.#pendingEnd?.reject(error);
678711
this.#broadcast[kAbort](error);
679712
}
680713

681714
[SymbolAsyncDispose](){
715+
if(this.#state ==='closing')returnthis.#getEndPromise();
682716
this.fail();
683717
returnPromiseResolve();
684718
}
@@ -688,11 +722,33 @@ class BroadcastWriter {
688722
}
689723

690724
[kCancelWriter](){
691-
if(this.#isClosed())return;
692-
this.#closed=PromiseResolve(this.#totalBytes);
725+
if(this.#state ==='closed'||this.#state ==='errored')return;
726+
this.#state='closed';
693727
this.#rejectPendingWrites(
694728
lazyDOMException('Broadcast cancelled','AbortError'));
695729
this.#resolvePendingDrains(false);
730+
this.#pendingEnd?.resolve(this.#totalBytes);
731+
}
732+
733+
#getEndPromise(){
734+
this.#pendingEnd ??=PromiseWithResolvers();
735+
returnthis.#pendingEnd.promise;
736+
}
737+
738+
#finishEndIfReady(){
739+
if(this.#state ==='closing'&&this.#pendingWrites.length===0){
740+
this.#broadcast[kEnd]();
741+
}
742+
}
743+
744+
#endDrained(){
745+
if(this.#state !=='closing')return;
746+
this.#state ='closed';
747+
this.#pendingEnd?.resolve(this.#totalBytes);
748+
}
749+
750+
[kPendingWriteRemoved](){
751+
this.#finishEndIfReady();
696752
}
697753

698754
/**
@@ -724,6 +780,7 @@ class BroadcastWriter {
724780
break;
725781
}
726782
}
783+
this.#finishEndIfReady();
727784
}
728785

729786
#rejectPendingWrites(error){
@@ -756,6 +813,7 @@ function wireBroadcastWriteSignal(entry, signal, resolve, reject, self) {
756813
if(idx!==-1)pendingWrites.removeAt(idx);
757814
entry.chunk=null;
758815
reject(signal.reason??lazyDOMException('Aborted','AbortError'));
816+
if(idx!==-1)self[kPendingWriteRemoved]();
759817
};
760818
entry.resolve=function(){
761819
signal.removeEventListener('abort',onAbort);

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Commit dc51c79

Browse files
trivikraduh95
authored andcommitted
stream: drain pending writes before broadcast end
Keep processing writes that were queued before end(). Signal the broadcast end only after those writes enter the shared buffer. Resolve end() after all consumers reach end-of-stream. This prevents backpressured writes from remaining pending forever. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65334Fixes: #65333 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5fbf6c5 commit dc51c79

4 files changed

Lines changed: 258 additions & 50 deletions

File tree

β€Ždoc/api/stream_iter.mdβ€Ž

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,14 +431,16 @@ the write. Use [`ondrain()`][] to wait for capacity rather than polling.
431431
the pending `end()` call; it does not fail the writer itself.
432432
* Returns: {Promise} Fulfills with the total number of bytes written.
433433

434-
Signal that no more data will be written.
434+
Signals that no more data will be written and waits for buffered data to drain.
435435

436436
#### `writer.endSync()`
437437

438-
* Returns: {number} Total bytes written, or `-1` if the writer is not open.
438+
* Returns: {number} Total bytes written, or `-1` if ending cannot complete
439+
synchronously.
439440

440-
Synchronous variant of `writer.end()`. Returns `-1` if the writer is already
441-
closed or errored. Can be used as a try-fallback pattern:
441+
Synchronous variant of `writer.end()`. A return value of `-1` means closing has
442+
started but requires asynchronous draining. Use the try-fallback pattern to
443+
await completion:
442444

443445
```cjs
444446
constresult=writer.endSync();

β€Žlib/internal/streams/iter/broadcast.jsβ€Ž

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const {
1414
PromiseReject,
1515
PromiseResolve,
1616
PromiseWithResolvers,
17+
SafePromisePrototypeFinally,
18+
SafePromiseRace,
1719
SafeSet,
1820
Symbol,
1921
SymbolAsyncDispose,
@@ -77,6 +79,22 @@ const kEnd = Symbol('kEnd');
7779
constkAbort=Symbol('kAbort');
7880
constkCanWrite=Symbol('kCanWrite');
7981
constkOnBufferDrained=Symbol('kOnBufferDrained');
82+
constkOnEndDrained=Symbol('kOnEndDrained');
83+
constkPendingWriteRemoved=Symbol('kPendingWriteRemoved');
84+
85+
functionraceEndWithSignal(promise,signal){
86+
if(!signal)returnpromise;
87+
88+
const{promise: aborted, reject }=PromiseWithResolvers();
89+
constonAbort=()=>reject(signal.reason);
90+
signal.addEventListener('abort',onAbort,{__proto__: null,once: true});
91+
if(signal.aborted)onAbort();
92+
93+
returnSafePromisePrototypeFinally(
94+
SafePromiseRace([promise,aborted]),
95+
()=>signal.removeEventListener('abort',onAbort),
96+
);
97+
}
8098

8199
// =============================================================================
82100
// Broadcast Implementation
@@ -100,6 +118,7 @@ class BroadcastImpl {
100118
constructor(options){
101119
this.#options =options;
102120
this[kOnBufferDrained]=null;
121+
this[kOnEndDrained]=null;
103122
}
104123

105124
setWriter(writer){
@@ -183,6 +202,7 @@ class BroadcastImpl {
183202
if(self.#deleteConsumer(state)){
184203
self.#tryTrimBuffer();
185204
}
205+
self.#notifyEndDrained();
186206
}
187207

188208
return{
@@ -358,10 +378,11 @@ class BroadcastImpl {
358378
}
359379
}
360380
}
381+
this.#notifyEndDrained();
361382
}
362383

363384
[kAbort](reason){
364-
if(this.#ended ||this.#error !==undefined)return;
385+
if(this.#error !==undefined)return;
365386
this.#error =reason;
366387
this.#ended =true;
367388

@@ -396,6 +417,12 @@ class BroadcastImpl {
396417

397418
// Private methods
398419

420+
#notifyEndDrained(){
421+
if(this.#ended &&this.#consumers.size===0){
422+
this[kOnEndDrained]?.();
423+
}
424+
}
425+
399426
#recomputeMinCursor(){
400427
const{ minCursor, minCursorConsumers }=getMinCursor(
401428
this.#consumers,this.#bufferStart +this.#buffer.length);
@@ -516,8 +543,9 @@ let getBroadcastPendingWrites;
516543
classBroadcastWriter{
517544
#broadcast;
518545
#totalBytes =0;
519-
#closed;
520-
#aborted =false;
546+
#state ='open';
547+
#error;
548+
#pendingEnd;
521549
#pendingWrites =newRingBuffer();
522550
#pendingDrains =[];
523551

@@ -532,8 +560,11 @@ class BroadcastWriter {
532560

533561
this.#broadcast[kOnBufferDrained]=()=>{
534562
this.#resolvePendingWrites();
535-
this.#resolvePendingDrains(true);
563+
if(this.#state ==='open'){
564+
this.#resolvePendingDrains(true);
565+
}
536566
};
567+
this.#broadcast[kOnEndDrained]=()=>this.#endDrained();
537568
}
538569

539570
// The drainable protocol works with Stream.ondrain to provide a notification
@@ -547,20 +578,12 @@ class BroadcastWriter {
547578
returnpromise;
548579
}
549580

550-
#isClosed(){
551-
returnthis.#closed !==undefined;
552-
}
553-
554-
#isClosedOrAborted(){
555-
returnthis.#isClosed()||this.#aborted;
556-
}
557-
558581
getcanWrite(){
559-
returnthis.#isClosedOrAborted() ? null :this.#broadcast[kCanWrite]();
582+
returnthis.#state ==='open' ?this.#broadcast[kCanWrite]() : null;
560583
}
561584

562585
#canUseWriteFastPath(signal){
563-
return!signal&&!this.#isClosed()&&!this.#aborted&&
586+
return!signal&&this.#state ==='open'&&
564587
this.#broadcast[kCanWrite]();
565588
}
566589

@@ -592,13 +615,15 @@ class BroadcastWriter {
592615
}
593616

594617
async #writevSlow(chunks,signal){
595-
// Check for pre-aborted
596-
signal?.throwIfAborted();
597-
598-
if(this.#isClosedOrAborted()){
618+
if(this.#state ==='errored'){
619+
throwthis.#error;
620+
}
621+
if(this.#state !=='open'){
599622
thrownewERR_INVALID_STATE.TypeError('Writer is closed');
600623
}
601624

625+
signal?.throwIfAborted();
626+
602627
constconverted=convertChunks(chunks);
603628

604629
if(this.#broadcast[kWrite](converted)){
@@ -624,7 +649,7 @@ class BroadcastWriter {
624649
}
625650

626651
writeSync(chunk){
627-
if(this.#isClosedOrAborted())returnfalse;
652+
if(this.#state !=='open')returnfalse;
628653
if(!this.#broadcast[kCanWrite]())returnfalse;
629654
constconverted=
630655
toUint8Array(chunk);
@@ -637,7 +662,7 @@ class BroadcastWriter {
637662

638663
writevSync(chunks){
639664
validateArray(chunks,'chunks');
640-
if(this.#isClosedOrAborted())returnfalse;
665+
if(this.#state !=='open')returnfalse;
641666
if(!this.#broadcast[kCanWrite]())returnfalse;
642667
constconverted=convertChunks(chunks);
643668
if(this.#broadcast[kWrite](converted)){
@@ -651,34 +676,43 @@ class BroadcastWriter {
651676

652677
end(options){
653678
constsignal=getWriterSignal(options);
679+
if(this.#state ==='errored')returnPromiseReject(this.#error);
680+
if(this.#state ==='closed')returnPromiseResolve(this.#totalBytes);
654681
if(signal?.aborted)returnPromiseReject(signal.reason);
655682

656-
if(this.#isClosed())returnthis.#closed;
657-
this.#closed =PromiseResolve(this.#totalBytes);
658-
this.#broadcast[kEnd]();
659-
this.#resolvePendingDrains(false);
660-
returnthis.#closed;
683+
constendPromise=this.#getEndPromise();
684+
if(this.#state ==='open'){
685+
this.#state ='closing';
686+
this.#resolvePendingDrains(false);
687+
this.#finishEndIfReady();
688+
}
689+
690+
returnraceEndWithSignal(endPromise,signal);
661691
}
662692

663693
endSync(){
664-
if(this.#closed)returnthis.#totalBytes;
665-
this.#closed =PromiseResolve(this.#totalBytes);
666-
this.#broadcast[kEnd]();
694+
if(this.#state ==='closed')returnthis.#totalBytes;
695+
if(this.#state ==='errored'||this.#state ==='closing')return-1;
696+
697+
this.#state ='closing';
667698
this.#resolvePendingDrains(false);
668-
returnthis.#totalBytes;
699+
this.#finishEndIfReady();
700+
returnthis.#state ==='closed' ? this.#totalBytes : -1;
669701
}
670702

671703
fail(reason){
672-
if(this.#isClosedOrAborted())return;
673-
this.#aborted =true;
674-
this.#closed =PromiseResolve(this.#totalBytes);
704+
if(this.#state ==='errored'||this.#state ==='closed')return;
705+
this.#state ='errored';
675706
consterror=reason??newERR_INVALID_STATE.TypeError('Failed');
707+
this.#error =error;
676708
this.#rejectPendingWrites(error);
677709
this.#rejectPendingDrains(error);
710+
this.#pendingEnd?.reject(error);
678711
this.#broadcast[kAbort](error);
679712
}
680713

681714
[SymbolAsyncDispose](){
715+
if(this.#state ==='closing')returnthis.#getEndPromise();
682716
this.fail();
683717
returnPromiseResolve();
684718
}
@@ -688,11 +722,33 @@ class BroadcastWriter {
688722
}
689723

690724
[kCancelWriter](){
691-
if(this.#isClosed())return;
692-
this.#closed=PromiseResolve(this.#totalBytes);
725+
if(this.#state ==='closed'||this.#state ==='errored')return;
726+
this.#state='closed';
693727
this.#rejectPendingWrites(
694728
lazyDOMException('Broadcast cancelled','AbortError'));
695729
this.#resolvePendingDrains(false);
730+
this.#pendingEnd?.resolve(this.#totalBytes);
731+
}
732+
733+
#getEndPromise(){
734+
this.#pendingEnd ??=PromiseWithResolvers();
735+
returnthis.#pendingEnd.promise;
736+
}
737+
738+
#finishEndIfReady(){
739+
if(this.#state ==='closing'&&this.#pendingWrites.length===0){
740+
this.#broadcast[kEnd]();
741+
}
742+
}
743+
744+
#endDrained(){
745+
if(this.#state !=='closing')return;
746+
this.#state ='closed';
747+
this.#pendingEnd?.resolve(this.#totalBytes);
748+
}
749+
750+
[kPendingWriteRemoved](){
751+
this.#finishEndIfReady();
696752
}
697753

698754
/**
@@ -724,6 +780,7 @@ class BroadcastWriter {
724780
break;
725781
}
726782
}
783+
this.#finishEndIfReady();
727784
}
728785

729786
#rejectPendingWrites(error){
@@ -756,6 +813,7 @@ function wireBroadcastWriteSignal(entry, signal, resolve, reject, self) {
756813
if(idx!==-1)pendingWrites.removeAt(idx);
757814
entry.chunk=null;
758815
reject(signal.reason??lazyDOMException('Aborted','AbortError'));
816+
if(idx!==-1)self[kPendingWriteRemoved]();
759817
};
760818
entry.resolve=function(){
761819
signal.removeEventListener('abort',onAbort);

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit dc51c79

Browse files
trivikraduh95
authored andcommitted
stream: drain pending writes before broadcast end
Keep processing writes that were queued before end(). Signal the broadcast end only after those writes enter the shared buffer. Resolve end() after all consumers reach end-of-stream. This prevents backpressured writes from remaining pending forever. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65334Fixes: #65333 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5fbf6c5 commit dc51c79

4 files changed

Lines changed: 258 additions & 50 deletions

File tree

β€Ždoc/api/stream_iter.mdβ€Ž

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,14 +431,16 @@ the write. Use [`ondrain()`][] to wait for capacity rather than polling.
431431
the pending `end()` call; it does not fail the writer itself.
432432
* Returns: {Promise} Fulfills with the total number of bytes written.
433433

434-
Signal that no more data will be written.
434+
Signals that no more data will be written and waits for buffered data to drain.
435435

436436
#### `writer.endSync()`
437437

438-
* Returns: {number} Total bytes written, or `-1` if the writer is not open.
438+
* Returns: {number} Total bytes written, or `-1` if ending cannot complete
439+
synchronously.
439440

440-
Synchronous variant of `writer.end()`. Returns `-1` if the writer is already
441-
closed or errored. Can be used as a try-fallback pattern:
441+
Synchronous variant of `writer.end()`. A return value of `-1` means closing has
442+
started but requires asynchronous draining. Use the try-fallback pattern to
443+
await completion:
442444

443445
```cjs
444446
constresult=writer.endSync();

β€Žlib/internal/streams/iter/broadcast.jsβ€Ž

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const {
1414
PromiseReject,
1515
PromiseResolve,
1616
PromiseWithResolvers,
17+
SafePromisePrototypeFinally,
18+
SafePromiseRace,
1719
SafeSet,
1820
Symbol,
1921
SymbolAsyncDispose,
@@ -77,6 +79,22 @@ const kEnd = Symbol('kEnd');
7779
constkAbort=Symbol('kAbort');
7880
constkCanWrite=Symbol('kCanWrite');
7981
constkOnBufferDrained=Symbol('kOnBufferDrained');
82+
constkOnEndDrained=Symbol('kOnEndDrained');
83+
constkPendingWriteRemoved=Symbol('kPendingWriteRemoved');
84+
85+
functionraceEndWithSignal(promise,signal){
86+
if(!signal)returnpromise;
87+
88+
const{promise: aborted, reject }=PromiseWithResolvers();
89+
constonAbort=()=>reject(signal.reason);
90+
signal.addEventListener('abort',onAbort,{__proto__: null,once: true});
91+
if(signal.aborted)onAbort();
92+
93+
returnSafePromisePrototypeFinally(
94+
SafePromiseRace([promise,aborted]),
95+
()=>signal.removeEventListener('abort',onAbort),
96+
);
97+
}
8098

8199
// =============================================================================
82100
// Broadcast Implementation
@@ -100,6 +118,7 @@ class BroadcastImpl {
100118
constructor(options){
101119
this.#options =options;
102120
this[kOnBufferDrained]=null;
121+
this[kOnEndDrained]=null;
103122
}
104123

105124
setWriter(writer){
@@ -183,6 +202,7 @@ class BroadcastImpl {
183202
if(self.#deleteConsumer(state)){
184203
self.#tryTrimBuffer();
185204
}
205+
self.#notifyEndDrained();
186206
}
187207

188208
return{
@@ -358,10 +378,11 @@ class BroadcastImpl {
358378
}
359379
}
360380
}
381+
this.#notifyEndDrained();
361382
}
362383

363384
[kAbort](reason){
364-
if(this.#ended ||this.#error !==undefined)return;
385+
if(this.#error !==undefined)return;
365386
this.#error =reason;
366387
this.#ended =true;
367388

@@ -396,6 +417,12 @@ class BroadcastImpl {
396417

397418
// Private methods
398419

420+
#notifyEndDrained(){
421+
if(this.#ended &&this.#consumers.size===0){
422+
this[kOnEndDrained]?.();
423+
}
424+
}
425+
399426
#recomputeMinCursor(){
400427
const{ minCursor, minCursorConsumers }=getMinCursor(
401428
this.#consumers,this.#bufferStart +this.#buffer.length);
@@ -516,8 +543,9 @@ let getBroadcastPendingWrites;
516543
classBroadcastWriter{
517544
#broadcast;
518545
#totalBytes =0;
519-
#closed;
520-
#aborted =false;
546+
#state ='open';
547+
#error;
548+
#pendingEnd;
521549
#pendingWrites =newRingBuffer();
522550
#pendingDrains =[];
523551

@@ -532,8 +560,11 @@ class BroadcastWriter {
532560

533561
this.#broadcast[kOnBufferDrained]=()=>{
534562
this.#resolvePendingWrites();
535-
this.#resolvePendingDrains(true);
563+
if(this.#state ==='open'){
564+
this.#resolvePendingDrains(true);
565+
}
536566
};
567+
this.#broadcast[kOnEndDrained]=()=>this.#endDrained();
537568
}
538569

539570
// The drainable protocol works with Stream.ondrain to provide a notification
@@ -547,20 +578,12 @@ class BroadcastWriter {
547578
returnpromise;
548579
}
549580

550-
#isClosed(){
551-
returnthis.#closed !==undefined;
552-
}
553-
554-
#isClosedOrAborted(){
555-
returnthis.#isClosed()||this.#aborted;
556-
}
557-
558581
getcanWrite(){
559-
returnthis.#isClosedOrAborted() ? null :this.#broadcast[kCanWrite]();
582+
returnthis.#state ==='open' ?this.#broadcast[kCanWrite]() : null;
560583
}
561584

562585
#canUseWriteFastPath(signal){
563-
return!signal&&!this.#isClosed()&&!this.#aborted&&
586+
return!signal&&this.#state ==='open'&&
564587
this.#broadcast[kCanWrite]();
565588
}
566589

@@ -592,13 +615,15 @@ class BroadcastWriter {
592615
}
593616

594617
async #writevSlow(chunks,signal){
595-
// Check for pre-aborted
596-
signal?.throwIfAborted();
597-
598-
if(this.#isClosedOrAborted()){
618+
if(this.#state ==='errored'){
619+
throwthis.#error;
620+
}
621+
if(this.#state !=='open'){
599622
thrownewERR_INVALID_STATE.TypeError('Writer is closed');
600623
}
601624

625+
signal?.throwIfAborted();
626+
602627
constconverted=convertChunks(chunks);
603628

604629
if(this.#broadcast[kWrite](converted)){
@@ -624,7 +649,7 @@ class BroadcastWriter {
624649
}
625650

626651
writeSync(chunk){
627-
if(this.#isClosedOrAborted())returnfalse;
652+
if(this.#state !=='open')returnfalse;
628653
if(!this.#broadcast[kCanWrite]())returnfalse;
629654
constconverted=
630655
toUint8Array(chunk);
@@ -637,7 +662,7 @@ class BroadcastWriter {
637662

638663
writevSync(chunks){
639664
validateArray(chunks,'chunks');
640-
if(this.#isClosedOrAborted())returnfalse;
665+
if(this.#state !=='open')returnfalse;
641666
if(!this.#broadcast[kCanWrite]())returnfalse;
642667
constconverted=convertChunks(chunks);
643668
if(this.#broadcast[kWrite](converted)){
@@ -651,34 +676,43 @@ class BroadcastWriter {
651676

652677
end(options){
653678
constsignal=getWriterSignal(options);
679+
if(this.#state ==='errored')returnPromiseReject(this.#error);
680+
if(this.#state ==='closed')returnPromiseResolve(this.#totalBytes);
654681
if(signal?.aborted)returnPromiseReject(signal.reason);
655682

656-
if(this.#isClosed())returnthis.#closed;
657-
this.#closed =PromiseResolve(this.#totalBytes);
658-
this.#broadcast[kEnd]();
659-
this.#resolvePendingDrains(false);
660-
returnthis.#closed;
683+
constendPromise=this.#getEndPromise();
684+
if(this.#state ==='open'){
685+
this.#state ='closing';
686+
this.#resolvePendingDrains(false);
687+
this.#finishEndIfReady();
688+
}
689+
690+
returnraceEndWithSignal(endPromise,signal);
661691
}
662692

663693
endSync(){
664-
if(this.#closed)returnthis.#totalBytes;
665-
this.#closed =PromiseResolve(this.#totalBytes);
666-
this.#broadcast[kEnd]();
694+
if(this.#state ==='closed')returnthis.#totalBytes;
695+
if(this.#state ==='errored'||this.#state ==='closing')return-1;
696+
697+
this.#state ='closing';
667698
this.#resolvePendingDrains(false);
668-
returnthis.#totalBytes;
699+
this.#finishEndIfReady();
700+
returnthis.#state ==='closed' ? this.#totalBytes : -1;
669701
}
670702

671703
fail(reason){
672-
if(this.#isClosedOrAborted())return;
673-
this.#aborted =true;
674-
this.#closed =PromiseResolve(this.#totalBytes);
704+
if(this.#state ==='errored'||this.#state ==='closed')return;
705+
this.#state ='errored';
675706
consterror=reason??newERR_INVALID_STATE.TypeError('Failed');
707+
this.#error =error;
676708
this.#rejectPendingWrites(error);
677709
this.#rejectPendingDrains(error);
710+
this.#pendingEnd?.reject(error);
678711
this.#broadcast[kAbort](error);
679712
}
680713

681714
[SymbolAsyncDispose](){
715+
if(this.#state ==='closing')returnthis.#getEndPromise();
682716
this.fail();
683717
returnPromiseResolve();
684718
}
@@ -688,11 +722,33 @@ class BroadcastWriter {
688722
}
689723

690724
[kCancelWriter](){
691-
if(this.#isClosed())return;
692-
this.#closed=PromiseResolve(this.#totalBytes);
725+
if(this.#state ==='closed'||this.#state ==='errored')return;
726+
this.#state='closed';
693727
this.#rejectPendingWrites(
694728
lazyDOMException('Broadcast cancelled','AbortError'));
695729
this.#resolvePendingDrains(false);
730+
this.#pendingEnd?.resolve(this.#totalBytes);
731+
}
732+
733+
#getEndPromise(){
734+
this.#pendingEnd ??=PromiseWithResolvers();
735+
returnthis.#pendingEnd.promise;
736+
}
737+
738+
#finishEndIfReady(){
739+
if(this.#state ==='closing'&&this.#pendingWrites.length===0){
740+
this.#broadcast[kEnd]();
741+
}
742+
}
743+
744+
#endDrained(){
745+
if(this.#state !=='closing')return;
746+
this.#state ='closed';
747+
this.#pendingEnd?.resolve(this.#totalBytes);
748+
}
749+
750+
[kPendingWriteRemoved](){
751+
this.#finishEndIfReady();
696752
}
697753

698754
/**
@@ -724,6 +780,7 @@ class BroadcastWriter {
724780
break;
725781
}
726782
}
783+
this.#finishEndIfReady();
727784
}
728785

729786
#rejectPendingWrites(error){
@@ -756,6 +813,7 @@ function wireBroadcastWriteSignal(entry, signal, resolve, reject, self) {
756813
if(idx!==-1)pendingWrites.removeAt(idx);
757814
entry.chunk=null;
758815
reject(signal.reason??lazyDOMException('Aborted','AbortError'));
816+
if(idx!==-1)self[kPendingWriteRemoved]();
759817
};
760818
entry.resolve=function(){
761819
signal.removeEventListener('abort',onAbort);

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit dc51c79

Browse files
trivikraduh95
authored andcommitted
stream: drain pending writes before broadcast end
Keep processing writes that were queued before end(). Signal the broadcast end only after those writes enter the shared buffer. Resolve end() after all consumers reach end-of-stream. This prevents backpressured writes from remaining pending forever. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65334Fixes: #65333 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5fbf6c5 commit dc51c79

4 files changed

Lines changed: 258 additions & 50 deletions

File tree

β€Ždoc/api/stream_iter.mdβ€Ž

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,14 +431,16 @@ the write. Use [`ondrain()`][] to wait for capacity rather than polling.
431431
the pending `end()` call; it does not fail the writer itself.
432432
* Returns: {Promise} Fulfills with the total number of bytes written.
433433

434-
Signal that no more data will be written.
434+
Signals that no more data will be written and waits for buffered data to drain.
435435

436436
#### `writer.endSync()`
437437

438-
* Returns: {number} Total bytes written, or `-1` if the writer is not open.
438+
* Returns: {number} Total bytes written, or `-1` if ending cannot complete
439+
synchronously.
439440

440-
Synchronous variant of `writer.end()`. Returns `-1` if the writer is already
441-
closed or errored. Can be used as a try-fallback pattern:
441+
Synchronous variant of `writer.end()`. A return value of `-1` means closing has
442+
started but requires asynchronous draining. Use the try-fallback pattern to
443+
await completion:
442444

443445
```cjs
444446
constresult=writer.endSync();

β€Žlib/internal/streams/iter/broadcast.jsβ€Ž

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const {
1414
PromiseReject,
1515
PromiseResolve,
1616
PromiseWithResolvers,
17+
SafePromisePrototypeFinally,
18+
SafePromiseRace,
1719
SafeSet,
1820
Symbol,
1921
SymbolAsyncDispose,
@@ -77,6 +79,22 @@ const kEnd = Symbol('kEnd');
7779
constkAbort=Symbol('kAbort');
7880
constkCanWrite=Symbol('kCanWrite');
7981
constkOnBufferDrained=Symbol('kOnBufferDrained');
82+
constkOnEndDrained=Symbol('kOnEndDrained');
83+
constkPendingWriteRemoved=Symbol('kPendingWriteRemoved');
84+
85+
functionraceEndWithSignal(promise,signal){
86+
if(!signal)returnpromise;
87+
88+
const{promise: aborted, reject }=PromiseWithResolvers();
89+
constonAbort=()=>reject(signal.reason);
90+
signal.addEventListener('abort',onAbort,{__proto__: null,once: true});
91+
if(signal.aborted)onAbort();
92+
93+
returnSafePromisePrototypeFinally(
94+
SafePromiseRace([promise,aborted]),
95+
()=>signal.removeEventListener('abort',onAbort),
96+
);
97+
}
8098

8199
// =============================================================================
82100
// Broadcast Implementation
@@ -100,6 +118,7 @@ class BroadcastImpl {
100118
constructor(options){
101119
this.#options =options;
102120
this[kOnBufferDrained]=null;
121+
this[kOnEndDrained]=null;
103122
}
104123

105124
setWriter(writer){
@@ -183,6 +202,7 @@ class BroadcastImpl {
183202
if(self.#deleteConsumer(state)){
184203
self.#tryTrimBuffer();
185204
}
205+
self.#notifyEndDrained();
186206
}
187207

188208
return{
@@ -358,10 +378,11 @@ class BroadcastImpl {
358378
}
359379
}
360380
}
381+
this.#notifyEndDrained();
361382
}
362383

363384
[kAbort](reason){
364-
if(this.#ended ||this.#error !==undefined)return;
385+
if(this.#error !==undefined)return;
365386
this.#error =reason;
366387
this.#ended =true;
367388

@@ -396,6 +417,12 @@ class BroadcastImpl {
396417

397418
// Private methods
398419

420+
#notifyEndDrained(){
421+
if(this.#ended &&this.#consumers.size===0){
422+
this[kOnEndDrained]?.();
423+
}
424+
}
425+
399426
#recomputeMinCursor(){
400427
const{ minCursor, minCursorConsumers }=getMinCursor(
401428
this.#consumers,this.#bufferStart +this.#buffer.length);
@@ -516,8 +543,9 @@ let getBroadcastPendingWrites;
516543
classBroadcastWriter{
517544
#broadcast;
518545
#totalBytes =0;
519-
#closed;
520-
#aborted =false;
546+
#state ='open';
547+
#error;
548+
#pendingEnd;
521549
#pendingWrites =newRingBuffer();
522550
#pendingDrains =[];
523551

@@ -532,8 +560,11 @@ class BroadcastWriter {
532560

533561
this.#broadcast[kOnBufferDrained]=()=>{
534562
this.#resolvePendingWrites();
535-
this.#resolvePendingDrains(true);
563+
if(this.#state ==='open'){
564+
this.#resolvePendingDrains(true);
565+
}
536566
};
567+
this.#broadcast[kOnEndDrained]=()=>this.#endDrained();
537568
}
538569

539570
// The drainable protocol works with Stream.ondrain to provide a notification
@@ -547,20 +578,12 @@ class BroadcastWriter {
547578
returnpromise;
548579
}
549580

550-
#isClosed(){
551-
returnthis.#closed !==undefined;
552-
}
553-
554-
#isClosedOrAborted(){
555-
returnthis.#isClosed()||this.#aborted;
556-
}
557-
558581
getcanWrite(){
559-
returnthis.#isClosedOrAborted() ? null :this.#broadcast[kCanWrite]();
582+
returnthis.#state ==='open' ?this.#broadcast[kCanWrite]() : null;
560583
}
561584

562585
#canUseWriteFastPath(signal){
563-
return!signal&&!this.#isClosed()&&!this.#aborted&&
586+
return!signal&&this.#state ==='open'&&
564587
this.#broadcast[kCanWrite]();
565588
}
566589

@@ -592,13 +615,15 @@ class BroadcastWriter {
592615
}
593616

594617
async #writevSlow(chunks,signal){
595-
// Check for pre-aborted
596-
signal?.throwIfAborted();
597-
598-
if(this.#isClosedOrAborted()){
618+
if(this.#state ==='errored'){
619+
throwthis.#error;
620+
}
621+
if(this.#state !=='open'){
599622
thrownewERR_INVALID_STATE.TypeError('Writer is closed');
600623
}
601624

625+
signal?.throwIfAborted();
626+
602627
constconverted=convertChunks(chunks);
603628

604629
if(this.#broadcast[kWrite](converted)){
@@ -624,7 +649,7 @@ class BroadcastWriter {
624649
}
625650

626651
writeSync(chunk){
627-
if(this.#isClosedOrAborted())returnfalse;
652+
if(this.#state !=='open')returnfalse;
628653
if(!this.#broadcast[kCanWrite]())returnfalse;
629654
constconverted=
630655
toUint8Array(chunk);
@@ -637,7 +662,7 @@ class BroadcastWriter {
637662

638663
writevSync(chunks){
639664
validateArray(chunks,'chunks');
640-
if(this.#isClosedOrAborted())returnfalse;
665+
if(this.#state !=='open')returnfalse;
641666
if(!this.#broadcast[kCanWrite]())returnfalse;
642667
constconverted=convertChunks(chunks);
643668
if(this.#broadcast[kWrite](converted)){
@@ -651,34 +676,43 @@ class BroadcastWriter {
651676

652677
end(options){
653678
constsignal=getWriterSignal(options);
679+
if(this.#state ==='errored')returnPromiseReject(this.#error);
680+
if(this.#state ==='closed')returnPromiseResolve(this.#totalBytes);
654681
if(signal?.aborted)returnPromiseReject(signal.reason);
655682

656-
if(this.#isClosed())returnthis.#closed;
657-
this.#closed =PromiseResolve(this.#totalBytes);
658-
this.#broadcast[kEnd]();
659-
this.#resolvePendingDrains(false);
660-
returnthis.#closed;
683+
constendPromise=this.#getEndPromise();
684+
if(this.#state ==='open'){
685+
this.#state ='closing';
686+
this.#resolvePendingDrains(false);
687+
this.#finishEndIfReady();
688+
}
689+
690+
returnraceEndWithSignal(endPromise,signal);
661691
}
662692

663693
endSync(){
664-
if(this.#closed)returnthis.#totalBytes;
665-
this.#closed =PromiseResolve(this.#totalBytes);
666-
this.#broadcast[kEnd]();
694+
if(this.#state ==='closed')returnthis.#totalBytes;
695+
if(this.#state ==='errored'||this.#state ==='closing')return-1;
696+
697+
this.#state ='closing';
667698
this.#resolvePendingDrains(false);
668-
returnthis.#totalBytes;
699+
this.#finishEndIfReady();
700+
returnthis.#state ==='closed' ? this.#totalBytes : -1;
669701
}
670702

671703
fail(reason){
672-
if(this.#isClosedOrAborted())return;
673-
this.#aborted =true;
674-
this.#closed =PromiseResolve(this.#totalBytes);
704+
if(this.#state ==='errored'||this.#state ==='closed')return;
705+
this.#state ='errored';
675706
consterror=reason??newERR_INVALID_STATE.TypeError('Failed');
707+
this.#error =error;
676708
this.#rejectPendingWrites(error);
677709
this.#rejectPendingDrains(error);
710+
this.#pendingEnd?.reject(error);
678711
this.#broadcast[kAbort](error);
679712
}
680713

681714
[SymbolAsyncDispose](){
715+
if(this.#state ==='closing')returnthis.#getEndPromise();
682716
this.fail();
683717
returnPromiseResolve();
684718
}
@@ -688,11 +722,33 @@ class BroadcastWriter {
688722
}
689723

690724
[kCancelWriter](){
691-
if(this.#isClosed())return;
692-
this.#closed=PromiseResolve(this.#totalBytes);
725+
if(this.#state ==='closed'||this.#state ==='errored')return;
726+
this.#state='closed';
693727
this.#rejectPendingWrites(
694728
lazyDOMException('Broadcast cancelled','AbortError'));
695729
this.#resolvePendingDrains(false);
730+
this.#pendingEnd?.resolve(this.#totalBytes);
731+
}
732+
733+
#getEndPromise(){
734+
this.#pendingEnd ??=PromiseWithResolvers();
735+
returnthis.#pendingEnd.promise;
736+
}
737+
738+
#finishEndIfReady(){
739+
if(this.#state ==='closing'&&this.#pendingWrites.length===0){
740+
this.#broadcast[kEnd]();
741+
}
742+
}
743+
744+
#endDrained(){
745+
if(this.#state !=='closing')return;
746+
this.#state ='closed';
747+
this.#pendingEnd?.resolve(this.#totalBytes);
748+
}
749+
750+
[kPendingWriteRemoved](){
751+
this.#finishEndIfReady();
696752
}
697753

698754
/**
@@ -724,6 +780,7 @@ class BroadcastWriter {
724780
break;
725781
}
726782
}
783+
this.#finishEndIfReady();
727784
}
728785

729786
#rejectPendingWrites(error){
@@ -756,6 +813,7 @@ function wireBroadcastWriteSignal(entry, signal, resolve, reject, self) {
756813
if(idx!==-1)pendingWrites.removeAt(idx);
757814
entry.chunk=null;
758815
reject(signal.reason??lazyDOMException('Aborted','AbortError'));
816+
if(idx!==-1)self[kPendingWriteRemoved]();
759817
};
760818
entry.resolve=function(){
761819
signal.removeEventListener('abort',onAbort);

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Commit dc51c79

Browse files
trivikraduh95
authored andcommitted
stream: drain pending writes before broadcast end
Keep processing writes that were queued before end(). Signal the broadcast end only after those writes enter the shared buffer. Resolve end() after all consumers reach end-of-stream. This prevents backpressured writes from remaining pending forever. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65334Fixes: #65333 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5fbf6c5 commit dc51c79

4 files changed

Lines changed: 258 additions & 50 deletions

File tree

β€Ždoc/api/stream_iter.mdβ€Ž

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,14 +431,16 @@ the write. Use [`ondrain()`][] to wait for capacity rather than polling.
431431
the pending `end()` call; it does not fail the writer itself.
432432
* Returns: {Promise} Fulfills with the total number of bytes written.
433433

434-
Signal that no more data will be written.
434+
Signals that no more data will be written and waits for buffered data to drain.
435435

436436
#### `writer.endSync()`
437437

438-
* Returns: {number} Total bytes written, or `-1` if the writer is not open.
438+
* Returns: {number} Total bytes written, or `-1` if ending cannot complete
439+
synchronously.
439440

440-
Synchronous variant of `writer.end()`. Returns `-1` if the writer is already
441-
closed or errored. Can be used as a try-fallback pattern:
441+
Synchronous variant of `writer.end()`. A return value of `-1` means closing has
442+
started but requires asynchronous draining. Use the try-fallback pattern to
443+
await completion:
442444

443445
```cjs
444446
constresult=writer.endSync();

β€Žlib/internal/streams/iter/broadcast.jsβ€Ž

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const {
1414
PromiseReject,
1515
PromiseResolve,
1616
PromiseWithResolvers,
17+
SafePromisePrototypeFinally,
18+
SafePromiseRace,
1719
SafeSet,
1820
Symbol,
1921
SymbolAsyncDispose,
@@ -77,6 +79,22 @@ const kEnd = Symbol('kEnd');
7779
constkAbort=Symbol('kAbort');
7880
constkCanWrite=Symbol('kCanWrite');
7981
constkOnBufferDrained=Symbol('kOnBufferDrained');
82+
constkOnEndDrained=Symbol('kOnEndDrained');
83+
constkPendingWriteRemoved=Symbol('kPendingWriteRemoved');
84+
85+
functionraceEndWithSignal(promise,signal){
86+
if(!signal)returnpromise;
87+
88+
const{promise: aborted, reject }=PromiseWithResolvers();
89+
constonAbort=()=>reject(signal.reason);
90+
signal.addEventListener('abort',onAbort,{__proto__: null,once: true});
91+
if(signal.aborted)onAbort();
92+
93+
returnSafePromisePrototypeFinally(
94+
SafePromiseRace([promise,aborted]),
95+
()=>signal.removeEventListener('abort',onAbort),
96+
);
97+
}
8098

8199
// =============================================================================
82100
// Broadcast Implementation
@@ -100,6 +118,7 @@ class BroadcastImpl {
100118
constructor(options){
101119
this.#options =options;
102120
this[kOnBufferDrained]=null;
121+
this[kOnEndDrained]=null;
103122
}
104123

105124
setWriter(writer){
@@ -183,6 +202,7 @@ class BroadcastImpl {
183202
if(self.#deleteConsumer(state)){
184203
self.#tryTrimBuffer();
185204
}
205+
self.#notifyEndDrained();
186206
}
187207

188208
return{
@@ -358,10 +378,11 @@ class BroadcastImpl {
358378
}
359379
}
360380
}
381+
this.#notifyEndDrained();
361382
}
362383

363384
[kAbort](reason){
364-
if(this.#ended ||this.#error !==undefined)return;
385+
if(this.#error !==undefined)return;
365386
this.#error =reason;
366387
this.#ended =true;
367388

@@ -396,6 +417,12 @@ class BroadcastImpl {
396417

397418
// Private methods
398419

420+
#notifyEndDrained(){
421+
if(this.#ended &&this.#consumers.size===0){
422+
this[kOnEndDrained]?.();
423+
}
424+
}
425+
399426
#recomputeMinCursor(){
400427
const{ minCursor, minCursorConsumers }=getMinCursor(
401428
this.#consumers,this.#bufferStart +this.#buffer.length);
@@ -516,8 +543,9 @@ let getBroadcastPendingWrites;
516543
classBroadcastWriter{
517544
#broadcast;
518545
#totalBytes =0;
519-
#closed;
520-
#aborted =false;
546+
#state ='open';
547+
#error;
548+
#pendingEnd;
521549
#pendingWrites =newRingBuffer();
522550
#pendingDrains =[];
523551

@@ -532,8 +560,11 @@ class BroadcastWriter {
532560

533561
this.#broadcast[kOnBufferDrained]=()=>{
534562
this.#resolvePendingWrites();
535-
this.#resolvePendingDrains(true);
563+
if(this.#state ==='open'){
564+
this.#resolvePendingDrains(true);
565+
}
536566
};
567+
this.#broadcast[kOnEndDrained]=()=>this.#endDrained();
537568
}
538569

539570
// The drainable protocol works with Stream.ondrain to provide a notification
@@ -547,20 +578,12 @@ class BroadcastWriter {
547578
returnpromise;
548579
}
549580

550-
#isClosed(){
551-
returnthis.#closed !==undefined;
552-
}
553-
554-
#isClosedOrAborted(){
555-
returnthis.#isClosed()||this.#aborted;
556-
}
557-
558581
getcanWrite(){
559-
returnthis.#isClosedOrAborted() ? null :this.#broadcast[kCanWrite]();
582+
returnthis.#state ==='open' ?this.#broadcast[kCanWrite]() : null;
560583
}
561584

562585
#canUseWriteFastPath(signal){
563-
return!signal&&!this.#isClosed()&&!this.#aborted&&
586+
return!signal&&this.#state ==='open'&&
564587
this.#broadcast[kCanWrite]();
565588
}
566589

@@ -592,13 +615,15 @@ class BroadcastWriter {
592615
}
593616

594617
async #writevSlow(chunks,signal){
595-
// Check for pre-aborted
596-
signal?.throwIfAborted();
597-
598-
if(this.#isClosedOrAborted()){
618+
if(this.#state ==='errored'){
619+
throwthis.#error;
620+
}
621+
if(this.#state !=='open'){
599622
thrownewERR_INVALID_STATE.TypeError('Writer is closed');
600623
}
601624

625+
signal?.throwIfAborted();
626+
602627
constconverted=convertChunks(chunks);
603628

604629
if(this.#broadcast[kWrite](converted)){
@@ -624,7 +649,7 @@ class BroadcastWriter {
624649
}
625650

626651
writeSync(chunk){
627-
if(this.#isClosedOrAborted())returnfalse;
652+
if(this.#state !=='open')returnfalse;
628653
if(!this.#broadcast[kCanWrite]())returnfalse;
629654
constconverted=
630655
toUint8Array(chunk);
@@ -637,7 +662,7 @@ class BroadcastWriter {
637662

638663
writevSync(chunks){
639664
validateArray(chunks,'chunks');
640-
if(this.#isClosedOrAborted())returnfalse;
665+
if(this.#state !=='open')returnfalse;
641666
if(!this.#broadcast[kCanWrite]())returnfalse;
642667
constconverted=convertChunks(chunks);
643668
if(this.#broadcast[kWrite](converted)){
@@ -651,34 +676,43 @@ class BroadcastWriter {
651676

652677
end(options){
653678
constsignal=getWriterSignal(options);
679+
if(this.#state ==='errored')returnPromiseReject(this.#error);
680+
if(this.#state ==='closed')returnPromiseResolve(this.#totalBytes);
654681
if(signal?.aborted)returnPromiseReject(signal.reason);
655682

656-
if(this.#isClosed())returnthis.#closed;
657-
this.#closed =PromiseResolve(this.#totalBytes);
658-
this.#broadcast[kEnd]();
659-
this.#resolvePendingDrains(false);
660-
returnthis.#closed;
683+
constendPromise=this.#getEndPromise();
684+
if(this.#state ==='open'){
685+
this.#state ='closing';
686+
this.#resolvePendingDrains(false);
687+
this.#finishEndIfReady();
688+
}
689+
690+
returnraceEndWithSignal(endPromise,signal);
661691
}
662692

663693
endSync(){
664-
if(this.#closed)returnthis.#totalBytes;
665-
this.#closed =PromiseResolve(this.#totalBytes);
666-
this.#broadcast[kEnd]();
694+
if(this.#state ==='closed')returnthis.#totalBytes;
695+
if(this.#state ==='errored'||this.#state ==='closing')return-1;
696+
697+
this.#state ='closing';
667698
this.#resolvePendingDrains(false);
668-
returnthis.#totalBytes;
699+
this.#finishEndIfReady();
700+
returnthis.#state ==='closed' ? this.#totalBytes : -1;
669701
}
670702

671703
fail(reason){
672-
if(this.#isClosedOrAborted())return;
673-
this.#aborted =true;
674-
this.#closed =PromiseResolve(this.#totalBytes);
704+
if(this.#state ==='errored'||this.#state ==='closed')return;
705+
this.#state ='errored';
675706
consterror=reason??newERR_INVALID_STATE.TypeError('Failed');
707+
this.#error =error;
676708
this.#rejectPendingWrites(error);
677709
this.#rejectPendingDrains(error);
710+
this.#pendingEnd?.reject(error);
678711
this.#broadcast[kAbort](error);
679712
}
680713

681714
[SymbolAsyncDispose](){
715+
if(this.#state ==='closing')returnthis.#getEndPromise();
682716
this.fail();
683717
returnPromiseResolve();
684718
}
@@ -688,11 +722,33 @@ class BroadcastWriter {
688722
}
689723

690724
[kCancelWriter](){
691-
if(this.#isClosed())return;
692-
this.#closed=PromiseResolve(this.#totalBytes);
725+
if(this.#state ==='closed'||this.#state ==='errored')return;
726+
this.#state='closed';
693727
this.#rejectPendingWrites(
694728
lazyDOMException('Broadcast cancelled','AbortError'));
695729
this.#resolvePendingDrains(false);
730+
this.#pendingEnd?.resolve(this.#totalBytes);
731+
}
732+
733+
#getEndPromise(){
734+
this.#pendingEnd ??=PromiseWithResolvers();
735+
returnthis.#pendingEnd.promise;
736+
}
737+
738+
#finishEndIfReady(){
739+
if(this.#state ==='closing'&&this.#pendingWrites.length===0){
740+
this.#broadcast[kEnd]();
741+
}
742+
}
743+
744+
#endDrained(){
745+
if(this.#state !=='closing')return;
746+
this.#state ='closed';
747+
this.#pendingEnd?.resolve(this.#totalBytes);
748+
}
749+
750+
[kPendingWriteRemoved](){
751+
this.#finishEndIfReady();
696752
}
697753

698754
/**
@@ -724,6 +780,7 @@ class BroadcastWriter {
724780
break;
725781
}
726782
}
783+
this.#finishEndIfReady();
727784
}
728785

729786
#rejectPendingWrites(error){
@@ -756,6 +813,7 @@ function wireBroadcastWriteSignal(entry, signal, resolve, reject, self) {
756813
if(idx!==-1)pendingWrites.removeAt(idx);
757814
entry.chunk=null;
758815
reject(signal.reason??lazyDOMException('Aborted','AbortError'));
816+
if(idx!==-1)self[kPendingWriteRemoved]();
759817
};
760818
entry.resolve=function(){
761819
signal.removeEventListener('abort',onAbort);

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit dc51c79

Browse files
trivikraduh95
authored andcommitted
stream: drain pending writes before broadcast end
Keep processing writes that were queued before end(). Signal the broadcast end only after those writes enter the shared buffer. Resolve end() after all consumers reach end-of-stream. This prevents backpressured writes from remaining pending forever. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65334Fixes: #65333 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5fbf6c5 commit dc51c79

4 files changed

Lines changed: 258 additions & 50 deletions

File tree

β€Ždoc/api/stream_iter.mdβ€Ž

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,14 +431,16 @@ the write. Use [`ondrain()`][] to wait for capacity rather than polling.
431431
the pending `end()` call; it does not fail the writer itself.
432432
* Returns: {Promise} Fulfills with the total number of bytes written.
433433

434-
Signal that no more data will be written.
434+
Signals that no more data will be written and waits for buffered data to drain.
435435

436436
#### `writer.endSync()`
437437

438-
* Returns: {number} Total bytes written, or `-1` if the writer is not open.
438+
* Returns: {number} Total bytes written, or `-1` if ending cannot complete
439+
synchronously.
439440

440-
Synchronous variant of `writer.end()`. Returns `-1` if the writer is already
441-
closed or errored. Can be used as a try-fallback pattern:
441+
Synchronous variant of `writer.end()`. A return value of `-1` means closing has
442+
started but requires asynchronous draining. Use the try-fallback pattern to
443+
await completion:
442444

443445
```cjs
444446
constresult=writer.endSync();

β€Žlib/internal/streams/iter/broadcast.jsβ€Ž

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const {
1414
PromiseReject,
1515
PromiseResolve,
1616
PromiseWithResolvers,
17+
SafePromisePrototypeFinally,
18+
SafePromiseRace,
1719
SafeSet,
1820
Symbol,
1921
SymbolAsyncDispose,
@@ -77,6 +79,22 @@ const kEnd = Symbol('kEnd');
7779
constkAbort=Symbol('kAbort');
7880
constkCanWrite=Symbol('kCanWrite');
7981
constkOnBufferDrained=Symbol('kOnBufferDrained');
82+
constkOnEndDrained=Symbol('kOnEndDrained');
83+
constkPendingWriteRemoved=Symbol('kPendingWriteRemoved');
84+
85+
functionraceEndWithSignal(promise,signal){
86+
if(!signal)returnpromise;
87+
88+
const{promise: aborted, reject }=PromiseWithResolvers();
89+
constonAbort=()=>reject(signal.reason);
90+
signal.addEventListener('abort',onAbort,{__proto__: null,once: true});
91+
if(signal.aborted)onAbort();
92+
93+
returnSafePromisePrototypeFinally(
94+
SafePromiseRace([promise,aborted]),
95+
()=>signal.removeEventListener('abort',onAbort),
96+
);
97+
}
8098

8199
// =============================================================================
82100
// Broadcast Implementation
@@ -100,6 +118,7 @@ class BroadcastImpl {
100118
constructor(options){
101119
this.#options =options;
102120
this[kOnBufferDrained]=null;
121+
this[kOnEndDrained]=null;
103122
}
104123

105124
setWriter(writer){
@@ -183,6 +202,7 @@ class BroadcastImpl {
183202
if(self.#deleteConsumer(state)){
184203
self.#tryTrimBuffer();
185204
}
205+
self.#notifyEndDrained();
186206
}
187207

188208
return{
@@ -358,10 +378,11 @@ class BroadcastImpl {
358378
}
359379
}
360380
}
381+
this.#notifyEndDrained();
361382
}
362383

363384
[kAbort](reason){
364-
if(this.#ended ||this.#error !==undefined)return;
385+
if(this.#error !==undefined)return;
365386
this.#error =reason;
366387
this.#ended =true;
367388

@@ -396,6 +417,12 @@ class BroadcastImpl {
396417

397418
// Private methods
398419

420+
#notifyEndDrained(){
421+
if(this.#ended &&this.#consumers.size===0){
422+
this[kOnEndDrained]?.();
423+
}
424+
}
425+
399426
#recomputeMinCursor(){
400427
const{ minCursor, minCursorConsumers }=getMinCursor(
401428
this.#consumers,this.#bufferStart +this.#buffer.length);
@@ -516,8 +543,9 @@ let getBroadcastPendingWrites;
516543
classBroadcastWriter{
517544
#broadcast;
518545
#totalBytes =0;
519-
#closed;
520-
#aborted =false;
546+
#state ='open';
547+
#error;
548+
#pendingEnd;
521549
#pendingWrites =newRingBuffer();
522550
#pendingDrains =[];
523551

@@ -532,8 +560,11 @@ class BroadcastWriter {
532560

533561
this.#broadcast[kOnBufferDrained]=()=>{
534562
this.#resolvePendingWrites();
535-
this.#resolvePendingDrains(true);
563+
if(this.#state ==='open'){
564+
this.#resolvePendingDrains(true);
565+
}
536566
};
567+
this.#broadcast[kOnEndDrained]=()=>this.#endDrained();
537568
}
538569

539570
// The drainable protocol works with Stream.ondrain to provide a notification
@@ -547,20 +578,12 @@ class BroadcastWriter {
547578
returnpromise;
548579
}
549580

550-
#isClosed(){
551-
returnthis.#closed !==undefined;
552-
}
553-
554-
#isClosedOrAborted(){
555-
returnthis.#isClosed()||this.#aborted;
556-
}
557-
558581
getcanWrite(){
559-
returnthis.#isClosedOrAborted() ? null :this.#broadcast[kCanWrite]();
582+
returnthis.#state ==='open' ?this.#broadcast[kCanWrite]() : null;
560583
}
561584

562585
#canUseWriteFastPath(signal){
563-
return!signal&&!this.#isClosed()&&!this.#aborted&&
586+
return!signal&&this.#state ==='open'&&
564587
this.#broadcast[kCanWrite]();
565588
}
566589

@@ -592,13 +615,15 @@ class BroadcastWriter {
592615
}
593616

594617
async #writevSlow(chunks,signal){
595-
// Check for pre-aborted
596-
signal?.throwIfAborted();
597-
598-
if(this.#isClosedOrAborted()){
618+
if(this.#state ==='errored'){
619+
throwthis.#error;
620+
}
621+
if(this.#state !=='open'){
599622
thrownewERR_INVALID_STATE.TypeError('Writer is closed');
600623
}
601624

625+
signal?.throwIfAborted();
626+
602627
constconverted=convertChunks(chunks);
603628

604629
if(this.#broadcast[kWrite](converted)){
@@ -624,7 +649,7 @@ class BroadcastWriter {
624649
}
625650

626651
writeSync(chunk){
627-
if(this.#isClosedOrAborted())returnfalse;
652+
if(this.#state !=='open')returnfalse;
628653
if(!this.#broadcast[kCanWrite]())returnfalse;
629654
constconverted=
630655
toUint8Array(chunk);
@@ -637,7 +662,7 @@ class BroadcastWriter {
637662

638663
writevSync(chunks){
639664
validateArray(chunks,'chunks');
640-
if(this.#isClosedOrAborted())returnfalse;
665+
if(this.#state !=='open')returnfalse;
641666
if(!this.#broadcast[kCanWrite]())returnfalse;
642667
constconverted=convertChunks(chunks);
643668
if(this.#broadcast[kWrite](converted)){
@@ -651,34 +676,43 @@ class BroadcastWriter {
651676

652677
end(options){
653678
constsignal=getWriterSignal(options);
679+
if(this.#state ==='errored')returnPromiseReject(this.#error);
680+
if(this.#state ==='closed')returnPromiseResolve(this.#totalBytes);
654681
if(signal?.aborted)returnPromiseReject(signal.reason);
655682

656-
if(this.#isClosed())returnthis.#closed;
657-
this.#closed =PromiseResolve(this.#totalBytes);
658-
this.#broadcast[kEnd]();
659-
this.#resolvePendingDrains(false);
660-
returnthis.#closed;
683+
constendPromise=this.#getEndPromise();
684+
if(this.#state ==='open'){
685+
this.#state ='closing';
686+
this.#resolvePendingDrains(false);
687+
this.#finishEndIfReady();
688+
}
689+
690+
returnraceEndWithSignal(endPromise,signal);
661691
}
662692

663693
endSync(){
664-
if(this.#closed)returnthis.#totalBytes;
665-
this.#closed =PromiseResolve(this.#totalBytes);
666-
this.#broadcast[kEnd]();
694+
if(this.#state ==='closed')returnthis.#totalBytes;
695+
if(this.#state ==='errored'||this.#state ==='closing')return-1;
696+
697+
this.#state ='closing';
667698
this.#resolvePendingDrains(false);
668-
returnthis.#totalBytes;
699+
this.#finishEndIfReady();
700+
returnthis.#state ==='closed' ? this.#totalBytes : -1;
669701
}
670702

671703
fail(reason){
672-
if(this.#isClosedOrAborted())return;
673-
this.#aborted =true;
674-
this.#closed =PromiseResolve(this.#totalBytes);
704+
if(this.#state ==='errored'||this.#state ==='closed')return;
705+
this.#state ='errored';
675706
consterror=reason??newERR_INVALID_STATE.TypeError('Failed');
707+
this.#error =error;
676708
this.#rejectPendingWrites(error);
677709
this.#rejectPendingDrains(error);
710+
this.#pendingEnd?.reject(error);
678711
this.#broadcast[kAbort](error);
679712
}
680713

681714
[SymbolAsyncDispose](){
715+
if(this.#state ==='closing')returnthis.#getEndPromise();
682716
this.fail();
683717
returnPromiseResolve();
684718
}
@@ -688,11 +722,33 @@ class BroadcastWriter {
688722
}
689723

690724
[kCancelWriter](){
691-
if(this.#isClosed())return;
692-
this.#closed=PromiseResolve(this.#totalBytes);
725+
if(this.#state ==='closed'||this.#state ==='errored')return;
726+
this.#state='closed';
693727
this.#rejectPendingWrites(
694728
lazyDOMException('Broadcast cancelled','AbortError'));
695729
this.#resolvePendingDrains(false);
730+
this.#pendingEnd?.resolve(this.#totalBytes);
731+
}
732+
733+
#getEndPromise(){
734+
this.#pendingEnd ??=PromiseWithResolvers();
735+
returnthis.#pendingEnd.promise;
736+
}
737+
738+
#finishEndIfReady(){
739+
if(this.#state ==='closing'&&this.#pendingWrites.length===0){
740+
this.#broadcast[kEnd]();
741+
}
742+
}
743+
744+
#endDrained(){
745+
if(this.#state !=='closing')return;
746+
this.#state ='closed';
747+
this.#pendingEnd?.resolve(this.#totalBytes);
748+
}
749+
750+
[kPendingWriteRemoved](){
751+
this.#finishEndIfReady();
696752
}
697753

698754
/**
@@ -724,6 +780,7 @@ class BroadcastWriter {
724780
break;
725781
}
726782
}
783+
this.#finishEndIfReady();
727784
}
728785

729786
#rejectPendingWrites(error){
@@ -756,6 +813,7 @@ function wireBroadcastWriteSignal(entry, signal, resolve, reject, self) {
756813
if(idx!==-1)pendingWrites.removeAt(idx);
757814
entry.chunk=null;
758815
reject(signal.reason??lazyDOMException('Aborted','AbortError'));
816+
if(idx!==-1)self[kPendingWriteRemoved]();
759817
};
760818
entry.resolve=function(){
761819
signal.removeEventListener('abort',onAbort);

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit dc51c79

Browse files
trivikraduh95
authored andcommitted
stream: drain pending writes before broadcast end
Keep processing writes that were queued before end(). Signal the broadcast end only after those writes enter the shared buffer. Resolve end() after all consumers reach end-of-stream. This prevents backpressured writes from remaining pending forever. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65334Fixes: #65333 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5fbf6c5 commit dc51c79

4 files changed

Lines changed: 258 additions & 50 deletions

File tree

β€Ždoc/api/stream_iter.mdβ€Ž

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,14 +431,16 @@ the write. Use [`ondrain()`][] to wait for capacity rather than polling.
431431
the pending `end()` call; it does not fail the writer itself.
432432
* Returns: {Promise} Fulfills with the total number of bytes written.
433433

434-
Signal that no more data will be written.
434+
Signals that no more data will be written and waits for buffered data to drain.
435435

436436
#### `writer.endSync()`
437437

438-
* Returns: {number} Total bytes written, or `-1` if the writer is not open.
438+
* Returns: {number} Total bytes written, or `-1` if ending cannot complete
439+
synchronously.
439440

440-
Synchronous variant of `writer.end()`. Returns `-1` if the writer is already
441-
closed or errored. Can be used as a try-fallback pattern:
441+
Synchronous variant of `writer.end()`. A return value of `-1` means closing has
442+
started but requires asynchronous draining. Use the try-fallback pattern to
443+
await completion:
442444

443445
```cjs
444446
constresult=writer.endSync();

β€Žlib/internal/streams/iter/broadcast.jsβ€Ž

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const {
1414
PromiseReject,
1515
PromiseResolve,
1616
PromiseWithResolvers,
17+
SafePromisePrototypeFinally,
18+
SafePromiseRace,
1719
SafeSet,
1820
Symbol,
1921
SymbolAsyncDispose,
@@ -77,6 +79,22 @@ const kEnd = Symbol('kEnd');
7779
constkAbort=Symbol('kAbort');
7880
constkCanWrite=Symbol('kCanWrite');
7981
constkOnBufferDrained=Symbol('kOnBufferDrained');
82+
constkOnEndDrained=Symbol('kOnEndDrained');
83+
constkPendingWriteRemoved=Symbol('kPendingWriteRemoved');
84+
85+
functionraceEndWithSignal(promise,signal){
86+
if(!signal)returnpromise;
87+
88+
const{promise: aborted, reject }=PromiseWithResolvers();
89+
constonAbort=()=>reject(signal.reason);
90+
signal.addEventListener('abort',onAbort,{__proto__: null,once: true});
91+
if(signal.aborted)onAbort();
92+
93+
returnSafePromisePrototypeFinally(
94+
SafePromiseRace([promise,aborted]),
95+
()=>signal.removeEventListener('abort',onAbort),
96+
);
97+
}
8098

8199
// =============================================================================
82100
// Broadcast Implementation
@@ -100,6 +118,7 @@ class BroadcastImpl {
100118
constructor(options){
101119
this.#options =options;
102120
this[kOnBufferDrained]=null;
121+
this[kOnEndDrained]=null;
103122
}
104123

105124
setWriter(writer){
@@ -183,6 +202,7 @@ class BroadcastImpl {
183202
if(self.#deleteConsumer(state)){
184203
self.#tryTrimBuffer();
185204
}
205+
self.#notifyEndDrained();
186206
}
187207

188208
return{
@@ -358,10 +378,11 @@ class BroadcastImpl {
358378
}
359379
}
360380
}
381+
this.#notifyEndDrained();
361382
}
362383

363384
[kAbort](reason){
364-
if(this.#ended ||this.#error !==undefined)return;
385+
if(this.#error !==undefined)return;
365386
this.#error =reason;
366387
this.#ended =true;
367388

@@ -396,6 +417,12 @@ class BroadcastImpl {
396417

397418
// Private methods
398419

420+
#notifyEndDrained(){
421+
if(this.#ended &&this.#consumers.size===0){
422+
this[kOnEndDrained]?.();
423+
}
424+
}
425+
399426
#recomputeMinCursor(){
400427
const{ minCursor, minCursorConsumers }=getMinCursor(
401428
this.#consumers,this.#bufferStart +this.#buffer.length);
@@ -516,8 +543,9 @@ let getBroadcastPendingWrites;
516543
classBroadcastWriter{
517544
#broadcast;
518545
#totalBytes =0;
519-
#closed;
520-
#aborted =false;
546+
#state ='open';
547+
#error;
548+
#pendingEnd;
521549
#pendingWrites =newRingBuffer();
522550
#pendingDrains =[];
523551

@@ -532,8 +560,11 @@ class BroadcastWriter {
532560

533561
this.#broadcast[kOnBufferDrained]=()=>{
534562
this.#resolvePendingWrites();
535-
this.#resolvePendingDrains(true);
563+
if(this.#state ==='open'){
564+
this.#resolvePendingDrains(true);
565+
}
536566
};
567+
this.#broadcast[kOnEndDrained]=()=>this.#endDrained();
537568
}
538569

539570
// The drainable protocol works with Stream.ondrain to provide a notification
@@ -547,20 +578,12 @@ class BroadcastWriter {
547578
returnpromise;
548579
}
549580

550-
#isClosed(){
551-
returnthis.#closed !==undefined;
552-
}
553-
554-
#isClosedOrAborted(){
555-
returnthis.#isClosed()||this.#aborted;
556-
}
557-
558581
getcanWrite(){
559-
returnthis.#isClosedOrAborted() ? null :this.#broadcast[kCanWrite]();
582+
returnthis.#state ==='open' ?this.#broadcast[kCanWrite]() : null;
560583
}
561584

562585
#canUseWriteFastPath(signal){
563-
return!signal&&!this.#isClosed()&&!this.#aborted&&
586+
return!signal&&this.#state ==='open'&&
564587
this.#broadcast[kCanWrite]();
565588
}
566589

@@ -592,13 +615,15 @@ class BroadcastWriter {
592615
}
593616

594617
async #writevSlow(chunks,signal){
595-
// Check for pre-aborted
596-
signal?.throwIfAborted();
597-
598-
if(this.#isClosedOrAborted()){
618+
if(this.#state ==='errored'){
619+
throwthis.#error;
620+
}
621+
if(this.#state !=='open'){
599622
thrownewERR_INVALID_STATE.TypeError('Writer is closed');
600623
}
601624

625+
signal?.throwIfAborted();
626+
602627
constconverted=convertChunks(chunks);
603628

604629
if(this.#broadcast[kWrite](converted)){
@@ -624,7 +649,7 @@ class BroadcastWriter {
624649
}
625650

626651
writeSync(chunk){
627-
if(this.#isClosedOrAborted())returnfalse;
652+
if(this.#state !=='open')returnfalse;
628653
if(!this.#broadcast[kCanWrite]())returnfalse;
629654
constconverted=
630655
toUint8Array(chunk);
@@ -637,7 +662,7 @@ class BroadcastWriter {
637662

638663
writevSync(chunks){
639664
validateArray(chunks,'chunks');
640-
if(this.#isClosedOrAborted())returnfalse;
665+
if(this.#state !=='open')returnfalse;
641666
if(!this.#broadcast[kCanWrite]())returnfalse;
642667
constconverted=convertChunks(chunks);
643668
if(this.#broadcast[kWrite](converted)){
@@ -651,34 +676,43 @@ class BroadcastWriter {
651676

652677
end(options){
653678
constsignal=getWriterSignal(options);
679+
if(this.#state ==='errored')returnPromiseReject(this.#error);
680+
if(this.#state ==='closed')returnPromiseResolve(this.#totalBytes);
654681
if(signal?.aborted)returnPromiseReject(signal.reason);
655682

656-
if(this.#isClosed())returnthis.#closed;
657-
this.#closed =PromiseResolve(this.#totalBytes);
658-
this.#broadcast[kEnd]();
659-
this.#resolvePendingDrains(false);
660-
returnthis.#closed;
683+
constendPromise=this.#getEndPromise();
684+
if(this.#state ==='open'){
685+
this.#state ='closing';
686+
this.#resolvePendingDrains(false);
687+
this.#finishEndIfReady();
688+
}
689+
690+
returnraceEndWithSignal(endPromise,signal);
661691
}
662692

663693
endSync(){
664-
if(this.#closed)returnthis.#totalBytes;
665-
this.#closed =PromiseResolve(this.#totalBytes);
666-
this.#broadcast[kEnd]();
694+
if(this.#state ==='closed')returnthis.#totalBytes;
695+
if(this.#state ==='errored'||this.#state ==='closing')return-1;
696+
697+
this.#state ='closing';
667698
this.#resolvePendingDrains(false);
668-
returnthis.#totalBytes;
699+
this.#finishEndIfReady();
700+
returnthis.#state ==='closed' ? this.#totalBytes : -1;
669701
}
670702

671703
fail(reason){
672-
if(this.#isClosedOrAborted())return;
673-
this.#aborted =true;
674-
this.#closed =PromiseResolve(this.#totalBytes);
704+
if(this.#state ==='errored'||this.#state ==='closed')return;
705+
this.#state ='errored';
675706
consterror=reason??newERR_INVALID_STATE.TypeError('Failed');
707+
this.#error =error;
676708
this.#rejectPendingWrites(error);
677709
this.#rejectPendingDrains(error);
710+
this.#pendingEnd?.reject(error);
678711
this.#broadcast[kAbort](error);
679712
}
680713

681714
[SymbolAsyncDispose](){
715+
if(this.#state ==='closing')returnthis.#getEndPromise();
682716
this.fail();
683717
returnPromiseResolve();
684718
}
@@ -688,11 +722,33 @@ class BroadcastWriter {
688722
}
689723

690724
[kCancelWriter](){
691-
if(this.#isClosed())return;
692-
this.#closed=PromiseResolve(this.#totalBytes);
725+
if(this.#state ==='closed'||this.#state ==='errored')return;
726+
this.#state='closed';
693727
this.#rejectPendingWrites(
694728
lazyDOMException('Broadcast cancelled','AbortError'));
695729
this.#resolvePendingDrains(false);
730+
this.#pendingEnd?.resolve(this.#totalBytes);
731+
}
732+
733+
#getEndPromise(){
734+
this.#pendingEnd ??=PromiseWithResolvers();
735+
returnthis.#pendingEnd.promise;
736+
}
737+
738+
#finishEndIfReady(){
739+
if(this.#state ==='closing'&&this.#pendingWrites.length===0){
740+
this.#broadcast[kEnd]();
741+
}
742+
}
743+
744+
#endDrained(){
745+
if(this.#state !=='closing')return;
746+
this.#state ='closed';
747+
this.#pendingEnd?.resolve(this.#totalBytes);
748+
}
749+
750+
[kPendingWriteRemoved](){
751+
this.#finishEndIfReady();
696752
}
697753

698754
/**
@@ -724,6 +780,7 @@ class BroadcastWriter {
724780
break;
725781
}
726782
}
783+
this.#finishEndIfReady();
727784
}
728785

729786
#rejectPendingWrites(error){
@@ -756,6 +813,7 @@ function wireBroadcastWriteSignal(entry, signal, resolve, reject, self) {
756813
if(idx!==-1)pendingWrites.removeAt(idx);
757814
entry.chunk=null;
758815
reject(signal.reason??lazyDOMException('Aborted','AbortError'));
816+
if(idx!==-1)self[kPendingWriteRemoved]();
759817
};
760818
entry.resolve=function(){
761819
signal.removeEventListener('abort',onAbort);

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Commit dc51c79

Browse files
trivikraduh95
authored andcommitted
stream: drain pending writes before broadcast end
Keep processing writes that were queued before end(). Signal the broadcast end only after those writes enter the shared buffer. Resolve end() after all consumers reach end-of-stream. This prevents backpressured writes from remaining pending forever. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #65334Fixes: #65333 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5fbf6c5 commit dc51c79

4 files changed

Lines changed: 258 additions & 50 deletions

File tree

β€Ždoc/api/stream_iter.mdβ€Ž

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,14 +431,16 @@ the write. Use [`ondrain()`][] to wait for capacity rather than polling.
431431
the pending `end()` call; it does not fail the writer itself.
432432
* Returns: {Promise} Fulfills with the total number of bytes written.
433433

434-
Signal that no more data will be written.
434+
Signals that no more data will be written and waits for buffered data to drain.
435435

436436
#### `writer.endSync()`
437437

438-
* Returns: {number} Total bytes written, or `-1` if the writer is not open.
438+
* Returns: {number} Total bytes written, or `-1` if ending cannot complete
439+
synchronously.
439440

440-
Synchronous variant of `writer.end()`. Returns `-1` if the writer is already
441-
closed or errored. Can be used as a try-fallback pattern:
441+
Synchronous variant of `writer.end()`. A return value of `-1` means closing has
442+
started but requires asynchronous draining. Use the try-fallback pattern to
443+
await completion:
442444

443445
```cjs
444446
constresult=writer.endSync();

β€Žlib/internal/streams/iter/broadcast.jsβ€Ž

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const {
1414
PromiseReject,
1515
PromiseResolve,
1616
PromiseWithResolvers,
17+
SafePromisePrototypeFinally,
18+
SafePromiseRace,
1719
SafeSet,
1820
Symbol,
1921
SymbolAsyncDispose,
@@ -77,6 +79,22 @@ const kEnd = Symbol('kEnd');
7779
constkAbort=Symbol('kAbort');
7880
constkCanWrite=Symbol('kCanWrite');
7981
constkOnBufferDrained=Symbol('kOnBufferDrained');
82+
constkOnEndDrained=Symbol('kOnEndDrained');
83+
constkPendingWriteRemoved=Symbol('kPendingWriteRemoved');
84+
85+
functionraceEndWithSignal(promise,signal){
86+
if(!signal)returnpromise;
87+
88+
const{promise: aborted, reject }=PromiseWithResolvers();
89+
constonAbort=()=>reject(signal.reason);
90+
signal.addEventListener('abort',onAbort,{__proto__: null,once: true});
91+
if(signal.aborted)onAbort();
92+
93+
returnSafePromisePrototypeFinally(
94+
SafePromiseRace([promise,aborted]),
95+
()=>signal.removeEventListener('abort',onAbort),
96+
);
97+
}
8098

8199
// =============================================================================
82100
// Broadcast Implementation
@@ -100,6 +118,7 @@ class BroadcastImpl {
100118
constructor(options){
101119
this.#options =options;
102120
this[kOnBufferDrained]=null;
121+
this[kOnEndDrained]=null;
103122
}
104123

105124
setWriter(writer){
@@ -183,6 +202,7 @@ class BroadcastImpl {
183202
if(self.#deleteConsumer(state)){
184203
self.#tryTrimBuffer();
185204
}
205+
self.#notifyEndDrained();
186206
}
187207

188208
return{
@@ -358,10 +378,11 @@ class BroadcastImpl {
358378
}
359379
}
360380
}
381+
this.#notifyEndDrained();
361382
}
362383

363384
[kAbort](reason){
364-
if(this.#ended ||this.#error !==undefined)return;
385+
if(this.#error !==undefined)return;
365386
this.#error =reason;
366387
this.#ended =true;
367388

@@ -396,6 +417,12 @@ class BroadcastImpl {
396417

397418
// Private methods
398419

420+
#notifyEndDrained(){
421+
if(this.#ended &&this.#consumers.size===0){
422+
this[kOnEndDrained]?.();
423+
}
424+
}
425+
399426
#recomputeMinCursor(){
400427
const{ minCursor, minCursorConsumers }=getMinCursor(
401428
this.#consumers,this.#bufferStart +this.#buffer.length);
@@ -516,8 +543,9 @@ let getBroadcastPendingWrites;
516543
classBroadcastWriter{
517544
#broadcast;
518545
#totalBytes =0;
519-
#closed;
520-
#aborted =false;
546+
#state ='open';
547+
#error;
548+
#pendingEnd;
521549
#pendingWrites =newRingBuffer();
522550
#pendingDrains =[];
523551

@@ -532,8 +560,11 @@ class BroadcastWriter {
532560

533561
this.#broadcast[kOnBufferDrained]=()=>{
534562
this.#resolvePendingWrites();
535-
this.#resolvePendingDrains(true);
563+
if(this.#state ==='open'){
564+
this.#resolvePendingDrains(true);
565+
}
536566
};
567+
this.#broadcast[kOnEndDrained]=()=>this.#endDrained();
537568
}
538569

539570
// The drainable protocol works with Stream.ondrain to provide a notification
@@ -547,20 +578,12 @@ class BroadcastWriter {
547578
returnpromise;
548579
}
549580

550-
#isClosed(){
551-
returnthis.#closed !==undefined;
552-
}
553-
554-
#isClosedOrAborted(){
555-
returnthis.#isClosed()||this.#aborted;
556-
}
557-
558581
getcanWrite(){
559-
returnthis.#isClosedOrAborted() ? null :this.#broadcast[kCanWrite]();
582+
returnthis.#state ==='open' ?this.#broadcast[kCanWrite]() : null;
560583
}
561584

562585
#canUseWriteFastPath(signal){
563-
return!signal&&!this.#isClosed()&&!this.#aborted&&
586+
return!signal&&this.#state ==='open'&&
564587
this.#broadcast[kCanWrite]();
565588
}
566589

@@ -592,13 +615,15 @@ class BroadcastWriter {
592615
}
593616

594617
async #writevSlow(chunks,signal){
595-
// Check for pre-aborted
596-
signal?.throwIfAborted();
597-
598-
if(this.#isClosedOrAborted()){
618+
if(this.#state ==='errored'){
619+
throwthis.#error;
620+
}
621+
if(this.#state !=='open'){
599622
thrownewERR_INVALID_STATE.TypeError('Writer is closed');
600623
}
601624

625+
signal?.throwIfAborted();
626+
602627
constconverted=convertChunks(chunks);
603628

604629
if(this.#broadcast[kWrite](converted)){
@@ -624,7 +649,7 @@ class BroadcastWriter {
624649
}
625650

626651
writeSync(chunk){
627-
if(this.#isClosedOrAborted())returnfalse;
652+
if(this.#state !=='open')returnfalse;
628653
if(!this.#broadcast[kCanWrite]())returnfalse;
629654
constconverted=
630655
toUint8Array(chunk);
@@ -637,7 +662,7 @@ class BroadcastWriter {
637662

638663
writevSync(chunks){
639664
validateArray(chunks,'chunks');
640-
if(this.#isClosedOrAborted())returnfalse;
665+
if(this.#state !=='open')returnfalse;
641666
if(!this.#broadcast[kCanWrite]())returnfalse;
642667
constconverted=convertChunks(chunks);
643668
if(this.#broadcast[kWrite](converted)){
@@ -651,34 +676,43 @@ class BroadcastWriter {
651676

652677
end(options){
653678
constsignal=getWriterSignal(options);
679+
if(this.#state ==='errored')returnPromiseReject(this.#error);
680+
if(this.#state ==='closed')returnPromiseResolve(this.#totalBytes);
654681
if(signal?.aborted)returnPromiseReject(signal.reason);
655682

656-
if(this.#isClosed())returnthis.#closed;
657-
this.#closed =PromiseResolve(this.#totalBytes);
658-
this.#broadcast[kEnd]();
659-
this.#resolvePendingDrains(false);
660-
returnthis.#closed;
683+
constendPromise=this.#getEndPromise();
684+
if(this.#state ==='open'){
685+
this.#state ='closing';
686+
this.#resolvePendingDrains(false);
687+
this.#finishEndIfReady();
688+
}
689+
690+
returnraceEndWithSignal(endPromise,signal);
661691
}
662692

663693
endSync(){
664-
if(this.#closed)returnthis.#totalBytes;
665-
this.#closed =PromiseResolve(this.#totalBytes);
666-
this.#broadcast[kEnd]();
694+
if(this.#state ==='closed')returnthis.#totalBytes;
695+
if(this.#state ==='errored'||this.#state ==='closing')return-1;
696+
697+
this.#state ='closing';
667698
this.#resolvePendingDrains(false);
668-
returnthis.#totalBytes;
699+
this.#finishEndIfReady();
700+
returnthis.#state ==='closed' ? this.#totalBytes : -1;
669701
}
670702

671703
fail(reason){
672-
if(this.#isClosedOrAborted())return;
673-
this.#aborted =true;
674-
this.#closed =PromiseResolve(this.#totalBytes);
704+
if(this.#state ==='errored'||this.#state ==='closed')return;
705+
this.#state ='errored';
675706
consterror=reason??newERR_INVALID_STATE.TypeError('Failed');
707+
this.#error =error;
676708
this.#rejectPendingWrites(error);
677709
this.#rejectPendingDrains(error);
710+
this.#pendingEnd?.reject(error);
678711
this.#broadcast[kAbort](error);
679712
}
680713

681714
[SymbolAsyncDispose](){
715+
if(this.#state ==='closing')returnthis.#getEndPromise();
682716
this.fail();
683717
returnPromiseResolve();
684718
}
@@ -688,11 +722,33 @@ class BroadcastWriter {
688722
}
689723

690724
[kCancelWriter](){
691-
if(this.#isClosed())return;
692-
this.#closed=PromiseResolve(this.#totalBytes);
725+
if(this.#state ==='closed'||this.#state ==='errored')return;
726+
this.#state='closed';
693727
this.#rejectPendingWrites(
694728
lazyDOMException('Broadcast cancelled','AbortError'));
695729
this.#resolvePendingDrains(false);
730+
this.#pendingEnd?.resolve(this.#totalBytes);
731+
}
732+
733+
#getEndPromise(){
734+
this.#pendingEnd ??=PromiseWithResolvers();
735+
returnthis.#pendingEnd.promise;
736+
}
737+
738+
#finishEndIfReady(){
739+
if(this.#state ==='closing'&&this.#pendingWrites.length===0){
740+
this.#broadcast[kEnd]();
741+
}
742+
}
743+
744+
#endDrained(){
745+
if(this.#state !=='closing')return;
746+
this.#state ='closed';
747+
this.#pendingEnd?.resolve(this.#totalBytes);
748+
}
749+
750+
[kPendingWriteRemoved](){
751+
this.#finishEndIfReady();
696752
}
697753

698754
/**
@@ -724,6 +780,7 @@ class BroadcastWriter {
724780
break;
725781
}
726782
}
783+
this.#finishEndIfReady();
727784
}
728785

729786
#rejectPendingWrites(error){
@@ -756,6 +813,7 @@ function wireBroadcastWriteSignal(entry, signal, resolve, reject, self) {
756813
if(idx!==-1)pendingWrites.removeAt(idx);
757814
entry.chunk=null;
758815
reject(signal.reason??lazyDOMException('Aborted','AbortError'));
816+
if(idx!==-1)self[kPendingWriteRemoved]();
759817
};
760818
entry.resolve=function(){
761819
signal.removeEventListener('abort',onAbort);

0 commit comments

Comments
Β (0)