Volodymyr Melnyk
Volodymyr Melnyk

Reputation: 482

Dynamically changed polling interval

I need my frontend application to poll my backend application and refresh the displayed data. I solved it in a certain way, but I have a feeling that it could be optimized. Here's my solution:

        this.pollDataSource$ = new BehaviorSubject<number>(this.pollingInterval);

        this.pollDataSource$
            .pipe(
                switchMap(duration =>
                    of(duration)
                        .pipe(
                            filter(duration => duration !== 0),
                            delay(duration),
                            concatMap(() => this.kubernetesObjectList$)
                        )
                )
            )
            .subscribe({
                next: (kubernetesObjectList) => {
                    this.dataSource.data = kubernetesObjectList;
                    if (this.pollingInterval > 0) {
                        this.pollDataSource$.next(this.pollingInterval);
                    }
                }
            });

        this.pollDataSource$.next(-1);

So, I have a drop-down selector which affects this.pollingInterval. Also, I have this.pollDataSource$ which is a BehaviorSubject<number>. It emits a number which is being used as the duration before the next poll.

When this.pollDataSource$ emits -1 (it happens when the user clicks the Refresh button), the data source must be polled immediately, disregarding what polling interval is set.

When this.pollDataSource$ emits some positive number (it happens when a user selects a certain polling interval from the drop-down selector), this number must be used as the duration before the next refresh.

When this.pollDataSource$ emits 0 (it happens when a user selects the Stop Polling option in the same drop-down selector), we must stop polling until the user selects a new polling interval.

And things work just perfectly. The page is loaded, this.pollingInterval has 10000 by default, so the user gets the data immediately, but in 10 seconds it's being updated. When the user hits Refresh, the data is being updated and the next automatic refresh happens after 10 seconds since that. When the user switches to Stop Polling, the data stay still. When the user switches to another interval, the data is being updated again. Everything's great! But I have a feeling that my solution is not optimal. I just don't like that construction: ...pipe...switchMap...of...pipe... Is there a way to simplify that?

Thanks in advance for all the clues.

Upvotes: 2

Views: 1325

Answers (2)

Ben Lesh
Ben Lesh

Reputation: 108471

If you're wanting to poll on an interval consider the trying the following:

const intervalDelays$ = new BehaviorSubject(10000);

intervalDelays.pipe(
  switchMap(ms => ms === 0 ? EMPTY : interval(ms).pipe(
    concatMap(() => fetchDataObservableHere$)
  ))
);

interval uses a setInterval under the hood. Just be sure that ms is longer than fetchDataObservableHere$ takes to complete. Otherwise you'll end up with backpressure issues.

If backpressure is a concern, you can do this:

const intervalDelays$ = new BehaviorSubject(10000);

intervalDelays.pipe(
  switchMap(ms => ms < 0
    // No wait? We'll assume it's "off"
    ? EMPTY
    // just an observable to start the recursive expand call.
    // 'start' doesn't matter. Could be `true` or anything, really.
    : of('start').pipe(
      expand(() => timer(ms).pipe(
        switchMap(() => fetchDataObservableHere$),
        tap(data => {
          // PROCESS DATA HERE
          // this ensures that it's done processing before you move on.
        }),
      )),
  ))
);

The above code will see incoming changes to the delay of the interval, and start a new inner observable that will recursively execute in such a way that it will wait for each value to come back and be processed before making another request.

Upvotes: 2

Picci
Picci

Reputation: 17752

The reason I was mentioning expand is that this operator can be used instead of the construct

someSubject.pipe(
  // do stuff
).subscribe(
  next: data => {
    // do some more stuff
    someSubject.next(something)
  }

This is the construct you are actually using, therefore the suggestion about expand.

Then, getting back to your question without introducing expand, what you may consider is something like

this.pollDataSource$
        .pipe(
            switchMap(duration =>
                return duration === 0 ?
                  NEVER :
                  this.kubernetesObjectList$.pipe(delay(duration));
            )
        )
        .subscribe({
            next: (kubernetesObjectList) => {
                this.dataSource.data = kubernetesObjectList;
                if (this.pollingInterval > 0) {
                    this.pollDataSource$.next(this.pollingInterval);
                }
            }
        });

    this.pollDataSource$.next(-1);

With expand it could look like

this.pollDataSource$
            .pipe(
                expand(duration =>
                    return duration === 0 ?
                      NEVER :
                      this.kubernetesObjectList$.pipe(
                         delay(duration),
                         map(() =>  duration)
                      );
                )
            )
            .subscribe({
                next: (kubernetesObjectList) => {
                    this.dataSource.data = kubernetesObjectList;
                }
            });

        this.pollDataSource$.next(-1);

Whether these versions of the code are cleaner that your original one, which I found clear and readable, is probably a matter of personal taste.

All the "could" and "would" I have used are due to the fact that I do not have a playground where to test this code, so it is very much possible that you will find something not working here.

Upvotes: 1

Related Questions