Search code examples
reduxrxjsrxjs5redux-observable

Redux observable retry on timeout


I'd like to handle ajax timeouts using redux-observable so that if a timeout occurs (after say 10 seconds) it will retry the request another two times (firing a SAVE_RETRYING action every time so the UI can notify the user that it's retrying).

For any other type of error or if we've already retried twice it should just fail and fire a SAVE_FAILURE action.

I can make it work if I trigger the SAVE_RETRYING action using store.dispatch but getting deprecation warnings about this and I'm a bit stuck figuring out how to do it the proper way (adding SAVE_RETRYING to the stream that is returned by the epic).

Here's what I have (simplified):

function saveEpic(action$, store) {
  return action$.ofType('SAVE_CLICKED')
    .mergeMap(action => (
      ajax({
        url: '/a-long-request',
      })
        .timeout(10000)
        .map(() => ({ type: 'SAVE_SUCCESS' }))
        .retryWhen(errors => (
          errors.scan((count, e) => {
            if (count >= 2 || e.name !== 'TimeoutError') {
              throw e;
            } else {
              store.dispatch({ type: 'SAVE_RETRYING', count });
              return count + 1;
            }
          }, 0)))
        .startWith({ type: 'SAVE_STARTED' })
        .catch(() =>
          Observable.of({ type: 'SAVE_FAILURE' }))
    ));
}

How can I get that SAVE_RETRYING action up to the main stream? Thx.


Solution

  • This is not ideal, but you could use catch and undocumented second argument (which is the source observable) to resubscribe. The downside I don't like is you have to count retries in the mergeMap callback closure.

    function saveEpic(action$, store) {
      return action$.ofType('SAVE_CLICKED')
        .mergeMap(action => {
          let retries = 0;
          return ajax({
            url: '/a-long-request',
          })
            .timeout(10000)
            .map(() => ({ type: 'SAVE_SUCCESS' }))
            .catch((error, source) => {
              retries += 1;
              if (retries >= 2 || error.name !== 'TimeoutError') {
                return Observable.of({ type: 'SAVE_FAILURE' });
              }
    
              return source.startWith({ type: 'SAVE_RETRYING', count: retries });
            })
            .startWith({ type: 'SAVE_STARTED' });
        });
    }