1

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]

U Avalos
  • 6,538
  • 7
  • 48
  • 81
  • So the checks should be evaluated in parallel to each other? – Bergi Jun 26 '15 at 09:59
  • No the checks are in sequence. You can't try the next check until you've cleared the current – U Avalos Jun 26 '15 at 12:03
  • Sounds like you are looking for promises actually, then. Though iirc Bacon's fire-once properties and `flatMap` have the same semantics. – Bergi Jun 26 '15 at 12:05
  • For this type of problem, a working example with jsfiddle or jsbin would be really useful. – OlliM Jun 26 '15 at 13:58

3 Answers3

3

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.
paulpdaniels
  • 18,395
  • 2
  • 51
  • 55
  • unfortunately, while this may work, I need to do each check.observable in serial, so concatAll isn't what I need (in Bacon, it's combineAsArray) – U Avalos Jun 26 '15 at 18:42
  • Not sure I understand. `concatAll` *does* work in serial. It doesn't subscribe to the next Observables until the previous one completes. – paulpdaniels Jun 26 '15 at 19:14
  • unfortunately, unless I'm mistaken, Bacon doesn't seem to have a #concatAll equivalent. (I could be wrong but I don't see it). Thanks though. Your answer pushed me in the right direction – U Avalos Jun 26 '15 at 19:33
2

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.

raimohanska
  • 3,265
  • 17
  • 28
1

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');
U Avalos
  • 6,538
  • 7
  • 48
  • 81