Bonio
Bonio

Reputation: 352

Nested observables where final API call needs to use result from both observables

I have the following code where I need to get a value from two separate observables (they are actually promises but have been converted to observables using "from") and then use the values returned from each to make a final API call where both the values are used.

The method should return the result from the API call which is an object called "AppUser".

My code also has some logic in it to retry from a backup API if the primary API fails and then if the backup fails it should return the value from local storage.

This code does work but having read up about nested observables not being good practice I wondered if there is a better way to write this? Any advice would be appreciated.

    // Get app user
    getAppUser(getFromLocal: boolean = false, isRetry: boolean = false): Observable<AppUser> {

      // If "get from local" is true
      if (getFromLocal) {
        return this.getAppUserFromLocal();
      } else {

        // Create observables to get values from local storage
        const customerObs = from(this.authLocal.getCustomerId());
        const authTokenObs = from(this.authLocal.getAuthToken());

        return customerObs
        .pipe(mergeMap((customerId) => {

          return authTokenObs
          .pipe(mergeMap((authToken) => {

            // API Header
            const headers = new HttpHeaders({
              'Content-Type': 'application/json',
              Accept: 'application/json',
              Authorization: `Bearer ${authToken}`
            });
            const url = (!isRetry ? this.baseUrl : this.backupBaseUrl) + '/GetAppUser?customerid=' + customerId;

            return this.http.get(url, { headers })
              .pipe(map((appUser: AppUser) => {
   
                // Set AppUser in local storage
                this.storage.set('appUser', appUser);
   
                return appUser;
              }),
              catchError(e => {

                if (!isRetry) {
                  // Try again using backup API
                  return this.getAppUser(getFromLocal, true);
                } else {
                  // Try again from local storage
                  return this.getAppUser(true, true);
                }
              }));
          }));
        }));
      }
    }

UPDATED with potential solution

I have updated as per the answers with the following potential solution:

    // Get app user
    getAppUser(getFromLocal: boolean = false, isRetry: boolean = false): Observable<AppUser> {

      // If "get from local" is true
      if (getFromLocal) {
        return this.getAppUserFromLocal();
      } else {

        // Create observables to get values from local storage
        const customerId$ = from(this.authLocal.getCustomerId());
        const authToken$ = from(this.authLocal.getAuthToken());

        return forkJoin([
          customerId$,
          authToken$
        ]).pipe(
          switchMap(([customerId, authToken]) => {
            const headers = new HttpHeaders({
              'Content-Type': 'application/json',
              Accept: 'application/json',
              Authorization: `Bearer ${authToken}`
            });
            const url = (!isRetry ? this.baseUrl : this.backupBaseUrl) + '/GetAppUser?customerId=' + customerId;
            return this.http.get(url, { headers })
            .pipe(
              map((appUser: AppUser) => {
                appUser.AuthToken = authToken;
                return appUser;
              }),
              catchError(e => {
                if (!isRetry) {
                  // Try again using backup API
                  return this.getAppUser(getFromLocal, true);
                } else {
                  // Try again from local storage
                  return this.getAppUser(true, true);
                }
              }));
          }),
          tap((appUser: AppUser)  => {

            // Set AppUser in local storage
            this.storage.set('appUser', appUser);
          }),
        );

Upvotes: 0

Views: 253

Answers (2)

SnorreDan
SnorreDan

Reputation: 2910

So you want two observables to get their values first, and then you want to jump over to your third observable (api observable). A general way of solving this problem could be with combineLatest and switchMap:

combineLatest([ customerObs, authTokenObs ])
  .pipe(
    switchMap(([customerId, authToken]) => {
      // Do the code that creates your api observable
      return apiObs;
    })
  )
  .subscribe(resultFromApiCall => console.log(resultFromApiCall));

Upvotes: 1

Will Alexander
Will Alexander

Reputation: 3571

You can use forkJoin to emit both values, especially because you know they will complete as they are from Promises (I've also renamed your Observables to better follow norms) :

return forkJoin({
  customerId: customerId$,
  authToken: authToken$
}).pipe(
  switchMap(({ customerId: string, authToken: string }) => {
    const headers = new HttpHeaders({
      'Content-Type': 'application/json',
      Accept: 'application/json',
      Authorization: `Bearer ${authToken}`
    });
    const url = (!isRetry ? this.baseUrl : this.backupBaseUrl) + '/GetAppUser?customerid=' + customerId;
    return this.http.get(url, { headers }).pipe(retry(1));
  }),
  tap(user => this.storage.set('appUser', appUser)),
);

The only error handling here is the retry operator (you can choose to retry more times), and I've used tap for calling setUser as it is the recommended method for side effects.

Upvotes: 1

Related Questions