1

Is there a way to make forEach loop in Typescript wait so that asynchronous code like http call can complete properly.

Let's say I have three Arrays a[], b[] & c[] in an Angular Component. There are three functions, the latter two depends on the completion of previous functions.

loadA(){
this.http.get<a[]>(http://getA).subscribe(a=> this.a = a, 
()=>loadB());
}

loadB(){
this.a.forEach((res, index)=>{
this.http.get<b[]>('http://getBbyA/res').subscribe(b=> this.b.push(...b),
()=> {
if(index===this.a.length-1){
loadC());
}
}
});

loadC(){
this.b.forEach(res=>{
this.http.get<c[]>('http://getCbyB/res').subscribe(c=> this.c.push(...c));
});
}

Now for the second Method the forEach loop makes it unpredictable to call the loadC() function after the b[] array is properly loaded with data fetched from http call. How to make forEach loop in loadB() wait for all the http results are fetched and then call loadC() to avoid unpredictability?

Update (With RxJs Operators):

I have tried the following in my project:

loadData(): void {
    this.http.post<Requirement[]>(`${this.authService.serverURI}/requirement/get/requirementsByDeal`, this.dealService.deal).pipe(
      concatAll(), // flattens the array from the http response into Observables
      concatMap(requirement => this.http.post<ProductSet[]>(`${this.authService.serverURI}/productSet/getProductSetsByRequirement`, requirement).pipe( // loads B for each value emitted by source observable. Source observable emits all elements from LoadA-result in this case
        concatAll(), // flattens the array from the http response of loadB
        concatMap(pSet => this.http.post<Product[]>(`${this.authService.serverURI}/product/get/productsByProductSet`, pSet).pipe( // foreach element of LoadB Response load c
          map(product => ({requirement, pSet, product})) // return a object of type { a: LoadAResult, b: LoadBResult, c: LoadCResult}
        ))
      )),
      toArray()
    ).subscribe((results: { requirement: Requirement, productSet: ProductSet, product: Product }[] => {
      results.forEach(result => {
        this.requirements.push(...result.requirement);
        this.productSets.push(...result.productSet);
        this.products.push(...result.product);
      });
    }));
  } 

But I am still getting some error (TS2345). Where I am going wrong?

javaland235
  • 743
  • 3
  • 11
  • 21
  • Related question (but not duplicate, OP asked about `foreach + Observables`, this is about `foreach + Promises`) https://stackoverflow.com/questions/18983138/callback-after-all-asynchronous-foreach-callbacks-are-completed – Nino Filiu Mar 11 '19 at 20:05

1 Answers1

1

There are several ways to achieve this. I suggest to use rxJs Operators concatAll and concatMap so fire the httpCalls in sequence. I prepared a quick snippet. It first loads A, then for each element in A it loads B and for each element in B it loads C. You may need to adjust the aggregation of results depending on your needs

load(): void {
this.http.get<LoadAResult[]>("/loadA").pipe(
  concatAll(), // flattens the array from the http response into Observables
  concatMap(a => this.http.get<LoadBResult[]>("/loadB").pipe( // loads B for each value emitted by source observable. Source observable emits all elements from LoadA-result in this case
    concatAll(), // flattens the array from the http response of loadB
    concatMap(b => this.http.get<LoadCResult[]>("/loadC").pipe( // foreach element of LoadB Response load c
      map(c => ({a, b,c })) // return a object of type { a: LoadAResult, b: LoadBResult, c: LoadCResult}
    ))
  )),
  toArray()
).subscribe((results: { a: LoadAResult, b: LoadBResult, c: LoadCResult[]}[]) => {
  // do something with results
}));

}

A.Winnen
  • 1,680
  • 1
  • 7
  • 13
  • Hi, I have tried your suggestion in my project (Please see UPDATE in the question), it is not working. I am getting some error (TS2345). Is there any syntactical error around "((results: { requirement: Requirement, productSet: ProductSet, product: Product }[] => {" ? – javaland235 Mar 11 '19 at 20:01
  • there was an ) missing. sry :) also note that if you just push the elements into your arrays, you'll have lot of duplicate entries in there. – A.Winnen Mar 11 '19 at 20:04
  • I have added the ). Still I am getting the following error: ERROR in src/app/Services/requirement.service.ts(185,17): error TS2345: Argument of type '(results: { requirement: Requirement; productSet: ProductSet; product: Product; }[]) => void' is not assignable to parameter of type '(value: { requirement: Requirement; pSet: ProductSet; product: Product[]; }[]) => void'. Types of parameters 'results' and 'value' are incompatible. – javaland235 Mar 11 '19 at 20:18
  • Type '{ requirement: Requirement; pSet: ProductSet; product: Product[]; }[]' is not assignable to type '{ requirement: Requirement; productSet: ProductSet; product: Product; }[]'. Type '{ requirement: Requirement; pSet: ProductSet; product: Product[]; }' is not assignable to type '{ requirement: Requirement; productSet: ProductSet; product: Product; }'. Property 'productSet' is missing in type '{ requirement: Requirement; pSet: ProductSet; product: Product[]; }'. – javaland235 Mar 11 '19 at 20:18
  • rename your pSet parameter to productSet – A.Winnen Mar 11 '19 at 21:12
  • did that. For some reason the compiler is expecting product: Product[] instead of product:Product in the line "(results: { requirement: Requirement, productSet: ProductSet, product: Product }[]) =>" Do you think the issue lies with "map(product => ({requirement, pSet, product}))"?? – javaland235 Mar 11 '19 at 21:27
  • just fix the type assertion in the subscribe callback. just use .subscribe(result => {...} without specifing the type. then you see the type which is emited by the observable. – A.Winnen Mar 11 '19 at 21:38
  • No I get that, but we are having a & b with object types (LoadAResult & LoadBResult) but c is returning LoadCResult[] array. Is this a logical result? – javaland235 Mar 11 '19 at 21:53
  • since i don't know what you want to do with that data, i cannot argue for or against array, As i wrote, you may need to adjust the aggregation of the result to fit your needs. E.g. if you dont want the array for LoadCResult, you could add another concatAll: `concatMap(b => this.http.get("/loadC").pipe( concatAll(), map(c => ({a, b,c })) ))` – A.Winnen Mar 11 '19 at 22:46