Skip to content

Commit 84b2863

Browse files
jasnelltargos
authored andcommitted
timers: allow promisified timeouts/immediates to be canceled
Using the new experimental AbortController... Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: #33833 Backport-PR-URL: #38386 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Gus Caplan <me@gus.host>
1 parent f30f931 commit 84b2863

3 files changed

Lines changed: 171 additions & 6 deletions

File tree

‎doc/api/timers.md‎

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,47 @@ The [`setImmediate()`][], [`setInterval()`][], and [`setTimeout()`][] methods
248248
each return objects that represent the scheduled timers. These can be used to
249249
cancel the timer and prevent it from triggering.
250250

251-
It is not possible to cancel timers that were created using the promisified
252-
variants of [`setImmediate()`][], [`setTimeout()`][].
251+
For the promisified variants of [`setImmediate()`][] and [`setTimeout()`][],
252+
an [`AbortController`][] may be used to cancel the timer. When canceled, the
253+
returned Promises will be rejected with an `'AbortError'`.
254+
255+
For `setImmediate()`:
256+
257+
```js
258+
constutil=require('util');
259+
constsetImmediatePromise=util.promisify(setImmediate);
260+
261+
constac=newAbortController();
262+
constsignal=ac.signal;
263+
264+
setImmediatePromise('foobar', { signal })
265+
.then(console.log)
266+
.catch((err) => {
267+
if (err.message==='AbortError')
268+
console.log('The immediate was aborted');
269+
});
270+
271+
ac.abort();
272+
```
273+
274+
For `setTimeout()`:
275+
276+
```js
277+
constutil=require('util');
278+
constsetTimeoutPromise=util.promisify(setTimeout);
279+
280+
constac=newAbortController();
281+
constsignal=ac.signal;
282+
283+
setTimeoutPromise(1000, 'foobar', { signal })
284+
.then(console.log)
285+
.catch((err) => {
286+
if (err.message==='AbortError')
287+
console.log('The timeout was aborted');
288+
});
289+
290+
ac.abort();
291+
```
253292

254293
### `clearImmediate(immediate)`
255294
<!-- YAML
@@ -280,6 +319,7 @@ added: v0.0.1
280319
Cancels a `Timeout` object created by [`setTimeout()`][].
281320

282321
[Event Loop]: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout
322+
[`AbortController`]: globals.md#globals_class_abortcontroller
283323
[`TypeError`]: errors.md#errors_class_typeerror
284324
[`clearImmediate()`]: timers.md#timers_clearimmediate_immediate
285325
[`clearInterval()`]: timers.md#timers_clearinterval_timeout

‎lib/timers.js‎

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ const {
2828
SymbolToPrimitive
2929
}=primordials;
3030

