Skip to content

Commit efaa073

Browse files
atlowChemidanielleadams
authored andcommittedJul 17, 2023
lib: implement AbortSignal.any()
PR-URL: #47821 Backport-PR-URL: #48800 Fixes: #47811 Refs: whatwg/dom#1152 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent d002f9b commit efaa073

File tree

10 files changed

+410
-19
lines changed

10 files changed

+410
-19
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` {AbortSignal\[]} 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
@@ -906,6 +918,7 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
906918
[`WritableStream`]: webstreams.md#class-writablestream
907919
[`__dirname`]: modules.md#__dirname
908920
[`__filename`]: modules.md#__filename
921+
[`abortSignal.reason`]: #abortsignalreason
909922
[`buffer.atob()`]: buffer.md#bufferatobdata
910923
[`buffer.btoa()`]: buffer.md#bufferbtoadata
911924
[`clearImmediate`]: timers.md#clearimmediateimmediate

‎lib/internal/abort_controller.js

+67-12
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const {
4242

4343
const {
4444
validateAbortSignal,
45+
validateAbortSignalArray,
4546
validateObject,
4647
validateUint32,
4748
} = require('internal/validators');
@@ -54,6 +55,7 @@ const {
5455
clearTimeout,
5556
setTimeout,
5657
} = require('timers');
58+
const assert = require('internal/assert');
5759

5860
const {
5961
messaging_deserialize_symbol: kDeserialize,
@@ -80,13 +82,16 @@ function lazyMakeTransferable(obj) {
8082
}
8183

8284
const clearTimeoutRegistry = new SafeFinalizationRegistry(clearTimeout);
83-
const timeOutSignals = new SafeSet();
85+
const gcPersistentSignals = new SafeSet();
8486

8587
const kAborted = Symbol('kAborted');
8688
const kReason = Symbol('kReason');
8789
const kCloneData = Symbol('kCloneData');
8890
const kTimeout = Symbol('kTimeout');
8991
const kMakeTransferable = Symbol('kMakeTransferable');
92+
const kComposite = Symbol('kComposite');
93+
const kSourceSignals = Symbol('kSourceSignals');
94+
const kDependantSignals = Symbol('kDependantSignals');
9095

9196
function customInspect(self, obj, depth, options) {
9297
if (depth < 0)
@@ -116,7 +121,7 @@ function setWeakAbortSignalTimeout(weakRef, delay) {
116121
const timeout = setTimeout(() => {
117122
const signal = weakRef.deref();
118123
if (signal !== undefined) {
119-
timeOutSignals.delete(signal);
124+
gcPersistentSignals.delete(signal);
120125
abortSignal(
121126
signal,
122127
new DOMException(
@@ -185,25 +190,68 @@ class AbortSignal extends EventTarget {
185190
return signal;
186191
}
187192

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

203250
[kRemoveListener](size, type, listener, capture) {
204251
super[kRemoveListener](size, type, listener, capture);
205-
if (this[kTimeout] && type === 'abort' && size === 0) {
206-
timeOutSignals.delete(this);
252+
const isTimeoutOrNonEmptyCompositeSignal = this[kTimeout] || (this[kComposite] && this[kSourceSignals]?.size);
253+
if (isTimeoutOrNonEmptyCompositeSignal && type === 'abort' && size === 0) {
254+
gcPersistentSignals.delete(this);
207255
}
208256
}
209257

@@ -287,7 +335,8 @@ defineEventHandler(AbortSignal.prototype, 'abort');
287335
* @param {{
288336
* aborted? : boolean,
289337
* reason? : any,
290-
* transferable? : boolean
338+
* transferable? : boolean,
339+
* composite? : boolean,
291340
* }} [init]
292341
* @returns {AbortSignal}
293342
*/
@@ -296,11 +345,13 @@ function createAbortSignal(init = kEmptyObject) {
296345
aborted = false,
297346
reason = undefined,
298347
transferable = false,
348+
composite = false,
299349
} = init;
300350
const signal = new EventTarget();
301351
ObjectSetPrototypeOf(signal, AbortSignal.prototype);
302352
signal[kAborted] = aborted;
303353
signal[kReason] = reason;
354+
signal[kComposite] = composite;
304355
return transferable ? lazyMakeTransferable(signal) : signal;
305356
}
306357

@@ -312,6 +363,10 @@ function abortSignal(signal, reason) {
312363
[kTrustEvent]: true,
313364
});
314365
signal.dispatchEvent(event);
366+
signal[kDependantSignals]?.forEach((s) => {
367+
const signalRef = s.deref();
368+
if (signalRef) abortSignal(signalRef, reason);
369+
});
315370
}
316371

317372
// TODO(joyeecheung): use private fields and we'll get invalid access

‎lib/internal/validators.js

+21
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,26 @@ 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+
const signal = value[i];
339+
const indexedName = `${name}[${i}]`;
340+
if (signal == null) {
341+
throw new ERR_INVALID_ARG_TYPE(indexedName, 'AbortSignal', signal);
342+
}
343+
validateAbortSignal(signal, indexedName);
344+
}
345+
}
346+
327347
/**
328348
* @param {*} signal
329349
* @param {string} [name='signal']
@@ -528,6 +548,7 @@ module.exports = {
528548
validateArray,
529549
validateStringArray,
530550
validateBooleanArray,
551+
validateAbortSignalArray,
531552
validateBoolean,
532553
validateBuffer,
533554
validateDictionary,

‎test/common/wpt.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ class WPTRunner {
614614

615615
process.on('exit', () => {
616616
for (const spec of this.inProgress) {
617-
this.fail(spec, { name: 'Unknown' }, kIncomplete);
617+
this.fail(spec, { name: 'Incomplete' }, kIncomplete);
618618
}
619619
inspect.defaultOptions.depth = Infinity;
620620
// Sorts the rules to have consistent output
@@ -738,11 +738,11 @@ class WPTRunner {
738738
* @param {object} harnessStatus - The status object returned by WPT harness.
739739
*/
740740
completionCallback(filename, harnessStatus) {
741+
const status = this.getTestStatus(harnessStatus.status);
742+
741743
// Treat it like a test case failure
742-
if (harnessStatus.status === 2) {
743-
const title = this.getTestTitle(filename);
744-
console.log(`---- ${title} ----`);
745-
this.resultCallback(filename, { status: 2, name: 'Unknown' });
744+
if (status === kTimeout) {
745+
this.fail(filename, { name: 'WPT testharness timeout' }, kTimeout);
746746
}
747747
this.inProgress.delete(filename);
748748
// Always force termination of the worker. Some tests allocate resources

‎test/common/wpt/worker.js

+9
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,17 @@ add_result_callback((result) => {
4141
});
4242
});
4343

44+
// Keep the event loop alive
45+
const timeout = setTimeout(() => {
46+
parentPort.postMessage({
47+
type: 'completion',
48+
status: { status: 2 },
49+
});
50+
}, 2 ** 31 - 1); // Max timeout is 2^31-1, when overflown the timeout is set to 1.
51+
4452
// eslint-disable-next-line no-undef
4553
add_completion_callback((_, status) => {
54+
clearTimeout(timeout);
4655
parentPort.postMessage({
4756
type: 'completion',
4857
status,

‎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/03c5072aff/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);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Tests for AbortSignal.any() and subclasses that don't use a controller.
2+
function abortSignalAnySignalOnlyTests(signalInterface) {
3+
const desc = `${signalInterface.name}.any()`
4+
5+
test(t => {
6+
const signal = signalInterface.any([]);
7+
assert_false(signal.aborted);
8+
}, `${desc} works with an empty array of signals`);
9+
}
10+
11+
// Tests for AbortSignal.any() and subclasses that use a controller.
12+
function abortSignalAnyTests(signalInterface, controllerInterface) {
13+
const suffix = `(using ${controllerInterface.name})`;
14+
const desc = `${signalInterface.name}.any()`;
15+
16+
test(t => {
17+
const controller = new controllerInterface();
18+
const signal = controller.signal;
19+
const cloneSignal = signalInterface.any([signal]);
20+
assert_false(cloneSignal.aborted);
21+
assert_true("reason" in cloneSignal, "cloneSignal has reason property");
22+
assert_equals(cloneSignal.reason, undefined,
23+
"cloneSignal.reason is initially undefined");
24+
assert_not_equals(signal, cloneSignal,
25+
`${desc} returns a new signal.`);
26+
27+
let eventFired = false;
28+
cloneSignal.onabort = t.step_func((e) => {
29+
assert_equals(e.target, cloneSignal,
30+
`The event target is the signal returned by ${desc}`);
31+
eventFired = true;
32+
});
33+
34+
controller.abort("reason string");
35+
assert_true(signal.aborted);
36+
assert_true(cloneSignal.aborted);
37+
assert_true(eventFired);
38+
assert_equals(cloneSignal.reason, "reason string",
39+
`${desc} propagates the abort reason`);
40+
}, `${desc} follows a single signal ${suffix}`);
41+
42+
test(t => {
43+
for (let i = 0; i < 3; ++i) {
44+
const controllers = [];
45+
for (let j = 0; j < 3; ++j) {
46+
controllers.push(new controllerInterface());
47+
}
48+
const combinedSignal = signalInterface.any(controllers.map(c => c.signal));
49+
50+
let eventFired = false;
51+
combinedSignal.onabort = t.step_func((e) => {
52+
assert_equals(e.target, combinedSignal,
53+
`The event target is the signal returned by ${desc}`);
54+
eventFired = true;
55+
});
56+
57+
controllers[i].abort();
58+
assert_true(eventFired);
59+
assert_true(combinedSignal.aborted);
60+
assert_true(combinedSignal.reason instanceof DOMException,
61+
"signal.reason is a DOMException");
62+
assert_equals(combinedSignal.reason.name, "AbortError",
63+
"signal.reason is a AbortError");
64+
}
65+
}, `${desc} follows multiple signals ${suffix}`);
66+
67+
test(t => {
68+
const controllers = [];
69+
for (let i = 0; i < 3; ++i) {
70+
controllers.push(new controllerInterface());
71+
}
72+
controllers[1].abort("reason 1");
73+
controllers[2].abort("reason 2");
74+
75+
const signal = signalInterface.any(controllers.map(c => c.signal));
76+
assert_true(signal.aborted);
77+
assert_equals(signal.reason, "reason 1",
78+
"The signal should be aborted with the first reason");
79+
}, `${desc} returns an aborted signal if passed an aborted signal ${suffix}`);
80+
81+
test(t => {
82+
const controller = new controllerInterface();
83+
const signal = signalInterface.any([controller.signal, controller.signal]);
84+
assert_false(signal.aborted);
85+
controller.abort("reason");
86+
assert_true(signal.aborted);
87+
assert_equals(signal.reason, "reason");
88+
}, `${desc} can be passed the same signal more than once ${suffix}`);
89+
90+
test(t => {
91+
const controller1 = new controllerInterface();
92+
controller1.abort("reason 1");
93+
const controller2 = new controllerInterface();
94+
controller2.abort("reason 2");
95+
96+
const signal = signalInterface.any([controller1.signal, controller2.signal, controller1.signal]);
97+
assert_true(signal.aborted);
98+
assert_equals(signal.reason, "reason 1");
99+
}, `${desc} uses the first instance of a duplicate signal ${suffix}`);
100+
101+
test(t => {
102+
for (let i = 0; i < 3; ++i) {
103+
const controllers = [];
104+
for (let j = 0; j < 3; ++j) {
105+
controllers.push(new controllerInterface());
106+
}
107+
const combinedSignal1 =
108+
signalInterface.any([controllers[0].signal, controllers[1].signal]);
109+
const combinedSignal2 =
110+
signalInterface.any([combinedSignal1, controllers[2].signal]);
111+
112+
let eventFired = false;
113+
combinedSignal2.onabort = t.step_func((e) => {
114+
eventFired = true;
115+
});
116+
117+
controllers[i].abort();
118+
assert_true(eventFired);
119+
assert_true(combinedSignal2.aborted);
120+
assert_true(combinedSignal2.reason instanceof DOMException,
121+
"signal.reason is a DOMException");
122+
assert_equals(combinedSignal2.reason.name, "AbortError",
123+
"signal.reason is a AbortError");
124+
}
125+
}, `${desc} signals are composable ${suffix}`);
126+
127+
async_test(t => {
128+
const controller = new controllerInterface();
129+
const timeoutSignal = AbortSignal.timeout(5);
130+
131+
const combinedSignal = signalInterface.any([controller.signal, timeoutSignal]);
132+
133+
combinedSignal.onabort = t.step_func_done(() => {
134+
assert_true(combinedSignal.aborted);
135+
assert_true(combinedSignal.reason instanceof DOMException,
136+
"combinedSignal.reason is a DOMException");
137+
assert_equals(combinedSignal.reason.name, "TimeoutError",
138+
"combinedSignal.reason is a TimeoutError");
139+
});
140+
}, `${desc} works with signals returned by AbortSignal.timeout() ${suffix}`);
141+
142+
test(t => {
143+
const controller = new controllerInterface();
144+
let combined = signalInterface.any([controller.signal]);
145+
combined = signalInterface.any([combined]);
146+
combined = signalInterface.any([combined]);
147+
combined = signalInterface.any([combined]);
148+
149+
let eventFired = false;
150+
combined.onabort = () => {
151+
eventFired = true;
152+
}
153+
154+
assert_false(eventFired);
155+
assert_false(combined.aborted);
156+
157+
controller.abort("the reason");
158+
159+
assert_true(eventFired);
160+
assert_true(combined.aborted);
161+
assert_equals(combined.reason, "the reason");
162+
}, `${desc} works with intermediate signals ${suffix}`);
163+
164+
test(t => {
165+
const controller = new controllerInterface();
166+
const signals = [];
167+
// The first event should be dispatched on the originating signal.
168+
signals.push(controller.signal);
169+
// All dependents are linked to `controller.signal` (never to another
170+
// composite signal), so this is the order events should fire.
171+
signals.push(signalInterface.any([controller.signal]));
172+
signals.push(signalInterface.any([controller.signal]));
173+
signals.push(signalInterface.any([signals[0]]));
174+
signals.push(signalInterface.any([signals[1]]));
175+
176+
let result = "";
177+
for (let i = 0; i < signals.length; i++) {
178+
signals[i].addEventListener('abort', () => {
179+
result += i;
180+
});
181+
}
182+
controller.abort();
183+
assert_equals(result, "01234");
184+
}, `Abort events for ${desc} signals fire in the right order ${suffix}`);
185+
}

‎test/fixtures/wpt/versions.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"path": "console"
99
},
1010
"dom/abort": {
11-
"commit": "8fadb381209a215280dc3ad96d0f135b6005f176",
11+
"commit": "d1f1ecbd52f2eab3b7fe5dc1b20b41174f1341ce",
1212
"path": "dom/abort"
1313
},
1414
"dom/events": {
+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import * as common from '../common/index.mjs';
2+
import { describe, it } from 'node:test';
3+
import { once } from 'node:events';
4+
import assert from 'node:assert';
5+
6+
describe('AbortSignal.any()', { concurrency: true }, () => {
7+
it('should throw when not receiving an array', () => {
8+
const expectedError = { code: 'ERR_INVALID_ARG_TYPE' };
9+
assert.throws(() => AbortSignal.any(), expectedError);
10+
assert.throws(() => AbortSignal.any(null), expectedError);
11+
assert.throws(() => AbortSignal.any(undefined), expectedError);
12+
});
13+
14+
it('should throw when input contains non-signal values', () => {
15+
assert.throws(
16+
() => AbortSignal.any([AbortSignal.abort(), undefined]),
17+
{
18+
code: 'ERR_INVALID_ARG_TYPE',
19+
message: 'The "signals[1]" argument must be an instance of AbortSignal. Received undefined'
20+
},
21+
);
22+
});
23+
24+
it('creates a non-aborted signal for an empty input', () => {
25+
const signal = AbortSignal.any([]);
26+
assert.strictEqual(signal.aborted, false);
27+
signal.addEventListener('abort', common.mustNotCall());
28+
});
29+
30+
it('returns a new signal', () => {
31+
const originalSignal = new AbortController().signal;
32+
const signalAny = AbortSignal.any([originalSignal]);
33+
assert.notStrictEqual(originalSignal, signalAny);
34+
});
35+
36+
it('returns an aborted signal if input has an aborted signal', () => {
37+
const signal = AbortSignal.any([AbortSignal.abort('some reason')]);
38+
assert.strictEqual(signal.aborted, true);
39+
assert.strictEqual(signal.reason, 'some reason');
40+
signal.addEventListener('abort', common.mustNotCall());
41+
});
42+
43+
it('returns an aborted signal with the reason of first aborted signal input', () => {
44+
const signal = AbortSignal.any([AbortSignal.abort('some reason'), AbortSignal.abort('another reason')]);
45+
assert.strictEqual(signal.aborted, true);
46+
assert.strictEqual(signal.reason, 'some reason');
47+
signal.addEventListener('abort', common.mustNotCall());
48+
});
49+
50+
it('returns the correct signal in the event target', async () => {
51+
const signal = AbortSignal.any([AbortSignal.timeout(5)]);
52+
const interval = setInterval(() => {}, 100000); // Keep event loop alive
53+
const [{ target }] = await once(signal, 'abort');
54+
clearInterval(interval);
55+
assert.strictEqual(target, signal);
56+
assert.ok(signal.aborted);
57+
assert.strictEqual(signal.reason.name, 'TimeoutError');
58+
});
59+
60+
it('aborts with reason of first aborted signal', () => {
61+
const controllers = Array.from({ length: 3 }, () => new AbortController());
62+
const combinedSignal = AbortSignal.any(controllers.map((c) => c.signal));
63+
controllers[1].abort(1);
64+
controllers[2].abort(2);
65+
assert.ok(combinedSignal.aborted);
66+
assert.strictEqual(combinedSignal.reason, 1);
67+
});
68+
69+
it('can accept the same signal more than once', () => {
70+
const controller = new AbortController();
71+
const signal = AbortSignal.any([controller.signal, controller.signal]);
72+
assert.strictEqual(signal.aborted, false);
73+
controller.abort('reason');
74+
assert.ok(signal.aborted);
75+
assert.strictEqual(signal.reason, 'reason');
76+
});
77+
78+
it('handles deeply aborted signals', async () => {
79+
const controllers = Array.from({ length: 2 }, () => new AbortController());
80+
const composedSignal1 = AbortSignal.any([controllers[0].signal]);
81+
const composedSignal2 = AbortSignal.any([composedSignal1, controllers[1].signal]);
82+
83+
composedSignal2.onabort = common.mustCall();
84+
controllers[0].abort();
85+
assert.ok(composedSignal2.aborted);
86+
assert.ok(composedSignal2.reason instanceof DOMException);
87+
assert.strictEqual(composedSignal2.reason.name, 'AbortError');
88+
});
89+
90+
it('executes abort handlers in correct order', () => {
91+
const controller = new AbortController();
92+
const signals = [];
93+
signals.push(controller.signal);
94+
signals.push(AbortSignal.any([controller.signal]));
95+
signals.push(AbortSignal.any([controller.signal]));
96+
signals.push(AbortSignal.any([signals[0]]));
97+
signals.push(AbortSignal.any([signals[1]]));
98+
99+
let result = '';
100+
signals.forEach((signal, i) => signal.addEventListener('abort', () => result += i));
101+
controller.abort();
102+
assert.strictEqual(result, '01234');
103+
});
104+
});

0 commit comments

Comments
 (0)
Please sign in to comment.