mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
lib: implement AbortSignal.any()
PR-URL: https://github.com/nodejs/node/pull/47821 Fixes: https://github.com/nodejs/node/issues/47811 Refs: https://github.com/whatwg/dom/pull/1152 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
This commit is contained in:
parent
62fd6bc4aa
commit
cd016a08c1
@ -121,6 +121,18 @@ added:
|
||||
|
||||
Returns a new `AbortSignal` which will be aborted in `delay` milliseconds.
|
||||
|
||||
#### Static method: `AbortSignal.any(signals)`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `signals` {AbortSignal\[]} The `AbortSignal`s of which to compose a new `AbortSignal`.
|
||||
|
||||
Returns a new `AbortSignal` which will be aborted if any of the provided
|
||||
signals are aborted. Its [`abortSignal.reason`][] will be set to whichever
|
||||
one of the `signals` caused it to be aborted.
|
||||
|
||||
#### Event: `'abort'`
|
||||
|
||||
<!-- YAML
|
||||
@ -1026,6 +1038,7 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
|
||||
[`WritableStream`]: webstreams.md#class-writablestream
|
||||
[`__dirname`]: modules.md#__dirname
|
||||
[`__filename`]: modules.md#__filename
|
||||
[`abortSignal.reason`]: #abortsignalreason
|
||||
[`buffer.atob()`]: buffer.md#bufferatobdata
|
||||
[`buffer.btoa()`]: buffer.md#bufferbtoadata
|
||||
[`clearImmediate`]: timers.md#clearimmediateimmediate
|
||||
|
@ -42,6 +42,7 @@ const {
|
||||
|
||||
const {
|
||||
validateAbortSignal,
|
||||
validateAbortSignalArray,
|
||||
validateObject,
|
||||
validateUint32,
|
||||
} = require('internal/validators');
|
||||
@ -54,6 +55,7 @@ const {
|
||||
clearTimeout,
|
||||
setTimeout,
|
||||
} = require('timers');
|
||||
const assert = require('internal/assert');
|
||||
|
||||
const {
|
||||
messaging_deserialize_symbol: kDeserialize,
|
||||
@ -80,13 +82,16 @@ function lazyMakeTransferable(obj) {
|
||||
}
|
||||
|
||||
const clearTimeoutRegistry = new SafeFinalizationRegistry(clearTimeout);
|
||||
const timeOutSignals = new SafeSet();
|
||||
const gcPersistentSignals = new SafeSet();
|
||||
|
||||
const kAborted = Symbol('kAborted');
|
||||
const kReason = Symbol('kReason');
|
||||
const kCloneData = Symbol('kCloneData');
|
||||
const kTimeout = Symbol('kTimeout');
|
||||
const kMakeTransferable = Symbol('kMakeTransferable');
|
||||
const kComposite = Symbol('kComposite');
|
||||
const kSourceSignals = Symbol('kSourceSignals');
|
||||
const kDependantSignals = Symbol('kDependantSignals');
|
||||
|
||||
function customInspect(self, obj, depth, options) {
|
||||
if (depth < 0)
|
||||
@ -116,7 +121,7 @@ function setWeakAbortSignalTimeout(weakRef, delay) {
|
||||
const timeout = setTimeout(() => {
|
||||
const signal = weakRef.deref();
|
||||
if (signal !== undefined) {
|
||||
timeOutSignals.delete(signal);
|
||||
gcPersistentSignals.delete(signal);
|
||||
abortSignal(
|
||||
signal,
|
||||
new DOMException(
|
||||
@ -185,25 +190,68 @@ class AbortSignal extends EventTarget {
|
||||
return signal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbortSignal[]} signals
|
||||
* @returns {AbortSignal}
|
||||
*/
|
||||
static any(signals) {
|
||||
validateAbortSignalArray(signals, 'signals');
|
||||
const resultSignal = createAbortSignal({ composite: true });
|
||||
const resultSignalWeakRef = new WeakRef(resultSignal);
|
||||
resultSignal[kSourceSignals] = new SafeSet();
|
||||
for (let i = 0; i < signals.length; i++) {
|
||||
const signal = signals[i];
|
||||
if (signal.aborted) {
|
||||
abortSignal(resultSignal, signal.reason);
|
||||
return resultSignal;
|
||||
}
|
||||
signal[kDependantSignals] ??= new SafeSet();
|
||||
if (!signal[kComposite]) {
|
||||
resultSignal[kSourceSignals].add(new WeakRef(signal));
|
||||
signal[kDependantSignals].add(resultSignalWeakRef);
|
||||
} else if (!signal[kSourceSignals]) {
|
||||
continue;
|
||||
} else {
|
||||
for (const sourceSignal of signal[kSourceSignals]) {
|
||||
const sourceSignalRef = sourceSignal.deref();
|
||||
if (!sourceSignalRef) {
|
||||
continue;
|
||||
}
|
||||
assert(!sourceSignalRef.aborted);
|
||||
assert(!sourceSignalRef[kComposite]);
|
||||
|
||||
if (resultSignal[kSourceSignals].has(sourceSignal)) {
|
||||
continue;
|
||||
}
|
||||
resultSignal[kSourceSignals].add(sourceSignal);
|
||||
sourceSignalRef[kDependantSignals].add(resultSignalWeakRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
return resultSignal;
|
||||
}
|
||||
|
||||
[kNewListener](size, type, listener, once, capture, passive, weak) {
|
||||
super[kNewListener](size, type, listener, once, capture, passive, weak);
|
||||
if (this[kTimeout] &&
|
||||
const isTimeoutOrNonEmptyCompositeSignal = this[kTimeout] || (this[kComposite] && this[kSourceSignals]?.size);
|
||||
if (isTimeoutOrNonEmptyCompositeSignal &&
|
||||
type === 'abort' &&
|
||||
!this.aborted &&
|
||||
!weak &&
|
||||
size === 1) {
|
||||
// If this is a timeout signal, and we're adding a non-weak abort
|
||||
// If this is a timeout signal, or a non-empty composite signal, and we're adding a non-weak abort
|
||||
// listener, then we don't want it to be gc'd while the listener
|
||||
// is attached and the timer still hasn't fired. So, we retain a
|
||||
// strong ref that is held for as long as the listener is registered.
|
||||
timeOutSignals.add(this);
|
||||
gcPersistentSignals.add(this);
|
||||
}
|
||||
}
|
||||
|
||||
[kRemoveListener](size, type, listener, capture) {
|
||||
super[kRemoveListener](size, type, listener, capture);
|
||||
if (this[kTimeout] && type === 'abort' && size === 0) {
|
||||
timeOutSignals.delete(this);
|
||||
const isTimeoutOrNonEmptyCompositeSignal = this[kTimeout] || (this[kComposite] && this[kSourceSignals]?.size);
|
||||
if (isTimeoutOrNonEmptyCompositeSignal && type === 'abort' && size === 0) {
|
||||
gcPersistentSignals.delete(this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,7 +335,8 @@ defineEventHandler(AbortSignal.prototype, 'abort');
|
||||
* @param {{
|
||||
* aborted? : boolean,
|
||||
* reason? : any,
|
||||
* transferable? : boolean
|
||||
* transferable? : boolean,
|
||||
* composite? : boolean,
|
||||
* }} [init]
|
||||
* @returns {AbortSignal}
|
||||
*/
|
||||
@ -296,11 +345,13 @@ function createAbortSignal(init = kEmptyObject) {
|
||||
aborted = false,
|
||||
reason = undefined,
|
||||
transferable = false,
|
||||
composite = false,
|
||||
} = init;
|
||||
const signal = new EventTarget();
|
||||
ObjectSetPrototypeOf(signal, AbortSignal.prototype);
|
||||
signal[kAborted] = aborted;
|
||||
signal[kReason] = reason;
|
||||
signal[kComposite] = composite;
|
||||
return transferable ? lazyMakeTransferable(signal) : signal;
|
||||
}
|
||||
|
||||
@ -312,6 +363,10 @@ function abortSignal(signal, reason) {
|
||||
[kTrustEvent]: true,
|
||||
});
|
||||
signal.dispatchEvent(event);
|
||||
signal[kDependantSignals]?.forEach((s) => {
|
||||
const signalRef = s.deref();
|
||||
if (signalRef) abortSignal(signalRef, reason);
|
||||
});
|
||||
}
|
||||
|
||||
class AbortController {
|
||||
|
@ -324,6 +324,26 @@ function validateBooleanArray(value, name) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @callback validateAbortSignalArray
|
||||
* @param {*} value
|
||||
* @param {string} name
|
||||
* @returns {asserts value is AbortSignal[]}
|
||||
*/
|
||||
|
||||
/** @type {validateAbortSignalArray} */
|
||||
function validateAbortSignalArray(value, name) {
|
||||
validateArray(value, name);
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const signal = value[i];
|
||||
const indexedName = `${name}[${i}]`;
|
||||
if (signal == null) {
|
||||
throw new ERR_INVALID_ARG_TYPE(indexedName, 'AbortSignal', signal);
|
||||
}
|
||||
validateAbortSignal(signal, indexedName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} signal
|
||||
* @param {string} [name='signal']
|
||||
@ -534,6 +554,7 @@ module.exports = {
|
||||
validateArray,
|
||||
validateStringArray,
|
||||
validateBooleanArray,
|
||||
validateAbortSignalArray,
|
||||
validateBoolean,
|
||||
validateBuffer,
|
||||
validateDictionary,
|
||||
|
@ -680,7 +680,7 @@ class WPTRunner {
|
||||
|
||||
process.on('exit', () => {
|
||||
for (const spec of this.inProgress) {
|
||||
this.fail(spec, { name: 'Unknown' }, kIncomplete);
|
||||
this.fail(spec, { name: 'Incomplete' }, kIncomplete);
|
||||
}
|
||||
inspect.defaultOptions.depth = Infinity;
|
||||
// Sorts the rules to have consistent output
|
||||
@ -796,9 +796,11 @@ class WPTRunner {
|
||||
* @param {object} harnessStatus - The status object returned by WPT harness.
|
||||
*/
|
||||
completionCallback(spec, harnessStatus) {
|
||||
const status = this.getTestStatus(harnessStatus.status);
|
||||
|
||||
// Treat it like a test case failure
|
||||
if (harnessStatus.status === 2) {
|
||||
this.resultCallback(spec, { status: 2, name: 'Unknown' });
|
||||
if (status === kTimeout) {
|
||||
this.fail(spec, { name: 'WPT testharness timeout' }, kTimeout);
|
||||
}
|
||||
this.inProgress.delete(spec);
|
||||
// Always force termination of the worker. Some tests allocate resources
|
||||
|
@ -48,8 +48,17 @@ add_result_callback((result) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Keep the event loop alive
|
||||
const timeout = setTimeout(() => {
|
||||
parentPort.postMessage({
|
||||
type: 'completion',
|
||||
status: { status: 2 },
|
||||
});
|
||||
}, 2 ** 31 - 1); // Max timeout is 2^31-1, when overflown the timeout is set to 1.
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
add_completion_callback((_, status) => {
|
||||
clearTimeout(timeout);
|
||||
parentPort.postMessage({
|
||||
type: 'completion',
|
||||
status,
|
||||
|
2
test/fixtures/wpt/README.md
vendored
2
test/fixtures/wpt/README.md
vendored
@ -12,7 +12,7 @@ Last update:
|
||||
|
||||
- common: https://github.com/web-platform-tests/wpt/tree/dbd648158d/common
|
||||
- console: https://github.com/web-platform-tests/wpt/tree/767ae35464/console
|
||||
- dom/abort: https://github.com/web-platform-tests/wpt/tree/8fadb38120/dom/abort
|
||||
- dom/abort: https://github.com/web-platform-tests/wpt/tree/d1f1ecbd52/dom/abort
|
||||
- dom/events: https://github.com/web-platform-tests/wpt/tree/ab8999891c/dom/events
|
||||
- encoding: https://github.com/web-platform-tests/wpt/tree/0c1b9d1622/encoding
|
||||
- fetch/data-urls/resources: https://github.com/web-platform-tests/wpt/tree/7c79d998ff/fetch/data-urls/resources
|
||||
|
4
test/fixtures/wpt/dom/abort/abort-signal-any.any.js
vendored
Normal file
4
test/fixtures/wpt/dom/abort/abort-signal-any.any.js
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
// META: script=./resources/abort-signal-any-tests.js
|
||||
|
||||
abortSignalAnySignalOnlyTests(AbortSignal);
|
||||
abortSignalAnyTests(AbortSignal, AbortController);
|
185
test/fixtures/wpt/dom/abort/resources/abort-signal-any-tests.js
vendored
Normal file
185
test/fixtures/wpt/dom/abort/resources/abort-signal-any-tests.js
vendored
Normal file
@ -0,0 +1,185 @@
|
||||
// Tests for AbortSignal.any() and subclasses that don't use a controller.
|
||||
function abortSignalAnySignalOnlyTests(signalInterface) {
|
||||
const desc = `${signalInterface.name}.any()`
|
||||
|
||||
test(t => {
|
||||
const signal = signalInterface.any([]);
|
||||
assert_false(signal.aborted);
|
||||
}, `${desc} works with an empty array of signals`);
|
||||
}
|
||||
|
||||
// Tests for AbortSignal.any() and subclasses that use a controller.
|
||||
function abortSignalAnyTests(signalInterface, controllerInterface) {
|
||||
const suffix = `(using ${controllerInterface.name})`;
|
||||
const desc = `${signalInterface.name}.any()`;
|
||||
|
||||
test(t => {
|
||||
const controller = new controllerInterface();
|
||||
const signal = controller.signal;
|
||||
const cloneSignal = signalInterface.any([signal]);
|
||||
assert_false(cloneSignal.aborted);
|
||||
assert_true("reason" in cloneSignal, "cloneSignal has reason property");
|
||||
assert_equals(cloneSignal.reason, undefined,
|
||||
"cloneSignal.reason is initially undefined");
|
||||
assert_not_equals(signal, cloneSignal,
|
||||
`${desc} returns a new signal.`);
|
||||
|
||||
let eventFired = false;
|
||||
cloneSignal.onabort = t.step_func((e) => {
|
||||
assert_equals(e.target, cloneSignal,
|
||||
`The event target is the signal returned by ${desc}`);
|
||||
eventFired = true;
|
||||
});
|
||||
|
||||
controller.abort("reason string");
|
||||
assert_true(signal.aborted);
|
||||
assert_true(cloneSignal.aborted);
|
||||
assert_true(eventFired);
|
||||
assert_equals(cloneSignal.reason, "reason string",
|
||||
`${desc} propagates the abort reason`);
|
||||
}, `${desc} follows a single signal ${suffix}`);
|
||||
|
||||
test(t => {
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
const controllers = [];
|
||||
for (let j = 0; j < 3; ++j) {
|
||||
controllers.push(new controllerInterface());
|
||||
}
|
||||
const combinedSignal = signalInterface.any(controllers.map(c => c.signal));
|
||||
|
||||
let eventFired = false;
|
||||
combinedSignal.onabort = t.step_func((e) => {
|
||||
assert_equals(e.target, combinedSignal,
|
||||
`The event target is the signal returned by ${desc}`);
|
||||
eventFired = true;
|
||||
});
|
||||
|
||||
controllers[i].abort();
|
||||
assert_true(eventFired);
|
||||
assert_true(combinedSignal.aborted);
|
||||
assert_true(combinedSignal.reason instanceof DOMException,
|
||||
"signal.reason is a DOMException");
|
||||
assert_equals(combinedSignal.reason.name, "AbortError",
|
||||
"signal.reason is a AbortError");
|
||||
}
|
||||
}, `${desc} follows multiple signals ${suffix}`);
|
||||
|
||||
test(t => {
|
||||
const controllers = [];
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
controllers.push(new controllerInterface());
|
||||
}
|
||||
controllers[1].abort("reason 1");
|
||||
controllers[2].abort("reason 2");
|
||||
|
||||
const signal = signalInterface.any(controllers.map(c => c.signal));
|
||||
assert_true(signal.aborted);
|
||||
assert_equals(signal.reason, "reason 1",
|
||||
"The signal should be aborted with the first reason");
|
||||
}, `${desc} returns an aborted signal if passed an aborted signal ${suffix}`);
|
||||
|
||||
test(t => {
|
||||
const controller = new controllerInterface();
|
||||
const signal = signalInterface.any([controller.signal, controller.signal]);
|
||||
assert_false(signal.aborted);
|
||||
controller.abort("reason");
|
||||
assert_true(signal.aborted);
|
||||
assert_equals(signal.reason, "reason");
|
||||
}, `${desc} can be passed the same signal more than once ${suffix}`);
|
||||
|
||||
test(t => {
|
||||
const controller1 = new controllerInterface();
|
||||
controller1.abort("reason 1");
|
||||
const controller2 = new controllerInterface();
|
||||
controller2.abort("reason 2");
|
||||
|
||||
const signal = signalInterface.any([controller1.signal, controller2.signal, controller1.signal]);
|
||||
assert_true(signal.aborted);
|
||||
assert_equals(signal.reason, "reason 1");
|
||||
}, `${desc} uses the first instance of a duplicate signal ${suffix}`);
|
||||
|
||||
test(t => {
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
const controllers = [];
|
||||
for (let j = 0; j < 3; ++j) {
|
||||
controllers.push(new controllerInterface());
|
||||
}
|
||||
const combinedSignal1 =
|
||||
signalInterface.any([controllers[0].signal, controllers[1].signal]);
|
||||
const combinedSignal2 =
|
||||
signalInterface.any([combinedSignal1, controllers[2].signal]);
|
||||
|
||||
let eventFired = false;
|
||||
combinedSignal2.onabort = t.step_func((e) => {
|
||||
eventFired = true;
|
||||
});
|
||||
|
||||
controllers[i].abort();
|
||||
assert_true(eventFired);
|
||||
assert_true(combinedSignal2.aborted);
|
||||
assert_true(combinedSignal2.reason instanceof DOMException,
|
||||
"signal.reason is a DOMException");
|
||||
assert_equals(combinedSignal2.reason.name, "AbortError",
|
||||
"signal.reason is a AbortError");
|
||||
}
|
||||
}, `${desc} signals are composable ${suffix}`);
|
||||
|
||||
async_test(t => {
|
||||
const controller = new controllerInterface();
|
||||
const timeoutSignal = AbortSignal.timeout(5);
|
||||
|
||||
const combinedSignal = signalInterface.any([controller.signal, timeoutSignal]);
|
||||
|
||||
combinedSignal.onabort = t.step_func_done(() => {
|
||||
assert_true(combinedSignal.aborted);
|
||||
assert_true(combinedSignal.reason instanceof DOMException,
|
||||
"combinedSignal.reason is a DOMException");
|
||||
assert_equals(combinedSignal.reason.name, "TimeoutError",
|
||||
"combinedSignal.reason is a TimeoutError");
|
||||
});
|
||||
}, `${desc} works with signals returned by AbortSignal.timeout() ${suffix}`);
|
||||
|
||||
test(t => {
|
||||
const controller = new controllerInterface();
|
||||
let combined = signalInterface.any([controller.signal]);
|
||||
combined = signalInterface.any([combined]);
|
||||
combined = signalInterface.any([combined]);
|
||||
combined = signalInterface.any([combined]);
|
||||
|
||||
let eventFired = false;
|
||||
combined.onabort = () => {
|
||||
eventFired = true;
|
||||
}
|
||||
|
||||
assert_false(eventFired);
|
||||
assert_false(combined.aborted);
|
||||
|
||||
controller.abort("the reason");
|
||||
|
||||
assert_true(eventFired);
|
||||
assert_true(combined.aborted);
|
||||
assert_equals(combined.reason, "the reason");
|
||||
}, `${desc} works with intermediate signals ${suffix}`);
|
||||
|
||||
test(t => {
|
||||
const controller = new controllerInterface();
|
||||
const signals = [];
|
||||
// The first event should be dispatched on the originating signal.
|
||||
signals.push(controller.signal);
|
||||
// All dependents are linked to `controller.signal` (never to another
|
||||
// composite signal), so this is the order events should fire.
|
||||
signals.push(signalInterface.any([controller.signal]));
|
||||
signals.push(signalInterface.any([controller.signal]));
|
||||
signals.push(signalInterface.any([signals[0]]));
|
||||
signals.push(signalInterface.any([signals[1]]));
|
||||
|
||||
let result = "";
|
||||
for (let i = 0; i < signals.length; i++) {
|
||||
signals[i].addEventListener('abort', () => {
|
||||
result += i;
|
||||
});
|
||||
}
|
||||
controller.abort();
|
||||
assert_equals(result, "01234");
|
||||
}, `Abort events for ${desc} signals fire in the right order ${suffix}`);
|
||||
}
|
2
test/fixtures/wpt/versions.json
vendored
2
test/fixtures/wpt/versions.json
vendored
@ -8,7 +8,7 @@
|
||||
"path": "console"
|
||||
},
|
||||
"dom/abort": {
|
||||
"commit": "8fadb381209a215280dc3ad96d0f135b6005f176",
|
||||
"commit": "d1f1ecbd52f2eab3b7fe5dc1b20b41174f1341ce",
|
||||
"path": "dom/abort"
|
||||
},
|
||||
"dom/events": {
|
||||
|
104
test/parallel/test-abortsignal-any.mjs
Normal file
104
test/parallel/test-abortsignal-any.mjs
Normal file
@ -0,0 +1,104 @@
|
||||
import * as common from '../common/index.mjs';
|
||||
import { describe, it } from 'node:test';
|
||||
import { once } from 'node:events';
|
||||
import assert from 'node:assert';
|
||||
|
||||
describe('AbortSignal.any()', { concurrency: true }, () => {
|
||||
it('should throw when not receiving an array', () => {
|
||||
const expectedError = { code: 'ERR_INVALID_ARG_TYPE' };
|
||||
assert.throws(() => AbortSignal.any(), expectedError);
|
||||
assert.throws(() => AbortSignal.any(null), expectedError);
|
||||
assert.throws(() => AbortSignal.any(undefined), expectedError);
|
||||
});
|
||||
|
||||
it('should throw when input contains non-signal values', () => {
|
||||
assert.throws(
|
||||
() => AbortSignal.any([AbortSignal.abort(), undefined]),
|
||||
{
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
message: 'The "signals[1]" argument must be an instance of AbortSignal. Received undefined'
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a non-aborted signal for an empty input', () => {
|
||||
const signal = AbortSignal.any([]);
|
||||
assert.strictEqual(signal.aborted, false);
|
||||
signal.addEventListener('abort', common.mustNotCall());
|
||||
});
|
||||
|
||||
it('returns a new signal', () => {
|
||||
const originalSignal = new AbortController().signal;
|
||||
const signalAny = AbortSignal.any([originalSignal]);
|
||||
assert.notStrictEqual(originalSignal, signalAny);
|
||||
});
|
||||
|
||||
it('returns an aborted signal if input has an aborted signal', () => {
|
||||
const signal = AbortSignal.any([AbortSignal.abort('some reason')]);
|
||||
assert.strictEqual(signal.aborted, true);
|
||||
assert.strictEqual(signal.reason, 'some reason');
|
||||
signal.addEventListener('abort', common.mustNotCall());
|
||||
});
|
||||
|
||||
it('returns an aborted signal with the reason of first aborted signal input', () => {
|
||||
const signal = AbortSignal.any([AbortSignal.abort('some reason'), AbortSignal.abort('another reason')]);
|
||||
assert.strictEqual(signal.aborted, true);
|
||||
assert.strictEqual(signal.reason, 'some reason');
|
||||
signal.addEventListener('abort', common.mustNotCall());
|
||||
});
|
||||
|
||||
it('returns the correct signal in the event target', async () => {
|
||||
const signal = AbortSignal.any([AbortSignal.timeout(5)]);
|
||||
const interval = setInterval(() => {}, 100000); // Keep event loop alive
|
||||
const [{ target }] = await once(signal, 'abort');
|
||||
clearInterval(interval);
|
||||
assert.strictEqual(target, signal);
|
||||
assert.ok(signal.aborted);
|
||||
assert.strictEqual(signal.reason.name, 'TimeoutError');
|
||||
});
|
||||
|
||||
it('aborts with reason of first aborted signal', () => {
|
||||
const controllers = Array.from({ length: 3 }, () => new AbortController());
|
||||
const combinedSignal = AbortSignal.any(controllers.map((c) => c.signal));
|
||||
controllers[1].abort(1);
|
||||
controllers[2].abort(2);
|
||||
assert.ok(combinedSignal.aborted);
|
||||
assert.strictEqual(combinedSignal.reason, 1);
|
||||
});
|
||||
|
||||
it('can accept the same signal more than once', () => {
|
||||
const controller = new AbortController();
|
||||
const signal = AbortSignal.any([controller.signal, controller.signal]);
|
||||
assert.strictEqual(signal.aborted, false);
|
||||
controller.abort('reason');
|
||||
assert.ok(signal.aborted);
|
||||
assert.strictEqual(signal.reason, 'reason');
|
||||
});
|
||||
|
||||
it('handles deeply aborted signals', async () => {
|
||||
const controllers = Array.from({ length: 2 }, () => new AbortController());
|
||||
const composedSignal1 = AbortSignal.any([controllers[0].signal]);
|
||||
const composedSignal2 = AbortSignal.any([composedSignal1, controllers[1].signal]);
|
||||
|
||||
composedSignal2.onabort = common.mustCall();
|
||||
controllers[0].abort();
|
||||
assert.ok(composedSignal2.aborted);
|
||||
assert.ok(composedSignal2.reason instanceof DOMException);
|
||||
assert.strictEqual(composedSignal2.reason.name, 'AbortError');
|
||||
});
|
||||
|
||||
it('executes abort handlers in correct order', () => {
|
||||
const controller = new AbortController();
|
||||
const signals = [];
|
||||
signals.push(controller.signal);
|
||||
signals.push(AbortSignal.any([controller.signal]));
|
||||
signals.push(AbortSignal.any([controller.signal]));
|
||||
signals.push(AbortSignal.any([signals[0]]));
|
||||
signals.push(AbortSignal.any([signals[1]]));
|
||||
|
||||
let result = '';
|
||||
signals.forEach((signal, i) => signal.addEventListener('abort', () => result += i));
|
||||
controller.abort();
|
||||
assert.strictEqual(result, '01234');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user