Reputation: 5144
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
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
searchTrigger
emits events when someone types in the
searchBox
.debounceTime( 500 )
and distinctUntilChanged()
prevent the event from triggering too often. map
pipe returns the value types in the searchBox.switchMap
has a conditional statement inside it. This is where the best way to proceed is chosen. switchMap
here is that a currently pending process will be cancelled when a new value is being emitted.searchTrigger
we can now process the latest search results.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
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
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