-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Timing design options for Promise
-returning operator abortion
#96
Comments
I modelled this in the reference impl, but basically, I'd prioritized teardowns THEN abort. As one is for cancellation, and the other is for the more important task of clean up. I'd also execute teardowns in LIFO order. Because usually things need to be torn down in the opposite order they were set up in (for example: end transaction, then close DB). This also mirrors DisposableStack. |
What do you mean by:
We don't have any control over this. Per DOM abort signal "algorithms" always run before abort signal "events" are fired. This means teardowns (which are run as a part of an abort signal "algorithm") will always run before abort events are fired. But that's not the question at hand. The question at hand is about the scheduling of Promise rejection, for Promises returned from the operators.
This is not relevant to this issue. |
What I mean, more specifically, is that if Also, handlers added to const source = new Observable(subscriber => {
subscriber.signal.addEventListener('abort', () => console.log('cancellation 1'));
subscriber.addTeardown(() => console.log('teardown 1'));
subscriber.addTeardown(() => console.log('teardown 2'));
subscriber.signal.addEventListener('abort', () => console.log('cancellation 2'));
});
const ac = new AbortController();
ac.signal.addEventListener('abort', () => console.log('outer cancellation'));
source.subscribe(() => {}, { signal: ac.signal });
ac.abort();
// Logs
// outer cancellation
// teardown 2
// teardown 1
// cancellation 1
// cancellation 2 I think your WPT cover this, but I want to make sure it's understood to anyone that comes to read this. |
And just to clarify for relevance to the issue... I would expect all of the above logs to show up synchronously when you call So given your example, which I've annotated a bit: const observable = new Observable(subscriber => {
// When we abort the subscription, this handler will
// synchronously fire SECOND
subscriber.signal.addEventListener('abort', e => {
console.log('inner signal abort event');
// This microtask is scheduled SECOND
Promise.reject().catch(e => console.log('inner signal abort promise rejection'));
});
// When we abort the subscription, this handler will
// synchronously fire FIRST
subscriber.addTeardown(() => {
console.log('teardown');
// This microtask is scheduled FIRST
Promise.reject().catch(e => console.log('teardown promise rejection'));
});
});
const controller = new AbortController();
// This handler will be fired synchronously when abort()
// is called. It's wired up before the `toArray` is called,
// so I'd expect this to fire before everything.
controller.signal.addEventListener('abort', e => {
console.log('outer signal abort event');
// This microtask will be scheduled before the others, because
// the handler it was created it was fired first.
Promise.reject().catch(e => console.log('outer signal abort promise rejection'));
});
const promise = observable.toArray({signal: controller.signal});
promise.catch(e => console.log('toArray() promise rejection'));
controller.abort(); I'd expect:
|
This sentence doesn't quite make sense to me, but I think I know what you mean, but if I misinterpret, please correct me. It is true that teardowns will always execute before the But that's not what this issue is about. The issue is about what signal the outer promise's rejection is attached to — the edit in your most recent comment covers this a bit more. Basically, the rejection of the returned Promise will be done as an algorithm associated with an If the rejection of the returned Promise is an algorithm on the outer signal, then when the outer signal aborts, we will do the following:
If instead rejection of the returned Promise is an algorithm on the inner signal, then when the outer signal aborts, we will do the following:
That's why I think the two options I provided in the OP are the only possible choices we have. I don't believe the final output provided in #96 (comment) after "I'd expect" is valid, since the Promise rejection microtask would never run after microtasks queued in the last |
After #154, much of the discussion above is mostly irrelevant since we will no longer be using dependent AbortSignals to signal abort in a chain of Observables. The timing issue of which AbortSignal to tie the rejection of the returned Promise to is still relevant, but I think in light of the timing decisions made in #154, it only makes sense to tie rejection to the "downstream" (i.e., outer, developer-supplied) |
Observables have a number of
Promise
-returning operators, and consequently there are a few subtle timing decisions we could make when it comes to:AbortSignal
passed into any of these operators viaSubscribeOptions
abort
event handlersThe timing of this is observable (pun intended) in the case where the AbortSignal passed into a Promise-returning operator gets aborted, and other Promises get scheduled during subscription cancellation. Consider:
Tie
Promise
rejection to abortion of the passed-in SubscribeOptions's signalConcretely, this means scheduling the rejection of the returned Promise in an abort algorithm added to the "outer" signal passed into the operator. This would produce the following log:
Tie
Promise
rejection to the abortion of the signal's subscriptionConcretely, this approach would mean scheduling the rejection of the returned Promise from within an abort algorithm added to the "inner" signal, that is a dependent signal on the one passed in. This would produce the following log:
I find the first option more consistent, where the Promise returned from the operator rejects before any of the other inner promises. I think this is consistent with the fact that the passed-in signal is mentally at the same level of hierarchy as the returned Promise it controls (they are both "outer" things, not "inner" subscription internals). Given that, I would expect all things to happen to the outer signal/Promise pair before things associated with the inner subscription start firing and happening.
This is probably the route I'll pursue in spec/implementation/WPTs for now, but I at least wanted to document this subtle design decision here in the form of a discussion in case there are any strong opinions about the ordering. /cc @benlesh
The text was updated successfully, but these errors were encountered: