9

I'm attempting to repeat a request until the response has data using RxJS, at which point I'd like to call a success (or failure) handler, but I'm having trouble w/RxJS. Here's my current approach:

// ... redux-observable action observable
.mergeMap(() =>
    fetchData()
    .repeatWhen(response =>
        response.takeWhile(({ data }) => !data.length)
        .of(response)
    )
)
.map(successFunction)
.catch(failureFunction);

Disclaimer: I'm quite new to RxJS....

Srdjan Pazin
  • 103
  • 2
  • 5
seitzej
  • 131
  • 1
  • 7

3 Answers3

6

It's simpler to repeat the request on an interval, filter on its result and take one emission.

Observable.timer(0, 500)
  .flatMap(() => fetchData())
  .filter(r => r.data && r.data.length)
  .take(1)
  .timeout(10000)

http://jsbin.com/cafericore/1/edit?js,console

teebot
  • 1,089
  • 2
  • 12
  • 25
  • Be careful about using only half a second for the timer. If it doesn't return within that time it will try again, and while RxJS will cancel the local call it's still hitting your server 20 times in 10 seconds. So have to use common sense here based on what the request is doing. – Simon_Weaver Feb 04 '19 at 00:18
  • I do however think this is the best approach. `RepeatWhen` / `RetryWhen` both seem appropriate but neither quite does what we want here. – Simon_Weaver Feb 04 '19 at 00:46
5

It sounds like you want to suppress ajax results and retry until you get the response you want. I'd do it like so:

// observable that will re-fetch each time it is subscribed
const request = Observable.defer(() => fetchData());

// each time request produces its value, check the value
// and if it is not what you want, return the request
// observable, else return an observable with the response
// use switchMap() to then subscribe to the returned
// observable.
const requestWithRetry = request.switchMap(r =>
    r.data.length ? Observable.of(r) : requestWithRetry);
Brandon
  • 38,310
  • 8
  • 82
  • 87
  • Brandon, I am dumbfounded. How does this work recursively? The first request with the wrong length leads to `request` being subscribed again which leads to a new execution of `fetchData` and subscription to that result. But why would that result flow again through the `switchMap`? – user3743222 Apr 13 '17 at 20:20
  • @user3743222 yeah you are right. Not sure what I was thinking. First version should return `requestWithRetry`. :facepalm: – Brandon Apr 13 '17 at 20:24
  • I get it now. Pretty neat. Now I wonder about how the stack evolves. But in any case for a limited amount of retrials there should not be any issue with that. – user3743222 Apr 14 '17 at 00:14
  • I believe this answer is more elegant than the one provided above, unfortunately, I couldn't get it to work properly (more than likely due to my ineptitude with RxJS). – cjones26 Sep 07 '17 at 14:37
  • 1
    And it never stops! – Simon_Weaver Feb 04 '19 at 00:16
5

Empty data is not an error so first we check if the data is empty and throw an error if it is. then retryWhen can be used to test for this error and retry as long as it's occurring.

.mergeMap(() =>
   fetchData()
   .map(data => {
       if (!data.length) {
          throw 'no data';
       } 
       return data;
    })
   .retryWhen(errors => errors.takeWhile(error => error === 'no data'))
)
.map(successFunction)
.catch(failureFunction);
Ofer Herman
  • 3,018
  • 1
  • 20
  • 21
  • the problem with `repeatWhen` is that you dont have access to `data`. The selector in `repeatWhen` only receives the `onComplete` notification of the source. Hence that makes it not usable in your case. The logic of `repeatWhen` is that you resubscribe to a source when that source has completed and then on some other condition independent of that source. The advantage of `retrywhen` is that the error notification allow to pass an argument. That is not the case for the `onComplete` notification – user3743222 Apr 14 '17 at 00:06
  • @seitzej `retryWhen` only works when the underlying observable enters an error state AFAIK this can be done when throwing, why is this a problem? – Ofer Herman Apr 14 '17 at 04:18
  • Another problem with this is there's no timer involved. It will just boom boom boom hit your server FOREVER! I think adding `.pipe(delay(3000))` to the retryWhen observable would fix this, and adding `timeout` or `timeoutWith()` to the end is the easiest way to stop it going on forever. – Simon_Weaver Feb 04 '19 at 00:48