How to chain promises within nested for loops? - javascript

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.

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:
I assume we can run all the verifyEmail() calls in parallel since they don't appear to have anything to do with one another.
Change verifyEmail.then(...) to verifyEmail(thisEmail).then(...)` to actually call the function
Collect all the verifyEmail() promises in an array
Call Promise.all() on the array of promises to monitor when they are all done
Return value from .then() so we get the returned values in Promise.all()
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.
Switch from var to let
Change from != to !== since it looks like your explicitly looking for a true or false value and don't want type casting.
Pass in the variables that saveValidity() needs.
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 ||.
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
});

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)();

Related

Twilio Method inside of promise does not work

Reading
https://www.twilio.com/blog/implementing-programmable-chat-php-laravel-vue-js
I try implement chat in my Laravel 8 / jQuery 3.5.1 / vue 2.6 app.
This docs has defined :
setupChannel(channel){
let vm = this;
return this.leaveCurrentChannel()
.then(function() {
return vm.initChannel(channel);
})
.then(function(_channel) {
return vm.joinChannel(_channel);
})
.then(this.initChannelEvents);
},
I want to extend joinChannel method, as I want to make checks if current logged user (laravel auth)
is already joined. I try to make it with promise and failes, as code inside of
vm.tc.messagingClient.getSubscribedUsers() is not run. I do
setupChannel(channel){
let vm = this;
return this.leaveCurrentChannel()
.then(function() {
return vm.initChannel(channel);
})
.then(function(_channel) {
let promise = new Promise((resolve, reject) => {
// debugger
vm.tc.messagingClient.getSubscribedUsers().then(function(users) {
// THESE CODE IS NOT RUN. If to uncomment console and debugging it is not triggered
// console.log('++ users::')
// console.log(users)
// debugger
for (let i = 0; i < users.length; i++) {
const user = users[i];
console.log('user.identity: ' + JSON.stringify(user.identity) );
// console.log('user: ' + JSON.stringify(user, null, 2) );
if( user.identity === vm.loggedUser.name ) {
resolve("result")
}
}
debugger // THESE CODE IS NOT RUN
resolve("error")
})
})
console.log('++ promise::')
console.log(promise) // I SEE this promise in pending state
promise
.then(
result => {
alert("result: " + result);
return _channel;
},
error => {
alert("error: " + error);
return vm.joinChannel(_channel);
}
)
// return vm.joinChannel(_channel);
})
.then(this.initChannelEvents);
If to run code
vm.tc.messagingClient.getSubscribedUsers().then(function(users)
...
inside of promise, it works ok and I got valid results.
What is wrong in my promise structure and how can I fix it?
MODIFIED BLOCK:
I try to follow your way with :
joinGeneralChannel() {
console.log('Attempting to join "general" chat channel...');
let vm = this;
if (this.tc.generalChannel == null) {
console.log('SETTING this.tc.messagingClient.createChannel')
vm.loadChannelList(vm.joinGeneralChannel);
}else {
// console.log('Found general channel:');
this.setupChannel(this.tc.generalChannel);
}
},
async setupChannel(channel) {
let vm = this
await this.leaveCurrentChannel()
const newChannel = await vm.initChannel(channel)
const subscribedUsers = vm.tc.messagingClient.getSubscribedUsers()
console.log('subscribedUsers::')
console.log(subscribedUsers)
let isUserJoined = false
for (let i = 0; i < subscribedUsers.length; i++) {
console.log('subscribedUsers[i] ' + JSON.stringify(subscribedUsers[i]) );
if( subscribedUsers[i].name === vm.loggedUser.name ) {
isUserJoined = true``
break
}
}
debugger
console.log('isUserJoined::')
console.log(isUserJoined)
But in the cosole of my browser I see :
Initialized channel General Channel
TeamChat.vue?e1c8:561 subscribedUsers::
TeamChat.vue?e1c8:562 PromiseĀ {<pending>}__proto__: Promise[[PromiseState]]: "pending"[[PromiseResult]]: undefined
TeamChat.vue?e1c8:573 isUserJoined::
looks like method getSubscribedUsers is asynchronous ?
Thanks!
Probably your Promise fails, that's why then() will never execute. To extend joinChannel method you can do something like this with async/await and ES6 syntax:
async setupChannel(channel) {
let vm = this;
try {
await this.leaveCurrentChannel();
const newChannel = await vm.initChannel(channel);
const users = await vm.tc.messagingClient.getSubscribedUsers();
const isUserJoined = users.some(({ name }) => name === vm.loggedUser.name);
const joinedChannel = isUserJoined ? newChannel : vm.joinChannel(_channel);
return this.initChannelEvents(joinedChannel);
} catch(err) {
// if some of promises below will fail, here you'll see details
console.log('Issue details here:', err);
}
}

How to stop a js promise.all() on a condition?

I have this piece of code
let promiseList = []
for (let i in data) {
let promise = checkIfMember(data[i].tg_id, chatId).then(res => {
if (res) {
//if the user has a undefined username it won't be rained
if (data[i].username != "undefined") {
console.log(data[i].username)
members.push([data[i].tg_id, data[i].username])
}
}
}).catch(err => {
console.log(err)
})
promiseList.push(promise)
}
return Promise.all(promiseList).then((res) => {
//list of members that are in the room , we randomize it before returning
shuffleArray(members)
if(numberOfUsers > members.length){
return[false, members.length]
}else{
members = members.slice(0,numberOfUsers)
return [true, members]
}
});
Basically I have a promise list that is being filled. And then all of them are executed with a promiseAll. The problem is that I dont want all of them to execute, I want to to do something like this:
let promiseList = []
let validmembers = 0;
for (let i in data) {
let promise = checkIfMember(data[i].tg_id, chatId).then(res => {
if (res) {
//if the user has a undefined username it won't be rained
if (data[i].username != "undefined") {
console.log(data[i].username)
members.push([data[i].tg_id, data[i].username])
//stop there the execution
validmembers++;
if(validmembers == numberOfUsers){
return;
}
}
}
}).catch(err => {
console.log(err)
})
promiseList.push(promise)
}
return Promise.all(promiseList).then((res) => {
//list of members that are in the room , we randomize it before returning
shuffleArray(members)
if(numberOfUsers > members.length){
return[false, members.length]
}else{
members = members.slice(0,numberOfUsers)
return [true, members]
}
});
But the problem is that the promises are async, so the promiselist filling doesnt stop.
How would I solve this?
It appears you want the members array to have no more than numberOfUsers entries in it due to the promises. Since the promises run asynchronously you can't stop the checkIfMember function from being called, but inside of its then function you could avoid accumulating more members like this:
// don't use `!== "undefined"` unless it can actually be the string "undefined"
if (data[i].username) {
if (members.length < numberOfUsers) {
console.log(data[i].username);
members.push([data[i].tg_id, data[i].username]);
}
}
By only pushing to members when it's not yet full you will avoid adding more than numberOfUsers and don't need to do the slice later.
If instead you want to avoid the entire promise work when enough have been collected, then you can serialize them like this:
async function yourFunction() {
for (let i in data) {
if (members.length < numberOfUsers) {
// don't continue to the next 'i' until this one is done
await checkIfMember(data[i].tg_id, chatId)
.then(res => {
if (res) {
//if the user has a undefined username it won't be rained
if (data[i].username != "undefined") {
console.log(data[i].username);
members.push([data[i].tg_id, data[i].username]);
}
}
})
.catch(err => {
console.log(err);
});
} else {
break; // no need to continue once members is full
}
}
return [true, members];
}

applying a promise to getCookies

I am trying to apply a promise to getting cookies from the browser
browserCookies = {
art_rfp : '',
art_token : ''
};
var promise = new Promise((resolve, reject) => {
chrome.cookies.getAll({"url":"https://url.com"}, function (cookies) {
for(var i=0; i<cookies.length; i++){
var name = cookies[i].name
// console.log(name)
if (name == 'sso_rfp') {
console.log(name) // line 13
browserCookies.art_rfp = cookies[i].value
resolve('cookies found')
}
else if (name == 'sso_token') {
console.log(name) // line 18
browserCookies.art_token = cookies[i].value
}
else {
reject('no cookies found')
}
}
});
})
promise.then((message) => {
console.log(message)
}).catch((message) =>{
console.log(message)
})
However it is just failing.
background.js:13 sso_rfp
background.js:18 amzn_sso_token
background.js:32 no cookies found
why isn't it resolving?
Promises can only be rejected / resolved once.
So what's likely happening inside your loop, the first cookie's name is neither sso_rfp or sso_token as such it will call reject('no cookies found'), so even if more are found later, they cannot get resolved because the reject has already been called.
So what you want to do is keep a track using a simple boolean wherever a cookie was found or not found, and then resolve / reject at the end.
eg..
var promise = new Promise((resolve, reject) => {
chrome.cookies.getAll({"url":"https://url.com"}, function (cookies) {
var found = false;
for(var i=0; i<cookies.length; i++){
var name = cookies[i].name
// console.log(name)
if (name == 'sso_rfp') {
console.log(name) // line 13
browserCookies.art_rfp = cookies[i].value
found = true;
}
else if (name == 'sso_token') {
console.log(name) // line 18
browserCookies.art_token = cookies[i].value;
found = true;
}
}
if (found) resolve("Cookies Found");
else reject("no cookies found");
});
})
Also just a coding standard, I would personally avoid using a global browserCookies, and instead resolve with these values instead.

Can we make promise to wait until resolved and onreject call back the promise again

I'm learning about Promise's and have a little doubt assuming that I want to get resolved status out of Promises
and not want reject! Can I just call back the promise function inside
catch to make sure that I get only approved value! Is that possible or
will it throw an error or goes to loop iteration
let promisetocleantheroom = new Promise(function cleanroom(resolve, reject) {
//code to clean the room
//then as a result the clean variable will have true or flase
if (clean == "true") {
resolve("cleaned");
} else {
reject("not cleaned");
}
});
promisetocleantheroom.then(function cleanroom(fromResolve) {
// wait for the function to finish only then it would run the function then
console.log("the room is " + fromResolve);
}).catch(function cleanroom(fromReject) {
//calling back the promise again
cleanroom();
});
If you don't mind having higher order functions and recursivity, here is my proposed solution.
First you need to wrap your promise in a function to recreate it when it fails. Then you can pass it to retryPromiseMaker with a partial error handler to create another function that will act as retrier. And this function will return a Promise that will fulfill only if one of the inner promises fulfills.
Sounds complicated but I promise you it is not!
const retryPromiseMaker = (fn, errorfn = null) => {
const retryPromise = (retries = 3, err = null) => {
if (err) {
errorfn(err);
}
if (retries === 0) {
return Promise.reject(err);
}
return fn()
.catch(err => retryPromise(retries - 1, err));
};
return retryPromise;
}
const cleanTheRoom = (resolve, reject) => {
// simulate cleaning as a probability of 33%
const clean = Math.random() < 0.33;
setTimeout(() => {
if (clean) {
resolve("cleaned");
} else {
reject("not cleaned");
}
}, Math.random() * 700 + 200);
};
const promiseToCleanTheRoom = () => new Promise(cleanTheRoom);
const logStatus = end => value => {
let text = '';
if (end){
text += "at the end ";
}
text += "the room is " + value;
console.log(text);
};
retryPromiseMaker(promiseToCleanTheRoom, logStatus(false))(4)
.then(logStatus(true),logStatus(true));

Using Promises to defer continuation within a forEach loop

My goal with the below is to:
Capture a list of resolutions
Scan through each of them (in order) to find the first one that results in a successful stream
To test this, I have testVideoPresence:
var testCounter = 0;
function testVideoPresence(videoElement) {
testCounter++;
if (testCounter >= 5) {
testCounter = 0;
return false;
}
if (!videoElement.videoWidth || videoElement.videoWidth < 10) { // check to prevent 2x2 issue
setTimeout(function() {
testVideoPresence(videoElement); // try again
}, 500);
} else if (video.videoWidth * video.videoHeight > 0) {
return true;
}
}
As you can see, I'm using a setTimeout to recurse at most 5 times. This is where things get tricky:
resolutionTestBuilder.buildTests().then(function (resolutionTests) {
// at this point, I have a set of resolutions that I want to try
resolutionTests.forEach(function (resolutionTest) {
// then I want to iterate over all of them until I find one that works
performTest(resolutionTest).then(function (result) {
video.srcObject = result.mediaStream; // start streaming to dom
if (testVideoPresence(video)) { // here is the pain point - how do I await the result of this within the forEach?
// return the dimensions
} else {
// continue scanning
}
}).catch(function (error) {
logger.internalLog(error);
});
// wait to continue until we have our result
});
}).catch(function (error) {
logger.internalLog(error);
});
function performTest(currentTest) {
return streamHelper.openStream(currentTest.device, currentTest.resolution).then(function(streamData) {
return streamData;
}).catch(function (error) {
logger.internalLog(error);
});;
};
streamHelper.openStream = function (device, resolution) {
var constraints = createVideoConstraints(device, resolution);
logger.internalLog("openStream:" + resolution.label + ": " + resolution.width + "x" + resolution.height);
return navigator.mediaDevices.getUserMedia(constraints)
.then(function (mediaStream) {
streamHelper.activeStream = mediaStream;
return { stream: mediaStream, resolution: resolution, constraints: constraints };
// video.srcObject = mediaStream; // push mediaStream into target element. This triggers doScan.
})
.catch(function (error) {
if (error.name == "NotAllowedError") {
return error.name;
} else {
return error;
}
});
};
I'm trying to wait for the result within the forEach before continuing through the array of resolutions. I know I can use some advanced techniques like async/await if I want to transpile - but I'm stuck with vanilla JS and promises / bluebird.js for now. What are my options? Disclaimer - I am new to promises so the above code could be very malformed.
Update:
Tests are defined in order of importance - so I do need resolutionTests[0] to resolve before resolutionTests[1].
If the order of trials isn't important, you can simply use a map combined with Promise.race to make sure the first promise of a list that resolves resolves the whole list. You also need to make sure your promises return other promises inside then.
resolutionTestBuilder.buildTests().then(function (resolutionTests) {
return Promise.race(resolutionTests.map(function (resolutionTest) {
return performTest(resolutionTest).then(function (result) {
video.srcObject = result.mediaStream; // start streaming to dom
return testVideoPresence(video);
}).catch(function (error) {
logger.internalLog(error);
});
}));
}).catch(function (error) {
logger.internalLog(error);
});
This of course assumes that testVideoPresence does NOT resolve when you the dimensions are not available.
If the order of trial is important then a reduce approach might work.
This will basically result in a sequential application of the promises and the resulting promise will until all of them are resolved.
However, once the solution is found we attach it to the collector of the reduce so that further trials simply return that as well and avoid further tests (because by the time this is found the chain is already registered)
return resolutionTests.reduce(function(result, resolutionTest) {
var nextPromise = result.intermPromise.then(function() {
if (result.found) { // result will contain found whenver the first promise that resolves finds this
return Promise.resolve(result.found); // this simply makes sure that the promises registered after a result found will return it as well
} else {
return performTest(resolutionTest).then(function (result) {
video.srcObject = result.mediaStream; // start streaming to dom
return testVideoPresence(video).then(function(something) {
result.found = something;
return result.found;
});
}).catch(function (error) {
logger.internalLog(error);
});
}
);
return { intermPromise: nextPromise, found: result.found };
}, { intermPromise: Promise.resolve() }); // start reduce with a result with no 'found' and a unit Promise
At first , your testVideoPresence returns undefined. It wont work that way. May do:
function testVideoPresence(videoElement,callback,counter=0) {
if(counter>10) callback(false);
if (!videoElement.videoWidth || videoElement.videoWidth < 10) {
setTimeout(testVideoPresence, 500,videoElement,callback,counter+1);
} else if (video.videoWidth * video.videoHeight > 0) {
callback(true);
}
}
SO you can do:
testVideoPresence(el, console.log);
Now to the forEach. You cannot yield the forEach in any way. However you could write your own recursive forEach:
(function forEach(el,index) {
if(index>=el.length) return false;
performTest(el[index]).then(function (result) {
video.srcObject = result.mediaStream; // start streaming to dom
testVideoPresence(video,function(success){
if(!success) return alert("NOO!");
//do sth
//proceed
setTimeout(forEach,0,el,index+1);
});
}).catch(function (error) {
logger.internalLog(error);
});
})(resolutionTests,0);//start with our element at index 0
function raceSequential(fns) {
if(!fns.length) {
return Promise.resolve();
}
return fns.slice(1)
.reduce(function(p, c) {
return p.catch(c);
}, fns[0]());
}
// "Resolution tests"
var t1 = function() { return new Promise(function(_, reject) { setTimeout(() => reject('t1'), 1000); })};
var t2 = function() { return new Promise(function(resolve) { setTimeout(() => resolve('t2'), 1000); })};
var t3 = function() { return new Promise(function(resolve) { setTimeout(() => resolve('t3'), 1000); })};
var prom = raceSequential([t1, t2, t3])
.then(function(result) { console.log('first successful result: ' + result); });
Scanning your code indicates you have other async-related problems.

Categories

Resources