Reset timeout on event with RxJS

2019-05-18 13:43发布

问题:

I'm experimenting with RxJS (with the JQuery extension) and I'm trying to solve the following use case:

Given that I have two buttons (A & B) I'd like to print a message if a certain "secret combination" is clicked within a given timeframe. For example the "secret combination" could be to click "ABBABA" within 5 seconds. If the combination is not entered within 5 seconds a timeout message should be displayed. This is what I currently have:

var secretCombination = "ABBABA";

var buttonA = $("#button-a").clickAsObservable().map(function () { return "A"; });
var buttonB = $("#button-b").clickAsObservable().map(function () { return "B"; });

var bothButtons = Rx.Observable.merge(buttonA, buttonB);

var outputDiv = $("#output");

bothButtons.do(function (buttonName) {
    outputDiv.append(buttonName);
}).bufferWithTimeOrCount(5000, 6).map(function (combination) {
    return  combination.reduce(function (combination, buttonName) {
        return combination + buttonName;
    }, "");
}).map(function (combination) {
    return combination === secretCombination;
}).subscribe(function (successfulCombination) {
    if (successfulCombination) {
        outputDiv.html("Combination unlocked!");
    } else {
        outputDiv.html("You're not fast enough, try again!");
    }
});

While this works fairly well it's not exactly what I want. I need the bufferWithTimeOrCount to be reset when button A is pressed for the first time in a new timeframe. What I'm looking for is that as soon as the secret combination is pressed (ABBABA) I'd like "Combination unlocked!" to be shown (I don't want to wait for the time window to be expired).

回答1:

Throttle is the typical operator for the delaying with reactive resetting effect you want.

Here's how you can use throttle in combination with scan to gather the combination inputted before the 5 seconds of silence:

var evaluationStream = bothButtons
  .merge(bothButtons.throttle(5000).map(function(){return "reset";})) // (2) and (3)
  .scan(function(acc, x) { // (1)
    if (x === "reset") return "";
    var newAcc = acc + x;
    if (newAcc.length > secretCombination.length) {
      return newAcc.substr(newAcc.length - secretCombination.length);
    }
    else {
      return newAcc;
    }
  })
  .map(function(combination) {
    return combination === secretCombination;  
  });

var wrongStream = evaluationStream
  .throttle(5000)
  .filter(function(result) { return result === false; });

var correctStream = evaluationStream
  .filter(function(result) { return result === true; });

wrongStream.subscribe(function() {
  outputDiv.html("Too slow or wrong!");
});

correctStream.subscribe(function() {
  outputDiv.html("Combination unlocked!");
});

(1) We scan to concatenate the input characters. (2) Throttle waits for 5 seconds of event silence and emits the last event before that silence. In other words, it's similar to delay, except it resets the inner timer when a new event is seen on the source Observable. We need to reset the scan's concatenation (1), so we just map the same throttled Observable to "reset" flags (3), which the scan will interpret as clearing the accumulator (acc).

And here's a JSFiddle.