From 73f9952588d96edbf387995561f97abf8f96e1cf Mon Sep 17 00:00:00 2001 From: Scott Haseley Date: Tue, 20 Aug 2024 14:04:04 -0700 Subject: [PATCH] AbortSignal: Propagate aborted state to dependent signals before firing events The implementation and spec of dependent signals assumes and asserts that a dependent signal is aborted if any of its sources have been aborted. But this property does not hold during the abort process, since intermediate states of abort propagation can be observed. For example, calling AbortSignal.any([signal]) in an "abort" event listener for one of signal's sources is a way to observe intermediate state, since the "abort" event fires before the source signal's dependents are updated. To fix this, this CL decouples setting the abort state and reacting to abort event: 1. Mark the source signal as aborted 2. Propagate the aborted state to any dependent signals 3. Run abort steps (run algorithms, fire events) for the source signal 4. Run abort steps for each of the dependent signals PR: https://github.com/whatwg/dom/pull/1295 Change-Id: I65a97eb46b01a0071d661e945f64c90e33954088 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5664649 Commit-Queue: Scott Haseley Reviewed-by: Joey Arhar Cr-Commit-Position: refs/heads/main@{#1344364} --- dom/abort/resources/abort-signal-any-tests.js | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/dom/abort/resources/abort-signal-any-tests.js b/dom/abort/resources/abort-signal-any-tests.js index 66e4141eaccb08e..929ee8a2e61ab44 100644 --- a/dom/abort/resources/abort-signal-any-tests.js +++ b/dom/abort/resources/abort-signal-any-tests.js @@ -182,4 +182,43 @@ function abortSignalAnyTests(signalInterface, controllerInterface) { controller.abort(); assert_equals(result, "01234"); }, `Abort events for ${desc} signals fire in the right order ${suffix}`); + + test(t => { + const controller = new controllerInterface(); + const signal1 = signalInterface.any([controller.signal]); + const signal2 = signalInterface.any([signal1]); + let eventFired = false; + + controller.signal.addEventListener('abort', () => { + const signal3 = signalInterface.any([signal2]); + assert_true(controller.signal.aborted); + assert_true(signal1.aborted); + assert_true(signal2.aborted); + assert_true(signal3.aborted); + eventFired = true; + }); + + controller.abort(); + assert_true(eventFired, "event fired"); + }, `Dependent signals for ${desc} are marked aborted before abort events fire ${suffix}`); + + test(t => { + const controller1 = new controllerInterface(); + const controller2 = new controllerInterface(); + const signal = signalInterface.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_equals(count, 1); + assert_true(signal.aborted); + assert_equals(signal.reason, "reason 1"); + }, `Dependent signals for ${desc} are aborted correctly for reentrant aborts ${suffix}`); }