Skip to content

Commit ed3f0f9

Browse files
committed
lib: add AbortSignal.timeout
Refs: whatwg/dom#1032 Signed-off-by: James M Snell <[email protected]>
1 parent 7633c86 commit ed3f0f9

File tree

3 files changed

+96
-1
lines changed

3 files changed

+96
-1
lines changed

doc/api/globals.md

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

105105
Returns a new already aborted `AbortSignal`.
106106

107+
#### Static method: `AbortSignal.timeout(delay)`
108+
109+
<!-- YAML
110+
added: REPLACEME
111+
-->
112+
113+
* `delay` {number} The number of milliseconds to wait before triggering
114+
the AbortSignal.
115+
116+
Returns a new `AbortSignal` which will be aborted in `delay` milliseconds.
117+
107118
#### Event: `'abort'`
108119

109120
<!-- YAML

lib/internal/abort_controller.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ const {
88
ObjectDefineProperties,
99
ObjectSetPrototypeOf,
1010
ObjectDefineProperty,
11+
SafeFinalizationRegistry,
1112
Symbol,
1213
SymbolToStringTag,
14+
WeakRef,
1315
} = primordials;
1416

1517
const {
@@ -29,9 +31,24 @@ const {
2931
}
3032
} = require('internal/errors');
3133

34+
const {
35+
validateUint32,
36+
} = require('internal/validators');
37+
38+
const {
39+
DOMException,
40+
} = internalBinding('messaging');
41+
42+
const {
43+
clearTimeout,
44+
setTimeout,
45+
} = require('timers');
46+
3247
const kAborted = Symbol('kAborted');
3348
const kReason = Symbol('kReason');
3449

50+
const clearTimeoutRegistry = new SafeFinalizationRegistry(clearTimeout);
51+
3552
function customInspect(self, obj, depth, options) {
3653
if (depth < 0)
3754
return self;
@@ -48,6 +65,29 @@ function validateAbortSignal(obj) {
4865
throw new ERR_INVALID_THIS('AbortSignal');
4966
}
5067

68+
// Because the AbortSignal timeout cannot be canceled, we don't want the
69+
// presence of the timer alone to keep the AbortSignal from being garbage
70+
// collected if it otherwise no longer accessible. We also don't want the
71+
// timer to keep the Node.js process open on it's own. Therefore, we wrap
72+
// the AbortSignal in a WeakRef and have the setTimeout callback close
73+
// over the WeakRef rather than directly over the AbortSignal, and we unref
74+
// the created timer object. Separately, we add the signal to a
75+
// FinalizerRegistry that will clear the timeout when the signal is gc'd.
76+
function setWeakAbortSignalTimeout(weakRef, delay) {
77+
const timeout = setTimeout(() => {
78+
const signal = weakRef.deref();
79+
if (signal !== undefined) {
80+
abortSignal(
81+
signal,
82+
new DOMException(
83+
'The operation was aborted due to timeout',
84+
'TimeoutError'));
85+
}
86+
}, delay);
87+
timeout.unref();
88+
return timeout;
89+
}
90+
5191
class AbortSignal extends EventTarget {
5292
constructor() {
5393
throw new ERR_ILLEGAL_CONSTRUCTOR();
@@ -82,6 +122,19 @@ class AbortSignal extends EventTarget {
82122
static abort(reason) {
83123
return createAbortSignal(true, reason);
84124
}
125+
126+
/**
127+
* @param {number} delay
128+
* @returns {AbortSignal}
129+
*/
130+
static timeout(delay) {
131+
validateUint32(delay, 'delay', true);
132+
const signal = createAbortSignal();
133+
clearTimeoutRegistry.register(
134+
signal,
135+
setWeakAbortSignalTimeout(new WeakRef(signal), delay));
136+
return signal;
137+
}
85138
}
86139

87140
ObjectDefineProperties(AbortSignal.prototype, {

test/parallel/test-abortcontroller.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
// Flags: --no-warnings
1+
// Flags: --no-warnings --expose-gc
22
'use strict';
33

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

77
const { ok, strictEqual, throws } = require('assert');
8+
const { setTimeout: sleep } = require('timers/promises');
89

910
{
1011
// Tests that abort is fired with the correct event type on AbortControllers
@@ -153,3 +154,33 @@ const { ok, strictEqual, throws } = require('assert');
153154
const signal = AbortSignal.abort('reason');
154155
strictEqual(signal.reason, 'reason');
155156
}
157+
158+
{
159+
// Test AbortSignal timeout
160+
const signal = AbortSignal.timeout(10);
161+
ok(!signal.aborted);
162+
setTimeout(common.mustCall(() => {
163+
ok(signal.aborted);
164+
strictEqual(signal.reason.name, 'TimeoutError');
165+
strictEqual(signal.reason.code, 23);
166+
}), 20);
167+
}
168+
169+
{
170+
(async () => {
171+
// Test AbortSignal timeout doesn't prevent the signal
172+
// from being garbage collected.
173+
let ref;
174+
{
175+
ref = new globalThis.WeakRef(AbortSignal.timeout(1_200_000));
176+
}
177+
178+
await sleep(10);
179+
globalThis.gc();
180+
strictEqual(ref.deref(), undefined);
181+
})().then(common.mustCall());
182+
183+
// Setting a long timeout (20 minutes here) should not
184+
// keep the Node.js process open (the timer is unref'd)
185+
AbortSignal.timeout(1_200_000);
186+
}

0 commit comments

Comments
 (0)