From be7dd22fca2f9ae5fa4ea2573cd912ed59696f7e Mon Sep 17 00:00:00 2001 From: jazelly Date: Sun, 8 Sep 2024 20:23:14 +0930 Subject: [PATCH] lib: propagate aborted state to dependent signals before firing events --- lib/internal/abort_controller.js | 40 ++++++++++++++++++++++--- test/parallel/test-abortsignal-any.mjs | 41 ++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/lib/internal/abort_controller.js b/lib/internal/abort_controller.js index 2fba1e3fdbe81b..a458ebb2048b46 100644 --- a/lib/internal/abort_controller.js +++ b/lib/internal/abort_controller.js @@ -4,12 +4,14 @@ // in https://github.com/mysticatea/abort-controller (MIT license) const { + ArrayPrototypePush, ObjectAssign, ObjectDefineProperties, ObjectDefineProperty, PromiseResolve, SafeFinalizationRegistry, SafeSet, + SetPrototypeForEach, Symbol, SymbolToStringTag, WeakRef, @@ -372,18 +374,48 @@ ObjectDefineProperty(AbortSignal.prototype, SymbolToStringTag, { defineEventHandler(AbortSignal.prototype, 'abort'); +// https://dom.spec.whatwg.org/#dom-abortsignal-abort function abortSignal(signal, reason) { + // 1. If signal is aborted, then return. if (signal[kAborted]) return; + + // 2. Set signal's abort reason to reason if it is given; + // otherwise to a new "AbortError" DOMException. signal[kAborted] = true; signal[kReason] = reason; + // 3. Let dependentSignalsToAbort be a new list. + const dependentSignalsToAbort = []; + // 4. For each dependentSignal of signal's dependent signals: + if (signal[kDependantSignals]) { + // Equivalent to Set.prototype.forEach + SetPrototypeForEach(signal[kDependantSignals], (s) => { + const dependentSignal = s.deref(); + // 1. If dependentSignal is not aborted, then: + if (dependentSignal && !dependentSignal[kAborted]) { + // 1. Set dependentSignal's abort reason to signal's abort reason. + dependentSignal[kReason] = reason; + dependentSignal[kAborted] = true; + // 2. Append dependentSignal to dependentSignalsToAbort. + ArrayPrototypePush(dependentSignalsToAbort, dependentSignal); + } + }); + } + // 5. Run the abort steps for signal + runAbort(signal); + // 6. For each dependentSignal of dependentSignalsToAbort, + // run the abort steps for dependentSignal. + for (let i = 0; i < dependentSignalsToAbort.length; i++) { + const dependentSignal = dependentSignalsToAbort[i]; + runAbort(dependentSignal); + }; +} + +// To run the abort steps for an AbortSignal signal +function runAbort(signal) { const event = new Event('abort', { [kTrustEvent]: true, }); signal.dispatchEvent(event); - signal[kDependantSignals]?.forEach((s) => { - const signalRef = s.deref(); - if (signalRef) abortSignal(signalRef, reason); - }); } class AbortController { diff --git a/test/parallel/test-abortsignal-any.mjs b/test/parallel/test-abortsignal-any.mjs index 4378c44d987f50..700e29775d07df 100644 --- a/test/parallel/test-abortsignal-any.mjs +++ b/test/parallel/test-abortsignal-any.mjs @@ -118,4 +118,45 @@ describe('AbortSignal.any()', { concurrency: !process.env.TEST_PARALLEL }, () => controller.abort(); assert.strictEqual(result, 1); }); + + // Refs: https://github.com/web-platform-tests/wpt/pull/47653 + it('marks dependent signals aborted before abort events fire', () => { + const controller = new AbortController(); + const signal1 = AbortSignal.any([controller.signal]); + const signal2 = AbortSignal.any([signal1]); + let eventFired = false; + + controller.signal.addEventListener('abort', () => { + const signal3 = AbortSignal.any([signal2]); + assert(controller.signal.aborted); + assert(signal1.aborted); + assert(signal2.aborted); + assert(signal3.aborted); + eventFired = true; + }); + + controller.abort(); + assert(eventFired, 'event fired'); + }); + + // Refs: https://github.com/web-platform-tests/wpt/pull/47653 + it('aborts dependent signals correctly for reentrant aborts', () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + const signal = AbortSignal.any([controller1.signal, controller2.signal]); + let count = 0; + + controller1.signal.addEventListener('abort', () => { + controller2.abort('reason 2'); + }); + + signal.addEventListener('abort', () => { + count++; + }); + + controller1.abort('reason 1'); + assert.strictEqual(count, 1); + assert(signal.aborted); + assert.strictEqual(signal.reason, 'reason 1'); + }); });