31

In my Angular 2 app i have many observables and subscriptions. Ofcourse I should unsubscribe when I leave the page, but i'm trying to find out if it's possible to get the number of active subscriptions. Just for debugging info or when i forget to unsubscribe.

Is there such information available in rxjs?

Stefan Koenen
  • 2,289
  • 2
  • 20
  • 32
  • I don't know (hence comment & not answer)... But I do know that if you use the `async` pipe in your views instead of subscribing to observables you do not need to unsubscribe because the framework will handle that for you. – Brocco Sep 08 '16 at 01:20

6 Answers6

21

Probably a bit late, but you could leverage the help of rxjs-spy.

This solution is equivalent the proposed one, and, in my opinion, better maintainable.

I usually enable it globally in main.ts as a fire and forget strategy. You simply have to:

  1. install rxjs-spy via your package manager

  2. import in main.ts the reference to the create function: import { create } from 'rxjs-spy';

  3. initialize the rxjs-spy in debug builds right after the angular initialization snippet:

    
    if (environment.production) {
        enableProdMode();
    }
    else {
        //we enable RXjs Spy on non production bulds only
        const spy = create();
        // we call show for two purposes: first is to log to the console an empty snapshot so we can see that everything is working as expected, then to suppress unused variable usage (the latter is a convention on mine)
        spy.show();
    }
    
    
  4. give your observables a name:

    
    import { tag } from 'rxjs-spy/operators';
    
    ...
    
    // This is a sample method which asks for a "Product" entity. Product and this.http is omitted as the focus is on tagging the observable
    public getProductById(productId: number): Observable<Product> {
        let params = new HttpParams()
            .append('productId', productId.toString())
            ;
        // we tag the returned observable with the name 'getProductById' (this is a convention on mine, you can choose whatsoever name)
        return this.http.get<Product>(this.baseUrl + "api/product", { params: params }).pipe(tag("getProductById"));
    }
    
    
  5. when you need to look at rxjs state, you can simply open the console window and use rxSpy.show() to have the current snapshot

You may use additional commands. A very detailed tutorial was Debugging with rxjs Spy (the link is dead as of 2020). Another good tutorial is Tooling with RxJS.

