1

I would like to flush a buffered observable based on the content of the buffer, but how to accomplish this? A simplified example of what I want to do:

observable.buffer(() => {
  // Filter based on the buffer content.
  // Assuming an observable here because buffer()
  // needs to return an observable.
  return buffer.filter(...);
})

Here is more specifically what I am trying to do with key events (bin here):

const handledKeySequences = ['1|2']

// Mock for document keydown event
keyCodes = Rx.Observable.from([1,2,3,4])

keyCodes
  .buffer(() => {
    /*
      The following doesn't work because it needs to be an
      observable, but how to observe what is in the buffer?
      Also would like to not duplicate the join and includes
      if possible
      return function (buffer) {
        return handledKeySequences.includes(buffer.join('|'));
      };
    */

    // Returning an empty subject to flush the buffer
    // every time to prevent an error, but this is not
    // what I want.
    return new Rx.Subject();
  })
  .map((buffer) => {
    return buffer.join('|')
  })
  .filter((sequenceId) => {
    return handledKeySequences.includes(sequenceId);
  })
  .subscribe((sequenceId) => {
    // Expecting to be called once with 1|2
    console.log("HANDLING", sequenceId)
  })

I feel like my approach is wrong, but I can't figure out what the right approach would be. I've tried using scan, but that scans all the events in the observable, which is not what I want.

Thanks for any help!

Andrew
  • 606
  • 4
  • 13
  • please post your whole code here in SO, since the linked code might become unavailable at some point in time – olsn Jan 07 '17 at 17:52

2 Answers2

0

This should be doable with bufferWithCount:

const handledKeySequences = ['1|2']

// Mock for document keydown event
keyCodes = Rx.Observable.from([0,1,2,3,4]);

const buffer$ = keyCodes
  .bufferWithCount(2, 1)  // first param = longest possible sequence, second param = param1 - 1
  .do(console.log)
  .map((buffer) => {
    return buffer.join('|')
  })
  .filter((sequenceId) => {
    return handledKeySequences.includes(sequenceId);

  });

  buffer$.subscribe((sequenceId) => {
    console.log("HANDLING", sequenceId)
  });

See live here.

Also have a look at this question.

Community
  • 1
  • 1
