U Avalos
U Avalos

Reputation: 6798

Complex operation with BaconJS (FRP)

I'm trying to do this relatively complex operation in BaconJs.

Basically, the idea is keep trying each check until you have a 'pass' status or they all fail. The catch is that 'pending' statuses have a list of Observables (built from jquery ajax requests) that will resolve the check. For performance reasons, you need to try each Observable in order until either they all pass or one fails.

Here's the full pseudo algorithm:

Here's the Bacon code. It doesn't work when the Observables are Ajax requests. Basically, what happens is that it skips over pending checks....it doesn't wait for the ajax calls to return. If I put a log() right before the filter(), it doesn't log pending requests:

    Bacon.fromArray(checks)
      .flatMap(function(check) {

        return check.status === 'pass' ? check.id :
          check.status === 'fail' ? null :
            Bacon.fromArray(check.observables)
              .flatMap(function(obs) { return obs; })
              .takeWhile(function(obsResult) { return obsResult; })
              .last()
              .map(function(obsResult) { return obsResult ? check.id : null; });
      })
      .filter(function(contextId) { return contextId !== null; })
      .first();

UPDATE: the code works when the checks look like this: [fail, fail, pending]. But it doesn't work when the checks look like this: [fail, pending, pass]

Upvotes: 1

Views: 122

Answers (3)

U Avalos
U Avalos

Reputation: 6798

Thanks to @raimohanska and @paulpdaniels. The answer is to use #flatMapConcat. This turns what is basically a list of async calls done in parallel into a sequence of calls done in order (and note that the last "check" is programmed to always pass so that this always outputs something):

   Bacon.fromArray(checks)
      .flatMapConcat(function(check) {

        var result = check();

        switch(result.status) {
          case 'pass' :
          case 'fail' :
            return result;
          case 'pending' :
            return Bacon.fromArray(result.observables)
              .flatMapConcat(function(obs) { return obs; })
              .takeWhile(function(obsResult) { return obsResult.result; })
              .last()
              .map(function (obsResult) { return obsResult ? {id: result.id, status: 'pass'} : {status: 'fail'}; });

        }
      })
      .filter(function(result) { return result.status === 'pass'; })
      .first()
      .map('.id');

Upvotes: 1

raimohanska
raimohanska

Reputation: 3405

I agree with @paulpdaniels Rx-based answer. The problem seems to be that when using flatMap, Bacon.js won't wait for your first "check-stream" to complete before launching a new one. Just replace flatMap with flatMapConcat.

Upvotes: 2

paulpdaniels
paulpdaniels

Reputation: 18663

I am more familiar with RxJS than Bacon, but I would say the reason you aren't seeing the desired behavior is because flatMap waits for no man.

It passes [fail, pending, pass] in quick succession, fail returns null and is filtered out. pending kicks off an observable, and then receives pass which immediately returns check.id (Bacon may be different, but in RxJS flatMap won't accept a single value return). The check.id goes through filter and hits first at which point it completes and it just cancels the subscription to the ajax request.

A quick fix would probably be to use concatMap rather than flatMap.

In RxJS though I would refactor this to be (Disclaimer untested):

Rx.Observable.fromArray(checks)
  //Process each check in order
  .concatMap(function(check) {
     var sources = {
       //If we pass then we are done
       'pass' : Rx.Observable.just({id : check.id, done : true}),
       //If we fail keep trying
       'fail' : Rx.Observable.just({done : false}),

       'pending' : Rx.Observable.defer(function(){ return check.observables;})
                                .concatAll()
                                .every()
                                .map(function(x) { 
                                  return x ? {done : true, id : check.id} : 
                                             {done : false};
                                })
     };

     return Rx.Observable.case(function() { return check.status; }, sources);
  })
  //Take the first value that is done
  .first(function(x) { return x.done; })
  .pluck('id');

What the above does is:

  1. Concatenate all of the checks
  2. Use the case operator to propagate instead of nested ternaries.
  3. Fail or pass fast
  4. If pending create a flattened observable out of check.observables, if they are all true then we are done, otherwise continue to the next one
  5. Use the predicate value of first to get the first value returned that is done
  6. [Optionally] strip out the value that we care about.

Upvotes: 3

Related Questions