Skip to content

Commit 5541300

Browse files
stream: handle generator destruction from Duplex.from()
PR-URL: #55096 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Jason Zhang <xzha4350@gmail.com>
1 parent 6cd1805 commit 5541300

2 files changed

Lines changed: 243 additions & 7 deletions

File tree

‎lib/internal/streams/duplexify.js‎

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,19 @@ module.exports = function duplexify(body, name) {
8383
}
8484

8585
if(typeofbody==='function'){
86-
const{ value, write, final, destroy }=fromAsyncGen(body);
86+
letd;
87+
88+
const{ value, write, final, destroy }=fromAsyncGen(body,()=>{
89+
destroyer(d);
90+
});
8791

8892
// Body might be a constructor function instead of an async generator function.
8993
if(isDuplexNodeStream(value)){
90-
returnvalue;
94+
returnd=value;
9195
}
9296

9397
if(isIterable(value)){
94-
returnfrom(Duplexify,value,{
98+
returnd=from(Duplexify,value,{
9599
// TODO (ronag): highWaterMark?
96100
objectMode: true,
97101
write,
@@ -102,12 +106,16 @@ module.exports = function duplexify(body, name) {
102106

103107
constthen=value?.then;
104108
if(typeofthen==='function'){
105-
letd;
109+
letfinalized=false;
106110

107111
constpromise=FunctionPrototypeCall(
108112
then,
109113
value,
110114
(val)=>{
115+
// The function returned without (fully) consuming the generator.
116+
if(!finalized){
117+
destroyer(d);
118+
}
111119
if(val!=null){
112120
thrownewERR_INVALID_RETURN_VALUE('nully','body',val);
113121
}
@@ -123,6 +131,7 @@ module.exports = function duplexify(body, name) {
123131
readable: false,
124132
write,
125133
final(cb){
134+
finalized=true;
126135
final(async()=>{
127136
try{
128137
awaitpromise;
@@ -208,11 +217,12 @@ module.exports = function duplexify(body, name) {
208217
body);
209218
};
210219

211-
functionfromAsyncGen(fn){
220+
functionfromAsyncGen(fn,destructor){
212221
let{ promise, resolve }=PromiseWithResolvers();
213222
constac=newAbortController();
214223
constsignal=ac.signal;
215-
constvalue=fn(asyncfunction*(){
224+
225+
constasyncGenerator=(asyncfunction*(){
216226
while(true){
217227
const_promise=promise;
218228
promise=null;
@@ -222,9 +232,44 @@ function fromAsyncGen(fn) {
222232
if(signal.aborted)
223233
thrownewAbortError(undefined,{cause: signal.reason});
224234
({ promise, resolve }=PromiseWithResolvers());
235+
// Next line will "break" the loop if the generator is returned/thrown.
225236
yieldchunk;
226237
}
227-
}(),{ signal });
238+
})();
239+
240+
constoriginalReturn=asyncGenerator.return;
241+
asyncGenerator.return=asyncfunction(value){
242+
try{
243+
returnawaitoriginalReturn.call(this,value);
244+
}finally{
245+
if(promise){
246+
const_promise=promise;
247+
promise=null;
248+
const{ cb }=await_promise;
249+
process.nextTick(cb);
250+
251+
process.nextTick(destructor);
252+
}
253+
}
254+
};
255+
256+
constoriginalThrow=asyncGenerator.throw;
257+
asyncGenerator.throw=asyncfunction(err){
258+
try{
259+
returnawaitoriginalThrow.call(this,err);
260+
}finally{
261+
if(promise){
262+
const_promise=promise;
263+
promise=null;
264+
const{ cb }=await_promise;
265+
266+
// asyncGenerator.throw(undefined) should cause a callback error
267+
process.nextTick(cb,err??newAbortError());
268+
}
269+
}
270+
};
271+
272+
constvalue=fn(asyncGenerator,{ signal });
228273

229274
return{
230275
value,

‎test/parallel/test-stream-duplex-from.js‎

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const assert = require('assert');
55
const{ Duplex, Readable, Writable, pipeline, PassThrough }=require('stream');
66
const{ ReadableStream, WritableStream }=require('stream/web');
77
const{ Blob }=require('buffer');
8+
constsleep=require('util').promisify(setTimeout);
89

910
{
1011
constd=Duplex.from({
@@ -401,3 +402,193 @@ function makeATestWritableStream(writeFunc) {
401402
assert.strictEqual(d.writable,false);
402403
}));
403404
}
405+
406+
{
407+
constr=Readable.from(['foo','bar','baz']);
408+
pipeline(
409+
r,
410+
Duplex.from(asyncfunction(asyncGenerator){
411+
constvalues=awaitArray.fromAsync(asyncGenerator);
412+
assert.deepStrictEqual(values,['foo','bar','baz']);
413+
414+
awaitasyncGenerator.return();
415+
awaitasyncGenerator.return();
416+
awaitasyncGenerator.return();
417+
}),
418+
common.mustSucceed(()=>{
419+
assert.strictEqual(r.destroyed,true);
420+
})
421+
);
422+
}
423+
424+
{
425+
constr=Readable.from(['foo','bar','baz']);
426+
pipeline(
427+
r,
428+
Duplex.from(asyncfunction(asyncGenerator){
429+
// eslint-disable-next-line no-unused-vars
430+
forawait(const_ofasyncGenerator)break;
431+
}),
432+
common.mustSucceed(()=>{
433+
assert.strictEqual(r.destroyed,true);
434+
})
435+
);
436+
}
437+
438+
{
439+
constr=Readable.from(['foo','bar','baz']);
440+
pipeline(
441+
r,
442+
Duplex.from(asyncfunction(asyncGenerator){
443+
consta=awaitasyncGenerator.next();
444+
assert.strictEqual(a.done,false);
445+
assert.strictEqual(a.value.toString(),'foo');
446+
constb=awaitasyncGenerator.return();
447+
assert.strictEqual(b.done,true);
448+
}),
449+
common.mustSucceed(()=>{
450+
assert.strictEqual(r.destroyed,true);
451+
})
452+
);
453+
}
454+
455+
{
456+
constr=Readable.from(['foo','bar','baz']);
457+
pipeline(
458+
r,
459+
Duplex.from(asyncfunction(asyncGenerator){
460+
// Note: the generator is not even started at this point
461+
awaitasyncGenerator.return();
462+
}),
463+
common.mustSucceed(()=>{
464+
assert.strictEqual(r.destroyed,true);
465+
})
466+
);
467+
}
468+
469+
{
470+
constr=Readable.from(['foo','bar','baz']);
471+
pipeline(
472+
r,
473+
Duplex.from(asyncfunction(asyncGenerator){
474+
// Same as before, with a delay
475+
awaitsleep(100);
476+
awaitasyncGenerator.return();
477+
}),
478+
common.mustSucceed(()=>{
479+
assert.strictEqual(r.destroyed,true);
480+
})
481+
);
482+
}
483+
484+
{
485+
constr=Readable.from(['foo','bar','baz']);
486+
pipeline(
487+
r,
488+
Duplex.from(asyncfunction(asyncGenerator){}),
489+
common.mustCall((err)=>{
490+
assert.strictEqual(err.code,'ERR_STREAM_PREMATURE_CLOSE');
491+
assert.strictEqual(r.destroyed,true);
492+
})
493+
);
494+
}
495+
496+
{
497+
constr=Readable.from(['foo','bar','baz']);
498+
pipeline(
499+
r,
500+
Duplex.from(asyncfunction(asyncGenerator){
501+
awaitsleep(100);
502+
}),
503+
common.mustCall((err)=>{
504+
assert.strictEqual(err.code,'ERR_STREAM_PREMATURE_CLOSE');
505+
assert.strictEqual(r.destroyed,true);
506+
})
507+
);
508+
}
509+
510+
{
511+
constr=Readable.from(['foo','bar','baz']);
512+
constd=Duplex.from(asyncfunction(asyncGenerator){
513+
while(!(awaitasyncGenerator.next()).done)awaitsleep(100);
514+
});
515+
516+
setTimeout(()=>d.destroy(),150);
517+
518+
pipeline(
519+
r,
520+
d,
521+
common.mustCall((err)=>{
522+
assert.strictEqual(err.code,'ERR_STREAM_PREMATURE_CLOSE');
523+
assert.strictEqual(r.destroyed,true);
524+
})
525+
);
526+
}
527+
528+
{
529+
constr=Duplex.from(asyncfunction*(){
530+
for(constvalueof['foo','bar','baz']){
531+
awaitsleep(50);
532+
yieldvalue;
533+
}
534+
});
535+
constd=Duplex.from(asyncfunction(asyncGenerator){
536+
while(!(awaitasyncGenerator.next()).done);
537+
});
538+
539+
setTimeout(()=>r.destroy(),75);
540+
541+
pipeline(
542+
r,
543+
d,
544+
common.mustCall((err)=>{
545+
assert.strictEqual(err.code,'ERR_STREAM_PREMATURE_CLOSE');
546+
assert.strictEqual(r.destroyed,true);
547+
assert.strictEqual(d.destroyed,true);
548+
})
549+
);
550+
}
551+
552+
{
553+
constr=Readable.from(['foo']);
554+
pipeline(
555+
r,
556+
Duplex.from(asyncfunction(asyncGenerator){
557+
awaitasyncGenerator.throw(newError('my error'));
558+
}),
559+
common.mustCall((err)=>{
560+
assert.strictEqual(err.message,'my error');
561+
assert.strictEqual(r.destroyed,true);
562+
})
563+
);
564+
}
565+
566+
{
567+
constr=Readable.from(['foo','bar']);
568+
pipeline(
569+
r,
570+
Duplex.from(asyncfunction(asyncGenerator){
571+
awaitasyncGenerator.next();
572+
awaitasyncGenerator.throw(newError('my error'));
573+
}),
574+
common.mustCall((err)=>{
575+
assert.strictEqual(err.message,'my error');
576+
assert.strictEqual(r.destroyed,true);
577+
})
578+
);
579+
}
580+
581+
{
582+
constr=Readable.from(['foo','bar']);
583+
pipeline(
584+
r,
585+
Duplex.from(asyncfunction(asyncGenerator){
586+
awaitasyncGenerator.next();
587+
awaitasyncGenerator.throw();
588+
}),
589+
common.mustCall((err)=>{
590+
assert.strictEqual(err.code,'ABORT_ERR');
591+
assert.strictEqual(r.destroyed,true);
592+
})
593+
);
594+
}

0 commit comments

Comments
 (0)