77
88const {
99 SymbolAsyncDispose,
10+ SymbolAsyncIterator,
1011} = primordials ;
1112
1213const {
1314 push,
1415} = require ( 'internal/streams/iter/push' ) ;
1516const {
17+ validateAbortSignal,
1618 validateObject,
1719} = require ( 'internal/validators' ) ;
1820
@@ -31,34 +33,54 @@ function duplex(options = { __proto__: null }) {
3133 if ( b !== undefined ) {
3234 validateObject ( b , 'options.b' ) ;
3335 }
36+ if ( signal !== undefined ) {
37+ validateAbortSignal ( signal , 'options.signal' ) ;
38+ }
3439
35- // Channel A writes to B's readable (A->B direction)
40+ // Channel A writes to B's readable (A->B direction).
41+ // Signal is NOT passed to push() -- we handle abort via close() below.
3642 const { writer : aWriter , readable : bReadable } = push ( {
3743 highWaterMark : a ?. highWaterMark ?? highWaterMark ,
3844 backpressure : a ?. backpressure ?? backpressure ,
39- signal,
4045 } ) ;
4146
4247 // Channel B writes to A's readable (B->A direction)
4348 const { writer : bWriter , readable : aReadable } = push ( {
4449 highWaterMark : b ?. highWaterMark ?? highWaterMark ,
4550 backpressure : b ?. backpressure ?? backpressure ,
46- signal,
4751 } ) ;
4852
49- let aWriterRef = aWriter ;
50- let bWriterRef = bWriter ;
53+ let aClosed = false ;
54+ let bClosed = false ;
55+ // Track active iterators so close() can call .return() on them
56+ let aReadableIterator = null ;
57+ let bReadableIterator = null ;
5158
5259 const channelA = {
5360 __proto__ : null ,
5461 get writer ( ) { return aWriter ; } ,
55- readable : aReadable ,
62+ // Wrap readable to track the iterator for cleanup on close()
63+ get readable ( ) {
64+ return {
65+ __proto__ : null ,
66+ [ SymbolAsyncIterator ] ( ) {
67+ const iter = aReadable [ SymbolAsyncIterator ] ( ) ;
68+ aReadableIterator = iter ;
69+ return iter ;
70+ } ,
71+ } ;
72+ } ,
5673 async close ( ) {
57- if ( aWriterRef === null ) return ;
58- const writer = aWriterRef ;
59- aWriterRef = null ;
60- if ( writer . endSync ( ) < 0 ) {
61- await writer . end ( ) ;
74+ if ( aClosed ) return ;
75+ aClosed = true ;
76+ // End the writer (signals end-of-stream to B's readable)
77+ if ( aWriter . endSync ( ) < 0 ) {
78+ await aWriter . end ( ) ;
79+ }
80+ // Stop iteration of this channel's readable
81+ if ( aReadableIterator ?. return ) {
82+ await aReadableIterator . return ( ) ;
83+ aReadableIterator = null ;
6284 }
6385 } ,
6486 [ SymbolAsyncDispose ] ( ) {
@@ -69,20 +91,48 @@ function duplex(options = { __proto__: null }) {
6991 const channelB = {
7092 __proto__ : null ,
7193 get writer ( ) { return bWriter ; } ,
72- readable : bReadable ,
94+ get readable ( ) {
95+ return {
96+ __proto__ : null ,
97+ [ SymbolAsyncIterator ] ( ) {
98+ const iter = bReadable [ SymbolAsyncIterator ] ( ) ;
99+ bReadableIterator = iter ;
100+ return iter ;
101+ } ,
102+ } ;
103+ } ,
73104 async close ( ) {
74- if ( bWriterRef === null ) return ;
75- const writer = bWriterRef ;
76- bWriterRef = null ;
77- if ( writer . endSync ( ) < 0 ) {
78- await writer . end ( ) ;
105+ if ( bClosed ) return ;
106+ bClosed = true ;
107+ if ( bWriter . endSync ( ) < 0 ) {
108+ await bWriter . end ( ) ;
109+ }
110+ if ( bReadableIterator ?. return ) {
111+ await bReadableIterator . return ( ) ;
112+ bReadableIterator = null ;
79113 }
80114 } ,
81115 [ SymbolAsyncDispose ] ( ) {
82116 return this . close ( ) ;
83117 } ,
84118 } ;
85119
120+ // Signal handler: fail both writers with the abort reason so consumers
121+ // see the error. This is an error-path shutdown, not a clean close.
122+ if ( signal ) {
123+ const abortBoth = ( ) => {
124+ const reason = signal . reason ;
125+ aWriter . fail ( reason ) ;
126+ bWriter . fail ( reason ) ;
127+ } ;
128+ if ( signal . aborted ) {
129+ abortBoth ( ) ;
130+ } else {
131+ signal . addEventListener ( 'abort' , abortBoth ,
132+ { __proto__ : null , once : true } ) ;
133+ }
134+ }
135+
86136 return [ channelA , channelB ] ;
87137}
88138
0 commit comments