Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use debounceTime but still trigger the function after certain time?

TLDR; I would like to use debounceTime to execute the function only if 300 milliseconds have passed without it being called. In the meanwhile, I also want to be able to trigger the function every 1 minutes If the process takes a long time. Otherwise, the function will only be triggered at the end of the process.


Basically, our system has a long process that will fire a lot of SignalR update to the client. When I received the server command on the client, I will make 2 additional HTTP requests back to server to get some information. So that It will hit the server back as long as the server sending me update.

I am using debounceTime to prevent sending too many requests back to the server If the time between two commands is within 300ms. But there is one use case where the server constantly sending the update to the client in, e.g 1 hour. Meaning the client will trigger the getItemCount at 1h and 300ms.

export class LeftNavigationComponent implements OnInit, OnDestroy {
    typeACount: number = 0;
    typeBCount: number = 0;    

    constructor(
        private itemService: service.ItemService,        
        private signalR: service.SignalRService) { }

    ngOnInit(): void {
        this.subscriptions = [            
            this.signalR.itemCreated.debounceTime(300).subscribe(item => this.onUpdatingData())]
    }

    onUpdatingData() {
        Promise.all([
            this.itemService.getItemCount(APP_SETTING.TYPE_A),
            this.itemService.getItemCount(APP_SETTING.TYPE_B)])
            .then(response => this.gettingCountDone(response))
    }

    gettingCountDone(response) {
        this.typeACount = <any>response[0];
        this.typeBCount = <any>response[1];        
    }
}

I still want to debounceTime to prevent sending too many requests to the server. But it should be smart enough to be automatically triggered every e.g 1 minutes after receiving the first updated. Has anyone had the use case before?

like image 608
trungk18 Avatar asked Oct 17 '17 07:10

trungk18


People also ask

How does debouncetime work in Java?

debounceTime waits for the time period mentioned and then calls the subscribe method. e.g; debounceTime (1000) will wait for 1 second. It is implemented through pipes.

What is debouncetime in angular?

DebounceTime & Debounce in Angular. DebounceTime & Debounce are the Angular RxJs Operators. Both emit values from the source observable, only after a certain amount of time has elapsed since the last value. Both emit only the latest value and discard any intermediate values.

How does the debouncetime operator count time?

The Debouncetime operator starts counting time after it receives a value. If the source observable emits a value before the timeout duration, then counting is reset to zero & started again. When the timeout duration elapses the operator emits the last value and the counting stops. Counting starts again when the operators receive another value.

How does debouncetime work in Swift?

When using debouceTime you only care about the final state. For example, current scroll position when a user stops scrolling, or a final text in a searchbox after a user stops typing in characters. debounceTime waits for the time period mentioned and then calls the subscribe method.


4 Answers

Pavel answer is close, but If I have understood well the question, you want this:

ngOnInit(): void {
    const debounced = this.signalR.itemCreated.debounceTime(300).share();
    this.subscriptions = [         
        debounced.subscribe(() => this.onUpdatingData()),
        debounced.switchMap(() => Observable.interval(60000).takeUntil(this.signalR.itemCreated)).subscribe(() => this.onUpdatingData())
    ]
}

This code will do the following, when the time between items created are major than 300ms onUpdatingData() will be called. After that Every time debounced emits a value, a throttleTime observable of 1 minit is created. Meaning that, if debounced doesn't emit for a minut since the last emision, onUpdatingData() will be executed, and so one.

And improvement would be to merge the observables, because they are from the same type and the execute the same function, for example like this:

ngOnInit(): void {
    const debounced = this.signalR.itemCreated.debounceTime(300).share();
    const merged = debounced.switchMap(() => Observable.interval(60000).takeUntil(this.signalR.itemCreated))
    this.subscriptions = [         
      merged.subscribe(() => this.onUpdatingData())
   ]
}

I posted a fiddle showing the working solution. In this fiddle, the event mousedown simulates the stream this.signalR.itemCreated.

https://jsfiddle.net/llpujol/e6b6o655/

like image 130
Llorenç Pujol Ferriol Avatar answered Oct 19 '22 07:10

Llorenç Pujol Ferriol


You can use throttleTime(60000) instead of or in parallel with debounceTime. To check this behavior move all balls to the beginning and you will see the result enter image description here

In your case you can for example do the following:

ngOnInit(): void {
        this.subscriptions = [            
            this.signalR.itemCreated.debounceTime(300).subscribe(item => this.onUpdatingData()),
            this.signalR.itemCreated.throttleTime(60000).subscribe(item => this.onUpdatingData())
        ]
    }

So method won't be called too often and also once per minute (or less if there is no events).

