Skip to content

Commit 413e398

Browse files
committed
lib: implement AbortSignal.any()
1 parent ab434d2 commit 413e398

File tree

7 files changed

+287
-13
lines changed

7 files changed

+287
-13
lines changed

doc/api/globals.md

+13
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ added:
121121

122122
Returns a new `AbortSignal` which will be aborted in `delay` milliseconds.
123123

124+
#### Static method: `AbortSignal.any(signals)`
125+
126+
<!-- YAML
127+
added: REPLACEME
128+
-->
129+
130+
* `signals` {Array} The `AbortSignal`s of which to compose a new `AbortSignal`.
131+
132+
Returns a new `AbortSignal` which will be aborted if any of the provided
133+
signals are aborted. Its [`abortSignal.reason`][] will be set to whichever
134+
one of the `signals` caused it to be aborted.
135+
124136
#### Event: `'abort'`
125137

126138
<!-- YAML
@@ -1043,3 +1055,4 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
10431055
[timers]: timers.md
10441056
[webassembly-mdn]: https://developer.mozilla.org/en-US/docs/WebAssembly
10451057
[webassembly-org]: https://webassembly.org
1058+
[`abortSignal.reason`]: #abortSignal.reason

lib/internal/abort_controller.js

+64-9
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const {
4141
} = require('internal/errors');
4242

4343
const {
44-
validateAbortSignal,
44+
validateAbortSignalArray,
4545
validateObject,
4646
validateUint32,
4747
} = require('internal/validators');
@@ -54,6 +54,7 @@ const {
5454
clearTimeout,
5555
setTimeout,
5656
} = require('timers');
57+
const assert = require('internal/assert');
5758

