which rxjs operators to use in which order in auto save scenario

Viewed 651

I want to autosave a form with RxJS in Angular. The form will emit an event when it is changed

This is the scenario:

  1. When the form output has changed, but does not change again within the next 10 seconds, it should emit a save event.

  2. Only If the form keeps changing more than once per 10 seconds, for at least 30 seconds, it should also emit a save event.

For the first I could use a debounce, but how do I combine it with my second rule?

3 Answers

You need to split your stream into two streams.

const source = fromEvent(document.getElementById("text"), "input");

source
  .pipe(debounceTime(10 * 1000))
  .subscribe(() => console.log("saved after typing!"));

source
  .pipe(
    auditTime(10 * 1000)
  )
  .subscribe(() => console.log("saved while typing!"));

In first case, as you wrote in question, using debounceTime value will be emitted 10 seconds after user finished editing form/input. In second case, auditTime every 10 second will check if there is any new value. If there is, then would be emitted value. In callback you can reuse the same function responsible for saving data from form. You can try it in this demo on stackblitz:

https://stackblitz.com/edit/rxjs-jj3kot?file=index.ts

I just changed time in demo to 1 second so it is easier to check result in console.

Side note:

This is plain JS demo. If you develop Angular app you should avoid getting elements with getElementById. Instead of this use direct reference to template: https://angular.io/api/core/ViewChild

Edit:

According to OP comment I've changed implementation a little bit. I merged two observables and used distinctUntilChanged operator to avoid emitting the same value twice. Demo in stackblitz is also changed

const afterTyping = source.pipe(debounceTime(1000));

const whileTyping = source.pipe(auditTime(1000));

merge(whileTyping, afterTyping)
  .pipe(distinctUntilChanged())
  .subscribe(console.log);

This one was a little tricky but a proper combination of debounceTime and throttleTime can solve the problem:

const s = new Subject();
s.subscribe();

const saveEvery3sSubject = new Subject();
saveEvery3sSubject.subscribe();

const saveEvery3s$ = saveEvery3sSubject.pipe(
  throttleTime(3000),
  mapTo('saving after 3000 ms'),
  startWith(null)
);


const debounce$ = s.pipe(
  tap(r => {
    saveEvery3sSubject.next(r); // this step starts the 3000ms timer
  }),
  debounceTime(1000),
  startWith(null),
);

// combining them both so the saving will occur on debounce or after 3000ms
const result$ = combineLatest([debounce$, saveEvery3s$]).pipe(
    filter(([d, s]) => s !== null)
)


// first 5 values will be debounced forever because 500 < 1000(debounceTime)
interval(500).pipe(
    take(5)
)
  .subscribe(i => {
    s.next('form changed #' + i);
})

// these values will be emitted correctly one by one
timer(9000, 1100).pipe(
    take(20)
)
  .subscribe(i => {
    s.next('after pause #' + i);
})

result$.subscribe();

Using an additional Subject is necessary because you operate two independent time frames:

  • debounceTime - while changing the form and emitting the changes
  • throttleTime - to make sure that every 30s the form is saved regardless the debounced input (if you are quicker than debounceTime, the values won't me emitted at all!)

After the first formChange enter the stream with debounce, it starts the timer (in my example - 3000ms) meaning that if there are no user input, there will be no necessary emissions.

Also pay attention to startWith(null) - this is required for combineLatest to emit the first value.

This solution in action: https://rxviz.com/v/38MZnMZo

I had to use race to make it work as merge was simply never triggered. So here's my working solution:

race([
  event.pipe(debounceTime(30 * 1000)),
  event.pipe(throttleTime(30 * 1000))
])
  .pipe(distinctUntilChanged())
  .subscribe(console.log);
Related