Skip to content

Commit 157753b

Browse files
jasnelldanielleadams
authored andcommitted
lib: add AbortSignal.timeout
Refs: whatwg/dom#1032 Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: #40899 Reviewed-By: Michaël Zasso <targos@protonmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 78a8c1b commit 157753b

3 files changed

Lines changed: 174 additions & 3 deletions

File tree

‎doc/api/globals.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,17 @@ changes:
9898

9999
Returns a new already aborted `AbortSignal`.
100100

101+
#### Static method: `AbortSignal.timeout(delay)`
102+
103+
<!-- YAML
104+
added: REPLACEME
105+
-->
106+
107+
*`delay` {number} The number of milliseconds to wait before triggering
108+
the AbortSignal.
109+
110+
Returns a new `AbortSignal` which will be aborted in `delay` milliseconds.
111+
101112
#### Event: `'abort'`
102113

103114
<!-- YAML

‎lib/internal/abort_controller.js‎

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,20 @@ const {
88
ObjectDefineProperties,
99
ObjectSetPrototypeOf,
1010
ObjectDefineProperty,
11+
SafeFinalizationRegistry,
12+
SafeSet,
1113
Symbol,
1214
SymbolToStringTag,
15+
WeakRef,
1316
}=primordials;
1417

1518
const{
1619
defineEventHandler,
1720
EventTarget,
1821
Event,
19-
kTrustEvent
22+
kTrustEvent,
23+
kNewListener,
24+
kRemoveListener,
2025
}=require('internal/event_target');
2126
const{
2227
customInspectSymbol,
@@ -29,8 +34,26 @@ const {
2934
}
3035
}=require('internal/errors');
3136

37+
const{
38+
validateUint32,
39+
}=require('internal/validators');
40+
41+
const{
42+
DOMException,
43+
}=internalBinding('messaging');
44+
45+
const{
46+
clearTimeout,
47+
setTimeout,
48+
}=require('timers');
49+
3250
constkAborted=Symbol('kAborted');
3351
constkReason=Symbol('kReason');
52+
constkTimeout=Symbol('kTimeout');
53+
54+
consttimeOutSignals=newSafeSet();
55+
56+
constclearTimeoutRegistry=newSafeFinalizationRegistry(clearTimeout);
3457

