Since Node added (experimental) support for AbortController, we have also added support for out event utility functions (that already work with EventTargets) for cancellation:
import { once, EventEmitter } from 'events';
import { setTimeout } from 'timers/promises;
const et = new EventTarget();
const ee = new EventEmitter();
const ac = new AbortController();
setTimeout(100).then(() => ac.abort());
let { signal } = ac;
// run with experimental tla flag or wrap in an iife
await once(ee, 'foo', { signal }); // cancel waiting for the event, removes the listener
await once(et, 'foo', { signal }); // same thing, with event target, removes the listener
for await(const item of on(ee, 'foo' , { signal } )) { // same thing, but with async iterator variant
}
It would be useful to "upstream" this behaviour to the DOM specification as it would be useful to have in browsers:
// Does not yet work, suggested:
const ac = new AbortController();
let { signal } = ac;
const et = new EventTarget();
et.addEventListener('foo', (e) => {
}, { signal } );
ac.abort(); // removes the listener from the event target, same as et.removeEventListener('foo', thatFunctionReference)
It's useful to use the web's cancellation platform (AbortController) to cancel listening to an event. This is also ergonomic for frameworks/libraries that set up a lot of event listeners and could unsubscribe from all of them in one go (through the controller).
Was this suggested at some point in the past? (I couldn't find it) Is this a good idea? A bad idea?
cc @domenic @annevk
I see, so this would remove event listeners upon the signal that is associated with them being aborted? I haven't seen suggestions for this.
I see, so this would remove event listeners upon the signal that is associated with them being aborted? I haven't seen suggestions for this.
Yes, basically it would be a way to make working with EventTargets more consistent with other DOM APIs.
For example: the person who asked me for this said "I need to make a few requests and subscribe to a few events when the user navigates to a certain page in my SPA. I am already using an AbortController to abort all the pending requests if the user navigates away - it would be very useful to also unsubscribe from all events the same way essentially having one way to 'cleanup' "
This was suggested in the context of .on() and observables in #544, but extending addEventListener seems like a nice incremental approach.
I'm quite supportive of this change. @mfreed7 do you think this would be something the Chromium DOM team could plan to implement? I know it's always a bit hard to make the case for spending time on minor ergonomic improvements, but see above for some web developer testimony on why this would be appreciated.
I'm quite supportive of this change. @mfreed7 do you think this would be something the Chromium DOM team could plan to implement? I know it's always a bit hard to make the case for spending time on minor ergonomic improvements, but see above for some web developer testimony on why this would be appreciated.
This definitely seems like a nice addition to the platform - I can see the use case for cleaning everything up in one way. I can't guarantee timing, but we could probably take a look at prototyping this, if someone else can handle the spec work?
Chromium bug filed: https://bugs.chromium.org/p/chromium/issues/detail?id=1146467
Sent an intent to prototype: https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/9396JedBBOM
Implementation almost done: https://chromium-review.googlesource.com/c/chromium/src/+/2527343
That's awesome @josepharhar thanks!
I've started working on WPTs here: https://github.com/web-platform-tests/wpt/pull/26472 I haven't run them or tested them fully yet but feel free to use the cases.
I merged the implementation into chromium.
To try it out, install chrome canary and enable the "Experimental Web Platform Features" flag here: chrome://flags/#enable-experimental-web-platform-features
It isn't actually in canary yet at the time of writing this comment, but it should be in the next day or two. If you want to check, see if this page has a release version, and if it does, see if the your version in canary in chrome://version is newer than or equal to the version on that webpage.
I filed a tag review here to get feedback for the feature: https://github.com/w3ctag/design-reviews/issues/569
I've started working on WPTs here: web-platform-tests/wpt#26472 I haven't run them or tested them fully yet but feel free to use the cases.
Thanks! I didn't catch this and I also added a WPT, but the more test coverage the better! I trust you much more than myself to capture all the use cases. If there are any edge cases you have in mind or possible differences between implementations, please test them.
@josepharhar that's great (and very quick!) thanks a bunch. I will play with it and I will also PR this feature into Node.js's emitter. I expect to make progress over the weekend (I don't actually work on this as part of my job, just a hobby :]).
It isn't actually in canary yet at the time of writing this comment, but it should be in the next day or two. If you want to check, see if this page has a release version, and if it does, see if the your version in canary in chrome://version is newer than or equal to the version on that webpage.
I'll just fetch (sync) and build (I have chromium from-source and depot-tools and all that stuff locally, it just builds slowly :])
Hey @josepharhar generally the implementation looks fine and works well. I've tested a few different cases (calling .abort during emit between listeners, once, passive, bubbling etc). I'll try testing it in a big code base to see if I run into more issue :]
The only issue I noticed is that passing a signal doesn't seem to work when passing capture:
{
const et = new EventTarget();
const ac = new AbortController();
et.addEventListener('foo', () => console.log('1'), { signal: ac.signal, once: true });
et.addEventListener('foo', () => console.log('2'), { signal: ac.signal });
et.addEventListener('foo', () => console.log('3'), { signal: ac.signal, capture: true });
ac.abort();
et.dispatchEvent(new Event('foo')); // 1 and 2 are not logged, 3 is
}

Thanks for finding that capture bug! Fix (with a WPT) is on the way: https://chromium-review.googlesource.com/c/chromium/src/+/2538368
Thanks, I now ran the actual WPTs I wrote - there is one more failure here - the following test (add a listener with an already aborted controller where the same listener was already added) seems to fail (and the "capture" one but that's already handled :])
./wpt run --channel dev --binary ../chromium/src/out/Default/Chromium.app/Contents/MacOS/Chromium chrome dom/events/AddEventListenerOptions-signal.html
test(function() {
let count = 0;
function handler() {
count++;
}
const et = new EventTarget();
const controller = new AbortController();
et.addEventListener('test', handler, { signal: controller.signal });
et.dispatchEvent(new Event('test'));
assert_equals(count, 1, "Adding a signal still adds a listener");
et.dispatchEvent(new Event('test'));
assert_equals(count, 2, "The listener was not added with the once flag");
controller.abort();
et.dispatchEvent(new Event('test'));
assert_equals(count, 2, "Aborting on the controller removes the listener");
et.addEventListener('test', handler, { signal: controller.signal });
et.dispatchEvent(new Event('test'));
assert_equals(count, 2, "Passing an aborted signal never adds the handler");
}, "Passing an AbortSignal to addEventListener options should allow removing a listener");
Thanks, I now ran the actual WPTs I wrote - there is one more failure here - the following test (add a listener with an already aborted controller where the same listener was already added) seems to fail (and the "capture" one but that's already handled :])
Thanks! Another fix on the way: https://chromium-review.googlesource.com/c/chromium/src/+/2541503
I'm copying the WPT you shared into dom/abort/addEventListenerAbortController.tentative.html, but feel free to move it out of there back into the new file in your WPT PR or whatever you'd like.
@josephhahar thanks! I've added a few more WPTs and changed the format to also work in workers in https://github.com/web-platform-tests/wpt/pull/26472 given Anne's suggestions :]
@annevk can you please explain what the next steps in the process are?
We have:
To be extra clear - I am in no rush, thankful that I get to work on this and am doing this for fun and to improve the web platform. I just want to understand what the next steps are.
Edit: I've also gone ahead and made a PR to Node.js to add this at https://github.com/nodejs/node/pull/36258/files
Hey @benjamingr, everything looks pretty splendid! I left some comments on the PR and test PR, but nothing major. I think you covered all the bullet points from https://whatwg.org/working-mode#changes.
I just filed an intent to ship for chrome so I can enable this feature by default.
This should probably get documented on MDN, right?
@whatwg/documentation it would be good to put this on MDN.
This will be enabled by default in chrome 90
Wow, grate feature!
essentially having one way to 'cleanup' "
FYI: I raised https://github.com/mdn/content/pull/2759 with a patch that documents this feature in MDN. Review welcome (from anybody interested).
Oh great feature, so something like that below can be rewritten with AbortController
button.addEventListener('click', () => {
// ...
function closeByClick() {
// ... do stuff
closeButton.removeEventListener('click', closeByClick);
window.removeEventListener('keydown', closeByEscape);
}
function closeByEscape(e) {
if (e.key !== 'Escape') return;
// ... do stuff
closeButton.removeEventListener('click', closeByClick);
window.removeEventListener('keydown', closeByEscape);
}
closeButton.addEventListener('click', closeByClick);
window.addEventListener('keydown', closeByEscape);
});
like this:
button.addEventListener('click', () => {
// ...
const controller = new AbortController();
closeButton.addEventListener('click', () => {
// ... do stuff
controller.abort();
}, { signal: controller.signal });
window.addEventListener('keydown', e => {
if (e.key !== 'Escape') return;
// ... do stuff
controller.abort();
}, { signal: controller.signal });
});
Most helpful comment
This was suggested in the context of
.on()and observables in #544, but extendingaddEventListenerseems like a nice incremental approach.I'm quite supportive of this change. @mfreed7 do you think this would be something the Chromium DOM team could plan to implement? I know it's always a bit hard to make the case for spending time on minor ergonomic improvements, but see above for some web developer testimony on why this would be appreciated.