ste2425
ste2425

Reputation: 4766

switchMap and debounceTime cancel pending

I have a method that will search for employee's based on a search term.

this._sub.pipe(
    debounceTime(500),
    filter(x => !!x),
    distinctUntilChanged(),
    switchMap(this.getResults.bind(this))
).subscribe((d: IDisplayEmp[]) => {
    console.log('RES', d)
    this.displayResults = d;
});

this._sub.next('mySearchTerm');

This works great, previous API calls are cancelled if a new one is issued. However here comes the issue.

Active API calls are only cancelled when the debounce has emitted. If an active API call returns whilst the debounce is waiting it still triggers my subscribe.

I understand this is because the cancelling only happens within the scope of the switchmap.

Is possible to refactor this so that if the debounce is waiting for input to stop it will cancel any pending API calls?

Probably a naive approach but i attempted something like below, it emits values but now there is no debounce in effect.

this._sub.pipe(
    filter(x => !!x),
    distinctUntilChanged(),
    switchMap((x) => {
        return of(x)
            .pipe(
                debounceTime(500),
                this.getResults.bind(this)
            );
    })
).subscribe((d: IDisplayEmp[]) => {
    console.log('RES', d)
    this.displayResults = d;
});

Thanks for any help.

Upvotes: 7

Views: 9614

Answers (4)

Christian Jensen
Christian Jensen

Reputation: 965

The accepted answer should work fine, but I wanted to share another way you can do this with timer instead of debounceTime:

this._sub.pipe(
    filter(x => !!x),
    distinctUntilChanged(),
    switchMap(val => timer(500).pipe(
        switchMap(() => this.getResults(val))
    ))
).subscribe(...);

Now, if an emissions reaches switchMap, the timer and everything in it (including an active network request) will be canceled, and a new timer will be created.

Upvotes: 9

fhewitt
fhewitt

Reputation: 121

Unsure here, need to be explored, but you could transform the _sub into two observables, one will be debounced and returns the original query, the second is not debounced and returned null/undefined.

Then you merge them both and call the switchMap.

The undebounced observable should allow you to cancel the call because it will reach the switchmap too. In with case the switchMap could return an empty or never observable. And you recreate the API call only when it's the debounched observable that came in with the value.

Something like that?

const debounced = this._sub.pipe(debounceTime(500), filter(x => !!x), distinctUntilChanged());
const undebounced = this._sub.pipe(map(_ => undefined));

merge(debounced, undebounced).pipe(
    switchMap(val => {
        if (val) {
            return this.getResults(val);
        } else {
            return never;
        }
    })
);

Upvotes: 2

frido
frido

Reputation: 14099

Use takeUntil to cancel your requests whenever this._sub emits.

this._sub.pipe(
    debounceTime(500),
    filter(x => !!x),
    distinctUntilChanged(),
    switchMap(searchTerm => this.getResults(searchTerm).pipe(
      takeUntil(this._sub)
    ))
).subscribe((d: IDisplayEmp[]) => {
    console.log('RES', d)
    this.displayResults = d;
});

Upvotes: 10

Roberto Zvjerković
Roberto Zvjerković

Reputation: 10127

I believe this should work for you:

Use concat so the second emission waits for the first one to complete.

const getFromAPI = (key: string): Observable<number> => {
    return;
};

const keys: Observable<string> = new Observable();

concat(
    keys.pipe(
        debounceTime(500),
        switchMap(key => getFromAPI(key)),
        take(1)
    )
).subscribe(console.log);

Another example:

const getFromAPI = (key: string): Observable<number> => {
    return;
};

const keys: Observable<string> = new Observable();

const debouncedKeys = keys.pipe(debounceTime(500));

concat(
    debouncedKeys.pipe(
        switchMap(key => getFromAPI(key)),
        take(1),
        takeUntil(debouncedKeys)
    )
).subscribe(console.log);

Upvotes: 2

Related Questions