Sunny Srivastava
Sunny Srivastava

Reputation: 179

how to make 2 observables / 2 http calls at a time where the first observable output is used as the input for 2nd?

I am stuck in a situation where I am using toPromise() with async method to get the http data in my angular app . I have to use the first promise data to be passed to second promise as an input to get the data again .

But the execution in not happening in the flow I want.

My TS -

    IdSelection() : number[]{

    var Ids : any[]=[];
    this.selection.selected.forEach(element => {
      Ids.push(element.Id);
    });

    var commIds : number[]=[];
    Ids.forEach(async element => {
    const res =  await this.TaskService.getCmntDetails(element , 0 , '%27%27').toPromise();
    if(res["commentId"] !=null){
      res["commentId"].forEach(async data => {
        const innerRes = await this.TaskService.getCmntDetails(0, Number(data) , '%27%27').toPromise();
        innerRes["commentNewId"].forEach(ele => {
          commIds.push(ele);
        });
      });
    }
    
    });

    return commIds;
  }

When I call this method, I am getting the value in res variable , but soon after that the execution is going to the return statement , and it returns null .

When I debugged and checked after it returns null , the execution comes back to the second promise and the thing is executed .

Can anyone help me out on how to make 2 observables / 2 http calls at a time where the first observable output is used as the input for 2nd ?

TIA

Upvotes: 1

Views: 761

Answers (3)

Barremian
Barremian

Reputation: 31105

This is yet another instance of unwarranted conversion from RxJS observables to promise using toPromise(). In fact, it'd be more elegant to map from one observable to another using a higher order mapping operator like switchMap without the conversion.

And since you're trying to trigger HTTP requests for each element of an array, you'd need to use RxJS forkJoin function. Additionally I also use map operator to transform the data and iif function to conditionally return a response.

import { Observable, forkJoin, iif, EMPTY } from 'rxjs';
import { switchMap, map } from 'rxjs/operators';

IdSelection() : Observable<any> {
  return forkJoin(    // <-- RxJS `forkJoin` function
    this.selection.selected.map(element =>   // <-- JS `Array#map` function
      this.TaskService.getCmntDetails(element , 0 , '%27%27').pipe(
        switchMap((res: any) =>      // <-- RxJS `switchMap` operator
          iif(             // <-- RxJS `iif` function
            () => res["commentId"] !=null,
            forkJoin(
              res["commentId"].map(data =>    // <-- update: add JS `Array#map` here
                this.TaskService.getCmntDetails(0, Number(data) , '%27%27').pipe(
                  map((innerRes: any) => innerRes["commentNewId"])  // <-- RxJS `map` operator
                )
              )
            ),
            EMPTY       // <-- RxJS `EMPTY` constant
          )
        )
      )
    )
  )
}

You'd then subscribe to this function where it's response is required.

this.IdSelection().subscribe({
  next: (res: any) => {
    // use response
  },
  error: (error: any) => {
    // handle error
  }
});

Disclaimer: The answer could work without issues, but the response might not be of the form you'd expect. For eg. it might have nested arrays and empty elements in the arrays. You could fine tune it using JS methods.

You could refer here for more info on nested subscriptions.

Also beware toPromise() is being deprecated in RxJS 7 and would be gone in RxJS 8.

Upvotes: 0

Aviad P.
Aviad P.

Reputation: 32629

You need to create an observable pipe that eventually returns the required ids:

Example in your case:

from(this.selection.selected).pipe(
  map(element => element.Id),
  mergeMap(element => this.TaskService.getCmntDetails(element, 0, '%27%27')),
  filter(res => res["commentId"] != null),
  mergeMap(res => from(res["commentId"])),
  mergeMap(data => this.TaskService.getCmntDetails(0, Number(data), '%27%27')),
  mergeMap(innerRes => from(innerRes["commentNewId"])),
  toArray()
);

Let me explain what this does, step by step:

This creates a stream of the selected elements (objects).

from(this.selection.selected).pipe(

This takes each object and maps it to its Id field.

  map(element => element.Id),

This starts asynchronous calls for every upstream element. These calls are started asynchronously and might resolve in any order.

  mergeMap(element => this.TaskService.getCmntDetails(element, 0, '%27%27')),

You want only those results which have valid data, this filters the results.

  filter(res => res["commentId"] != null),

This "expands" the commentId field into a stream of new ids.

  mergeMap(res => from(res["commentId"])),

This again starts parallel calls to get data for each id, again, results might return in any order.

  mergeMap(data => this.TaskService.getCmntDetails(0, Number(data), '%27%27')),

Next we again "expand" the field commentNewId and return a stream of those.

  mergeMap(innerRes => from(innerRes["commentNewId"]))

And finally we collect all results and make them into an array

  toArray()

You can then use the above stream by subscribing to it, or converting it into a promise, there are many options.

EDIT

Extra tip, if you want error handling, you can do it either on the entire stream, or you can catch and ignore errors in the individual calls, e.g:

  this.TaskService.getCmntDetails(element, 0, '%27%27').pipe(catchError(_ => EMPTY))

Upvotes: 3

Kari F.
Kari F.

Reputation: 1434

Check this Stackblitz example: https://stackblitz.com/edit/angular-ivy-vawyuq

It follows quite closely your example although it's not exactly the same. I'm sure you can find a way to apply the solution in your specific case.

Check here the sample data used: https://jsonplaceholder.typicode.com/

Here is the relevant code:

  getComments() {
    const postIds = [1, 3, 5];

    const posts$ = merge(from(postIds)).pipe(
      concatMap(id =>this.http.get(`https://jsonplaceholder.typicode.com/posts/${id}`)),
      tap((post: any) => {console.log('We get post number: ', post.id);}),
      concatMap((post: any) =>this.http.get(`https://jsonplaceholder.typicode.com/posts/${post.id}/comments`)),
      tap((comments) => console.log(`Now we get comments of the post`, comments)),
      map((comments: any) => comments.filter((comment: any) => comment.id % 2 !== 0)), // example of how to filter the results
      concatAll(),
      map((comment: any) => comment.id),
      toArray()
    );

    posts$.subscribe(resultIds => {
      console.log('Final result: ', resultIds);
      document.getElementById('result').innerHTML += `<div>Final result: ${resultIds} </div>`;
    });
  }

First you iterate over the input array of post IDs (merge-from). Then you make an http request to get the post details (concatMap). After that you request the comments related to the post (2nd concatMap). Then comes the optional filter to select the desired comments (in your case commentId != null). concatAll combines the result arrays into one array. The 2nd map operator flattens the array of comment objects to a comment ids. Finally toArray converts the stream of ids to an array.

You can, of course, remove all the tap operators which are there just to console.log intermediate results during the process. The resulting code is quite clean and easy to follow.

Upvotes: 0

Related Questions