Rusty Rob
Rusty Rob

Reputation: 17213

debounceTime only after first value

Is there a simple way to make debounceTime instant on the first value?

searchQueries.pipe(debounceTime(1000))

let's say i'm debouncing search queries to 1 second.

My understanding is that this will cause a 1 second delay on the first search, But, I want the first search query to be instant.

(e.g. in this example https://stackblitz.com/edit/typescript-adheqt?file=index.ts&devtoolsheight=50 if i type 123 quickly, it will only log 123, but i want it to log 1 and then log 123)

i could try something like

merge(searchQueries.pipe(first()),searchQueries.pipe(debounceTime(1000)))

but then that would potentially add a delay to a second search but is probably good enough.

Is there a way of configuring debounceTime that i'm missing? or should I potentially be using throttle or something else?

Upvotes: 17

Views: 9030

Answers (5)

Wojtek Majerski
Wojtek Majerski

Reputation: 3637

It's more flexible to use debounce instead of debounceTime.

searchQueries.pipe(debounceTime(1000))

is equal to:

searchQueries.pipe(debounce(() => timer(1000))

You can create your own heuristic to determine what timeout needs to be used. For example:

searchQueries.pipe(debounce(() => timer(getTimeout()))
...

const getTimeout = () => {
    return iterations === 1 ? 0 : 1000;
};

In that scenario you need to track the iterations count on your own and increase it with each value but there are many ways to do it without messing a lot with the code. I simply created a wrapped observable object that contains the original observable and the counting logic. Something like this:

export default class ObservableWrapper {

    ...

    next(parameters) {
        this.iterations++;
        this.observable.next(parameters);
    }
}

Upvotes: 2

j3ff
j3ff

Reputation: 6099

Inspired by the answer provided by @Wojtek Majerski.

I've created an operator that...

  • fires the value the number of times you configured it to
  • then debounce for the specified time
  • won't return the value after the debounce time if no value has been debounced
  • will reset its behavior once debounce time is over

Codesandbox: https://codesandbox.io/s/stackoverflow-55130781-lgzy8o

const debounceTimeAfter = <T>(
  amount: number,
  duration: number,
  scheduler: SchedulerLike = asyncScheduler
): MonoTypeOperatorFunction<T> => {
  return (source$: Observable<T>): Observable<T> => {
    return new Observable<T>(subscriber => {
      // keep track of iteration count until flow completes
      let iterationCount = 0;

      return source$
        .pipe(
          tap(value => {
            // increment iteration count
            iterationCount++;
            // emit value to subscriber when it is <= iteration amount
            if (iterationCount <= amount) {
              subscriber.next(value);
            }
          }),
          // debounce according to provided duration
          debounceTime(duration, scheduler),
          tap(value => {
            // emit subsequent values to subscriber
            if (iterationCount > amount) {
              subscriber.next(value);
            }
            // reset iteration count when debounce is completed
            iterationCount = 0;
          }),
        )
        .subscribe();
    });
  };
};

And here is how to use it...

mySubject$
  .pipe(
    debounceTimeAfter(1, 2000),
    tap(value => console.log(value))
  )
  .subscribe();

Upvotes: 2

Muhamed Karajic
Muhamed Karajic

Reputation: 63

I have updated it so its not using deprecated rxjs functions

export function debounceTimeAfter<T>(
    amount: number,
    dueTime: number,
    scheduler: SchedulerLike = asyncScheduler,
  ): OperatorFunction<T, T> {
    return connect(value =>
      concat(
        value.pipe(take(amount)),
        value.pipe(debounceTime(dueTime, scheduler))
      )
    )
  }

Upvotes: 0

martin
martin

Reputation: 96969

You could use multicast or maybe even throttleTime:

searchQueries.pipe(
  multicast(new Subject(), s => merge(
    s.pipe(take(1)),
    s.pipe(skip(1), debounceTime(1000)),
  )),
);

Since RxJS 6 the throttleTime operator accepts a config parameter where you can tell it to emit both leading and trailing emissions. Maybe this will do what you want instead of debounceTime.

searchQueries.pipe(
  throttleTime(1000, undefined, { leading: true, trailing: true }),
);

Upvotes: 22

christo8989
christo8989

Reputation: 6826

Here's my 2 cents / an answer I modified from another post with java-ngrx.

dbounce-time-after.ts

import { OperatorFunction, SchedulerLike, concat } from "rxjs";
import { async } from "rxjs/internal/scheduler/async";
import { debounceTime, publish, take } from "rxjs/operators";

export function debounceTimeAfter(
  amount: number,
  dueTime: number,
  scheduler: SchedulerLike = async,
): OperatorFunction<number, number> {
  return publish(value =>
    concat(
      value.pipe(take(amount)),
      value.pipe(debounceTime(dueTime, scheduler))),
    )
  );
}

export function debounceTimeAfterFirst(
  dueTime: number,
  scheduler: SchedulerLike = async,
): OperatorFunction<number, number> {
  return debounceTimeAfter(1, dueTime, scheduler);
}

example.ts

of(1, 2, 3, 4, 5)
  .pipe(
    tap(value => console.log("TAP", value)),
    debounceTimeAfterFirst(50)
  )
  .subscribe(value => console.log(value));

console

TAP 1
1
TAP 2
TAP 3
TAP 4
TAP 5
5

But you could also start debouncing after n number of emits with debounceTimeAfter.

Upvotes: 14

Related Questions