dom icon indicating copy to clipboard operation
dom copied to clipboard

Userland AbortSignal

Open annevk opened this issue 2 years ago • 12 comments
trafficstars

Splitting this from #1016.

Writing a robust userland AbortSignal consumer is tricky. In particular you might not get your event listener due to someone else listening calling stopImmediatePropagation().

Possible solutions:

  1. We invent a new way of dispatching that disallows preventing stopImmediatePropagation(). (@smaug---- is not a fan of this as per the other thread.)
  2. The consumer uses AbortSignal.any() to get a copy without other event listeners and registers their event listener there.
  3. We define userland "abort algorithms" somehow.

cc @rniwa @smaug---- @mfreed7 @domenic @shaseley

annevk avatar May 05 '23 08:05 annevk

Thanks for opening,

Using AbortSignal.any([signal]) just to use the fact it exposes an underlying different mechanism in the DOM seems like not a great design. It's similar to making a dummy hanging fetch request and .catching it to know cancellation happened.

I'm trying to get around having another way to dispatch things, @smaug---- can you elaborate what specifically about not being able to prevent other listeners from running bothers you?

benjamingr avatar May 08 '23 05:05 benjamingr

It would be odd to have special event dispatch mechanism just for AbortSignal. We've had the consistent way for decades - stop*Propagation works everywhere. Why it should behave differently here?

Also, I'm having trouble to see the issue with using AbortSignal.any.

Btw, is there a concrete issue here, or is this just some theoretical one?

smaug---- avatar May 08 '23 13:05 smaug----

There is a real issue, userland uses addEventListener("abort" for resource cleanup and other listeners being able to prevent that is unfortunate.

At the moment AbortSignal already has a second dispatch mechanism but it's only exposed to the dom internally and AbortSignal.any adds a third.

benjamingr avatar May 09 '23 18:05 benjamingr

Any update on this? I feel like we're adding a third way to do something (with any) - and the first two are problematic and there is no safe userland way to use AbortSignal until this is resolved (though to be fair in practice we haven't received complaints).

benjamingr avatar May 17 '23 09:05 benjamingr

Well, AbortSignal.any() does give userland a way. And by-and-large folks are satisfied with the AbortSignal.any() design so I'm going to land that change, especially now it's getting multiple implementations.

I'll keep this open to see if many other people are running into this and if we need to make further improvements.

annevk avatar May 17 '23 11:05 annevk

So we should be recommending people always do AbortSignal.any([signal]).addEventListener("abort" when adding abort listeners?

benjamingr avatar May 17 '23 17:05 benjamingr

Maybe? That's certainly the only way to do it if you run into this problem. As I said above, if enough people run into this problem we should consider solving it better.

annevk avatar May 17 '23 17:05 annevk

@annevk should we maybe add a note in the specification suggesting it as a safe way to ensure a listener runs?

benjamingr avatar May 20 '23 11:05 benjamingr

I would prefer if I could depend on abort signals being delivered. It seems odd if the signal offers it, I say I want it, and some other unrelated code shoots the messenger and prevents it from ever arriving.

This makes sense in a DOM tree where one node can say "OK I've handled this, it's done, no one else needs to handle it again". But responses to an abort are independent. Stopping an abort is like saying "OK I've stopped my piece, no one else needs to stop anything else, go ahead and leak all those resources". When would you ever need to legitimately cancel an abort partway through? The abort is the cancelation. You shouldn't be able to cancel cancelation.

What about a new property on Event? bubbles can prevent bubbling. cancelable can shut off preventDefault(). What if unstoppable could shut off stop*Propagation()?

signal.addEventListener('abort', event => {
  // Does nothing and raises a warning,
  // just like preventDefault() when cancelable === false
  event.stopImmediatePropagation()
})
const event = new Event('abort', { unstoppable: true })
signal.dispatchEvent(event)

This is in line with existing functionality. It keeps the existing dispatch mechanism. It keeps the current default of events being stoppable, and adds an option to control it for events that need it.

bojavou avatar Jul 01 '23 19:07 bojavou

Here's an example of the effect. The user tries to stop everything but only one operation stops.

https://bojavou.github.io/canceled-abort/

example

function aborted (event) {
  // The rest of my app doesn't need this, stop the event for efficiency
  event.stopImmediatePropagation()
  reject(signal.reason)
}

The developer thinks stopping the event is good for efficiency. That's reasonable and has been legitimate and beneficial since events began. But it's no longer true for an abort. Stopping an abort breaks user expectations.

Once delivered, the abort signal cannot be triggered again, and there's no way to provide a new signal to the ongoing operations, so this breaks cancelation nonrecoverably. User intuition is cancelation should work when requested. Developer intuition is an abort signal should deliver what it offers. This is counterintuitive for everyone involved.

Since there's no legitimate reason to stop an abort, and it has this highly visible and frustrating-to-the-user effect which the app can't recover from, I say it shouldn't even be possible. If AbortSignal becomes the standard for cancelation across the entire ecosystem, it shouldn't have this fragility built into it.

bojavou avatar Jul 02 '23 03:07 bojavou

We (node) shipped (are shipping) our own helper to add a listener without the stopImmediatePropagation issue as since Node is a server runtime a resource leak is potentially catastrophic for us. We'll hopefully also add a resource argument for it so it holds the listener weakly on the resource but that's planned for later.

If AbortSignal becomes the standard for cancelation across the entire ecosystem, it shouldn't have this fragility built into it.

I honestly wish I would have listened better to library authors when they were complaining about this before adding it to Node APIs but I think this is mostly blocked in the WHATWG side on someone doing the spec work to fix this API wise.

benjamingr avatar Jul 04 '23 12:07 benjamingr

We (node) shipped (are shipping) our own helper to add a listener without the stopImmediatePropagation issue

Nice. I have special utils just for listening to abort signals, I'll at least be able to build it around this in Node. That's valuable.

bojavou avatar Jul 04 '23 17:07 bojavou