@matthewwithanm presented a scenerio here that I think we can address with a non-breaking change to the finally operator.
That is that we could provide, as an argument to finally's callback, a flag of some sort to declare whether or not it was finalized because of error, complete or unsubscribe.
Strawman:
source$.finally((type: string) => {
switch(type) {
case 'complete':
doThing1();
break;
case 'error':
doThing2();
break;
case 'unsubscribe':
doThing3();
break;
}
})
The only additional thing I can think of is additionally providing the error in the event of an error, but I'm not sure how helpful that would be.
Thoughts?
One observation is that it's like passing a materialized Notification - except that there's no unsubscribe Notification.
I'm thinking how could this work with #3122 (Using AbortSignal and AbortController). If the finally operator just adds another dispose function should I be able to handle abort with finally()? I guess there would have to be another type because it's not the same as unsubscribe if I understand it correctly.
I've stumbled upon exactly the same issue as described here where I wanted to know if the chain is being disposed because all observers unsubscribed or whether the chain completed.
I made a custom operator for this because it seems to be a pretty simple thing and like @benlesh said this would be a non-breaking change if finalize supported this.
https://stackblitz.com/edit/rxjs-scmt4v?file=index.ts
import { defer, of, never, throwError, Observable, MonoTypeOperatorFunction } from 'rxjs';
import { tap, finalize } from 'rxjs/operators';
enum DisposeReason {
Unsubscribe = 'unsubscribe',
Complete = 'complete',
Error = 'error',
}
type CallbackFunc = (reason: DisposeReason) => void;
const finalizeWithReason = <T>(callback: CallbackFunc): MonoTypeOperatorFunction<T> =>
(source: Observable<T>) =>
defer(() => {
let completed = false;
let errored = false;
return source.pipe(
tap({
error: () => errored = true,
complete: () => completed = true,
}),
finalize(() => {
if (errored) {
callback(DisposeReason.Error);
} else if (completed) {
callback(DisposeReason.Complete);
} else {
callback(DisposeReason.Unsubscribe);
}
}),
);
});
const finalizeCallback = (reason: DisposeReason) => console.log(reason);
const observer = {
next: () => {},
error: () => {},
complete: () => {},
};
throwError(new Error()).pipe(
finalizeWithReason(finalizeCallback),
).subscribe(observer);
of().pipe(
finalizeWithReason(finalizeCallback),
).subscribe(observer);
never().pipe(
finalizeWithReason(finalizeCallback),
).subscribe(observer).unsubscribe();
This prints the following output to console:
complete
unsubscribe
@martinsik We wound up with basically the same thing. I think you need to wrap your operator body in a defer() though. Otherwise you're using the same completed and errored variables for every subscription and once one errors they all will.
@matthewwithanm You're right, it should be wrapped inside defer.
This has gone pretty stale, and I don't think there's much appetite to add this at the moment. Closing for now.
Most helpful comment
@martinsik We wound up with basically the same thing. I think you need to wrap your operator body in a
defer()though. Otherwise you're using the samecompletedanderroredvariables for every subscription and once one errors they all will.