Search code examples
angulartypescriptrxjsobservableasync-pipe

How can I make my chained observable timer stop executing when component is destroyed?


Below are the base observables that mimic my code:

class DataService {
    #data = this.http.get<Data[]>('https://example.com/getData').pipe(
        timeout(15000),
        catchError(err => of([]))
    )
    
    #adaptedData = this.#data.pipe(
        map(data => data.map(obj => new AdaptedObj(obj)))
    )

    #pollAdapted = timer(0, 60000).pipe(
        switchMap(() => this.#adaptedData),
        shareReplay(1)
    )

    get get() { return this.#adaptedData }
    get poll() { return this.#pollAdapted }
}

Then elsewhere in my code I have an additional layer of observables that further manipulate the data for use in a particular set of components. Examples:

class DataAnalyzerService {
    constructor(private dataService: DataService){}
    
    #analyzedDataStore = new BehaviorSubject<AnalyzedData[]>([])
    get analyzedData() { return this.#analyzedDataStore.value }

    #analyzedData = this.dataService.poll.pipe(
        filter(objs => objs.length > 0),
        map(objs => objs.map(obj => new AnalyzedObj(obj))),
        tap(analyzedData => this.#analyzedDataStore.next(analyzedData))
    )

    #metricCalculator = () => {
        const data = this.analyzedData
        // Do calculations and filtration
        return dashboardMetricsArr
    }

    #dashboardStats = this.#analyzedData.pipe(
        switchMap(() => of(this.#metricCalculator))
    )

    get dashboardStats() { return this.#dashboardStats }
}

Then in my component I am using the dashboard metrics with an async pipe like this:

<ng-container *ngIf="(dashboardStats | async) as stats">
    // Use the Data
<ng-container>

The reason I have the observables broken up like this is because there are different components that will need the data at different layers of analysis, additionally, some components will only need the data once and others will need it constantly updated at the 1 minute interval.

Currently, the application works fine, but when I logout or exit the components that use the data, I notice the http requests are ongoing each minute because the DataService poll observable is never unsubscribed. Right now, I only have a single subscription initialized using the async pipe in the template, so I know I am closing that specific subscription when I navigate away from that component or log out all-together. Additionally, I have verified that both the #analyzedData and #dashboardStats observables cease execution after navigating away. Only the poll observable continues.

Any ideas on how to get the polling to stop until I am back in a component that needs it?


Solution

  • shareReplay has the refCount config option as described in the manual:

    As of RXJS version 6.4.0 a new overload signature was added to allow for manual control over what happens when the operators internal reference counter drops to zero. If refCount is true, the source will be unsubscribed from once the reference count drops to zero, i.e. the inner ReplaySubject will be unsubscribed. All new subscribers will receive value emissions from a new ReplaySubject which in turn will cause a new subscription to the source observable. If refCount is false on the other hand, the source will not be unsubscribed meaning that the inner ReplaySubject will still be subscribed to the source (and potentially run for ever).

    You use shareReplay(1), which uses the default value refCount: false, hence it will not unsubscribe. Turn on reference counting for automatic unsubscription:

    shareReplay({bufferSize: 1, refCount: true})