AbortController as Subscription === Currently AbortController and AbortSignal exist in all browser platforms, and there appears to be work to get it added to Node as well. AbortController and AbortSignal are a part of the Fetch API and they allow cancellation. Together, they are *very* similar to RxJS's `Subscription` class. The intent of this document is to explore the possiblity that Subscription could be either replaced by AbortController or created from AbortController. ## Similarities |Subscription|AbortController |:-----|:--- |`x.unsubscribe()`|`x.abort()` |`x.add(fn)`[^1]|`x.signal.addEventListener('abort', fn)` |`x.remove(fn)`[^2]|`x.signal.removeEventListener('abort', fn)` |`x.closed`|`x.signal.aborted` [^1]: `Subscription#add` in RxJS will accept `Subscription`, `() => void`, or `void`. It also has the difference that it will execute any of those provided teardowns immediately if the subscription happens to be closed. `AbortController`/`AbortSignal` has no such behavior. In RxJS v6 it also always returns a `Subscription`. This is so there's something to be passed to `Subscription#remove` if a function is passed to `add`. [^2]: `Subscription#remove` in RxJS v6 only removes Subscriptions, not functions. This should probably change in the future. ## Option 1: AbortController replacing Subscription In this scenario, we would make `Subscription` be `AbortController`-shaped, and migrate people's code over to a world where `subscribe` returned an `AbortController` instead. ### Pros - We ship one less type to the browser/Node. - The return value of a subscription can be used to signal things like `fetch` to abort. - A unified, platform provided, cancellation type for all. ### Cons - Substantially less ergonomic for the "Composite Subscription" use-case, which is very common. - `subscribe`/`abort` makes a lot less sense than `subscribe`/`unsubscribe`. - Adding a handler to an already `aborted` `AbortSignal` will not execute the handler. This means less safety in scenarios where subscriptions are being programmatically created, as opposed to the behavior of `Subscription#add`, which will unsubscribe the passed teardown immediately if the subscription instance is `closed`. ## Option 2: Subscription derives from AbortController This is actually probably a transitional point for option 1 above. But in this scenario, `Subscription extends AbortController`. I'm not sure this is totally possible without "real ES6 classes" rather than compiled ES5 classes, but it seems like it should work. ### Pros - Provides all the same safety and ergonomics of `Subscription`. - Can leave `unsubscribe`, but deprecate it. - Subscriptions themselves will have a usable `signal` property that can be used with other APIs expecting this type. ### Cons - Broader, more confusing API. Docs may be more confusing. - `add` and `signal.addEventListener('abort',...)` are not equivalent in behavior, and that could confuse people. - Still shipping a custom class to the browser, so it's not as much of a win. ## Transitional Class Example This is just a rough idea of what a Subscription that extended AbortController might look like: ```typescript= class Subscription extends AbortController { private _lookup = new Map<any, any>(); /** @deprecated use `signal.aborted` instead. */ get closed() { return this.signal.aborted; } /** @deprecated use `abort` */ unsubscribe() { this.abort(); } /** @deprecated use `signal.addEventListener('abort', fn)` */ add(child: AbortController|(() => void)|undefined): void { const { _lookup, signal } = this; if (!child || _lookup.has(child)) return; if (signal.aborted) { if (typeof child === 'function') { child(); } else { child.abort(); } return; } if (typeof child !== 'function') { child.signal.addEventListener('abort', () => this.remove(child)); } const handler = () => { if (typeof child === 'function') { child(); } else { child.abort(); } this.remove(child); }; _lookup.set(child, handler); signal.addEventListener('abort', handler); } /** @deprecated use `signal.removeEventListener('abort', fn)` */ remove(child: AbortController|(() => void)|undefined): void { const { signal, _lookup } = this; if (child && _lookup.has(child)) { signal.removeEventListener('abort', _lookup.get(child)); _lookup.delete(child); } } } ```