Skip to content

Commit 5658c63

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

5 files changed

Lines changed: 416 additions & 164 deletions

File tree

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

Lines changed: 141 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
const {
77
Promise,
88
PromisePrototypeThen,
9+
ReflectApply,
10+
Symbol,
911
SymbolDispose,
1012
} = primordials;
1113

@@ -62,6 +64,50 @@ function bindAsyncResource(fn, type) {
6264
};
6365
}
6466

67+
/**
68+
* Returns the current stream error tracked by eos(), if any.
69+
* @param {import('stream').Stream} stream
70+
* @returns {Error | null}
71+
*/
72+
function getEosErrored(stream) {
73+
const errored = isWritableErrored(stream) || isReadableErrored(stream);
74+
return typeof errored !== 'boolean' && errored || null;
75+
}
76+
77+
/**
78+
* Returns the error eos() would report from an immediate close, including
79+
* premature close detection for unfinished readable or writable sides.
80+
* @param {import('stream').Stream} stream
81+
* @param {boolean} readable
82+
* @param {boolean | null} readableFinished
83+
* @param {boolean} writable
84+
* @param {boolean | null} writableFinished
85+
* @returns {Error | null}
86+
*/
87+
function getEosOnCloseError(stream, readable, readableFinished, writable, writableFinished) {
88+
const errored = getEosErrored(stream);
89+
if (errored) {
90+
return errored;
91+
}
92+
93+
if (readable && !readableFinished && isReadableNodeStream(stream, true)) {
94+
if (!isReadableFinished(stream, false)) {
95+
return new ERR_STREAM_PREMATURE_CLOSE();
96+
}
97+
}
98+
if (writable && !writableFinished) {
99+
if (!isWritableFinished(stream, false)) {
100+
return new ERR_STREAM_PREMATURE_CLOSE();
101+
}
102+
}
103+
104+
return null;
105+
}
106+
107+
// Internal only: if eos() can settle immediately, invoke the callback before
108+
// returning cleanup. Callers must tolerate cleanup yet to be assigned.
109+
const kEosNodeSynchronousCallback = Symbol('kEosNodeSynchronousCallback');
110+
65111
function eos(stream, options, callback) {
66112
if (arguments.length === 2) {
67113
callback = options;
@@ -74,10 +120,6 @@ function eos(stream, options, callback) {
74120
validateFunction(callback, 'callback');
75121
validateAbortSignal(options.signal, 'options.signal');
76122

77-
// Avoid AsyncResource.bind() because it calls ObjectDefineProperties which
78-
// is a bottleneck here.
79-
callback = once(bindAsyncResource(callback, 'STREAM_END_OF_STREAM'));
80-
81123
if (isReadableStream(stream) || isWritableStream(stream)) {
82124
return eosWeb(stream, options, callback);
83125
}
@@ -89,15 +131,6 @@ function eos(stream, options, callback) {
89131
const readable = options.readable ?? isReadableNodeStream(stream);
90132
const writable = options.writable ?? isWritableNodeStream(stream);
91133

92-
const wState = stream._writableState;
93-
const rState = stream._readableState;
94-
95-
const onlegacyfinish = () => {
96-
if (!stream.writable) {
97-
onfinish();
98-
}
99-
};
100-
101134
// TODO (ronag): Improve soft detection to include core modules and
102135
// common ecosystem modules that do properly emit 'close' but fail
103136
// this generic check.
@@ -106,8 +139,83 @@ function eos(stream, options, callback) {
106139
isReadableNodeStream(stream) === readable &&
107140
isWritableNodeStream(stream) === writable
108141
);
109-
110142
let writableFinished = isWritableFinished(stream, false);
143+
let readableFinished = isReadableFinished(stream, false);
144+
145+
const wState = stream._writableState;
146+
const rState = stream._readableState;
147+
148+
/**
149+
* @type {Error | null | undefined}
150+
* undefined: to be determined
151+
* null: no error
152+
* Error: an error occurred
153+
*/
154+
let immediateResult;
155+
if (isClosed(stream)) {
156+
immediateResult = getEosOnCloseError(
157+
stream,
158+
readable,
159+
readableFinished,
160+
writable,
161+
writableFinished,
162+
);
163+
} else if (wState?.errorEmitted || rState?.errorEmitted) {
164+
if (!willEmitClose) {
165+
immediateResult = getEosErrored(stream);
166+
}
167+
} else if (
168+
!readable &&
169+
(!willEmitClose || isReadable(stream)) &&
170+
(writableFinished || isWritable(stream) === false) &&
171+
(wState == null || wState.pendingcb === undefined || wState.pendingcb === 0)
172+
) {
173+
immediateResult = getEosErrored(stream);
174+
} else if (
175+
!writable &&
176+
(!willEmitClose || isWritable(stream)) &&
177+
(readableFinished || isReadable(stream) === false)
178+
) {
179+
immediateResult = getEosErrored(stream);
180+
} else if ((rState && stream.req && stream.aborted)) {
181+
immediateResult = getEosErrored(stream);
182+
}
183+
let cleanup = () => {
184+
callback = nop;
185+
};
186+
if (immediateResult !== undefined) {
187+
if (options.error !== false) {
188+
stream.on('error', nop);
189+
cleanup = () => {
190+
callback = nop;
191+
stream.removeListener('error', nop);
192+
};
193+
}
194+
} else if (options.signal?.aborted) {
195+
immediateResult = new AbortError(undefined, { cause: options.signal.reason });
196+
}
197+
if (immediateResult !== undefined && options[kEosNodeSynchronousCallback]) {
198+
ReflectApply(callback, stream, immediateResult === null ? [] : [immediateResult]);
199+
return cleanup;
200+
}
201+
202+
// Avoid AsyncResource.bind() because it calls ObjectDefineProperties which
203+
// is a bottleneck here.
204+
callback = bindAsyncResource(callback, 'STREAM_END_OF_STREAM');
205+
206+
if (immediateResult !== undefined) {
207+
process.nextTick(() => ReflectApply(callback, stream, immediateResult === null ? [] : [immediateResult]));
208+
return cleanup;
209+
}
210+
211+
callback = once(callback);
212+
213+
const onlegacyfinish = () => {
214+
if (!stream.writable) {
215+
onfinish();
216+
}
217+
};
218+
111219
const onfinish = () => {
112220
writableFinished = true;
113221
// Stream should not be destroyed here. If it is that
@@ -126,7 +234,6 @@ function eos(stream, options, callback) {
126234
}
127235
};
128236

129-
let readableFinished = isReadableFinished(stream, false);
130237
const onend = () => {
131238
readableFinished = true;
132239
// Stream should not be destroyed here. If it is that
@@ -149,41 +256,13 @@ function eos(stream, options, callback) {
149256
callback.call(stream, err);
150257
};
151258

152-
let closed = isClosed(stream);
153-
154259
const onclose = () => {
155-
closed = true;
156-
157-
const errored = isWritableErrored(stream) || isReadableErrored(stream);
158-
159-
if (errored && typeof errored !== 'boolean') {
160-
return callback.call(stream, errored);
161-
}
162-
163-
if (readable && !readableFinished && isReadableNodeStream(stream, true)) {
164-
if (!isReadableFinished(stream, false))
165-
return callback.call(stream,
166-
new ERR_STREAM_PREMATURE_CLOSE());
167-
}
168-
if (writable && !writableFinished) {
169-
if (!isWritableFinished(stream, false))
170-
return callback.call(stream,
171-
new ERR_STREAM_PREMATURE_CLOSE());
172-
}
173-
174-
callback.call(stream);
175-
};
176-
177-
const onclosed = () => {
178-
closed = true;
179-
180-
const errored = isWritableErrored(stream) || isReadableErrored(stream);
181-
182-
if (errored && typeof errored !== 'boolean') {
183-
return callback.call(stream, errored);
260+
const error = getEosOnCloseError(stream, readable, readableFinished, writable, writableFinished);
261+
if (error === null) {
262+
callback.call(stream);
263+
} else {
264+
callback.call(stream, error);
184265
}
185-
186-
callback.call(stream);
187266
};
188267

189268
const onrequest = () => {
@@ -217,30 +296,7 @@ function eos(stream, options, callback) {
217296
}
218297
stream.on('close', onclose);
219298

220-
if (closed) {
221-
process.nextTick(onclose);
222-
} else if (wState?.errorEmitted || rState?.errorEmitted) {
223-
if (!willEmitClose) {
224-
process.nextTick(onclosed);
225-
}
226-
} else if (
227-
!readable &&
228-
(!willEmitClose || isReadable(stream)) &&
229-
(writableFinished || isWritable(stream) === false) &&
230-
(wState == null || wState.pendingcb === undefined || wState.pendingcb === 0)
231-
) {
232-
process.nextTick(onclosed);
233-
} else if (
234-
!writable &&
235-
(!willEmitClose || isWritable(stream)) &&
236-
(readableFinished || isReadable(stream) === false)
237-
) {
238-
process.nextTick(onclosed);
239-
} else if ((rState && stream.req && stream.aborted)) {
240-
process.nextTick(onclosed);
241-
}
242-
243-
const cleanup = () => {
299+
cleanup = () => {
244300
callback = nop;
245301
stream.removeListener('aborted', onclose);
246302
stream.removeListener('complete', onfinish);
@@ -255,7 +311,7 @@ function eos(stream, options, callback) {
255311
stream.removeListener('close', onclose);
256312
};
257313

258-
if (options.signal && !closed) {
314+
if (options.signal) {
259315
const abort = () => {
260316
// Keep it because cleanup removes it.
261317
const endCallback = callback;
@@ -264,23 +320,23 @@ function eos(stream, options, callback) {
264320
stream,
265321
new AbortError(undefined, { cause: options.signal.reason }));
266322
};
267-
if (options.signal.aborted) {
268-
process.nextTick(abort);
269-
} else {
270-
addAbortListener ??= require('internal/events/abort_listener').addAbortListener;
271-
const disposable = addAbortListener(options.signal, abort);
272-
const originalCallback = callback;
273-
callback = once((...args) => {
274-
disposable[SymbolDispose]();
275-
originalCallback.apply(stream, args);
276-
});
277-
}
323+
addAbortListener ??= require('internal/events/abort_listener').addAbortListener;
324+
const disposable = addAbortListener(options.signal, abort);
325+
const originalCallback = callback;
326+
callback = once((...args) => {
327+
disposable[SymbolDispose]();
328+
ReflectApply(originalCallback, stream, args);
329+
});
278330
}
279331

280332
return cleanup;
281333
}
282334

283335
function eosWeb(stream, options, callback) {
336+
// Avoid AsyncResource.bind() because it calls ObjectDefineProperties which
337+
// is a bottleneck here.
338+
callback = once(bindAsyncResource(callback, 'STREAM_END_OF_STREAM'));
339+
284340
let isAborted = false;
285341
let abort = nop;
286342
if (options.signal) {
@@ -339,4 +395,5 @@ function finished(stream, opts) {
339395
module.exports = {
340396
eos,
341397
finished,
398+
kEosNodeSynchronousCallback,
342399
};

0 commit comments

Comments
 (0)