Search code examples
javascriptangularfor-loopconcatenationes6-promise

Promise all to speed up api call in for loop - javascript


I have the following for loop which works correctly in my code. However it needs to do multiple api calls as it loops through, which is taking a long time, so i am trying to figure out whether i could do a promise all and run the api calls all at once.

But really not sure how to go about it, with how my code works, so hoping someone might be able to help.

Below is my code:

let terms = []
this.selectedGroup.groups = ['A', 'B', 'C', 'D', 'E', 'F']

//loop through each term
for (let term of this.terms) {
  let subjects: any = [];

  //loop through each terms assessments
  for (let assessment of term.assessments) {

    //loop through the assessment year groups
    for (let assessmentGroup of assessment.year_groups) {

      //only get the assessments for the particular year group we are looking at
      if (this.selectedGroup.groups.includes(assessmentGroup.name)) {

        //get the subject data for this assessment
        let newSubjects = await this.dataService.getData(
          assessment,
          assessmentGroup,
          this.selectedGroup.students
        );

        //add the assessment subjects to the overall subjects array
        subjects = subjects.concat(newSubjects)
      }
    }
  }

  //push all the subjects for the term into a terms array
  terms.push({
    name: term._id,
    subjects: subjects,
  });
}

Is there anyway of introducing a promise all to this, so the api calls can run at once? Thanks in advance!


Solution

  • Here are the steps to take:

    1. Remove the await keyword where you currently have it. So now subjects will be an array of promises.

    2. Add await Promise.all in your push call:

      terms.push({
          name: term._id,
          subjects: (await Promise.all(subjects)).flat(),
      });
      

    That's it: now there will multiple pending promises until one item is pushed to the terms array. This should already give some improvement.

    If you want to have even more simultaneous pending promises, then also make terms an array of promises:

    terms.push(Promise.all(subjects).then(subjects => ({
        name: term._id,
        subjects: subjects.flat(),
    })));
    

    And then, after the outermost loop, have a one-for-all await:

    terms = await Promise.all(terms);
    

    Demo

    I created some mock data along the structure that your code expects, with a mock getData function, ...etc, so that code runs without errors. Here are the three versions of the code:

    Original code

    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
    const app = {
        selectedGroup: {groups: ["A"],students: ["Student1", "Student2"]},
        terms: [{assessments: [{year_groups: [{name: "A"}]}, {year_groups: [{name: "E"}]}],_id: 123}, 
                {assessments: [{year_groups: [{name: "C"}]}],_id: 567}],
        dataService: {
            async getData(assessment, group, students) {
                await delay(100);
                return students.map(student => "++" + student);
            }
        },
        
        async f() { // The function in the question (no change)
            let terms = [];
            this.selectedGroup.groups = ['A', 'B', 'C', 'D', 'E', 'F'];
            for (let term of this.terms) {
              let subjects = [];
    
              //loop through each terms assessments
              for (let assessment of term.assessments) {
    
                //loop through the assessment year groups
                for (let assessmentGroup of assessment.year_groups) {
    
                  //only get the assessments for the particular year group we are looking at
                  if (this.selectedGroup.groups.includes(assessmentGroup.name)) {
    
                    //get the subject data for this assessment
                    let newSubjects =  await this.dataService.getData(
                      assessment,
                      assessmentGroup,
                      this.selectedGroup.students
                    );
    
                    //add the assessment subjects to the overall subjects array
                    subjects = subjects.concat(newSubjects)
                  }
                }
              }
    
              //push all the subjects for the term into a terms array
              terms.push({
                name: term._id,
                subjects: subjects,
              });
            }
            return terms;
        }
    };
    
    app.f().then(console.log);

    With subjects being promises

    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
    const app = {
        selectedGroup: {groups: ["A"],students: ["Student1", "Student2"]},
        terms: [{assessments: [{year_groups: [{name: "A"}]}, {year_groups: [{name: "E"}]}],_id: 123}, 
                {assessments: [{year_groups: [{name: "C"}]}],_id: 567}],
        dataService: {
            async getData(assessment, group, students) {
                await delay(100);
                return students.map(student => "++" + student);
            }
        },
        
        async f() { // The function with first modification
            let terms = [];
            this.selectedGroup.groups = ['A', 'B', 'C', 'D', 'E', 'F'];
            for (let term of this.terms) {
              let subjects = [];
    
              //loop through each terms assessments
              for (let assessment of term.assessments) {
    
                //loop through the assessment year groups
                for (let assessmentGroup of assessment.year_groups) {
    
                  //only get the assessments for the particular year group we are looking at
                  if (this.selectedGroup.groups.includes(assessmentGroup.name)) {
    
                    // Removed `await`
                    let newSubjects = this.dataService.getData(
                      assessment,
                      assessmentGroup,
                      this.selectedGroup.students
                    );
    
                    //add the assessment subjects to the overall subjects array
                    subjects = subjects.concat(newSubjects)
                  }
                }
              }
    
              //Wait for all subjects promises to resolve
              terms.push({
                name: term._id,
                subjects: (await Promise.all(subjects)).flat(),
              });
            }
            return terms;
        }
    };
    
    app.f().then(console.log);

    With subjects and terms being promises

    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
    const app = {
        selectedGroup: {groups: ["A"],students: ["Student1", "Student2"]},
        terms: [{assessments: [{year_groups: [{name: "A"}]}, {year_groups: [{name: "E"}]}],_id: 123}, 
                {assessments: [{year_groups: [{name: "C"}]}],_id: 567}],
        dataService: {
            async getData(assessment, group, students) {
                await delay(100);
                return students.map(student => "++" + student);
            }
        },
        
        async f() { // The function with second modification
            let terms = [];
            this.selectedGroup.groups = ['A', 'B', 'C', 'D', 'E', 'F'];
            for (let term of this.terms) {
              let subjects = [];
    
              //loop through each terms assessments
              for (let assessment of term.assessments) {
    
                //loop through the assessment year groups
                for (let assessmentGroup of assessment.year_groups) {
    
                  //only get the assessments for the particular year group we are looking at
                  if (this.selectedGroup.groups.includes(assessmentGroup.name)) {
    
                    // Removed `await`
                    let newSubjects = this.dataService.getData(
                      assessment,
                      assessmentGroup,
                      this.selectedGroup.students
                    );
    
                    //add the assessment subjects to the overall subjects array
                    subjects = subjects.concat(newSubjects)
                  }
                }
              }
    
              //push a promise for all subjects promises to resolve
              terms.push(Promise.all(subjects).then(subjects => ({
                name: term._id,
                subjects: subjects.flat(),
              })));
            }
            // One final spot to await all terms promises
            terms = await Promise.all(terms);
            return terms;
        }
    };
    
    app.f().then(console.log);

    As you can see the output is the same for each version.

    But all of them output duplicate data: I don't know what your getData does, but if it uses the students argument, like I did, for building the result, then surely the result will have duplicate students. Similar duplication can occur because of the assessment parameter, which also is repeated by the inner loop. As you said your code was correct, I leave that for you to further assess.