Skip to content

Commit d84b05a

Browse files
jasnelltargos
authored andcommitted
timers: cleanup abort listener on awaitable timers
Co-authored-by: Benjamin Gruenbaum <benjamingr@gmail.com> Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: #36006 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net>
1 parent e19478a commit d84b05a

2 files changed

Lines changed: 48 additions & 17 deletions

File tree

‎lib/internal/timers/promises.js‎

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const{
44
Promise,
5+
PromisePrototypeFinally,
56
PromiseReject,
67
}=primordials;
78

@@ -26,6 +27,13 @@ const lazyDOMException = hideStackFrames((message, name) => {
2627
returnnewDOMException(message,name);
2728
});
2829

30+
functioncancelListenerHandler(clear,reject){
31+
if(!this._destroyed){
32+
clear(this);
33+
reject(lazyDOMException('The operation was aborted','AbortError'));
34+
}
35+
}
36+
2937
functionsetTimeout(after,value,options={}){
3038
constargs=value!==undefined ? [value] : value;
3139
if(options==null||typeofoptions!=='object'){
@@ -55,20 +63,21 @@ function setTimeout(after, value, options = {}) {
5563
returnPromiseReject(
5664
lazyDOMException('The operation was aborted','AbortError'));
5765
}
58-
returnnewPromise((resolve,reject)=>{
66+
letoncancel;
67+
constret=newPromise((resolve,reject)=>{
5968
consttimeout=newTimeout(resolve,after,args,false,true);
6069
if(!ref)timeout.unref();
6170
insert(timeout,timeout._idleTimeout);
6271
if(signal){
63-
signal.addEventListener('abort',()=>{
64-
if(!timeout._destroyed){
65-
// eslint-disable-next-line no-undef
66-
clearTimeout(timeout);
67-
reject(lazyDOMException('The operation was aborted','AbortError'));
68-
}
69-
},{once: true});
72+
// eslint-disable-next-line no-undef
73+
oncancel=cancelListenerHandler.bind(timeout,clearTimeout,reject);
74+
signal.addEventListener('abort',oncancel);
7075
}
7176
});
77+
returnoncancel!==undefined ?
78+
PromisePrototypeFinally(
79+
ret,
80+
()=>signal.removeEventListener('abort',oncancel)) : ret;
7281
}
7382

7483
functionsetImmediate(value,options={}){
@@ -99,19 +108,20 @@ function setImmediate(value, options = {}) {
99108
returnPromiseReject(
100109
lazyDOMException('The operation was aborted','AbortError'));
101110
}
102-
returnnewPromise((resolve,reject)=>{
111+
letoncancel;
112+
constret=newPromise((resolve,reject)=>{
103113
constimmediate=newImmediate(resolve,[value]);
104114
if(!ref)immediate.unref();
105115
if(signal){
106-
signal.addEventListener('abort',()=>{
107-
if(!immediate._destroyed){
108-
// eslint-disable-next-line no-undef
109-
clearImmediate(immediate);
110-
reject(lazyDOMException('The operation was aborted','AbortError'));
111-
}
112-
},{once: true});
116+
// eslint-disable-next-line no-undef
117+
oncancel=cancelListenerHandler.bind(immediate,clearImmediate,reject);
118+
signal.addEventListener('abort',oncancel);
113119
}
114120
});
121+
returnoncancel!==undefined ?
122+
PromisePrototypeFinally(
123+
ret,
124+
()=>signal.removeEventListener('abort',oncancel)) : ret;
115125
}
116126

117127
module.exports={

‎test/parallel/test-timers-promisified.js‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
// Flags: --no-warnings --experimental-abortcontroller
1+
// Flags: --no-warnings --expose-internals --experimental-abortcontroller
22
'use strict';
33
constcommon=require('../common');
44
constassert=require('assert');
55
consttimers=require('timers');
66
const{ promisify }=require('util');
77

8+
// TODO(benjamingr) - refactor to use getEventListeners when #35991 lands
9+
const{ NodeEventTarget }=require('internal/event_target');
10+
811
/* eslint-disable no-restricted-syntax */
912

1013
constsetTimeout=promisify(timers.setTimeout);
@@ -85,6 +88,24 @@ process.on('multipleResolves', common.mustNotCall());
8588
});
8689
}
8790

91+
{
92+
// Check that timer adding signals does not leak handlers
93+
constsignal=newNodeEventTarget();
94+
signal.aborted=false;
95+
setTimeout(0,null,{ signal }).finally(common.mustCall(()=>{
96+
assert.strictEqual(signal.listenerCount('abort'),0);
97+
}));
98+
}
99+
100+
{
101+
// Check that timer adding signals does not leak handlers
102+
constsignal=newNodeEventTarget();
103+
signal.aborted=false;
104+
setImmediate(0,{ signal }).finally(common.mustCall(()=>{
105+
assert.strictEqual(signal.listenerCount('abort'),0);
106+
}));
107+
}
108+
88109
{
89110
Promise.all(
90111
[1,'',false,Infinity].map((i)=>assert.rejects(setImmediate(10,i)),{

0 commit comments

Comments
 (0)