-
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
finally()
operator timing semantics
#151
Comments
Teardowns should be executed during finalization of observation. They are roughly what you'd put in a "finally" block if we had syntax for observable in JavaScript. Since the result of the In fact, once an upstream source is complete or errored, or when it's notified of abort, the downstream consumer should not be able to act until finalization is complete. Abort notifications should be sent upstream immediately. During abort, I'd expect the following order of operations within a given subscription:
Similarly in a situation where the subscription was errored or completed by the producer, I would expect:
So as prior art here, we can look at generators or async generators: With generators set up as so: function* a() {
try { yield 'a'; } finally { console.log('finally a'); }
}
function* b() {
try { yield* a(); } finally { console.log('finally b'); }
}
function* c() {
try { yield* b(); } finally { console.log('finally c'); }
} If you create an instance of the generator and prime it: const g = c();
g.next(); // primed { value: 'c', done: false } And then you either next (resulting in a completion notification of c.next(); // { done: true }
// logs
// "finally a"
// "finally b"
// "finally c" or c.return();
// logs
// "finally a"
// "finally b"
// "finally c" |
Other prior art: Promise.resolve(1)
.finally(() => console.log('one'))
.finally(() => console.log('two'))
// Logs
// "one"
// "two" Yeah... the observable implementation needs to abort the upstream subscription before it fires the finalizer. Similarly, it should abort upstream subscriptions as soon as it knows it can in the case of "complete" or "error" being called in the subscriber. |
TL;DR: On unsubscription (i.e.,
abort()
), what should the timing of thefinally()
operator's callback be with respect to the source Observable's teardowns?When writing the spec for, and implementing, the currently-unspecified
finally()
operator, I was reviewing the proposed web platform tests and had a comment about this bit: https://github.com/web-platform-tests/wpt/pull/44482/files#diff-b1a6cf94fa2c551227215a7556fbc3114006fca82549ba48a27f3dafe2564f01R186. This made me think more about the timing of thefinally()
operator's callback with respect to the source Observable's teardowns.Background: unsubscription teardown semantics
When you subscribe (with an
AbortSignal
) to an Observable returned by an operator, two things happen:subscribe()
.So in the following code:
There are three
AbortSignals
involved:ac.signal
.take()
s Subscriber'sSubscriber#signal
. (It is purely internal and not web accessible).subscriber
above.If you call
ac.abort()
, then the abort signals are aborted in the order they're listed above. That's because DOM defines that a parent signal gets fully aborted before any of its dependents: https://dom.spec.whatwg.org/#abortsignal-signal-abort. Practically speaking, this means if you added anabort
event listener toac.signal
andsubscriber.signal
, theac.signal
one would fire first, and thesubscriber.signal
one would fire last.Integrating
finally()
Now consider the requirement that the
finally()
callback needs to be called when a consumer unsubscribes from an Observable:The simplest way to make this happen is just to add
finally()
's callback to its Subscriber's teardown list. This essentially meansfinally()
is syntactic sugar around that operator's Subscriber'saddTeardown()
method. But this would break the ordering suggested in the test: https://github.com/web-platform-tests/wpt/pull/44482/files#diff-b1a6cf94fa2c551227215a7556fbc3114006fca82549ba48a27f3dafe2564f01R186. Specifically, in the following example:If the finally callback were part of its intermediate Subscriber's teardown list, then it would necessarily run before any of the source Observable's teardowns, since the intermediate signal (associated with
finally()
) aborts before its dependent signal (the one associated with the source Observable's Subscriber). Is this OK? It doesn't match RxJS's behavior, wherefinalize()
callbacks execute after the teardown callback runs.If we reallllly wanted to, we could probably match RxJS's behavior by building some bespoke infrastructure on
Subscriber
, or rejiggering around the dependencies of AbortSignals. But I'm really not sure what that would look like, or if it is even desirable at all. It would break the general precedent with AbortSignal.I lean towards the code example / output immediately above, but I'd like to get opinions from others, to know if this is a real problem.
The text was updated successfully, but these errors were encountered: