Unable to call the nested function with timer until met condition?

Viewed 490

I have 2 methods that call some APIs:

  1. getData(param).
  2. getAnotherData(resp from get data).

The below method calls the first method:

this.service.getData(param).subscribe(res => {
    this.variable = res;

    // here based on the first response i will call the second api 

    this.service.getAnotherData(this.variable['data']).subscribe(res2 =>  

    // Here, I will get a response which will be either "started"
    // or not "started". If it's not "started", then I have to call
    // this API until I get "started" for up to 1 minute.
)})

How can I achieve this?

5 Answers
  1. Try to avoid nested subscriptions. Instead try to use higher order mapping operators like switchMap.

  2. You could use RxJS repeat and takeWhile operators to subscribe again to an observable until it satisfies a condition.

Try the following

this.service.getData(param).pipe(
  switchMap(res => 
    this.service.getAnotherData(res['data']).pipe(
      repeat(),
      takeWhile(res => res === 'started')      // poll `getAnotherData()` until response is 'started'
    )
  )
).subscribe(
  res => { },
  err => { }
);

You may consider below approach using timer operator

  private readonly _stop = new Subject<void>();
  private readonly _start = new Subject<void>();
  trialTime = 60 * 1000;
  trialInterval = 500;
  timer$ = timer(0, this.trialInterval).pipe(
    takeUntil(this._stop),
    takeWhile( i => (this.trialTime / this.trialInterval) > i),
    repeatWhen(() => this._start),
  );
  start(): void { this._start.next();}
  stop(): void { this._stop.next();}

  responses = [];

  ngOnInit()  {
    const param = 1;
    this.timer$.pipe(
      mergeMap(() => this.service.getData(param)),
      switchMap(res => this.service.getAnotherData(res.data)),
      tap(res => this.responses.push(res)),
      tap(res => { if (res === 'started') { this.stop(); }}),
    ).subscribe();
  }

The above approach will run for 60s and if not started, this will automatically stop.

A user can also manually stop and start the observable

See Below link on stackblitz

If I were you and had to do something similar with vanilla JS, I would use this approach:

1. Subscribe to `getData` function call result
2. In the scope of subscription handler:
  2.1 Add a new method to indicate if 1 minute has elapsed with initial value to `false`
  2.2 Use `setTimeout` to callback a function and update flag after a minute to `true`
  2.3 Create inline function which would call `getAnotherData` method and subscribe to it's result and within its subscription scope
    2.3.1 If result is started, clear timeout so that timeout event handler will be cleared
    2.3.2 If one minute is not elapsed, call inline function again
  2.4 call inline function

something like:

this.service.getData(param).subscribe(res => {
    this.variable = res;
    const minuteElapsed = false;

    // based on the first response
    const timeoutHandler = setTimeout(
        () => { minuteElapsed = true; },
        60000
    );
    const fetchAnotherData = () => {
        this.service.getAnotherData(this.variable['data']).subscribe(res2 =>  
            if (res === 'started') {
                clearTimeout(timeoutHandler); // clean up
                // do stuff!
            } else if (!minuteElapsed) {
                fetchAnotherData(); // calling again!
            }
        });
    };
    fetchAnotherData(); // call inline function
})

Like what was mentioned in the other answers, avoid nested subscriptions whenever possible. I'd approach this with rxjs expand operator, which we will use to re-run the second API-request until your desired result is received.

expand will pass-through the observed value from the API request and, depending on a condition, you can have it either stop additional processing or just repeat the call.

We can then use takeUntil to implement a timeout to the request. It might look something like this (I'm borrowing the scaffolding from another answer):

this.service.getData(param).pipe(
  switchMap(res => 
    this.service.getAnotherData(res['data']).pipe(
      expand((result) => result === 'finished' ? EMPTY : this.service.getAnotherData(res['data']).pipe(delay(500))),
      takeUntil(timer(60000).pipe(
        // Here, you can react to the timeout
        tap(() => console.log('Timeout occurred'))
      )),
    )
  )
).subscribe(
  (res) => {
    /**
      Do whatever you want with the result, you need to check again for 'finished',
      since everything is passed through
     */
  }
  err => { }
);

Have a play with it here: https://retry-api.stackblitz.io

EDIT: usage of toPromise will be depreciated, see comments on this answer


if your function containing the calls is async you could use await to wait for the result in this particular case.

  const res = await this.service.getData(param).toPromise();
  this.variable = res;
  
  let anotherData;
  while (anotherData !== 'started') {
    anotherData = await this.service.getAnotherData(this.variable['data']).toPromise();
  }
Related