1
votes

It excites me to see rxjs observables in action and it confuses every single time, but each time I fell more in love with them.

Well, I have three observables -

  1. an observable that refreshed based on timer this.autoRefreshView$
  2. a default observable that fetches based on today date this.intraDayView
  3. an observable that fetches based on yesterday's date (let's say) this.priorDayView$

There is a BehaviourSubject<> that emits if default observable view is to be fetched (based on current date) or yesterday's view based on a view property - today or yesterday.

I want to execute this.autoRefreshView$ only when emitted value is today. But, below it's executed if value is yesterday as well. Is my merge here incorrect? Following is what I tried -

this.dashboardViewService.dashboardView$
      .pipe(
        mergeMap((v) =>
          iif(
            () => v === 'today',
            merge(this.intraDayView$, this.autoRefreshView$),
            this.priorDayView$
          )
        )
      )
      .subscribe((response) => {
        if (response.date) {
          this.dateService.setDate(response.date);
          this.date= response.date;
        }
      });

This seems to work for me but the problem is when the view is not today, it still gets into merge(this.intraDayView$, this.autoRefreshView$) and executes this.autoRefreshView$ and updates the view, which I need to update only when event is today and not yesterday.

Any suggestion as what is missing? Please let me know if any other details are required.

I think it's because autoRefreshView is emitted based on timer, so may be when it's called for first time, timer is set? Is there a way to handle that elegantly ?

this.autoRefreshView$ = timer(0, 25000).pipe(
      switchMap(() => {
 
          );
      ....
      ....

UPDATE - Updating the question with complete code snippet -

ngOnInit(): void {
    this.intraDayView$ = this.api.getLatestData();
    this.priorDayView$ = this.api.getPastData();

    
    this.autoRefreshView$ = timer(0, 500000)
      .pipe(
        switchMap(() => {
                // new data should be available, call the api
                return this.api.getLatestData();
             });

    this.getDashboardView();
  }

  ngOnDestroy(): void {
    this.dashboardViewSusbscription.unsubscribe();
  }

  getDashboardView() {
    this.dashboardViewService.dashboardView$
      .pipe(
        switchMap((v) =>
          iif(
            () => v === 'today',
            merge(this.intraDayView$, this.autoRefreshView$),
            this.priorDayView$
          )
        )
      )
      .subscribe((response) => {
        if (response.date) {
          this.dateService.setDate(response.date);
          this.date= response.date;
        }
      });
  }
2

2 Answers

1
votes

I've put together a StackBlitz here based on what I think you are asking.

I have made a few assumptions. I have made a fake API call to show that something is returned on every emission of the timer if you want today's data and that only one request is made if you request a previous day's data. You'll see the view update accordingly.

Instead of the intraDayView$, why not do something like this:

  autoRefreshView$: Observable<{ data: string, requestedAt: string }> = timer(0, 1000).pipe(
    switchMap(() =>
      this.dashboardService.getDataForDay(new Date().toISOString())
    )
  );

We can then use autoRefreshView$ in a similar way you had, using iif.

finalView$: Observable<{ data: string, requestedAt: string }> = this.dashboardView$.pipe(
    switchMap((dashboardView: string) =>
      iif(
        () => dashboardView === 'today',
        this.autoRefreshView$,
        this.dashboardService.getDataForDay(dashboardView).pipe(take(1))
      )
    )
  );

Part of your issue was that you needed to use a switchMap, not mergeMap. switchMap will only maintain one inner source observable, so will cancel an in-flight observable should a new emission from your BehaviorSubject happen. So, in this case, what happens is:

  1. Subscribe to whatever the dashboard view is set to
  2. If it is 'today', then subscribe to a timer, where on every emission, you call your service to fetch your dashboard data.
  3. If it changes to a given date that isn't today, fetch the data for that day, making only one API call with take(1) (I am assuming that if it's not today, you will just want one API call for all the data for that day).

Also, some advice I would give is to make use of Angular templates like so:

<p *ngIf="finalView$ | async; let finalView">
  {{ finalView | json }}
</p>

Angular will manage the subscription and unsubscription for you here, so you won't have any trailing subscriptions left lying around.

0
votes

The reason autoRefresh$ continues to be executed, is because you are using mergeMap, which will create an additional "inner observable" every time it receives an emission. It does not stop listening to the previous source, which continues to propagate emissions from autoRefresh$.

You can instead use switchMap which maintains only a single inner source; so it stops listening to old sources when it receives a new emission:

this.dashboardViewService.dashboardView$.pipe(
    switchMap(v => iif(
        () => v === 'today',
        merge(this.intraDayView$, this.autoRefreshView$),
        this.priorDayView$
    ))
)