13

I'm trying to get RxJs to loop over an Observable in my stream until it is in a certain state, then have the stream continue. Specifically I'm converting a synchronous do/while loop to RxJs, but I assume the same answer could be used for a for or while loop as well.

I thought I could use doWhile() for this, but it seems like the condition function does not have access to the item in the stream, which seems to defeat the purpose to me.

I'm not completely sure what the correct reactive terminology is for what I want, but here is an example of what I am going for:

var source = new Rx.Observable.of({val: 0, counter: 3});

source.map(o => {
  o.counter--;
  console.log('Counter: ' + o.counter);

  if (!o.counter) {
    o.val = "YESS!";
  }
  return o;
})
.doWhile(o => { 
  return o.counter > 0; 
})
.subscribe(
    function (x) {
        console.log('Next: ' + x.val);
    },
    function (err) {
        console.log('Error: ' + err);   
    },
    function () {
        console.log('Completed');   
    });

The expected output would be:

Counter: 3
Counter: 2
Counter: 1
Counter: 0
Next: YESS!
Completed

Assuming this is a solvable problem, I am unclear on how you mark the 'start' of where you want to return when you loop.

JBCP
  • 13,109
  • 9
  • 73
  • 111

2 Answers2

12

There is the expand operator which gets you close by allowing you to recursively call a selector function. Returning an empty observable would be your break in that case. See jsbin:

var source = Rx.Observable.return({val: 0, counter: 3})
    .expand(value =>  { 
      if(!value.counter) return Rx.Observable.empty();
      value.counter -= 1;
      if(!value.counter) value.val = 'YESS';
      return Rx.Observable.return(value)
    })
    .subscribe(value => console.log(value.counter ? 
                                    'Counter: ' + value.counter : 
                                    'Next: ' + value.val));
Niklas Fasching
  • 1,326
  • 11
  • 15
  • Hum... interesting, I might be able to make that work, I'll try and post back – JBCP Dec 13 '15 at 01:02
  • using this, if I did .expand().map() my downstream map would end up processing value multiple times. I want the downstream to only process the final result. I guess expand().filter().map() might work. – JBCP Dec 13 '15 at 01:07
  • Hm. The only other way that comes to mind allowing to circumvent passing multiple values downstream is a subject. I'd be interested in a clean solution to that myself :). – Niklas Fasching Dec 13 '15 at 01:13
  • Ok, the problem I have adapting this solution is that `value.counter` is an over simplification - in reality the exit condition is determined by another asynchronous stream. – JBCP Dec 13 '15 at 01:26
  • I know it's a lot of work, but could you expand your example to better reflect your situation? Why can't you return a subject, do your work independent of the stream (so you're not bound by the available Observable operators) and then call onNext on the subject once you are done? – Niklas Fasching Dec 15 '15 at 16:55
7

Not exactly what you want but close, using expand operator, and signalling end of recursion with Rx.Observable.empty (http://jsfiddle.net/naaycu71/3/):

var source = new Rx.Observable.of({val: 0, counter: 3});

source.expand(function(o) {
  console.log('Counter: ' + o.counter);
  o.counter--;

return (o.counter >= 0) ? Rx.Observable.just(o) : Rx.Observable.empty()
})
.subscribe(
    function (x) {
        console.log('Next: ' , x);
    },
    function (err) {
        console.log('Error: ' + err);   
    },
    function () {
        console.log('Completed');   
    });

Output :

Next:  Object {val: 0, counter: 3}
Counter: 3
Next:  Object {val: 0, counter: 2}
Counter: 2
Next:  Object {val: 0, counter: 1}
Counter: 1
Next:  Object {val: 0, counter: 0}
Counter: 0
Completed
user3743222
  • 18,345
  • 5
  • 69
  • 75