5859
const {
5960
messaging_deserialize_symbol: kDeserialize,
@@ -80,13 +81,16 @@ function lazyMakeTransferable(obj) {
8081
}
8182

8283
const clearTimeoutRegistry = new SafeFinalizationRegistry(clearTimeout);
83-
const timeOutSignals = new SafeSet();
84+
const gcPersistentSignals = new SafeSet();
8485

8586
const kAborted = Symbol('kAborted');
8687
const kReason = Symbol('kReason');
8788
const kCloneData = Symbol('kCloneData');
8889
const kTimeout = Symbol('kTimeout');
8990
const kMakeTransferable = Symbol('kMakeTransferable');
91+
const kComposite = Symbol('kComposite');
92+
const kSourceSignals = Symbol('kSourceSignals');
93+
const kDependantSignals = Symbol('kDependantSignals');
9094

9195
function customInspect(self, obj, depth, options) {
9296
if (depth < 0)
@@ -116,7 +120,7 @@ function setWeakAbortSignalTimeout(weakRef, delay) {
116120
const timeout = setTimeout(() => {
117121
const signal = weakRef.deref();
118122
if (signal !== undefined) {
119-
timeOutSignals.delete(signal);
123+
gcPersistentSignals.delete(signal);
120124
abortSignal(
121125
signal,
122126
new DOMException(
@@ -185,25 +189,68 @@ class AbortSignal extends EventTarget {
185189
return signal;
186190
}
187191

192+
/**
193+
* @param {AbortSignal[]} signals
194+
* @returns {AbortSignal}
195+
*/
196+
static any(signals) {
197+
validateAbortSignalArray(signals, 'signals');
198+
const resultSignal = createAbortSignal({ composite: true });
199+
const resultSignalWeakRef = new WeakRef(resultSignal);
200+
for (const signal of signals) {
201+
if (signal.aborted) {
202+
abortSignal(resultSignal, signal.reason);
203+
return resultSignal;
204+
}
205+
resultSignal[kSourceSignals] ??= new SafeSet();
206+
signal[kDependantSignals] ??= new SafeSet();
207+
if (!signal[kComposite]) {
208+
resultSignal[kSourceSignals].add(new WeakRef(signal));
209+
signal[kDependantSignals].add(resultSignalWeakRef);
210+
} else {
211+
if (!signal[kSourceSignals]) {
212+
continue;
213+
}
214+
for (const sourceSignal of signal[kSourceSignals]) {
215+
const sourceSignalRef = sourceSignal.deref();
216+
if (!sourceSignalRef) {
217+
continue;
218+
}
219+
assert(!sourceSignalRef.aborted);
220+
assert(!sourceSignalRef[kComposite]);
221+
222+
if (resultSignal[kSourceSignals].has(sourceSignal)) {
223+
continue;
224+
}
225+
resultSignal[kSourceSignals].add(sourceSignal);
226+
sourceSignalRef[kDependantSignals].add(resultSignalWeakRef);
227+
}
228+
}
229+
}
230+
return resultSignal;
231+
}
232+
188233
[kNewListener](size, type, listener, once, capture, passive, weak) {
189234
super[kNewListener](size, type, listener, once, capture, passive, weak);
190-
if (this[kTimeout] &&
235+
const isTimeoutOrNonEmptyCompositeSignal = this[kTimeout] || (this[kComposite] && this[kSourceSignals]?.size);
236+
if (isTimeoutOrNonEmptyCompositeSignal &&
191237
type === 'abort' &&
192238
!this.aborted &&
193239
!weak &&
194240
size === 1) {
195-
// If this is a timeout signal, and we're adding a non-weak abort
241+
// If this is a timeout signal, or a non-empty composite signal, and we're adding a non-weak abort
196242
// listener, then we don't want it to be gc'd while the listener
197243
// is attached and the timer still hasn't fired. So, we retain a
198244
// strong ref that is held for as long as the listener is registered.
199-
timeOutSignals.add(this);
245+
gcPersistentSignals.add(this);
200246
}
201247
}
202248

203249
[kRemoveListener](size, type, listener, capture) {
204250
super[kRemoveListener](size, type, listener, capture);
205-
if (this[kTimeout] && type === 'abort' && size === 0) {
206-
timeOutSignals.delete(this);
251+
const isTimeoutOrNonEmptyCompositeSignal = this[kTimeout] || (this[kComposite] && this[kSourceSignals]?.size);
252+
if (isTimeoutOrNonEmptyCompositeSignal && type === 'abort' && size === 0) {
253+
gcPersistentSignals.delete(this);
207254
}
208255
}
209256

@@ -287,7 +334,8 @@ defineEventHandler(AbortSignal.prototype, 'abort');
287334
* @param {{
288335
* aborted? : boolean,
289336
* reason? : any,
290-
* transferable? : boolean
337+
* transferable? : boolean,
338+
* composite? : boolean,
291339
* }} [init]
292340
* @returns {AbortSignal}
293341
*/
@@ -296,11 +344,13 @@ function createAbortSignal(init = kEmptyObject) {
296344
aborted = false,
297345
reason = undefined,
298346
transferable = false,
347+
composite = false,
299348
} = init;
300349
const signal = new EventTarget();
301350
ObjectSetPrototypeOf(signal, AbortSignal.prototype);
302351
signal[kAborted] = aborted;
303352
signal[kReason] = reason;
353+
signal[kComposite] = composite;
304354
return transferable ? lazyMakeTransferable(signal) : signal;
305355
}
306356

@@ -312,6 +362,11 @@ function abortSignal(signal, reason) {
312362
[kTrustEvent]: true,
313363
});
314364
signal.dispatchEvent(event);
365+
signal[kDependantSignals]?.forEach(s => {
366+
const signalRef = s.deref();
367+
if (!signalRef) return;
368+
abortSignal(signalRef, reason);
369+
});
315370
}
316371

317372
class AbortController {

lib/internal/validators.js

+19-2
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,21 @@ function validateBooleanArray(value, name) {
324324
}
325325
}
326326

327+
/**
328+
* @callback validateAbortSignalArray
329+
* @param {*} value
330+
* @param {string} name
331+
* @returns {asserts value is AbortSignal[]}
332+
*/
333+
334+
/** @type {validateAbortSignalArray} */
335+
function validateAbortSignalArray(value, name) {
336+
validateArray(value, name);
337+
for (let i = 0; i < value.length; i++) {
338+
validateAbortSignal(value[i], `${name}[${i}]`, false);
339+
}
340+
}
341+
327342
/**
328343
* @param {*} signal
329344
* @param {string} [name='signal']
@@ -395,11 +410,12 @@ function validatePort(port, name = 'Port', allowZero = true) {
395410
* @callback validateAbortSignal
396411
* @param {*} signal
397412
* @param {string} name
413+
* @param {boolean} [acceptUndefined=true]
398414
*/
399415

400416
/** @type {validateAbortSignal} */
401-
const validateAbortSignal = hideStackFrames((signal, name) => {
402-
if (signal !== undefined &&
417+
const validateAbortSignal = hideStackFrames((signal, name, acceptUndefined = true) => {
418+
if ((!acceptUndefined || signal !== undefined) &&
403419
(signal === null ||
404420
typeof signal !== 'object' ||
405421
!('aborted' in signal))) {
@@ -534,6 +550,7 @@ module.exports = {
534550
validateArray,
535551
validateStringArray,
536552
validateBooleanArray,
553+
validateAbortSignalArray,
537554
validateBoolean,
538555
validateBuffer,
539556
validateDictionary,

test/fixtures/wpt/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Last update:
1212

1313
- common: https://github.com/web-platform-tests/wpt/tree/dbd648158d/common
1414
- console: https://github.com/web-platform-tests/wpt/tree/767ae35464/console
15-
- dom/abort: https://github.com/web-platform-tests/wpt/tree/8fadb38120/dom/abort
15+
- dom/abort: https://github.com/web-platform-tests/wpt/tree/d1f1ecbd52/dom/abort
1616
- dom/events: https://github.com/web-platform-tests/wpt/tree/ab8999891c/dom/events
1717
- encoding: https://github.com/web-platform-tests/wpt/tree/0c1b9d1622/encoding
1818
- fetch/data-urls/resources: https://github.com/web-platform-tests/wpt/tree/7c79d998ff/fetch/data-urls/resources
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// META: script=./resources/abort-signal-any-tests.js
2+
3+
abortSignalAnySignalOnlyTests(AbortSignal);
4+
abortSignalAnyTests(AbortSignal, AbortController);

0 commit comments

Comments
 (0)