3558
functioncustomInspect(self,obj,depth,options){
3659
if(depth<0)
@@ -48,6 +71,30 @@ function validateAbortSignal(obj) {
4871
thrownewERR_INVALID_THIS('AbortSignal');
4972
}
5073

74+
// Because the AbortSignal timeout cannot be canceled, we don't want the
75+
// presence of the timer alone to keep the AbortSignal from being garbage
76+
// collected if it otherwise no longer accessible. We also don't want the
77+
// timer to keep the Node.js process open on it's own. Therefore, we wrap
78+
// the AbortSignal in a WeakRef and have the setTimeout callback close
79+
// over the WeakRef rather than directly over the AbortSignal, and we unref
80+
// the created timer object. Separately, we add the signal to a
81+
// FinalizerRegistry that will clear the timeout when the signal is gc'd.
82+
functionsetWeakAbortSignalTimeout(weakRef,delay){
83+
consttimeout=setTimeout(()=>{
84+
constsignal=weakRef.deref();
85+
if(signal!==undefined){
86+
timeOutSignals.delete(signal);
87+
abortSignal(
88+
signal,
89+
newDOMException(
90+
'The operation was aborted due to timeout',
91+
'TimeoutError'));
92+
}
93+
},delay);
94+
timeout.unref();
95+
returntimeout;
96+
}
97+
5198
classAbortSignalextendsEventTarget{
5299
constructor(){
53100
thrownewERR_ILLEGAL_CONSTRUCTOR();
@@ -82,6 +129,42 @@ class AbortSignal extends EventTarget {
82129
staticabort(reason){
83130
returncreateAbortSignal(true,reason);
84131
}
132+
133+
/**
134+
* @param {number} delay
135+
* @returns {AbortSignal}
136+
*/
137+
statictimeout(delay){
138+
validateUint32(delay,'delay',true);
139+
constsignal=createAbortSignal();
140+
signal[kTimeout]=true;
141+
clearTimeoutRegistry.register(
142+
signal,
143+
setWeakAbortSignalTimeout(newWeakRef(signal),delay));
144+
returnsignal;
145+
}
146+
147+
[kNewListener](size,type,listener,once,capture,passive,weak){
148+
super[kNewListener](size,type,listener,once,capture,passive,weak);
149+
if(this[kTimeout]&&
150+
type==='abort'&&
151+
!this.aborted&&
152+
!weak&&
153+
size===1){
154+
// If this is a timeout signal, and we're adding a non-weak abort
155+
// listener, then we don't want it to be gc'd while the listener
156+
// is attached and the timer still hasn't fired. So, we retain a
157+
// strong ref that is held for as long as the listener is registered.
158+
timeOutSignals.add(this);
159+
}
160+
}
161+
162+
[kRemoveListener](size,type,listener,capture){
163+
super[kRemoveListener](size,type,listener,capture);
164+
if(this[kTimeout]&&type==='abort'&&size===0){
165+
timeOutSignals.delete(this);
166+
}
167+
}
85168
}
86169

87170
ObjectDefineProperties(AbortSignal.prototype,{

‎test/parallel/test-abortcontroller.js‎

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1-
// Flags: --no-warnings
1+
// Flags: --no-warnings --expose-gc --expose-internals
22
'use strict';
33

44
constcommon=require('../common');
55
const{ inspect }=require('util');
66

7-
const{ ok, strictEqual, throws }=require('assert');
7+
const{
8+
ok,
9+
notStrictEqual,
10+
strictEqual,
11+
throws,
12+
}=require('assert');
13+
14+
const{
15+
kWeakHandler,
16+
}=require('internal/event_target');
17+
18+
const{setTimeout: sleep}=require('timers/promises');
819

920
{
1021
// Tests that abort is fired with the correct event type on AbortControllers
@@ -153,3 +164,69 @@ const { ok, strictEqual, throws } = require('assert');
153164
constsignal=AbortSignal.abort('reason');
154165
strictEqual(signal.reason,'reason');
155166
}
167+
168+
{
169+
// Test AbortSignal timeout
170+
constsignal=AbortSignal.timeout(10);
171+
ok(!signal.aborted);
172+
setTimeout(common.mustCall(()=>{
173+
ok(signal.aborted);
174+
strictEqual(signal.reason.name,'TimeoutError');
175+
strictEqual(signal.reason.code,23);
176+
}),20);
177+
}
178+
179+
{
180+
(async()=>{
181+
// Test AbortSignal timeout doesn't prevent the signal
182+
// from being garbage collected.
183+
letref;
184+
{
185+
ref=newglobalThis.WeakRef(AbortSignal.timeout(1_200_000));
186+
}
187+
188+
awaitsleep(10);
189+
globalThis.gc();
190+
strictEqual(ref.deref(),undefined);
191+
})().then(common.mustCall());
192+
193+
(async()=>{
194+
// Test that an AbortSignal with a timeout is not gc'd while
195+
// there is an active listener on it.
196+
letref;
197+
functionhandler(){}
198+
{
199+
ref=newglobalThis.WeakRef(AbortSignal.timeout(1_200_000));
200+
ref.deref().addEventListener('abort',handler);
201+
}
202+
203+
awaitsleep(10);
204+
globalThis.gc();
205+
notStrictEqual(ref.deref(),undefined);
206+
ok(ref.deref()instanceofAbortSignal);
207+
208+
ref.deref().removeEventListener('abort',handler);
209+
210+
awaitsleep(10);
211+
globalThis.gc();
212+
strictEqual(ref.deref(),undefined);
213+
})().then(common.mustCall());
214+
215+
(async()=>{
216+
// If the event listener is weak, however, it should not prevent gc
217+
letref;
218+
functionhandler(){}
219+
{
220+
ref=newglobalThis.WeakRef(AbortSignal.timeout(1_200_000));
221+
ref.deref().addEventListener('abort',handler,{[kWeakHandler]: {}});
222+
}
223+
224+
awaitsleep(10);
225+
globalThis.gc();
226+
strictEqual(ref.deref(),undefined);
227+
})().then(common.mustCall());
228+
229+
// Setting a long timeout (20 minutes here) should not
230+
// keep the Node.js process open (the timer is unref'd)
231+
AbortSignal.timeout(1_200_000);
232+
}

0 commit comments

Comments
 (0)