ryangineer
ryangineer

Reputation: 519

How to chain promises within nested for loops?

var verifyEmail = function (thisEmail){
    return new Promise(
        function (resolve, reject) {
            quickemailverification.verify(thisEmail, function (err, response) {
              // Print response object
              console.log(response.body);
              if (response.body["success"] == "true"){
                  var validity = response.body["result"];
                  if (validity == "valid"){
                    console.log("Email Valid!");
                    resolve(validity);
                  } else {
                    console.log("Email Invalid!")
                    resolve(validity);
                  }
              } else {
                  var reason = new Error("API unsuccessful");
                  reject(reason);
              }
            });
        }
    );
};

var saveValidity = function (validity){
    return new Promise(
        function (resolve, reject){
            if (validity == "valid"){
                var state = true;
                admin.database().ref("/users_unverified/"+keys[i]+"/emails/"+x+"/verified/").set(state, function(error) {
                  if (error) {
                    console.log("Email ("+thisEmail+") verfication could not be saved" + error);
                  }
                  console.log("Email verification saved: " +thisEmail);
                });
            } else {
                state = false;
                admin.database().ref("/users_unverified/"+keys[i]+"/emails/"+x+"/verified/").set(state, function(error) {
                  if (error) {
                    console.log("Email ("+thisEmail+") verfication could not be saved" + error);
                  }
                  console.log("Email verification saved: " +thisEmail);
                });
            }
        }
    );
};

admin.database().ref("/users_unverified/").once('value').then(function(snapshot) {
    var snap = snapshot.val();
    keys = Object.keys(snap);

    for (var i = 0; i < 100; i++){
        var emails = snap[keys[i]]["emails"];
        if (emails){
            for (var x = 0; x<emails.length; x++){
                var thisEmail = emails[x]["email"];
                var emailVerified = emails[x]["verified"];
                if (emailVerified != true || emailVerified != false){
                    verifyEmail
                        .then(saveValidity)
                        .then(function (fulfilled) {
                            console.log(fulfilled);
                        })
                        .catch(function (error){
                            console.log(error.message);
                        });
                }
            }
        }
    }
});

Above is the code I put together. I'm not all too convinced that it will work. I'm new to promises, so I'm trying to understand how to do this right.

The verifyEmail function should take in the email address from the firebase query in the third chunk of the code. The saveValidity function should take on the validity response from verifyEmail.

But, what I'm also worried about the nested for loop I have in the firebase query block. I'm looping through each user to validate their emails, but each user sometimes also has multiple emails. I'm worried that it will loop on to the next user before finishing checking all the emails of the previous user.

I'm also not sure if I can pass data into the promise functions the way I did.

Could definitely use some help here. Really trying hard to understand how this works.

Upvotes: 0

Views: 640

Answers (2)

luochen1990
luochen1990

Reputation: 3867

If ES2017 is available in your case, you can just use the keywords await and async to do that directly. Following is an example:

function resolveAfter2Seconds(x) { 
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

async function f1() {
  var x = await resolveAfter2Seconds(10);
  console.log(x); // 10
}
f1();

And you can read more about async/await here.

If you want to do that without async/await to achieve better browser compatibility, you can use Babel to do the pre-compile.

If you really want a lightwight implementation, you can use a function named chainPromiseThunks, or chain for short. This chain function accepts an Array of Thunks of Promises, And returns a new Thunk of Promise, Following is an one-line-implementation of chain:

const chain = thunks => thunks.reduce((r, a) => () => r().then(a));

And here is a usage demo:

const echo = x =>
    new Promise(function(resolve) {
        return setTimeout((function() {
            console.log(x);
            return resolve(x);
        }), 1000);
    })
;

const pThunks = [1,2,3,4,5].map(i => () => echo(i));

chain(pThunks)();

Upvotes: 1

jfriend00
jfriend00

Reputation: 708046

First, you need to fix saveValidity() to always resolve or reject the promise and to pass in the other variables key and thisEmail that it references:

const saveValidity = function (validity, key, thisEmail){
    return new Promise(
        function (resolve, reject){
            if (validity == "valid"){
                let state = true;
                admin.database().ref("/users_unverified/"+key+"/emails/"+x+"/verified/").set(state, function(error) {
                  if (error) {
                    let msg = "Email ("+thisEmail+") verfication could not be saved" + error;
                    console.log(msg);
                    reject(new Error("Email ("+thisEmail+") verfication could not be saved" + error));
                  } else {
                    resolve("Email verification saved: " +thisEmail);
                  }
                });
            } else {
                state = false;
                admin.database().ref("/users_unverified/"+keys[i]+"/emails/"+x+"/verified/").set(state, function(error) {
                  if (error) {
                    let msg = "Email ("+thisEmail+") verfication could not be saved" + error;
                    console.log(msg);
                    reject(new Error(msg));
                  } else {
                    resolve("Email verification saved: " +thisEmail);
                  }
                });
            }
        }
    );
};

Then, several changes are made to your main loop:

  1. I assume we can run all the verifyEmail() calls in parallel since they don't appear to have anything to do with one another.
  2. Change verifyEmail.then(...) to verifyEmail(thisEmail).then(...)` to actually call the function
  3. Collect all the verifyEmail() promises in an array
  4. Call Promise.all() on the array of promises to monitor when they are all done
  5. Return value from .then() so we get the returned values in Promise.all()
  6. rethrow in .catch() so promise stays rejected and will filter back to Promise.all(). You could eat errors here if you want to ignore them and continue with others.
  7. Switch from var to let
  8. Change from != to !== since it looks like your explicitly looking for a true or false value and don't want type casting.
  9. Pass in the variables that saveValidity() needs.
  10. Change logic when comparing emailVerified because what you had before was always true and thus probably not the right logic. I think what you want is to know when emailVerified is not yet set to true or to false which means you have to use &&, not ||.
  11. Compare outer for loop with keys.length, not hard-coded value of 100.

And, here's the resulting code for the main nested for loop:

admin.database().ref("/users_unverified/").once('value').then(function(snapshot) {
    let snap = snapshot.val();
    let keys = Object.keys(snap);

    let promises = [];
    for (let i = 0; i < keys.length; i++){
        let key = keys[i];
        let emails = snap[key]["emails"];
        if (emails){
            for (let x = 0; x < emails.length; x++) {
                let currentKey = key;
                let thisEmail = emails[x]["email"];
                let emailVerified = emails[x]["verified"];
                if (emailVerified !== true && emailVerified !== false){
                    promises.push(verifyEmail(thisEmail).then(validity => {
                        return saveValidity(validity, currentKey, thisEmail);
                    }).then(function (fulfilled) {
                        console.log(fulfilled);
                        return fulfilled;      // after logging return value so it stays the resolved value
                    }).catch(function (error) {
                        console.log(error.message);
                        throw error;           // rethrow so promise stays rejected
                    }));
                }
            }
        }
    }
    return Promise.all(promises);
}).then(results => {
    // all results done here
}).catch(err => {
    // error here
});

Upvotes: 1

Related Questions