Reputation: 6798
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:
check
contains an id
and status
= fail/pass/pending. If pending, it contains a list of observables
.
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
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
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
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:
case
operator to propagate instead of nested ternaries.check.observables
, if they are all true then we are done, otherwise continue to the next onefirst
to get the first value returned that is doneUpvotes: 3