31+
const{
32+
codes: {ERR_INVALID_ARG_TYPE}
33+
}=require('internal/errors');
34+
35+
letDOMException;
36+
3137
const{
3238
immediateInfo,
3339
toggleImmediateRef
@@ -129,6 +135,11 @@ function enroll(item, msecs) {
129135
* DOM-style timers
130136
*/
131137

138+
functionlazyDOMException(message){
139+
if(DOMException===undefined)
140+
DOMException=internalBinding('messaging').DOMException;
141+
returnnewDOMException(message);
142+
}
132143

133144
functionsetTimeout(callback,after,arg1,arg2,arg3){
134145
validateCallback(callback);
@@ -160,11 +171,40 @@ function setTimeout(callback, after, arg1, arg2, arg3) {
160171
returntimeout;
161172
}
162173

163-
setTimeout[customPromisify]=function(after,value){
174+
setTimeout[customPromisify]=function(after,value,options={}){
164175
constargs=value!==undefined ? [value] : value;
165-
returnnewPromise((resolve)=>{
176+
if(options==null||typeofoptions!=='object'){
177+
returnPromise.reject(
178+
newERR_INVALID_ARG_TYPE(
179+
'options',
180+
'Object',
181+
options));
182+
}
183+
const{ signal }=options;
184+
if(signal!==undefined&&
185+
(signal===null||
186+
typeofsignal!=='object'||
187+
!('aborted'insignal))){
188+
returnPromise.reject(
189+
newERR_INVALID_ARG_TYPE(
190+
'options.signal',
191+
'AbortSignal',
192+
signal));
193+
}
194+
// TODO(@jasnell): If a decision is made that this cannot be backported
195+
// to 12.x, then this can be converted to use optional chaining to
196+
// simplify the check.
197+
if(signal&&signal.aborted)
198+
returnPromise.reject(lazyDOMException('AbortError'));
199+
returnnewPromise((resolve,reject)=>{
166200
consttimeout=newTimeout(resolve,after,args,false,true);
167201
insert(timeout,timeout._idleTimeout);
202+
if(signal){
203+
signal.addEventListener('abort',()=>{
204+
clearTimeout(timeout);
205+
reject(lazyDOMException('AbortError'));
206+
},{once: true});
207+
}
168208
});
169209
};
170210

@@ -300,8 +340,39 @@ function setImmediate(callback, arg1, arg2, arg3) {
300340
returnnewImmediate(callback,args);
301341
}
302342

303-
setImmediate[customPromisify]=function(value){
304-
returnnewPromise((resolve)=>newImmediate(resolve,[value]));
343+
setImmediate[customPromisify]=function(value,options={}){
344+
if(options==null||typeofoptions!=='object'){
345+
returnPromise.reject(
346+
newERR_INVALID_ARG_TYPE(
347+
'options',
348+
'Object',
349+
options));
350+
}
351+
const{ signal }=options;
352+
if(signal!==undefined&&
353+
(signal===null||
354+
typeofsignal!=='object'||
355+
!('aborted'insignal))){
356+
returnPromise.reject(
357+
newERR_INVALID_ARG_TYPE(
358+
'options.signal',
359+
'AbortSignal',
360+
signal));
361+
}
362+
// TODO(@jasnell): If a decision is made that this cannot be backported
363+
// to 12.x, then this can be converted to use optional chaining to
364+
// simplify the check.
365+
if(signal&&signal.aborted)
366+
returnPromise.reject(lazyDOMException('AbortError'));
367+
returnnewPromise((resolve,reject)=>{
368+
constimmediate=newImmediate(resolve,[value]);
369+
if(signal){
370+
signal.addEventListener('abort',()=>{
371+
clearImmediate(immediate);
372+
reject(lazyDOMException('AbortError'));
373+
},{once: true});
374+
}
375+
});
305376
};
306377

307378
functionclearImmediate(immediate){

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Flags: --no-warnings --experimental-abortcontroller
12
'use strict';
23
constcommon=require('../common');
34
constassert=require('assert');
@@ -36,3 +37,56 @@ const setImmediate = promisify(timers.setImmediate);
3637
assert.strictEqual(value,'foobar');
3738
}));
3839
}
40+
41+
{
42+
constac=newAbortController();
43+
constsignal=ac.signal;
44+
assert.rejects(setTimeout(10,undefined,{ signal }),/AbortError/);
45+
ac.abort();
46+
}
47+
48+
{
49+
constac=newAbortController();
50+
constsignal=ac.signal;
51+
ac.abort();// Abort in advance
52+
assert.rejects(setTimeout(10,undefined,{ signal }),/AbortError/);
53+
}
54+
55+
{
56+
constac=newAbortController();
57+
constsignal=ac.signal;
58+
assert.rejects(setImmediate(10,{ signal }),/AbortError/);
59+
ac.abort();
60+
}
61+
62+
{
63+
constac=newAbortController();
64+
constsignal=ac.signal;
65+
ac.abort();// Abort in advance
66+
assert.rejects(setImmediate(10,{ signal }),/AbortError/);
67+
}
68+
69+
{
70+
Promise.all(
71+
[1,'',false,Infinity].map((i)=>assert.rejects(setImmediate(10,i)),{
72+
code: 'ERR_INVALID_ARG_TYPE'
73+
})).then(common.mustCall());
74+
75+
Promise.all(
76+
[1,'',false,Infinity,null,{}].map(
77+
(signal)=>assert.rejects(setImmediate(10,{ signal })),{
78+
code: 'ERR_INVALID_ARG_TYPE'
79+
})).then(common.mustCall());
80+
81+
Promise.all(
82+
[1,'',false,Infinity].map(
83+
(i)=>assert.rejects(setTimeout(10,null,i)),{
84+
code: 'ERR_INVALID_ARG_TYPE'
85+
})).then(common.mustCall());
86+
87+
Promise.all(
88+
[1,'',false,Infinity,null,{}].map(
89+
(signal)=>assert.rejects(setTimeout(10,null,{ signal })),{
90+
code: 'ERR_INVALID_ARG_TYPE'
91+
})).then(common.mustCall());
92+
}

0 commit comments

Comments
 (0)