Skip to content

Commit 538ec98

Browse files
committed
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 1687197 commit 538ec98

File tree

2 files changed

+48
-17
lines changed

2 files changed

+48
-17
lines changed

lib/internal/timers/promises.js

+26-16
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
return new DOMException(message, name);
2728
});
2829

30+
function cancelListenerHandler(clear, reject) {
31+
if (!this._destroyed) {
32+
clear(this);
33+
reject(lazyDOMException('The operation was aborted', 'AbortError'));
34+
}
35+
}
36+
2937
function setTimeout(after, value, options = {}) {
3038
const args = value !== undefined ? [value] : value;
3139
if (options == null || typeof options !== 'object') {
@@ -55,20 +63,21 @@ function setTimeout(after, value, options = {}) {
5563
return PromiseReject(
5664
lazyDOMException('The operation was aborted', 'AbortError'));
5765
}
58-
return new Promise((resolve, reject) => {
66+
let oncancel;
67+
const ret = new Promise((resolve, reject) => {
5968
const timeout = new Timeout(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+
return oncancel !== undefined ?
78+
PromisePrototypeFinally(
79+
ret,
80+
() => signal.removeEventListener('abort', oncancel)) : ret;
7281
}
7382

7483
function setImmediate(value, options = {}) {
@@ -99,19 +108,20 @@ function setImmediate(value, options = {}) {
99108
return PromiseReject(
100109
lazyDOMException('The operation was aborted', 'AbortError'));
101110
}
102-
return new Promise((resolve, reject) => {
111+
let oncancel;
112+
const ret = new Promise((resolve, reject) => {
103113
const immediate = new Immediate(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+
return oncancel !== undefined ?
122+
PromisePrototypeFinally(
123+
ret,
124+
() => signal.removeEventListener('abort', oncancel)) : ret;
115125
}
116126

117127
module.exports = {

test/parallel/test-timers-promisified.js

+22-1
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
const common = require('../common');
44
const assert = require('assert');
55
const timers = 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
const setTimeout = 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+
const signal = new NodeEventTarget();
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+
const signal = new NodeEventTarget();
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)