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);
}
}
}
```