Home > Software design >  rxjs timer subscription being called multiple times
rxjs timer subscription being called multiple times

Time:12-30

I have an API from which I'm getting json array of events. In the json, I'm getting a date for which I am showing a timer using RxJS timer. When one of the timer is finished, I'm calling the API again to get latest array. Issue is when I'm calling the API after timer is finished, that API is being called 4 times instead of 1 time.

I also tried keeping the subs variable in another array and unsubscribing all of them in loop. But on doing this, getEvents() method was being called continuously without stopping. So if I can know what is the issue in current code that would be great.

//helper.service.ts
getCounter() {
    return timer(0, 1000);
}
getEvents() {
    const url = '/app/events'
    this.auth.get(url).then(x => {
      this.eventArr = x?.data.events
      this.showTimer()
    }).catch(err=>{
      console.log(err)
    })
}

async showTimer() {
    let unsubscribe = new Subject()
    for (let i = 0; i < this.eventArr.length; i  ) {
      let x = this.eventArr[i]
      const dateFuture = x?.inaugurateDt * 1000
      const now = new Date().getTime()
      const t = dateFuture - now
      x.timer = Math.floor(t / 1000)
        const subs = this.getCounter().pipe(takeUntil(unsubscribe)).subscribe(() => {
          x.timer--;
          if (x.timer <= 0) {
            this.unSubscribeCoundown(unsubscribe)
            this.getEvents()
          }
        });
      }  
    }
  }

CodePudding user response:

when you call getEvents after x.timer =0 then again showtimer function is calling and this loop continues for infinite time.

you have to mention some condition in the getEvents function so that you can check whether the showtime function should be called or not

CodePudding user response:

I created a working example that:

  1. includes multiple simultaneous countdowns
  2. unsubscribes all countdowns, whenever the first countdown reaches 0
  3. execute getEvents() exactly once after each global unsubscription
  4. mocks the backend-API with the getValuesFromMockAPI()-method (for testing-purposes)

Stackblitz example with additional features

Apart from the example below, I created an extended example on stackblitz where the countdowns are displayed on the UI and unsubscribing 'onDestroy()' is added.


The model-class

In order not to divert the focus from the essential, I assumed that there is just a simple EventDto-model, that contains a property with the number of seconds for the countdown:

class EventDto {
    timeInSec = 0; // number of seconds for countdown
}

The main logic

For a better understanding please note the comments in the code.

unsubscribeAllCountdowns = new Subject<void>();

ngOnInit(): void {
    this.getEvents();
}

getEvents() {
    console.log('*** Get Events ***');
    const url = '/app/events'
    this.getValuesFromMockAPI(url)
        .then(arr => this.showTimers(arr))
        .catch(err=> console.log(err));
}

showTimers(eventArray: EventDto[]) {
    // Create an array of countdown-observables:
    const eventsAsObservables = eventArray.map((x, i) => this.getCountdown(i, x.timeInSec));

    // Execute observables until all of them are terminated resp. unsubscribed:
    forkJoin(eventsAsObservables).pipe(
        // Unsubscribe this pipe after one execution:
        first(),
        // After all observables are done, execute 'getEvents' exactly once:
        tap(() => this.getEvents())
    )
    .subscribe();
}

getCountdown(eventIndex: number, timeInSec: number) {
    // First emission after 1000ms, then one emission every 1000ms:
    return timer(1000, 1000).pipe(
        // Do the countdown by decrementing the previous value:
        scan((acc, curr) => acc - 1, timeInSec),
        tap(timeRemainingInSec => console.log(`Event-Type [${eventIndex}]: ${timeRemainingInSec} sec`)),
        // If timer <= 0 is reached: Unsubscribe all running counters:
        tap(timeRemainingInSec => timeRemainingInSec > 0 || this.unsubscribeAllCountdowns.next()),
        // Unsubscribe when the 'unsubscribe'-subject is invoked:
        takeUntil(this.unsubscribeAllCountdowns)
    );
}

/* Mock backend API: */

getValuesFromMockAPI(url: string): Promise<EventDto[]> {
    return firstValueFrom(of([{ timeInSec: 5 } as EventDto, { timeInSec: 19 }, { timeInSec: 30 }, { timeInSec: 44 }]));
}
  • Related