Solving Race Condition in search operation

INTRING
3 min readMar 14, 2021

--

Race Condition issue
Race Condition

Issue: Race condition, when the user makes changes to input, there are cases where the previous data overrides the latest API call.

To solve this race condition, we can either make streams synchronous so that the last stream gets started if the previous stream is completed or cancel the previous stream and have only one stream active. This can be achieved by using ConcatMap and SwitchMap respectively. We will also be using debounce time and distinctUntilChanged to emit events once the user stops making changes to the data and will not emit events if the data is the same.

ConcatMap: Projects each source value to an Observable which is merged in the output Observable, in a serialized fashion waiting for each one to complete before merging the next.

Solution:

this.repositories$=fromEvent(this.searchRepositories.nativeElement,"input").pipe(
pluck("target","value"),
debounceTime(500),
distinctUntilChanged(),
concatMap(val => this.appService.getRepositories(val))
);

Issue: The issue using concat map is, it is serialized which means it is supposed to complete all the previous requests which are no longer needed. This is going to take a lot of time to show the results to the user.

SwitchMap: Projects each source value to an Observable which is merged in the output Observable, emitting values only from the most recently projected Observable.

Solution:

this.repositories$=fromEvent(this.searchRepositories.nativeElement,"input").pipe(
pluck("target","value"),
debounceTime(500),
distinctUntilChanged(),
switchMap(val => this.appService.getRepositories(val))
);

I had one issue with implementing SwitchMap in my nonreactive code. To implement SwitchMap the API call which needs to be canceled out should be inner observable. This would take a lot of time to refactor the code as the code which needs to be refactored is a lot. So I wanted to be lazy about this and implement the SwitchMap functionality with debounceTime and distinctUntilChanged.

I created a directive called lazy search which listens for modelChanges and in modelChanges, we have a debounceTime, distinctUntilChanged operator which stops further processing until 500ms has elapsed since the last user’s input and also verifies the last input is not equal to the previous input. Then we emit two output events one is a search event and the other is a discardPrevious event.

lazySearch: This event gives the data the user entered and it will not be emitted at every keystroke and also will not be emitted if the keyword is the same as the previous keystroke.

discardPrevious: This will be emitted before emitting the search event. This tells us to discard the previous streams. This way we are implementing the SwitchMap.

LazySearch Directive

@Directive({selector: '[appLazySearch]'})export class LazySearchDirective implements OnInit, OnDestroy {public scvng = new Scavenger(this);@Output() lazySearch: EventEmitter<string> = new EventEmitter<string>();@Output() discardPrevious: EventEmitter<boolean> = new EventEmitter<boolean>();searchKeyword$: Subject<string> = new Subject();constructor() { }ngOnInit(): void {
this.lazyFilter();
}
@HostListener('ngModelChange', ['$event'])onModelChange(searchKeyword) {
this.searchKeyword$.next(searchKeyword);
}
lazyFilter() {
this.searchKeyword$.pipe(
this.scvng.collect(),
startWith(''),
debounceTime(500),
distinctUntilChanged(),
tap(data => this.discardPrevious.emit(true)),
).subscribe(searchKeyword => {
this.lazySearch.emit(searchKeyword);
})
}ngOnDestroy(): void {}}

Implementing LazySearch Directive

<input
type="text"
class="form-control border-blue-color"
placeholder="Search Repositories..."
id="searchRepositories"
appLazySearch
(lazySearch)="filterRepositories($event)"(discardPrevious)="stopSearch.next($event)"
/>
public stopSearch = new Subject();filterRepositories(val) {
this.appService.getRepositories(val).pipe(takeUntil(this.stopSearch))
}

Using takeUntil rxjs operator

takeUntil: Emit values until provided observable emits.

I created a Subject in the code called cancelPrevious and whenever the discardPrevious emits I do cancelPrevious.next() and to the method which is making API calls I added the takeUntil Operator. So whenever the subject emits a value the API call will be canceled. This way we are implementing the switchMap operation.

The race condition is resolved now by creating the directive and implementing it in the component. It is resolved with minimal effort of refactoring.

--

--

INTRING

INTRING: India transforming chaos to order. Passionate about simplifying lives, driving positive change. Innovative solutions for an organized India.