Rob Monhemius
Rob Monhemius

Reputation: 5144

How to cancel a http.post() while it is waiting for a response?

This is some code from an angular component. It is a search component where characterIndexes is an array of search results.

The search results are retrieved by typing in a searchbox which triggers the searchtrigger or searchEmptyTrigger depending on its content. After getting the first results I have to perform another http.post() to get the names belonging to the indexes so I can sort them ( I omitted that part from the code ). Then the results are inserted in the characterIndex-array.

A similar thing happens when searchEmptyTrigger is triggered. Except for the characterIndex-array is just set to empty and no http-requests are needed.

The issue I run into is that in some cases, when the searchEmptyTrigger is triggered, the code from the searchtrigger is still running ( due to delays because of the http-requests ).

The result is that the characterIndexes are empty first. And then it would fill up again after receiving the result from the http-request in the searchtrigger.

So the big question is: 'How to cancel my running http.post() while it is waiting for a response?'

  public characters: any[];
  public characterIndexes: number[];

  let searchBox = document.getElementById('search-box');
  let searchTrigger = fromEvent(searchBox, 'input')
  .pipe(
    map((event: any) => event.target.value ),
    filter( text => text.length > 2 ),
    debounceTime( 500 ),
    distinctUntilChanged(),
    switchMap( text =>  ajax(`https://esi.evetech.net/v2/search/?categories=character&datasource=tranquility&language=en-us&search=^${text}&strict=false`)
    )
  );

  let searchEmptyTrigger = fromEvent(searchBox, 'input')
  .pipe(
    map((event: any) => event.target.value ),
    filter( text => text.length <= 2 )
  );

  searchTrigger.subscribe( response => {
    if( response.response.character ){
      let characterIndexes = response.response.character;
        this.http.post('https://esi.evetech.net/latest/universe/names/?datasource=tranquility', characterIndexes)
        .subscribe( (charactersInfo: any[]) => {
           // do some stuff with this.characterIndexes and this.characters = [];
        });
      } else {
        this.characterIndexes = [];
        this.characters = [];
      }
    });

    searchEmptyTrigger.subscribe( () => {
      // reset values 
      this.characterIndexes = [];
      this.characters = [];
    });

PS: I am also open for an alternate approach that performs the same operations as the code above, where I can cancel the http-request.

Upvotes: 4

Views: 2537

Answers (3)

Rob Monhemius
Rob Monhemius

Reputation: 5144

I think I figured it out. I will try to explain what went wrong first, after that I ll go over my solution.

The problem

In the question's code there was a problem. Before a new value was emitted, I would have already performed a http-request in the searchTrigger, but not in the searchTriggerEmpty. Therefore searchTrigger could be emitted after searchTriggerEmpty even tough searchTrigger had been initiated first.

The solution

The solution was to fix the emitting order. To do that the event would have to be emitted before any requests are made. And when a new event is being triggered the previous event should be cancelled ( switchMap-behavior ).

Code explained

  • The searchTrigger emits events when someone types in the searchBox.
  • Some pipes are added. debounceTime( 500 ) and distinctUntilChanged() prevent the event from triggering too often.
  • The map pipe returns the value types in the searchBox.
  • The switchMap has a conditional statement inside it. This is where the best way to proceed is chosen.
  • If the string is longer than 2 characters it will return an observable of an array of characterIndexes (it gets these by doing some http-requests and processing them).
  • If the string is shorter than 2 characters it will directly return an empty array.
  • The reason for using a switchMap here is that a currently pending process will be cancelled when a new value is being emitted.
  • By subscribing to searchTrigger we can now process the latest search results.
  • I also provided the function I used for process_searchString() so you have a better idea what it does / returns.

Code

let searchBox = document.getElementById('search-box');
let searchTrigger = fromEvent(searchBox, 'input')
.pipe(
  debounceTime( 500 ),
  distinctUntilChanged(),
  map((event: any) => event.target.value ),
  switchMap( searchString => {
    if( searchString.length > 2 ){
      return this.process_searchString( searchString );
    } else if ( searchString.length <= 2 ) {
      return of( [] );
    }
  }),
);

searchTrigger.subscribe( ( characterIndexes: number[] ) => {
  this.characterIndexes = characterIndexes;
  this.characters = [];
  if( characterIndexes.length > 0 ){
    this.load_10characters();
  }
});

Disclaimer: At the moment of writing this I just learned to use rxjs. So if you see anything that should be improved drop in a comment.

PS: For those interested this video explains the switchMap better than I can ( and is entertaining too ): https://www.youtube.com/watch?v=rUZ9CjcaCEw

Extra process_searchString():

  private process_searchString( searchString: string ): Observable<number[]>{
    return new BehaviorSubject( searchString )
    .pipe(
      concatMap( ( text: string ) => this.request_characterSearch( text ) ),
      concatMap( ( response: any ) => {
        if( response.character ){
          return this.request_characterNames( response.character )
        } else {
          return of([]);
        }
      }),
      map( (charactersInfo: any[]) => this.sort_alphabetically( charactersInfo ) ),
      map( (charactersInfo: any[]) => charactersInfo.map( characterinfo => characterinfo.id ) ),
    );
  }

Upvotes: 0

Milad
Milad

Reputation: 28590

I think you should use a takeUntil(searchEmptyTrigger).

this.http.post('https://esi.evetech.net/latest/universe/names/?datasource=tranquility', characterIndexes)
        .pipe(takeUntil(searchEmptyTrigger)) // make sure to cancel the post if `searchEmptyTrigger` emits
        .subscribe( (charactersInfo: any[]) => {
           // do some stuff with this.characterIndexes and this.characters = [];
        });

PS, you could totally simplify your whole code into much less code.

Stay away from having subscription inside another subscription, like your post request.

You can merge them by using operators. I'm lazy to write it all, :d

Upvotes: 2

Sachin Shah
Sachin Shah

Reputation: 4533

Try this way ...

Example : The concept is to unsubscribe the observable object...

const request = this.searchService.search(this.searchField.value)
  .subscribe(
    result => { this.result = result.artists.items; },
    err => { this.errorMessage = err.message; },
    () => { console.log('Completed'); }
  );

  request.unsubscribe();  // <-- Hear you can cancel the API request.. 
  //Just set in when you need to cancel. It will works fine. 
  // E.x use with timeout or delay option of observable. 
}

Upvotes: 0

Related Questions