Complex operation with BaconJS (FRP)

130 views Asked by At

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:

  • Go thru each check. A check contains an id and status = fail/pass/pending. If pending, it contains a list of observables.
    • If status = pass, then return the id (you're done!)
    • if status = fail, then try the next check
    • if status = pending
      • try each observable in order
        • if observable result is 'false', then try the next check
      • if reach end of observable list and result is 'true', then return the id (you're done!)

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]

3

There are 3 answers

1
raimohanska On BEST ANSWER

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.

3
paulpdaniels On

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.
0
U Avalos On

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');