Subscribe inside Promise

Hi. My app checks if there are data in Ionic Storage and if there are none, it will load data from a JSON file to the Ionic Storage. This are my code:

quote.page.ts

quotes: Quote[] = [];

this.plt.ready().then(() => {
      this.storageService.loadQuotes().then(quotes => {
        this.quotes = quotes;
      });
    });

storage.service.ts

quotes: Promise<Quote[]>;
data: any;

loadQuotes(): Promise<any> {
    return this.storage.get(QUOTES_KEY).then((quotes: Quote[]) => {
      if (!quotes || quotes.length === 0) {
        this.http.get("../../assets/files/quotes.json").subscribe(result => {
          this.data = result["quotes"];
          this.quotes = result["quotes"];
          this.storage.set(QUOTES_KEY, this.data);
          return this.quotes;
        });
      }
    });
  }

My problem is, there are no data loaded in quotes in the quote.page.ts but data was loaded in the ionic storage.

When u like to combine promises (like storage) eith observables (like http) u need to convert either one of them to the other and then concat them correctly

This u r not doing (convert and concat)

While it is generally considered sinfull to convert an observable to a promise i think it will be the most pragmatic way for you

Meaning u need to put the word return in front of the http.get. And the operator .toPromise() right after the get a bit closer to the desired state (and maybe fix a bit syntax stuff)

The observable way would be to use the from keyword to convert the storage get into an observable and then switchMap yourself into rxjs heaven

The if statement will give u the next headache as u need to provide for an alternative return value using an else statement. Otherwise the chain is broken and the calling code will be confused

1 Like

Here’s how I would approach this. We have three possible sources of truth that we want to try in order:

A. already sitting in our service from a previous run of this method
B. in storage
C. from assets

If we’re reading from A, things are simple. Otherwise, we want to put what we found into A. If B comes back empty, we ignore it (from a design perspective, I would argue that this might not be what we want to do, because it completely closes the door on the possibility that the user might want an empty list of quotes, but leaving it this way because that’s how OP wrote it). If we have to read from C, then we also want to stick the result into storage for the next run of the app.

So let’s first make those three building blocks, which will make the final weaving of them together more elegant:

interface QuoteAsset {
  quotes: Quote[];
}

quotes?: Quote[];

cachedQuotes(): Observable<Quote[] | undefined> {
  return of(this.quotes);
}

storedQuotes(): Observable<Quote[] | undefined> {
  return from(this.storage.ready().then(() => this.storage.get(QUOTES_KEY)).pipe(
    map(quotes => !!quotes && quotes.length > 0 ? quotes : undefined));
}

fetchedQuotes(): Observable<Quote[]> {
  return this.http.get<QuoteAsset>("../../assets/files/quotes.json").pipe(
    map(qa => qa.quotes),
    tap(quotes => this.storage.set(QUOTES_KEY, quotes)));
}

Now we’ve reduced the problem to “I have N Observables, and I want the first emission from any one of them that passes a specific test”, which sounds like something that Rx is very likely to have in its toolkit, and indeed it does:

getQuotes(): Promise<Quote[]> {
  return concat(cachedQuotes(), storedQuotes(), fetchedQuotes()).pipe(
    first(quotes => !!quotes),
    tap(quotes => this.quotes = quotes)
  ).toPromise();
}

A few subtle notes:

  • we’re not waiting on Storage.ready before the set call. why this is will become clear at the end of the post
  • we’re not waiting for set to finish. i only use Storage to communicate with future app runs, never for in-app communication, therefore i don’t care when it finishes
  • technically we’re reassigning the cache even when retrieving from cache. i would argue that if this matters, then something else more problematic is going on (such as those getter and setter functions i do my best to avoid)
  • concat subscribes to its arguments serially, which means that if we have reached storage.set, then it is provable that storage is ready: the subscription to storedQuotes has already completed (and was undefined). for that to have happened, we must have already read from storage, which means in turn that we have already waited for storage to be ready
1 Like

Wow. Thank you for enlightening me. As a beginner in Ionic, this information is of great help to me especially what you did on the concat part. Thanks again.