Skip to content

Commit 7902a99

Browse files
committed
stream: implement finished() for ReadableStream and WritableStream
Refs: #39316 PR-URL: #46205 Reviewed-By: Robert Nagy <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Darshan Sen <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 1e11247 commit 7902a99

File tree

5 files changed

+301
-9
lines changed

5 files changed

+301
-9
lines changed

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,23 @@ const {
2222
validateBoolean
2323
} = require('internal/validators');
2424

25-
const { Promise } = primordials;
25+
const { Promise, PromisePrototypeThen } = primordials;
2626

2727
const {
2828
isClosed,
2929
isReadable,
3030
isReadableNodeStream,
31+
isReadableStream,
3132
isReadableFinished,
3233
isReadableErrored,
3334
isWritable,
3435
isWritableNodeStream,
36+
isWritableStream,
3537
isWritableFinished,
3638
isWritableErrored,
3739
isNodeStream,
3840
willEmitClose: _willEmitClose,
41+
kIsClosedPromise,
3942
} = require('internal/streams/utils');
4043

4144
function isRequest(stream) {
@@ -58,14 +61,17 @@ function eos(stream, options, callback) {
5861

5962
callback = once(callback);
6063

61-
const readable = options.readable ?? isReadableNodeStream(stream);
62-
const writable = options.writable ?? isWritableNodeStream(stream);
64+
if (isReadableStream(stream) || isWritableStream(stream)) {
65+
return eosWeb(stream, options, callback);
66+
}
6367

6468
if (!isNodeStream(stream)) {
65-
// TODO: Webstreams.
66-
throw new ERR_INVALID_ARG_TYPE('stream', 'Stream', stream);
69+
throw new ERR_INVALID_ARG_TYPE('stream', ['ReadableStream', 'WritableStream', 'Stream'], stream);
6770
}
6871

72+
const readable = options.readable ?? isReadableNodeStream(stream);
73+
const writable = options.writable ?? isWritableNodeStream(stream);
74+
6975
const wState = stream._writableState;
7076
const rState = stream._readableState;
7177

@@ -255,6 +261,15 @@ function eos(stream, options, callback) {
255261
return cleanup;
256262
}
257263

264+
function eosWeb(stream, opts, callback) {
265+
PromisePrototypeThen(
266+
stream[kIsClosedPromise].promise,
267+
() => process.nextTick(() => callback.call(stream)),
268+
(err) => process.nextTick(() => callback.call(stream, err)),
269+
);
270+
return nop;
271+
}
272+
258273
function finished(stream, opts) {
259274
let autoCleanup = false;
260275
if (opts === null) {

lib/internal/streams/utils.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ const {
44
Symbol,
55
SymbolAsyncIterator,
66
SymbolIterator,
7+
SymbolFor,
78
} = primordials;
89

910
const kDestroyed = Symbol('kDestroyed');
1011
const kIsErrored = Symbol('kIsErrored');
1112
const kIsReadable = Symbol('kIsReadable');
1213
const kIsDisturbed = Symbol('kIsDisturbed');
1314

15+
const kIsClosedPromise = SymbolFor('nodejs.webstream.isClosedPromise');
16+
1417
function isReadableNodeStream(obj, strict = false) {
1518
return !!(
1619
obj &&
@@ -55,6 +58,25 @@ function isNodeStream(obj) {
5558
);
5659
}
5760

61+
function isReadableStream(obj) {
62+
return !!(
63+
obj &&
64+
!isNodeStream(obj) &&
65+
typeof obj.pipeThrough === 'function' &&
66+
typeof obj.getReader === 'function' &&
67+
typeof obj.cancel === 'function'
68+
);
69+
}
70+
71+
function isWritableStream(obj) {
72+
return !!(
73+
obj &&
74+
!isNodeStream(obj) &&
75+
typeof obj.getWriter === 'function' &&
76+
typeof obj.abort === 'function'
77+
);
78+
}
79+
5880
function isIterable(obj, isAsync) {
5981
if (obj == null) return false;
6082
if (isAsync === true) return typeof obj[SymbolAsyncIterator] === 'function';
@@ -269,18 +291,21 @@ module.exports = {
269291
kIsErrored,
270292
isReadable,
271293
kIsReadable,
294+
kIsClosedPromise,
272295
isClosed,
273296
isDestroyed,
274297
isDuplexNodeStream,
275298
isFinished,
276299
isIterable,
277300
isReadableNodeStream,
301+
isReadableStream,
278302
isReadableEnded,
279303
isReadableFinished,
280304
isReadableErrored,
281305
isNodeStream,
282306
isWritable,
283307
isWritableNodeStream,
308+
isWritableStream,
284309
isWritableEnded,
285310
isWritableFinished,
286311
isWritableErrored,

lib/internal/webstreams/readablestream.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const {
8484
kIsDisturbed,
8585
kIsErrored,
8686
kIsReadable,
87+
kIsClosedPromise,
8788
} = require('internal/streams/utils');
8889

8990
const {
@@ -231,9 +232,11 @@ class ReadableStream {
231232
port1: undefined,
232233
port2: undefined,
233234
promise: undefined,
234-
}
235+
},
235236
};
236237

238+
this[kIsClosedPromise] = createDeferredPromise();
239+
237240
// The spec requires handling of the strategy first
238241
// here. Specifically, if getting the size and
239242
// highWaterMark from the strategy fail, that has
@@ -625,8 +628,9 @@ function TransferredReadableStream() {
625628
writable: undefined,
626629
port: undefined,
627630
promise: undefined,
628-
}
631+
},
629632
};
633+
this[kIsClosedPromise] = createDeferredPromise();
630634
},
631635
[], ReadableStream));
632636
}
@@ -1195,8 +1199,9 @@ function createTeeReadableStream(start, pull, cancel) {
11951199
writable: undefined,
11961200
port: undefined,
11971201
promise: undefined,
1198-
}
1202+
},
11991203
};
1204+
this[kIsClosedPromise] = createDeferredPromise();
12001205
setupReadableStreamDefaultControllerFromSource(
12011206
this,
12021207
ObjectCreate(null, {
@@ -1869,6 +1874,7 @@ function readableStreamCancel(stream, reason) {
18691874
function readableStreamClose(stream) {
18701875
assert(stream[kState].state === 'readable');
18711876
stream[kState].state = 'closed';
1877+
stream[kIsClosedPromise].resolve();
18721878

18731879
const {
18741880
reader,
@@ -1890,6 +1896,8 @@ function readableStreamError(stream, error) {
18901896
assert(stream[kState].state === 'readable');
18911897
stream[kState].state = 'errored';
18921898
stream[kState].storedError = error;
1899+
stream[kIsClosedPromise].reject(error);
1900+
setPromiseHandled(stream[kIsClosedPromise].promise);
18931901

18941902
const {
18951903
reader

lib/internal/webstreams/writablestream.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ const {
6767
kState,
6868
} = require('internal/webstreams/util');
6969

70+
const {
71+
kIsClosedPromise,
72+
} = require('internal/streams/utils');
73+
7074
const {
7175
AbortController,
7276
} = require('internal/abort_controller');
@@ -175,9 +179,11 @@ class WritableStream {
175179
port1: undefined,
176180
port2: undefined,
177181
promise: undefined,
178-
}
182+
},
179183
};
180184

185+
this[kIsClosedPromise] = createDeferredPromise();
186+
181187
const size = extractSizeAlgorithm(strategy?.size);
182188
const highWaterMark = extractHighWaterMark(strategy?.highWaterMark, 1);
183189

@@ -347,6 +353,7 @@ function TransferredWritableStream() {
347353
readable: undefined,
348354
},
349355
};
356+
this[kIsClosedPromise] = createDeferredPromise();
350357
},
351358
[], WritableStream));
352359
}
@@ -726,6 +733,10 @@ function writableStreamRejectCloseAndClosedPromiseIfNeeded(stream) {
726733
resolve: undefined,
727734
};
728735
}
736+
737+
stream[kIsClosedPromise].reject(stream[kState]?.storedError);
738+
setPromiseHandled(stream[kIsClosedPromise].promise);
739+
729740
const {
730741
writer,
731742
} = stream[kState];
@@ -839,6 +850,7 @@ function writableStreamFinishInFlightClose(stream) {
839850
stream[kState].state = 'closed';
840851
if (stream[kState].writer !== undefined)
841852
stream[kState].writer[kState].close.resolve?.();
853+
stream[kIsClosedPromise].resolve?.();
842854
assert(stream[kState].pendingAbortRequest.abort.promise === undefined);
843855
assert(stream[kState].storedError === undefined);
844856
}

0 commit comments

Comments
 (0)