It is also possible to write your own implementation and combine debounceTime with throttleTime but I'm not experienced enough too provide such example...

like image 1
Pavel Agarkov Avatar answered Oct 19 '22 06:10

Pavel Agarkov


This is my take on it - if I understood the question correctly, which I'm not sure of... Nevertheless, code is below.

// this is just simulation of source of events - in the real world it is this.signalR.itemCreated
// values are such that they would be distinguishable from interval numbers.
// and yes, it is Igor's idea :)
const sourceObservable = Observable.create(observer => {
    setTimeout(() => observer.next(100), 0);
    setTimeout(() => observer.next(101), 1000);
    setTimeout(() => observer.next(102), 1100);
    setTimeout(() => observer.next(103), 1500);
    setTimeout(() => observer.next(104), 1700);
    setTimeout(() => observer.next(105), 2100);
    setTimeout(() => observer.next(106), 4200);
    setTimeout(() => observer.next(107), 5000);
    setTimeout(() => observer.next(108), 8000);
});

// debouncing too fast emits
const itemCreated = sourceObservable.debounceTime(300);

// starting timer after last emitted event
// again, in the real world interval should be 1 minute, this is just for illustrative purposes
const timeout = itemCreated.switchMap(() => Observable.interval(2000));

// then just merging those two
// debounceTime(300) - to suppress possible fast timer->source consequent events
// take(12) is just to limit example length, it is not needed in real application
itemCreated.merge(timeout).debounceTime(300).take(12).subscribe((val) => console.log(`${val}`));

This produces the following sequence:

100
// 101 skipped here by sourceObservable.debounceTime(300)
102
// 103 skipped here by sourceObservable.debounceTime(300)
104
105
// 0 from interval() skipped here by merge().debounceTime(300)
106
107
0
108
0
1
2
3

PS. And I agree with Igor - that's an interesting brainteaser, thanks for interesting question!

like image 1
Alexander Leonov Avatar answered Oct 19 '22 06:10

Alexander Leonov


Here's my take on it. The code is way way less elegant than one written by Pavel.

You can try it out in the plunker I prepared. (You'll need to open browser console to see the generated output stream). You may also want to play with the normalEventDebounceTime and forcedInterval key configuration parameters, and/or with the event timing in sourceObservable.

The idea is to merge two streams (sourceObervable and reschedulingObservable) into one which will be triggered by either of the inputs. Whenever the merged observable emits an event, we call reschedulingSubject.next() thus delaying reschedulingObservable by 1000ms (because it's constructed with debounceTime applied to Subject).

The sourceObservable is supposed to be truly independent, i.e. produced by user input, or -- in your case -- by SignalR as I understand.


const normalEventDebounceTime = 450;
const forcedInterval = 1000;

const sourceObservable = Rx.Observable.create(observer => {
  setTimeout(() => observer.next('event-0'), 0);
  setTimeout(() => observer.next('event-1'), 1000);
  setTimeout(() => observer.next('event-2'), 1100);
  setTimeout(() => observer.next('event-3'), 1500);
  setTimeout(() => observer.next('event-4'), 2000);
  setTimeout(() => observer.next('event-5'), 5000);
  setTimeout(() => observer.next('event-6'), 8000);
  setTimeout(() => observer.complete(), 9000);
});

const reschedulingSubject = new Rx.Subject();

const reschedulingObservable = reschedulingSubject.asObservable().debounceTime(forcedInterval);

const debouncedSourceObservable = sourceObservable.debounceTime(normalEventDebounceTime);

let keepWatching = true;

sourceObservable.subscribe(
  event => {},
  error => {},
  () => {
    keepWatching = false;
    console.info('Source observable is complete. Stop watching please');
  }
);

Rx.Observable
  .merge(debouncedSourceObservable, reschedulingObservable)
  .do(() => {
    if (keepWatching) {
      setTimeout(() => reschedulingSubject.next('forced-next'), 100);
    }
  })
  .subscribe(event => console.info(event));

That code produces the following stream:

event-0
forced-next
event-3
event-4
forced-next
forced-next
event-5
forced-next
forced-next
event-6
Source observable is complete. Stop watching please
forced-next

Pros of this code are:

  • Does almost exactly what you asked in the question. (Saying almost because of setTimeout(() => reschedulingSubject.next('forced-next'), 100)).
  • Does not require custom operators.

Cons are:

  • Pretty complex code for "such a simple problem".
  • Uses Subject which is a last resort kind of a thing, IMO.

Again, you asked a very good question. Always interesting to deal with puzzles like this. Starring the question!

like image 1
Igor Soloydenko Avatar answered Oct 19 '22 07:10

Igor Soloydenko