Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

timers: cleanup abort listener on awaitable timers #36006

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 26 additions & 16 deletions lib/timers/promises.js
Expand Up @@ -2,6 +2,7 @@

const {
Promise,
PromisePrototypeFinally,
PromiseReject,
} = primordials;

Expand All @@ -24,6 +25,13 @@ const lazyDOMException = hideStackFrames((message, name) => {
return new DOMException(message, name);
});

function cancelListenerHandler(clear, reject) {
if (!this._destroyed) {
clear(this);
reject(lazyDOMException('The operation was aborted', 'AbortError'));
}
}

function setTimeout(after, value, options = {}) {
const args = value !== undefined ? [value] : value;
if (options == null || typeof options !== 'object') {
Expand Down Expand Up @@ -58,20 +66,21 @@ function setTimeout(after, value, options = {}) {
return PromiseReject(
lazyDOMException('The operation was aborted', 'AbortError'));
}
return new Promise((resolve, reject) => {
let oncancel;
const ret = new Promise((resolve, reject) => {
const timeout = new Timeout(resolve, after, args, false, true);
if (!ref) timeout.unref();
insert(timeout, timeout._idleTimeout);
if (signal) {
signal.addEventListener('abort', () => {
if (!timeout._destroyed) {
// eslint-disable-next-line no-undef
clearTimeout(timeout);
reject(lazyDOMException('The operation was aborted', 'AbortError'));
}
}, { once: true });
// eslint-disable-next-line no-undef
oncancel = cancelListenerHandler.bind(timeout, clearTimeout, reject);
signal.addEventListener('abort', oncancel);
}
});
return oncancel !== undefined ?
PromisePrototypeFinally(
ret,
() => signal.removeEventListener('abort', oncancel)) : ret;
}

function setImmediate(value, options = {}) {
Expand Down Expand Up @@ -107,19 +116,20 @@ function setImmediate(value, options = {}) {
return PromiseReject(
lazyDOMException('The operation was aborted', 'AbortError'));
}
return new Promise((resolve, reject) => {
let oncancel;
const ret = new Promise((resolve, reject) => {
const immediate = new Immediate(resolve, [value]);
if (!ref) immediate.unref();
if (signal) {
signal.addEventListener('abort', () => {
if (!immediate._destroyed) {
// eslint-disable-next-line no-undef
clearImmediate(immediate);
reject(lazyDOMException('The operation was aborted', 'AbortError'));
}
}, { once: true });
// eslint-disable-next-line no-undef
oncancel = cancelListenerHandler.bind(immediate, clearImmediate, reject);
signal.addEventListener('abort', oncancel);
}
});
return oncancel !== undefined ?
PromisePrototypeFinally(
ret,
() => signal.removeEventListener('abort', oncancel)) : ret;
}

module.exports = {
Expand Down
23 changes: 22 additions & 1 deletion test/parallel/test-timers-promisified.js
@@ -1,11 +1,14 @@
// Flags: --no-warnings
// Flags: --no-warnings --expose-internals
'use strict';
const common = require('../common');
const assert = require('assert');
const timers = require('timers');
const { promisify } = require('util');
const child_process = require('child_process');

// TODO(benjamingr) - refactor to use getEventListeners when #35991 lands
const { NodeEventTarget } = require('internal/event_target');

const timerPromises = require('timers/promises');

/* eslint-disable no-restricted-syntax */
Expand Down Expand Up @@ -92,6 +95,24 @@ process.on('multipleResolves', common.mustNotCall());
});
}

{
// Check that timer adding signals does not leak handlers
const signal = new NodeEventTarget();
signal.aborted = false;
setTimeout(0, null, { signal }).finally(common.mustCall(() => {
assert.strictEqual(signal.listenerCount('abort'), 0);
}));
}

{
// Check that timer adding signals does not leak handlers
const signal = new NodeEventTarget();
signal.aborted = false;
setImmediate(0, { signal }).finally(common.mustCall(() => {
assert.strictEqual(signal.listenerCount('abort'), 0);
}));
}

{
Promise.all(
[1, '', false, Infinity].map((i) => assert.rejects(setImmediate(10, i)), {
Expand Down