I'm experimenting with reactive programming for the first time, using bacon.js, 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, rN
s represent the events of each of the request streams and the x
es 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!