3

I'd like to use angular-rx for a simple refresh button for results. If the user clicks the refresh button the results are reloaded. If the user clicks the the refresh button 100times in 1 second, only the latest results are loaded. If the results failed for some reason, that doesn't mean the refresh button should stop working.

To achieve the last point I'd like to keep a subscription (or resubscribe) even if it fails, but I can not work out how to do that?

This doesn't work, but here's a simple example where I try resubscribing on error:

var refreshObs = $scope.$createObservableFunction('refresh');

var doSubscribe = function () {
  refreshObs
  .select(function (x, idx, obs) {
      // get the results.
      // in here might throw an exception
  })
  .switch()
  .subscribe(
  function (x) { /*show the results*/ }, // on next
  function (err) { // on error
      doSubscribe(); // re-subscribe
  },
  function () { } // on complete
  );
};
doSubscribe();

I figure this is common enough there should be some standard practice to achieve this?

UPDATE

Using the suggested solution, this is what I've made to test:

// using angularjs and the rx.lite.js library
var testCount = 0;
var obsSubject = new rx.Subject(); // note. rx is injected but is really Rx
$scope.refreshButton = function () { // click runs this
  obsSubject.onNext();
};

obsSubject.map(function () {
  testCount++;
  if (testCount % 2 === 0) {
      throw new Error("something to catch");
  }
  return 1;
})
.catch(function (e) {
  return rx.Observable.return(1);
})
.subscribe(
    function (x) {
    // do something with results
});

And these are my test results:

  1. Refresh button clicked
  2. obsSubject.onNext() called
  3. map function returns 1.
  4. subscribe onNext is fired
  5. Refresh button clicked
  6. obsSubject.onNext() called
  7. map function throw error
  8. enters catch function
  9. subscribe onNext is fired
  10. Refresh button clicked
  11. obsSubject.onNext() called
  12. Nothing. I need to keep subscription

My understanding is that catch should keep the subscription, but my testing indicates it doesn't. Why?

TmTron
  • 17,012
  • 10
  • 94
  • 142
Steve
  • 1,584
  • 2
  • 18
  • 32
  • What are you trying to achieve? Resubscribing on error is an anti-pattern. – André Staltz Sep 03 '14 at 15:52
  • @AndréStaltz A refresh button for results. If it fails it'll inform the user. If the user presses it again it'd be expected to attempt to get the results again. Why is that an anti-pattern? – Steve Sep 03 '14 at 22:38
  • Because you should typically have only one 'doSubscribe', and I can't even imagine a case where it's strictly necessary to resubscribe on error. – André Staltz Sep 04 '14 at 11:51

1 Answers1

5

Based on the context given in your comment, you want:

  • Every refresh button to trigger a 'get results'
  • Every error to be displayed to the user

You really do not need the resubscribing, it's an anti-pattern because code in Rx never depends on that, and the additional recursive call just confuses a reader. It also reminds us of callback hell.

In this case, you should:

  • Remove the doSubscribe() calls, because you don't need them. With that code, you already have the behavior that every refresh click will trigger a new 'get results'.
  • Replace select().switch() with .flatMap() (or .flatMapLatest()). When you do the select(), the result is a metastream (stream of streams), and you are using switch() to flatten the metastream into a stream. That's all what flatMap does, but in one operation only. You can also understand flatMap as .then() of JS Promises.
  • Include the operator .catch() which will treat your error, as in a catch block. The reason you can't get more results after an error happens, is that an Observable is always terminated on an error or on a 'complete' event. With the catch() operator, we can replace errors with sane events on the Observable, so that it can continue.

To improve your code:

var refreshObs = $scope.$createObservableFunction('refresh');

refreshObs
  .flatMapLatest(function (x, idx, obs) {
    // get the results.
    // in here might throw an exception
    // should return an Observable of the results
  })
  .catch(function(e) {
      // do something with the error
      return Rx.Observable.empty(); // replace the error with nothing
  })
  .subscribe(function (x) { 
      // on next
  });

Notice also that I removed onError and onComplete handlers since there isn't anything to do inside them.

Also take a look at more operators. For instance retry() can be used to automatically 'get results' again every time an error happens. See https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/retry.md

Use retry() in combination with do() in order to handle the error (do), and allow the subscriber to automatically resubscribe to the source observable (retry).

refreshObs
  .flatMapLatest(function (x, idx, obs) {
    // get the results.
    // in here might throw an exception
    // should return an Observable of the results
  })
  .do(function(){}, // noop for onNext
  function(e) {
      // do something with the error
  })
  .retry()
  .subscribe(function (x) { 
      // on next
  });

See a working example here: http://jsfiddle.net/staltz/9wd13gp9/9/

André Staltz
  • 13,304
  • 9
  • 48
  • 58
  • Thanks for the example André (yes, want to avoid callback hell) tho I still don't understand how the refresh button can recover from an error. Say the user has bad internet and the results time out. The onError function will tell the user the results timed out, so the user will try again by hitting refresh, but the refresh button won't work (note I'm using refresh button as a simple example, it could also be a keyword search that runs half a second after the user finishes typing). It looks like retry() keeps the subscription but swallows the error, so how will the user ever know? – Steve Sep 04 '14 at 23:02
  • The refresh button "working" to actually get results again depends on how `refreshObs` is constructed at `$scope.$createObservableFunction('refresh')`. For instance, if `refreshObs` is something like a plain `Rx.Observable.fromEvent(button, 'click')`, then yes, every time button is clicked, the results will be get. So the question is, what does `$scope.$createObservableFunction` do? – André Staltz Sep 05 '14 at 14:19
  • Yes, it is like that (eg https://github.com/Reactive-Extensions/rx.angular.js/tree/master/examples/%24createObservableFunction) so in that sense it does always fire. But the subscription is no longer active after an exception such as a timeout. I need to inform the user from the exception but also keep the results coming through on retry. I was hoping to avoid my own object with result and possible exception but do you think the answer at this link is the best approach? http://stackoverflow.com/a/13323923/1330601 – Steve Sep 06 '14 at 03:40
  • Ok, now I understand your problem, you need a `catch()` operator, see the updated answer above. – André Staltz Sep 06 '14 at 08:50
  • Thanks for your patience with this André. What you say makes sense but it's not working for me. I've updated my post question to show the test I am using. The subscription is still cleared. Why is this? – Steve Sep 07 '14 at 02:14
  • 2
    The important things to keep in mind here are: (1) an error on an Observable always terminates it, just like an onComplete event does; (2) `Rx.Observable.return(1)` terminates right after emitting `1`; (3) `catch()` just replaces your Observable ending on an error with the new given Observable. Join these three facts, and hence your subscriber won't see anything after `Rx.Observable.return(1)`. What you want is a side-effect for errors `.do()` and then a `.retry()`. Checkout the updated answer. – André Staltz Sep 07 '14 at 08:57