3

I want to buffer values of an EventStream in Bacon.js exactly like buffer(closingSelector) behaves in RxJava. When the "controller stream" (closingSelector in RxJava method) emits a new value, then the event buffer gets flushed.

So I want that the stream output is similar as in stream.bufferWithTimeOrCount, but instead of controlling buffering with time interval or event count I want to control buffering with other stream.

Is there an easy way to implement this in Bacon.js?

attekei
  • 491
  • 4
  • 10

3 Answers3

1

Bacon.holdWhen available since about 0.7.14 does almost what you want, although buffered events are emitted one by one:

stream.holdWhen(valve) pauses and buffers the event stream if last event in valve is truthy. All buffered events are released when valve becomes falsy.

If you need to emit buffered events as a single event, you can try something like the following:

// source streams
var sourceObservable = Bacon.interval(1000);
var closingSelector = new Bacon.Bus();

// Constructing a new Observable where we're going to keep our state.
// 
// We need to keep track of two things: 
//   - the buffer that is currently being filled, and
//   -  a previous buffer that is being flushed.
// The state will then look like this:
//   [ buffer, flushed]
// where both buffer and flushed is an array of events from the source observable.

// empty initial state
var initialState = {buffer: [], flushed: []}

// There are two operations on the state: appending a new element to the buffer 
// and flushing the current buffer:

// append each event from the source observable to the buffer,
// keeping flushed unchanged
var appends = sourceObservable.map(function(e) {
   return function(state) {
       state.buffer.push(e); return state; 
   } 
});

// each event from the closingSelector replaces the `flushed` with 
// the `buffer`'s contents, inserting an empty buffer.
var flushes = closingSelector.map(function(_) {
   return function(state) { return {buffer: [], flushed: state.buffer} }
})

// merge appends and flushes into a single stream and apply them to the initial state
var ops = appends.merge(flushes)
var state = ops.scan(initialState, function(acc, f) { return f(acc) });

// resulting stream of flushed events
var flushed = state.sampledBy(closingSelector).map(function(state) { return state.flushed })

// triggered with `closingSelector.push({})`
flushed.onValue(function(x) { console.log("flushed", x) })
1

Bacon.js didn't have the function as you needed it, so I looked at the bacon.js source and wrote a modified version of holdWhen.

Bacon.EventStream.prototype.bufferUntilValue = function(valve) {
var valve_ = valve.startWith(false);

  return this.filter(false).merge(valve_.flatMapConcat((function(_this) {
    return function() {
        return _this.scan([], (function(xs, x) {
            return xs.concat(x);
        }), {
            eager: true
        }).sampledBy(valve).take(1);
    };
  })(this)));
};

To see this in action, check out this jsFiddle.

OlliM
  • 7,023
  • 1
  • 36
  • 47
  • Really nice approach! Cleaner than my solution (using `holdWhen` and `bufferWithTime` together). It took a while to understand how each valve event spawns a new "scanner" which returns the buffered values with `sampledBy(valve).take(1)` when the following valve event arrives. – attekei Jul 18 '14 at 16:09
  • I simplified the solution, see it here: [jsFiddle](http://jsfiddle.net/7DeQy/) (though later I noticed that you modified `holdWhen` only a little, so maybe there is a reason for `this.filter(false).merge(..)` etc.) – attekei Jul 18 '14 at 16:11
0

stream.holdWhen(valve) looks like almost exactly what you want. It works a little different than buffer(closingSelector): instead of buffering all the time and flush buffer on event from closingSelector, it toggles buffering depend on last value in value stream.

Maybe you could use holdWhen as it is, but if you want behavior like in buffer(closingSelector), you might do something like this:

var result = sourceStream.holdWhen(closingSelector.flatMap(function(){
  return Bacon.fromArray([false, true]);
}).toProperty(true));

On each event from closingSelector we generate two events in value stream with values true and false, i.e. turning off buffering (which triggers flush) and then immediately turning it back on.

Roman Pominov
  • 1,403
  • 1
  • 12
  • 17
  • Actually my initial solution was very similar, see [jsFiddle](http://jsfiddle.net/RRWJf/). Only difference is that because I wanted the output as array of values, I collected them with `bufferWithTime(1)`. It does its job but I thought that there must be a better solution :) – attekei Jul 18 '14 at 15:40