4

I'm experimenting with reactive programming for the first time, using , and hit an odd interaction that's annoying me. I came up with a working version of this event chain, but the hackish extra stream manipulations makes it stand out from the rest of the code -- as you'll see.

I found FRP/Bacon.js to generate some pretty clean code, so it feels like I'm just missing some obvious operators. To the problem, shall we?

Several sources recommend using Observable.flatMapLatest to discard stale events. Suppose we want to complete an IO operation for each event in an input stream. In the following diagram, i represents the input stream events, rNs represent the events of each of the request streams and the xes mark the point in time when a request stream was cancelled. These are most likely one-event streams, so the . means the stream is actually closed after the event (if it wasn't cancelled before!).

             input stream:  i──i────────i───i─────────>   
                  request:  │  │        │   └────r4.──>   
                  request:  │  │        └───x─r3.─────>   
                  request:  │  └──r2.───x─────────────>   
                  request:  └──x─────r1.──────────────>   
                                                        + 
i.flatMapLatest(requests):  ──────r2─────────────r4───>   

This is perfectly okay for several use cases, but pay attention to r3: it never gets accepted, even though it was still a valid response until r4 arrived. In the worst case scenario, lots of requests are wasted:

             input stream:  i──i──i───i──i──────>   
                  request:  │  │  │   └──x─r4.──>   
                  request:  │  │  └───x─r3.─────>   
                  request:  │  └──x─r2.─────────>   
                  request:  └──x─────r1.────────>   
                                                  + 
i.flatMapLatest(requests):  ────────────────────>   

I know we can use throttle and debounce to deal with these kinds of situation, but they're still susceptible to (bigger) timing problems. For example, a user on a spotty connection (on which requests take between 0.5s-5s+) could be the severely affected by this.

Now, even if it's not a real problem, it looks like it'd be valuable for my FRP study. If it's not clear by now, this is what I want to do:

             input stream:  i──i────────i───i─────────>   
                  request:  │  │        │   └────r4.──>   
                  request:  │  │        └─────r3.─x───>   
                  request:  │  └──r2.──────────x──────>   
                  request:  └──────x─r1.──────────────>   
                                                        + 
i.flatMap_What?(requests):  ──────r2──────────r3─r4───>   

And here's my current implementation vs. the basic flatMapLatest one:

var wrong = function (interval, max_duration) {
    var last_id = 0,
        interval = interval || 1000,
        max_duration = max_duration || 5000;

    Bacon.interval(interval, 'bang').log('interval:')
        .flatMapLatest(function () {
            return Bacon.later(Math.random()*max_duration, last_id++).log(' request:');
        })
        .log('accepted: %s **********');
};

var right = function (interval, max_duration) {
    var last_id = 0,
        interval = interval || 1000,
        max_duration = max_duration || 5000;

    Bacon.interval(interval, 'bang').log('interval:')
        .flatMap(function () {
            return Bacon.later(Math.random()*max_duration, last_id++).log(' request:');
        })
        .diff(-1, function (a, b) { return b > a ? b : -1 })
        .filter(function (id) { return id !== -1 })
        .log('accepted: %s **********');    
};

The wrong function ignores successive requests as expected, while the right function operates on a plain flatMap with a diff and filter. But not only that looks very hacky, it adds the timestamp/id as a requirement for the requests, just so we can sort it later.

I guess my question is, then: What am I missing? Is there a better way to do this? How can I takeUntil each of the flatMap event streams (request) instead of the outer one (input)?

Any help is appreciated, thanks!

mrkishi
  • 4,229
  • 1
  • 17
  • 14
  • I think most of the time `flatMapLatest` is doing what you want: if the input has changed, the result of the previous request is invalid. A simple example would be search field text and corresponding search results. – OlliM Apr 27 '15 at 09:05
  • In your example you should use `scan` instead of diff and maintain the max id that has come through: if results come in order [r3, r1, r2], the wrong thing will go through. – OlliM Apr 27 '15 at 10:08

1 Answers1

0

Here is a solution that uses max id and scan:

var right = function (interval, max_duration) {
    var bang_id = 0, last_id = 0,
        interval = interval || 1000,
        max_duration = max_duration || 5000;

    Bacon.interval(interval, 'bang')
        .map(function() { return 'Bang'+(bang_id++) })
        .flatMap(function (val) {
            return Bacon.later(Math.random()*max_duration, {value: val, id: last_id++});
        })
        .scan({id: -1, value: null}, function(memo, v) {
          if(v.id <= memo.id) {
            return memo;
          }
          return v;
        })
        .changes()
        .skipDuplicates(function(a, b) { return a.id === b.id; })
        .map('.value')
        .log();    
};

http://jsbin.com/doreqa/3/edit?js,console

OlliM
  • 6,505
  • 1
  • 32
  • 45