Replies: 7 comments
-
I think we should decide on the best approach above and move forward during 7.x (so long as we don't need to introduce any breaking changes). |
Beta Was this translation helpful? Give feedback.
-
FWIW: I'm most in favor of option 3 above. As I feel like it's the most flexible and it doesn't require a polyfill. However, that doesn't mean I'm not apprehensive about the possible confusion it could cause our users. |
Beta Was this translation helpful? Give feedback.
-
So it turns out that Likewise, subclassing |
Beta Was this translation helpful? Give feedback.
-
Did you try a |
Beta Was this translation helpful? Give feedback.
-
Not a |
Beta Was this translation helpful? Give feedback.
-
I'd like to point out another gotcha regarding AbortSignals with subscriptions, if I'm not wrong: You can't rely on an AbortSignal for cleaning up your resources, as the observable can also close by completing (or emitting an error) without the AbortSignal ever triggering. The issue lies in that what Observable receives is an AbortSignal from an AbortController. AbortSignal has a property const observable = new Observable((subscriber, signal) => {
const resource = new Resource();
addToSignal(signal, () => resource.destroy());
resource.onData = data => {
if(isError(data)) {
subscriber.error(data);
// Here it would also need resource.destroy()
} else {
subscriber.next(data);
}
}
})
const controller = new AbortController;
observable.subscribe(handle, controller.signal); When resource emits something that's identified as an error, the stream would be closed through This is specially relevant when you need to close some inner subscription based on a completion of another stream: const observable = new Observable((subscriber, signal) => {
sourceThatCompletes.subscribe({
next: ...,
complete: () => subscriber.complete
}, signal)
sourceThatNeverCompletes.subscribe({ next: ... }, signal); // This won't be cleaned until the outer AbortController calls .abort()
})
const controller = new AbortController;
observable.subscribe(handle, controller.signal); With a I like the third solution, as it adds more flexibility. On this line, I have some suggestions (or rather they are questions whether that's possible, as I'm probably overlooking things): Keep teardown function (or similar) in Observable.createMakes clean up easier in some cases Have .subscribe also return a Subscription (or similar)This way the consumer can close the stream in the case it doesn't have access to signal's Controller Have the signal embedded within both Observer and SubscriberI think this is minor, but I think that semantically it makes sense for both cases: const observer = {
next: () => ...,
error: () => ...,
complete: () => ...,
signal // This notifies when this specific observer becomes closed
}
new Observable((subscriber) => {
subscriber.signal // Likewise, this subscriber notifies when it becomes closed
}).subscribe(observer) With these three changes we'd have an API very similar to the one we currently have, which would keep the existing ergonomics, but in turn it would solve the "firehose issues" easily by leveraging AbortSignal. |
Beta Was this translation helpful? Give feedback.
-
@voliva Yeah, it's about how you use the paradigm internally. In Rx, we'd have to create our own (TL;DR: no concern there) |
Beta Was this translation helpful? Give feedback.
-
Here I want to discuss possible approaches to improving and modernizing cancellation in RxJS with AbortSignal. I know this has been discussed elsewhere, but the goal here is to outline concrete implementation details and considerations.
Relevant Info:
AbortSignal
andAbortController
have shipped on all major platforms (including Node).AbortSignal
plainly lacks a few features that our currentSubscription
gives us:a.
AbortSignal
's method for adding and removing handlers usesaddEventListener
andremoveEventListener
which is very inconvenient.b.
AbortSignal
will not fire event handlers automatically if they're added to a signal that is alreadyaborted
.c. Absolutely no ergonomics for adding "child" signals to "parent" signals. (
subscription1.add(subscription2)
will set up behaviors, such assubscription2.unsubscribe()
removing itself fromsubscription1
).Subscriber
semantics, so we can transition to usingAbortSignal
smoothly.Possible Solutions
Note that in all of these options below, the idea would be to call something like this to subscribe:
And to get some sort of cancellation signal/token in the observable ctor initializer here:
This needs to work in tandem with existing behaviors for some time. So we don't want to introduce wildly breaking changes.
1.
Subclass AbortSignal(Not an option, can't be done, see comments below in thread)Here we would create some class
RxAbortSignal
or the like, that had more convenientadd
andremove
methods on it, just likeSubscription
does. It would also accommodate the missing behaviors we see above.Pros: May prove to be a useful type in general outside of RxJS. APIs might be more discoverable, and it might be easier for people to move to this from
Subscription
.Cons: Will require polyfills in some environments. Subclassing a type someone else owns is always a deal with the devil. We'd also be forced to subclass sometimes incomplete polyfills for some amount of time, putting us on the hook for supporting strange quirks for that.
2. Provide "helpers" for dealing with AbortSignal
Here we would provide a bunch of methods that users could use to do things like create child signals and controllers, etc.
Otherwise, it would be largely the same as the above.
Pros: We don't have to subclass anything. Functions aren't too hard to reason about and it would always be tree-shakable, so you only pay for what you use. We would only be relying on surface-level behaviors of any
AbortSignal
orAbortController
instance, so less could go wrong, IMO than with subclassing.Cons: Will require polyfills in some environments. A bunch more functions everyone needs to memorize. The suite of functions would need to have more thought put into the design (IMO) than the subclassing idea (as there we'd probably stick with known behaviors and lessons learned from
Subscription
).3. Use
Observable | AbortSignal
to signal teardown insteadWith this, we could set everything up to run off of
Observable
as the teardown mechanism internally.AbortSignal
we could make work OOTB, by insuring that anyAbortSignal
passed tosubscribe
was automatically converted to anObservable
in a seamless way. ThusAbortSignal
would "just work", but so would a LOT more things. Internally, we'd need to make sure the observable was multicast/hot as we'd only want to subscribe to it once per subscription to the source.Setting up unsubscription becomes straight forward:
We could also automatically convert
AbortSignal
internally:In theory,
AbortObservable
could implementeverything required to make it "AbortSignalLike" for(This isn't a thing, see comments below):fetch
usePros: No polyfills required. Easily the most flexible design. On some level, we wouldn't need to document much more than "you can pass an observable or
AbortSignal
in here".Cons: Increased flexibility comes with more footguns and edge cases we'd have to account for. Observable lack an analoque for
aborted
that can be checked to see if a signal has been given... (this could be handled internally, however). I have a feeling that people will freakout about subscribing to an "abort observable" over muscle memory of "I must always unsubscribe from my observable subscriptions" and feel themselves going down a rabbit hole of providing abort observables to abort observables. We'll need to re-educate people around that probably.Other thoughts:
AbortSignal
or somethingAbortSignalLike
to theObservable
constructor initialization function, I suppose. That might prove useful for other APIs beyond ours (such as fetch, et al).AbortSignal
, and we could provide an adapter for that... But that's not that important, just food for thought.Additional Considerations
People will probably start to want to use
async/await
on contructor initializers in order to use it with Promise-heavy APIs likefetch
and more. This could technically work, as we don't have to return a teardown synchronously anymore, however we'd need to account for it internally (by checking for Promise returns and ignoring them).Related to #3122 #5545 #5683 #5591 ..
Beta Was this translation helpful? Give feedback.
All reactions