How to refactor API JS code to make more serial?


#1

I have some existing JS API code that I want to refactor to make more serial, because each button click causes over 20+ API requests to be sent, which often results in a 503 server at capacity http response. This is the JS code that sends the API requests in parallel. I know that this below code is sending the API requests in parallel because .all() is present.

      $scope.importContent = function(){
        const promises = [], successes = [], failures = [], selectedForImport = [];

        angular.forEach(selectedForImport, (profileWork) => {

          const collectedWork = createCollectedWorkFromProfileWork(bepress.page_context.current_user.uuid, profileWork);
          profileWork.content_categories = collectedWork.content_categories;

          promises.push(
            worksApiClient.createWork(collectedWork).then(
              (resp) => {
                $scope.imported += 1;

                profileWork.work = resp.data;
                profileWork.is_collected = true;

                return worksApiClient.createProfileWork(bepress.page_context.current_user.profile_id, profileWork)
                  .then(
                    () => {
                      $scope.imported += 1;
                      successes.push(profileWork);
                    },
                    () => { failures.push(profileWork); }
                  );
              },
              () => { failures.push(profileWork); }
            )
          );

        });

        $q.all(promises).then( () => {
          let message = successes.length === 1 ? "The work was successfully imported." : successes.length + " works imported.";
          let title = "Success";

          if (failures.length) {
            title = failures.length === selectedForImport.length ? "Error importing works" : "Some errors were encountered";

            message += "<h4>There was a problem importing the following works</h4><ol>";
            angular.forEach(failures, (pw) => {
              message += "<li>" + _.escape(pw.title) + "</li>";
            });
            message += "</ol><p>Please attempt to import these works again.</p>";
          }

            return worksApiClient.getProfile(bepress.page_context.current_user.profile_id)
            .then( (resp) => {
              const profile = resp.data;
              const now = new Date();
              profile.settings.last_viewed_works_to_collect = now.toISOString();
              return worksApiClient.updateProfile(profile);
            })
            .finally(
              () => {
                const hasNoErrors = failures.length === 0;
                $modalInstance.close(hasNoErrors);
                // display this message for a successful import
                window.bepress.modals[hasNoErrors ? 'success' : 'error']({
                  title: `<h3>${title}</h3>`,
                  message: message
                });
              }
              );
        });
      };

This is the correct refactored code.

      $scope.importContent = function() {
        const successes = [], failures = [], selectedForImport = [];
        angular.forEach($scope.profileWorks, (w) => { if (w.selected) { selectedForImport.push(w); }});

        // * 2 because we 1) create a Work and 2) create a ProfileWork for each selected item
        $scope.importCount = selectedForImport.length * 2;
        $scope.imported = 0;

        if ($scope.importCount === 0) {
          return;
        }

        $scope.currentlyImporting = $scope.importCount > 0;
        let chain = Promise.resolve([]);
        angular.forEach(selectedForImport, (profileWork) => {
          chain = chain.then((listings) => {
            return new Promise((resolve, reject) => {

              const collectedWork = createCollectedWorkFromProfileWork(bepress.page_context.current_user.uuid, profileWork);
              profileWork.content_categories = collectedWork.content_categories;

              listings.push(
                worksApiClient.createWork(collectedWork).then(
                  (resp) => {
                    $scope.imported += 1;
                    profileWork.work = resp.data;
                    profileWork.is_collected = true;
                    console.log("success createWork");

                    return worksApiClient.createProfileWork(bepress.page_context.current_user.profile_id, profileWork)
                      .then(
                        () => {
                          $scope.imported += 1;
                          successes.push(profileWork);
                          console.log("success createProfileWork");
                          return resolve(listings);
                        },
                        () => {
                          failures.push(profileWork);
                          console.log("reject in createProfileWork");
                          return reject(listings);
                        }
                      )
                  },
                  () => {
                    failures.push(profileWork);
                    console.log("reject inside createWork");
                    return reject(listings);
                  }
                )
              );
            })
          })
          return chain;
        })
         chain.then(function () {
              showConclusionModal(successes, failures, selectedForImport);
         });
    };

Here are the results of the print statements:
success createWork
success createProfileWork
success createWork
success createProfileWork
success createWork
success createProfileWork
foreach loop completed. Do something after it…


#2

I’ll let one of our JS community help with the refactoring. In the meantime this may help with getting some data on the problem.

I believe browsers cap the number of concurrent requests well below 20. It used to be 6 but a quick search looks as though Chrome may have upped that to 10.

Might be worth checking the network section of the developer tools in the browser to profile a button click. You should be able to see the timings of the network requests presented as a waterfall chart (sorry no screenshot as I’m on a mobile).

Does each of these imports trigger something fairly complex server side?


#3

Yes, none of the requests happen instantly; if a user clicks the “Import Works” button with several works checked in modal, several API requests are sent and the user currently waits for 1-2 minutes before all the requests are done.


#4

When you have time, any chance you could update this post to share final conclusions based on what you learned? Thanks!