olsn
  • 16,644
  • 6
  • 59
  • 65
  • That's interesting and possibly useful, but I don't want to hard code the count of the buffer. In the example above, it should be able to accept any number of key sequences, collecting them in the buffer until I tell it to flush. – Andrew Jan 07 '17 at 18:04
  • What is the flush based on? – olsn Jan 07 '17 at 18:06
  • It would be based on whether or not there are any handled keys. Actually the above is quite simplified as well, but the idea is to retain the buffer until an "endpoint" is reached, either an unknown key sequence or one that is being handled (in which case a handler would be fired). Or to put it another way, only retain the buffer if we are receiving a part of a known sequence. In any case that code might be somewhat complex, so manually flushing is preferred. – Andrew Jan 07 '17 at 18:14
  • So if I understand this correctly, you want to empty the buffer (without propagating its value) if no valid sequence is detected? – olsn Jan 07 '17 at 18:23
  • Well, the end goal is to get only valid sequences to the subscriber. If that could be accomplished by somehow flushing without propagating the value then that might work, but I suspect just telling the buffer WHEN to flush and then filtering is more in the spirit of responsive? Edit: Hit send too soon. – Andrew Jan 07 '17 at 19:13
  • I don't really see the point why you want to handle that kind of logic in the buffer so badly - a `buffer` in rxjs is meant to _buffer_ the data. Besides, you cannot check your sequences like that - imagine the following scenario: _Valid Seqs:_ `1|4|2` and `4|3` - and you currently have the following buffer: `1, 4, 4` - with a simple check, no sequence matches -> buffer is flushed, now the next input is a `3` -> which WOULD have been a hit, but the previous 4 has already been flushed - as you can see, the only way to do it is to check for overlapping, which is what`.bufferWithCount(2, 1)` does – olsn Jan 07 '17 at 19:23
  • Well I don't see anything I'm doing that doesn't fit the definition of "buffering the data". But in any case it doesn't have to be using `buffer`. As I mentioned, my approach could very well be wrong. I simply need to handle multiple events as a collection that can be "cleared", and I would like to do it in a reactive way. As for checking sequences, for my use case it's valid for the sequence `1, 4, 4, 3` to not be handled as `1, 4` was already stepping into a different sequence. Think something like shortcut sequences (for example vim, if you're familiar). – Andrew Jan 07 '17 at 20:02
  • not sure if I fully understand your problem yet - the provided stream works for your given usecases so far (after adjusting the bufferCount, which could also be calculated of course) - in case you want to implemented some advanced heuristic via a machine-state-model, I would implemented those parts separately, most likely in a custom operator, since you'd need to keep an independent state – olsn Jan 07 '17 at 20:17
  • bufferWithCount would not work for me due to the reason mentioned above: if both `1,2` and `2,3` were handled, then the sequence `1,2,3` would trigger both, when it should only trigger `1,2`. As for adjusting the count, in a real application I suppose I could decide on a reasonable limit or set it to an arbitrarily high number, but it doesn't feel quite right because my intent is not to place a limitation on the length of sequences that should be handled. – Andrew Jan 07 '17 at 20:23
  • Another reason that bufferWithCount will not work is that no matter what combination of `count` and `skip` are passed, the criterion for where the buffer begins will always be based on a fixed number. This is not necessarily the case, as sequences may vary in length. – Andrew Jan 07 '17 at 20:57
  • Ah, now I see - okay - but I'm afraid you'd have to implement your own operator or maybe keep an external state - at least I don't know any operator currently that supports this kind of handling – olsn Jan 07 '17 at 20:59
  • Have a look at this bin http://jsbin.com/bilasixeqa/1/edit?js,console - is that what you have in mind? I'm guessing without the external state of course. – olsn Jan 07 '17 at 21:17
  • I'm not sure about the `indexOf(3)` part, but the approach looks correct: flushing the buffer based on some criterion that determines if a partial key sequence is encountered. – Andrew Jan 08 '17 at 11:02
  • `indexOf` is just some random criteria, that's where you put in your own logic, that determins when to flush - but as I mentioned, it's not really a pure reactive way, because it holds an external state - wrap it in a custom operator and that should do it then – olsn Jan 08 '17 at 11:04
  • To be a bit pedantic though, the state still needs to exist in the custom operator. I tried it out and it works great, but if I don't do it in a generalizable way, then to a certain degree it's just pushing around my application logic. Instead, I raised an issue with rxjs to see if it's something that would be useful: https://github.com/ReactiveX/rxjs/issues/2264 – Andrew Jan 08 '17 at 11:10
  • Yes, the state does need to exist, but there are plenty of rxjs-operators that already keep internal states: e.g. `buffer` does this - it's just not accessible – olsn Jan 08 '17 at 11:15
  • Right that's what I meant. In that sense something that keeps state would be better part of the library itself, or at least something that can be reused elsewhere in an application. – Andrew Jan 08 '17 at 11:35
0

It seems that this functionality is not currently available in Rxjs, so as suggested by @olsn I wrote a custom operator that works by passing a function to tell when to flush the buffer:

(function() {

  // Buffer with function support.
  function bufferWithContent(bufferFn) {
    let buffer = [];

    return this.flatMap(item => {
      buffer.push(item);
      if (bufferFn(buffer)) {
        // Flush buffer and return mapped.
        let result = Rx.Observable.just(buffer);
        buffer = [];
      } else {
        // Return empty and retain buffer.
        let result = Rx.Observable.empty();
      }
      return result;
    });
  }

  Rx.Observable.prototype.bufferWithContent = bufferWithContent;

})();

I also opened an issue here that proposes adding this functionality.

Andrew
  • 606
  • 4
  • 13