21

I am looking an operator to debounce a series of event, let us say user's click. The input and output should be like this:

interval :      ->     <-            ->     <-
in       : 1--2--3-------4--5--5--6-7-8--------
out      : 1-------------4---------------------

The idea is like underscore's debounce with immediate option on http://underscorejs.org/#debounce. The operator can be presented/implemented in any language that supports Reactive Extensions

Edit: Clarify the interval, let say 5 seconds (5 spaces between two arrows) : -> <-

Edit2: A more understandable version: I have a user, he clicks repeatedly a button (1, 2, 3); I want to catch the first click (1) and ignore the rest. After a while, he is tired and rest for 7 seconds (which is longer than the 5 seconds interval between two arrows) and continue clicking the button again (4, 5, 6, 7, 8) I want to catch the first click (4) and ignore the rest.

If he clicks after the forth arrow, I want to catch that click, too.

Edit3: Here is an image image which can be found at the original article

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Quang Linh Le
  • 751
  • 1
  • 7
  • 16
  • 1
    http://stackoverflow.com/questions/30140044/deliver-the-first-item-immediately-debounce-following-items/42665820 is related. – Adrian Baker Mar 08 '17 at 08:03

4 Answers4

18

Edit: Based on the clarifications, RxJava doesn't have an operator for this type of flow but it can be composed from a non-trivial set of other operators:

import java.util.concurrent.TimeUnit;

import rx.Observable;

public class DebounceFirst {

    public static void main(String[] args) {
        Observable.just(0, 100, 200, 1500, 1600, 1800, 2000, 10000)
        .flatMap(v -> Observable.timer(v, TimeUnit.MILLISECONDS).map(w -> v))
        .doOnNext(v -> System.out.println("T=" + v))
        .compose(debounceFirst(500, TimeUnit.MILLISECONDS))
        .toBlocking()
        .subscribe(v -> System.out.println("Debounced: " + v));
    }

    static <T> Observable.Transformer<T, T> debounceFirst(long timeout, TimeUnit unit) {
        return f -> 
            f.publish(g ->
                g.take(1)
                .concatWith(
                    g.switchMap(u -> Observable.timer(timeout, unit).map(w -> u))
                    .take(1)
                    .ignoreElements()
                )
                .repeatWhen(h -> h.takeUntil(g.ignoreElements()))
            );
    }
}
akarnokd
  • 69,132
  • 14
  • 157
  • 192
  • Thank you @akarnokd. That's the correct implementation. – Quang Linh Le Feb 01 '17 at 13:45
  • I really liked your test code and stole it for one of my answers. – Adrian Baker Mar 08 '17 at 08:06
  • Yes, the idea with `compose` is really nice. @akarnokd Is it possible to explain us how did you come up with the implement? – Quang Linh Le Mar 08 '17 at 16:30
  • @akarnokd Could we maybe add this as an operator. It feels like a common use case and difficult to implement correctly without finding your answer. – FriendlyMikhail Mar 27 '17 at 23:04
  • 2
    I don't think so; you have to know RxJava operators at a very deep level and see the underlying dataflow to be able to compose such diverse set of operators together. @FriendlyMikhail this is an uncommon operator because how the debouncing timeout has to be extended over and over. Maybe you can ask the rxjava-extras to have it for 1.x. 2.x has this in RxJava2Extensions. – akarnokd Mar 28 '17 at 08:34
11

The behavior you want is not what debounce operator does in Rx.

This is called throttle, throttleTime or throttleWithTimeout (however, it falls under debounce category of operators). I don't know what language you use but in RxJS it looks like the following image:

enter image description here

See http://reactivex.io/documentation/operators/debounce.html.

martin
  • 93,354
  • 25
  • 191
  • 226
  • 1
    This is very close, but what let's assume that the interval I want is `yb` is also longer than `bx`, `xc`, `xc` I want the output to contain only `a` and `b` – Quang Linh Le Feb 01 '17 at 10:51
  • @QuangLinhLe You can set whatever interval you want. Or maybe I don't understand what you want... – martin Feb 01 '17 at 10:57
  • I have just edited the question, the interval is from the last two `click`s, not from start – Quang Linh Le Feb 01 '17 at 11:03
  • I still think you're exactly describing what `throttle` does. – martin Feb 01 '17 at 11:35
  • 1
    please correct me I my assumption is wrong. With `throttle` and a delay of 5 seconds, if the user clicks repeatedly the button, I will received about 100 clicks in 500 second? What I want is only the first one. – Quang Linh Le Feb 01 '17 at 11:47
  • No, `throttle` will reemit the first item and then **ignore** everything for another 5 seconds. – martin Feb 01 '17 at 12:53
  • Thanks @martin, I have just edited my question again with an image, please have a look at the last event group (white rectangle). Only the first of the group has been caught. – Quang Linh Le Feb 01 '17 at 12:57
1

Because debounce() is inherently asynchronous, you need to bring the result back to the current thread explicitly.

seriesOfUnfortunateEvents
  .debounce( 14, TimeUnit.MILLISECONDS )
  .observeOn( Schedulers.immediate() )
  .subscribe( v -> yourStuff() );
Bob Dalgleish
  • 8,167
  • 4
  • 32
  • 42
1

According to documentation there are two debounce operators in RxJS. You might be interested in debounceTime in particular.

debounceTime

From documentation

Emits a value from the source Observable only after a particular time span has passed without another source emission.

Example:

Rx.Observable
    .fromEvent(document.querySelector('button'), 'click')
    .debounceTime(200)
    .mapTo(() => 'clicked!')
    .subscribe(v => console.log(v));

It will emit one clicked! if button was clicked in given timespan (200ms in this example).

debounce

From documentation

Emits a value from the source Observable only after a particular time span determined by another Observable has passed without another source emission.

Community
  • 1
  • 1
Michał Pietraszko
  • 5,666
  • 3
  • 21
  • 27
  • 2
    Thanks @Michal, what I want is a `click` event right when I click the button, then suppress every other `click` until the interval from the last two `click`s longer than 200ms – Quang Linh Le Feb 01 '17 at 10:55