Rxjs: Add close state to finally operator?

Created on 2 Sep 2017  路  6Comments  路  Source: ReactiveX/rxjs

@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?

discussion feature

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 same completed and errored variables for every subscription and once one errors they all will.

All 6 comments

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

samherrmann picture samherrmann  路  3Comments

dooreelko picture dooreelko  路  3Comments

peterbakonyi05 picture peterbakonyi05  路  4Comments

LittleFox94 picture LittleFox94  路  3Comments

benlesh picture benlesh  路  3Comments