(I'd be very glad if somebody reading this answer manages to fix the formatting as I cannot have it properly within the list)

Yennefer
  • 5,704
  • 7
  • 31
  • 44
15

The following utility function may help...

function subscriberCount<T>(sourceObservable: Observable<T>, description: string) {
  let counter = 0;
  return new Observable((subscriber: Subscriber<T>) => {
    const subscription = sourceObservable.subscribe(subscriber);
    counter++;
    console.log(`${description} subscriptions: ${counter}`);

    return () => {
      subscription.unsubscribe();
      counter--;
      console.log(`${description} subscriptions: ${counter}`);
    }
  });
}

Use it like this:

const timer$ = subscriberCount(Observable.timer(1000), 'Timer');

And the console will log whenever the subscriber count changes

Miller
  • 2,742
  • 22
  • 20
4

There's actually a much simpler way:

you can just convert your observables to "BehaviorSubject", they have a property called "observers" which gives you the number of subscriptions.

before:

  let myObservable = somethingObservable;
  // Do something complicated to get the subscription count

after:

let myObservable = new BehaviorSubject();
let count = myObservable.observers.length

note that if you just need to know if there's at least one observable, you can also just do

myBehaviorSubject.observed
  • Do you have a way to know where these observers have been created? I'm in the case where I know I have a memory leak (observers grow indefinitely) but I'm having a hard time pinpointing which part (which component/service) of the code creates the observers that leak. – Sébastien Tromp Jan 24 '23 at 10:25
3

In your case you could make good use of RxJS Subjects with refCounting. You would give the Subject your source observable and let refCount manage its subscriptions. refCount will unsubscribe from your source observable if there are no observers listening to it. On the other hand it creates new instance of source observable if observer count was 0 and an observer subscribed to it.

Subject would act as a proxy between the observer(s) and the source observable and manage subscribing and unsubscribing from it. In essence how it would work is when you have your first observer subscribe to your Subject the Subject in turn subscribes to your source observable (refCount went from 0 to 1). Subjects allow multiple observers listening to the unicast source observable making it multicast. When observers start to unsubscribe and the refCount falls down to 0 again the Subject itself will unsubscribe from source observable.

It's better understood in code:

const {
  Observable
} = Rx;

let sourceObservable = Observable
  .create((observer) => {
    let count = 0;
    let interval = setInterval(() => {
      observer.next(count++)
    }, 700);

    setTimeout(() => {
      clearInterval(interval);
      observer.complete();
    }, 5500);

    return () => {
      clearInterval(interval);
      console.log('######## Source observable unsubscribed');
    }
  })
  .do((x) => console.log('#### Source emits: ' + x));

let subject = sourceObservable
  .share()
  //.do((x) => console.log('#### Subject emits: ' + x))
  ;

let pageOneObserver;
let pageTwoObserver;
let pageThreeObserver;

setTimeout(() => {
  console.log('pageOneObserver will subscribe');
  pageOneObserver = subject.subscribe({
    next: (x) => {
      console.log('pageOneObserver gets: ' + x);
    },
    complete: () => {
      console.log('pageOneObserver: complete');
    }
  });
}, 1000);

setTimeout(() => {
  console.log('pageTwoObserver will subscribe');
  pageTwoObserver = subject.subscribe({
    next: (x) => {
      console.log('pageTwoObserver gets: ' + x);
    },
    complete: () => {
      console.log('pageTwoObserver: complete');
    }
  });
}, 4000);

setTimeout(() => {
  console.log('pageOneObserver will unsubscribe');
  pageOneObserver.unsubscribe();
}, 7000);

setTimeout(() => {
  console.log('pageTwoObserver will unsubscribe');
  pageTwoObserver.unsubscribe();
}, 10000);

setTimeout(() => {
  console.log('pageThreeObserver will subscribe');
  pageThreeObserver = subject.subscribe({
    next: (x) => {
      console.log('pageThreeObserver gets: ' + x);
    },
    complete: () => {
      console.log('pageThreeObserver: complete');
    }
  });
}, 13000);

setTimeout(() => {
  console.log('pageThreeObserver will unsubscribe');
  pageThreeObserver.unsubscribe();
}, 16000);
<script src="https://unpkg.com/rxjs@5.1.1/bundles/Rx.min.js"></script>

There are some shorthand ways of creating subjects. For example:

sourceObservable.share();
// is the same as
sourceObservable.publish().refCount();
sourceObservable.publish().refCount();
// is the same as
sourceObservable.multicast(new Rx.Subject()).refCount();
sourceObservable.publishReplay().refCount();
// is the same as
sourceObservable.multicast(new Rx.ReplaySubject(1)).refCount();
sourceObservable.publishBehavior().refCount();
// is the same as
sourceObservable.multicast(new Rx.BehaviorSubject(0)).refCount();
sourceObservable.publishLast().refCount();
// is the same as
sourceObservable.multicast(new Rx.AsyncSubject()).refCount();

sourceObservable.share(); has also built in subject factory which means when the source observable is complete at some point we have to create a new instance of the sourceObservable but that can be done with only a new instance of the subject of your choice. Using the other available subjects below we have to explicitly return a factory function to the multicast operator.

When you would like to use other subject types besides the Rx.Subject() and wanting to make your observable subscription truly reusable you have to use subject factories (which is just a function that return a new instance of any subject you like to use) which is illustrated below:

const {
  Observable
} = Rx;

let sourceObservable = Observable
  .create((observer) => {
    let count = 0;
    let interval = setInterval(() => {
      observer.next(count++)
    }, 700);

    setTimeout(() => {
      clearInterval(interval);
      observer.complete();
    }, 5500);

    return () => {
      clearInterval(interval);
      console.log('######## Source observable unsubscribed');
    }
  })
  .do((x) => console.log('#### Source emits: ' + x));

/* You could return whatever subject instance you like here */
let subjectFactory = () => new Rx.ReplaySubject(1);

let subject = sourceObservable
 .multicast(subjectFactory)
 .refCount();
 //.do((x) => console.log('#### Subject emits: ' + x))
 ;

let pageOneObserver;
let pageTwoObserver;
let pageThreeObserver;

setTimeout(() => {
  console.log('pageOneObserver will subscribe');
  pageOneObserver = subject.subscribe({
    next: (x) => {
      console.log('pageOneObserver gets: ' + x);
    },
    complete: () => {
      console.log('pageOneObserver: complete');
    }
  });
}, 1000);

setTimeout(() => {
  console.log('pageTwoObserver will subscribe');
  pageTwoObserver = subject.subscribe({
    next: (x) => {
      console.log('pageTwoObserver gets: ' + x);
    },
    complete: () => {
      console.log('pageTwoObserver: complete');
    }
  });
}, 4000);

setTimeout(() => {
  console.log('pageOneObserver will unsubscribe');
  pageOneObserver.unsubscribe();
}, 7000);

setTimeout(() => {
  console.log('pageTwoObserver will unsubscribe');
  pageTwoObserver.unsubscribe();
}, 10000);

setTimeout(() => {
  console.log('pageThreeObserver will subscribe');
  pageThreeObserver = subject.subscribe({
    next: (x) => {
      console.log('pageThreeObserver gets: ' + x);
    },
    complete: () => {
      console.log('pageThreeObserver: complete');
    }
  });
}, 13000);

setTimeout(() => {
  console.log('pageThreeObserver will unsubscribe');
  pageThreeObserver.unsubscribe();
}, 16000);
<script src="https://unpkg.com/rxjs@5.1.1/bundles/Rx.min.js"></script>

Feel free to ask anything if there is still something unclear.

Robert
  • 457
  • 7
  • 19
0

You can make use the Subscription class will come with an add, remove and unsubscribe method for you to manage subscription. The length of _subscriptions properties basically tells you how many active subscription that you have. This approach play very well with observable unsubscription upon onDestroy in Angular

import { interval, Subscription } from "rxjs";

const subs = new Subscription();
const sub = interval(1000).subscribe();

subs.add(sub);
console.log(sub);
Fan Cheung
  • 10,745
  • 3
  • 17
  • 39
-3

Use this operation to unsubscribe from your subscription after the component is destroyed

Example:

ngOnInit() {
    interval(1000)
        .pipe(
            untilComponentDestroyed(this)       // <-- use the operator
        )
        .subscribe();
}
Armen Vardanyan
  • 3,214
  • 1
  • 13
  • 34