bygrace
bygrace

Reputation: 5988

rxjs poll for data on timer and reset timerwhen manually refreshed

I am using the following libraries in the relevant application: Angular 4.x, ngrx 4.x, rxjs 5.4.x

I have an api that I need to poll every 5 minutes. The user is also able to manually refresh the data. That data is stored in an ngrx store. I am using ngrx effects so the data is retrieved by dispatching an action of type 'FETCH'.

I want to setup a rxjs stream where it will dispatch the 'FETCH' action to the ngrx store. It will be a sliding 5 minute timer that resets when the user manually updates the store. The stream should initially emit a value when subscribed.

I'm not sure how I can reset the timer. In plain javascript I would do something like the following:

console.clear();
let timer;
let counter = 0;

function fetch() {
  console.log('fetch', counter++);
  poll();
}

function poll() {
  if (timer != null) {
    window.clearTimeout(timer);
  }
  timer = window.setTimeout(() => {
    console.log('poll');
    fetch();
  }, 5000);
}

function manualGet() {
  console.log('manual');
  fetch();
}

fetch();
<button onClick="manualGet()">Get Data</button>

Question: How do I emit on an interval that is reset when another stream emits like the example again?

Upvotes: 4

Views: 2049

Answers (3)

Ruzihm
Ruzihm

Reputation: 20249

My data collection was in the form of an observable that would make the request when subscribed/resubscribed so I adjusted Ingo Bürk's answer for my needs:

// observable that does the requesting goes here
const dataCollection$ = defer(()=>of(this.count++));

const clickEvent$ = fromEvent(button, 'click');
const clickOrWait$ = clickEvent$.pipe(
  // startWith null to start the timer before button is clicked
  startWith(null),

  // emit immediately so clicking button immediately emits
  switchMap(()=>timer(0,5000)),

  // skip the first immediate emit so we don't infinitely repeat
  skip(1),
);
const result$ = dataCollection$.pipe(
  repeat({delay:()=>clickOrWait$}),
);

result$.subscribe((res)=>console.log("value output: "+res));

My particular use case was to use this polling to handle data in an angular component using async/without subscribe. example:

@Component({
  imports: [
    AsyncPipe,
  ],
  selector: 'app-root',
  standalone: true,
  template: "<p>count: {{result$ | async}}</p> <button #button>Click me!</button>"
})
export class AppComponent implements AfterViewInit {
  @ViewChild('button') button: any;
  result$: Observable<number> = EMPTY;
  count = 1

  public ngAfterViewInit() {
    // HttpClient call could go here
    const dataCollection$ = defer(()=>of(this.count++));

    const clickEvent$ = fromEvent(this.button.nativeElement, 'click');
    const clickOrRepeat$ = clickEvent$.pipe(
      startWith(null),
      switchMap(()=>timer(0,5000)),
      skip(1)
    );
    this.result$ = dataCollection$.pipe(
      repeat({delay:()=> clickOrRepeat$}),
    );
  }
}

Stackblitz: https://stackblitz.com/edit/stackblitz-starters-5beot4?file=src%2Fmain.ts

Upvotes: 0

Ingo B&#252;rk
Ingo B&#252;rk

Reputation: 20033

You want two components to your stream – a timer and some user input. So let's start with the user input. I'll assume some button which can be clicked:

const userInput$ = Observable.fromEvent(button, 'click');

Now we want to start a timer which resets everytime userInput$ emits. We can do that using

userInput$.switchMap(() => Observable.timer(0, 5000));

However, we also want this stream to start without the user having to first click the button. But that's also not a problem:

userInput$.startWith(null);

Now we put it all together:

Observable.fromEvent(button, 'click')
    .startWith(null)
    .switchMap(() => Observable.timer(0, 5000))
    .subscribe(() => dispatchFetch());

Note that I am following your examples of using a 5 second timer, not a 5 minute timer (which you mentioned in the question.)

Upvotes: 7

bygrace
bygrace

Reputation: 5988

After writing it out in vanilla JS I realized that the source of the timer should be the data. I was struggling to figure out what the source would be. Clearly it couldn't be the timer since I needed to reset it.

I'm open to better options but here is how I solved it:

console.clear();
let counter = 0;
const data = new Rx.BehaviorSubject(null);

function fetch() {
  data.next(counter++);
}

function manualGet() {
  console.log('manual');
  fetch();
}

// setup poll
data.switchMap(() => Rx.Observable.timer(5000))
  .subscribe(() => { 
    console.log('poll');
    fetch();
  });

// subscribe to the data
data.filter(x => x != null).
  subscribe(x => { console.log('data', x); });

// do the first fetch
fetch();
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
<button onClick="manualGet()">Get Data</button>

With ngrx I am listening for the success action related to the fetch event.

Upvotes: 0

Related Questions