Skip to content

Commit cbee0de

Browse files
ikeyanaduh95
authored andcommitted
stream: align Readable.toWeb termination with eos
PR-URL: #62394 Reviewed-By: Mattias Buelens <mattias@buelens.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 8f7418d commit cbee0de

5 files changed

Lines changed: 421 additions & 168 deletions

File tree

‎lib/internal/streams/end-of-stream.js‎

Lines changed: 146 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const {
77
Promise,
88
PromisePrototypeThen,
99
ReflectApply,
10+
Symbol,
1011
SymbolDispose,
1112
}=primordials;
1213

@@ -66,6 +67,50 @@ function bindAsyncResource(fn, type) {
6667
};
6768
}
6869

70+
/**
71+
* Returns the current stream error tracked by eos(), if any.
72+
* @param {import('stream').Stream} stream
73+
* @returns {Error | null}
74+
*/
75+
functiongetEosErrored(stream){
76+
consterrored=isWritableErrored(stream)||isReadableErrored(stream);
77+
returntypeoferrored!=='boolean'&&errored||null;
78+
}
79+
80+
/**
81+
* Returns the error eos() would report from an immediate close, including
82+
* premature close detection for unfinished readable or writable sides.
83+
* @param {import('stream').Stream} stream
84+
* @param {boolean} readable
85+
* @param {boolean | null} readableFinished
86+
* @param {boolean} writable
87+
* @param {boolean | null} writableFinished
88+
* @returns {Error | null}
89+
*/
90+
functiongetEosOnCloseError(stream,readable,readableFinished,writable,writableFinished){
91+
consterrored=getEosErrored(stream);
92+
if(errored){
93+
returnerrored;
94+
}
95+
96+
if(readable&&!readableFinished&&isReadableNodeStream(stream,true)){
97+
if(!isReadableFinished(stream,false)){
98+
returnnewERR_STREAM_PREMATURE_CLOSE();
99+
}
100+
}
101+
if(writable&&!writableFinished){
102+
if(!isWritableFinished(stream,false)){
103+
returnnewERR_STREAM_PREMATURE_CLOSE();
104+
}
105+
}
106+
107+
returnnull;
108+
}
109+
110+
// Internal only: if eos() can settle immediately, invoke the callback before
111+
// returning cleanup. Callers must tolerate cleanup yet to be assigned.
112+
constkEosNodeSynchronousCallback=Symbol('kEosNodeSynchronousCallback');
113+
69114
functioneos(stream,options,callback){
70115
if(arguments.length===2){
71116
callback=options;
@@ -78,14 +123,6 @@ function eos(stream, options, callback) {
78123
validateFunction(callback,'callback');
79124
validateAbortSignal(options.signal,'options.signal');
80125

81-
if(AsyncContextFrame.current()||enabledHooksExist()){
82-
// Avoid AsyncResource.bind() because it calls ObjectDefineProperties which
83-
// is a bottleneck here.
84-
callback=once(bindAsyncResource(callback,'STREAM_END_OF_STREAM'));
85-
}else{
86-
callback=once(callback);
87-
}
88-
89126
if(isReadableStream(stream)||isWritableStream(stream)){
90127
returneosWeb(stream,options,callback);
91128
}
@@ -97,15 +134,6 @@ function eos(stream, options, callback) {
97134
constreadable=options.readable??isReadableNodeStream(stream);
98135
constwritable=options.writable??isWritableNodeStream(stream);
99136

100-
constwState=stream._writableState;
101-
constrState=stream._readableState;
102-
103-
constonlegacyfinish=()=>{
104-
if(!stream.writable){
105-
onfinish();
106-
}
107-
};
108-
109137
// TODO (ronag): Improve soft detection to include core modules and
110138
// common ecosystem modules that do properly emit 'close' but fail
111139
// this generic check.
@@ -114,8 +142,85 @@ function eos(stream, options, callback) {
114142
isReadableNodeStream(stream)===readable&&
115143
isWritableNodeStream(stream)===writable
116144
);
117-
118145
letwritableFinished=isWritableFinished(stream,false);
146+
letreadableFinished=isReadableFinished(stream,false);
147+
148+
constwState=stream._writableState;
149+
constrState=stream._readableState;
150+
151+
/**
152+
* @type {Error | null | undefined}
153+
* undefined: to be determined
154+
* null: no error
155+
* Error: an error occurred
156+
*/
157+
letimmediateResult;
158+
if(isClosed(stream)){
159+
immediateResult=getEosOnCloseError(
160+
stream,
161+
readable,
162+
readableFinished,
163+
writable,
164+
writableFinished,
165+
);
166+
}elseif(wState?.errorEmitted||rState?.errorEmitted){
167+
if(!willEmitClose){
168+
immediateResult=getEosErrored(stream);
169+
}
170+
}elseif(
171+
!readable&&
172+
(!willEmitClose||isReadable(stream))&&
173+
(writableFinished||isWritable(stream)===false)&&
174+
(wState==null||wState.pendingcb===undefined||wState.pendingcb===0)
175+
){
176+
immediateResult=getEosErrored(stream);
177+
}elseif(
178+
!writable&&
179+
(!willEmitClose||isWritable(stream))&&
180+
(readableFinished||isReadable(stream)===false)
181+
){
182+
immediateResult=getEosErrored(stream);
183+
}elseif((rState&&stream.req&&stream.aborted)){
184+
immediateResult=getEosErrored(stream);
185+
}
186+
letcleanup=()=>{
187+
callback=nop;
188+
};
189+
if(immediateResult!==undefined){
190+
if(options.error!==false){
191+
stream.on('error',nop);
192+
cleanup=()=>{
193+
callback=nop;
194+
stream.removeListener('error',nop);
195+
};
196+
}
197+
}elseif(options.signal?.aborted){
198+
immediateResult=newAbortError(undefined,{cause: options.signal.reason});
199+
}
200+
if(immediateResult!==undefined&&options[kEosNodeSynchronousCallback]){
201+
ReflectApply(callback,stream,immediateResult===null ? [] : [immediateResult]);
202+
returncleanup;
203+
}
204+
205+
if(AsyncContextFrame.current()||enabledHooksExist()){
206+
// Avoid AsyncResource.bind() because it calls ObjectDefineProperties which
207+
// is a bottleneck here.
208+
callback=bindAsyncResource(callback,'STREAM_END_OF_STREAM');
209+
}
210+
211+
if(immediateResult!==undefined){
212+
process.nextTick(()=>ReflectApply(callback,stream,immediateResult===null ? [] : [immediateResult]));
213+
returncleanup;
214+
}
215+
216+
callback=once(callback);
217+
218+
constonlegacyfinish=()=>{
219+
if(!stream.writable){
220+
onfinish();
221+
}
222+
};
223+
119224
constonfinish=()=>{
120225
writableFinished=true;
121226
// Stream should not be destroyed here. If it is that
@@ -134,7 +239,6 @@ function eos(stream, options, callback) {
134239
}
135240
};
136241

137-
letreadableFinished=isReadableFinished(stream,false);
138242
constonend=()=>{
139243
readableFinished=true;
140244
// Stream should not be destroyed here. If it is that
@@ -157,41 +261,13 @@ function eos(stream, options, callback) {
157261
callback.call(stream,err);
158262
};
159263

160-
letclosed=isClosed(stream);
161-
162264
constonclose=()=>{
163-
closed=true;
164-
165-
consterrored=isWritableErrored(stream)||isReadableErrored(stream);
166-
167-
if(errored&&typeoferrored!=='boolean'){
168-
returncallback.call(stream,errored);
169-
}
170-
171-
if(readable&&!readableFinished&&isReadableNodeStream(stream,true)){
172-
if(!isReadableFinished(stream,false))
173-
returncallback.call(stream,
174-
newERR_STREAM_PREMATURE_CLOSE());
175-
}
176-
if(writable&&!writableFinished){
177-
if(!isWritableFinished(stream,false))
178-
returncallback.call(stream,
179-
newERR_STREAM_PREMATURE_CLOSE());
180-
}
181-
182-
callback.call(stream);
183-
};
184-
185-
constonclosed=()=>{
186-
closed=true;
187-
188-
consterrored=isWritableErrored(stream)||isReadableErrored(stream);
189-
190-
if(errored&&typeoferrored!=='boolean'){
191-
returncallback.call(stream,errored);
265+
consterror=getEosOnCloseError(stream,readable,readableFinished,writable,writableFinished);
266+
if(error===null){
267+
callback.call(stream);
268+
}else{
269+
callback.call(stream,error);
192270
}
193-
194-
callback.call(stream);
195271
};
196272

197273
constonrequest=()=>{
@@ -225,30 +301,7 @@ function eos(stream, options, callback) {
225301
}
226302
stream.on('close',onclose);
227303

228-
if(closed){
229-
process.nextTick(onclose);
230-
}elseif(wState?.errorEmitted||rState?.errorEmitted){
231-
if(!willEmitClose){
232-
process.nextTick(onclosed);
233-
}
234-
}elseif(
235-
!readable&&
236-
(!willEmitClose||isReadable(stream))&&
237-
(writableFinished||isWritable(stream)===false)&&
238-
(wState==null||wState.pendingcb===undefined||wState.pendingcb===0)
239-
){
240-
process.nextTick(onclosed);
241-
}elseif(
242-
!writable&&
243-
(!willEmitClose||isWritable(stream))&&
244-
(readableFinished||isReadable(stream)===false)
245-
){
246-
process.nextTick(onclosed);
247-
}elseif((rState&&stream.req&&stream.aborted)){
248-
process.nextTick(onclosed);
249-
}
250-
251-
constcleanup=()=>{
304+
cleanup=()=>{
252305
callback=nop;
253306
stream.removeListener('aborted',onclose);
254307
stream.removeListener('complete',onfinish);
@@ -263,7 +316,7 @@ function eos(stream, options, callback) {
263316
stream.removeListener('close',onclose);
264317
};
265318

266-
if(options.signal&&!closed){
319+
if(options.signal){
267320
constabort=()=>{
268321
// Keep it because cleanup removes it.
269322
constendCallback=callback;
@@ -272,23 +325,27 @@ function eos(stream, options, callback) {
272325
stream,
273326
newAbortError(undefined,{cause: options.signal.reason}));
274327
};
275-
if(options.signal.aborted){
276-
process.nextTick(abort);
277-
}else{
278-
addAbortListener??=require('internal/events/abort_listener').addAbortListener;
279-
constdisposable=addAbortListener(options.signal,abort);
280-
constoriginalCallback=callback;
281-
callback=once((...args)=>{
282-
disposable[SymbolDispose]();
283-
ReflectApply(originalCallback,stream,args);
284-
});
285-
}
328+
addAbortListener??=require('internal/events/abort_listener').addAbortListener;
329+
constdisposable=addAbortListener(options.signal,abort);
330+
constoriginalCallback=callback;
331+
callback=once((...args)=>{
332+
disposable[SymbolDispose]();
333+
ReflectApply(originalCallback,stream,args);
334+
});
286335
}
287336

288337
returncleanup;
289338
}
290339

291340
functioneosWeb(stream,options,callback){
341+
if(AsyncContextFrame.current()||enabledHooksExist()){
342+
// Avoid AsyncResource.bind() because it calls ObjectDefineProperties which
343+
// is a bottleneck here.
344+
callback=once(bindAsyncResource(callback,'STREAM_END_OF_STREAM'));
345+
}else{
346+
callback=once(callback);
347+
}
348+
292349
letisAborted=false;
293350
letabort=nop;
294351
if(options.signal){
@@ -347,4 +404,5 @@ function finished(stream, opts) {
347404
module.exports={
348405
eos,
349406
finished,
407+
kEosNodeSynchronousCallback,
350408
};

0 commit comments

Comments
 (0)