Race condition between a Firestore query function, result of its' subscription and Promises


#1

I’m in need of some advice on reconciling those few things to complete a task in my application. I have a pretty solid understanding of the concepts individually but am struggiling with gettting them all to work together in this instance.

It seems to me that it’s a matter of timing. The process goes like this, with the gist of the code included.

  1. I have a query function that I call from a provider.
    this._categoryLevelTerm$ is a Behavior Subject that I use as my search term in that provider. The function works fine.
// MY PROVIDER
searchCategoryItemsByLevel(term: string | null): void {
 this._categoryLevelTerm$.next(term);
}

queryCategoryItems(uid: string): Observable<ItemElement[]> {
  return this.categoryLevelTerm$.pipe(
  switchMap((term) => this.fs.collection<string>('users').doc(uid).collection<ItemElement>('item_elements', ref => {
   let query: firebase.firestore.CollectionReference | firebase.firestore.Query = ref;
   if (term) { query = query.where('_level', '==', term)}
       return query;
  }).valueChanges())
 )
}
  1. I enter a page in my app and subscribe to the query on enter. Also no problem here. The results come through, I do a few necessary things with the value of the result and all is well.
// MY COMPONENT
ionViewDidEnter() {
 this.database.queryCategoryItems(this.uid).pipe(takeUntil(this.destroy$))
 .subscribe(result => {
  // I do a few things with my result
 }
}
  1. From my page, I call the function that changes the search term in my provider from 3 seperate functions in my component. Each provides a different search term, so the subscription updates with each term. Works just fine. The problem comes when I follow up with a function that depends on the result of that subscription and involves Promises.

    What’s obviously happening is that the Promise chain completes before the subscription updates so it’s using the previous result as the necessary parameter. I click the button once and it’s using the previous resutls, then on click 2 it uses the results that I expected to recieve on click 1.

A representation of the process

// ALSO CALLED FROM MY COMPONENT
searchBasic() {
  this.database.searchCategoryByTitle(this.BASIC_TAG);
  /* results of the subscription are stored in this.subscriptionResult */
  this.reduceItems(this.subscriptionResult) 
 .then((results) => this.finishFunction(results))
 }

searchProfessional() {
  this.database.searchCategoryByTitle(this.PROFESSIONAL_TAG)
  this.reduceItems(this.subscriptionResult) 
 .then((results) => this.finishFunction(results))
 }

At the moment, this.reduceItems(this.subscriptionResult) is returning

Observable.of(this.reducedItems).toPromise();

This is the latest in my attempts to reconcile that race condition. I have tried transforming this.subscriptionReult itself, assigning the result to a locally created variable and returning that, etc.

Nothing is working which leads me to the belief that nothing WILL work unless I ensure the subscription updates before the Promise chain begins.

I glossed over some stuff because it’s wordy, and I don’t think anything I left out will change the fact that the subscription needs to update before anything else happens.

More info available on request!

If anyone has any advice, I would appreciate it very much. I’m hoping there’s a way to ensure that the subscription updates, then begin the promise chain. But I realize I may have to fundamentally change the way I’m going about this.


#2

It looks as though you are reading all the items from the database, and then filtering them down in memory. Is this correct? If so, you can keep a master list of items stored in a provider, and the page instead listens to a method in the FilterManager provider. Let’s say you have defined an interface FilterOptions that includes all possible filter options. Then your method could look like

filteredList(filterOptions: FilterOptions): Array<Thing>

and the Observable on your page looks like

currentFilteredList(): Observable<Array<Thing>> = 
  // whenever your filter options change, emit the value of filteredList(filterOptions)

#4

That’s exactly right. I think you hit the nail on the head with your solution. I’ll be able to test it today and should get the result I want. Grazi


#5

I ended up going a bit of a different route, but based on your suggestion and fresh eyes.
I just wrote seperate functions for each seperate search term (there are only 3 possible so it was feasible) and am returning promises based on the search term.

I dont know if its sound reasoning or just personal preference but I’m always hesitant to store arrays of data in providers. I prefer just sending them some params and getting fresh output.

queryBasicItems(uid: string): Promise<ItemElement[]> {
  return this.fs.collection<string>('users').doc(uid).collection<ItemElement[]>('item_elements').ref.where('_level', '==', 'Basic')
 .get().then((ref) => {
  let results = ref.docs.map(doc => doc.data() as ItemElement);
  return results;
 })
}

etc…

Thank you for guiding me toward a simple solution. They are sometimes the most difficult ones to see when you’re in the thick of problem solving.


#6

Marked my addition as the solution, but the credit goes to @AaronSterling’s suggestion.