How can I turn my javascript callback flow into a Promise? - javascript

function getMentionedUsers(str, next){
var array = getUsernamesFromString(str); //['john','alex','jess'];
if(array.length > 0){
var users = [];
var pending = array.length;
array.forEach(function(username){
getUserByUsername(username).then(function(model){
users.push(model.key);
--pending || next(users); //this is a callback model
});
});
}
};
function getUserByUsername(username){
return admin.database().ref('/users').orderByChild('username').equalTo(username).once('value').then(function(snapshot) {
return snapshot.val(); //this is the firebase example of a promise
});
};
Right now, I'm doing this:
getMentionedUsers(model.body, function(users){
console.log("Mentions", users);
});
However, I'd like to turn getMentionedUsers into a promise. How can I do that? I'm new to Promises

You could use Promise.all and Array#map:
function getMentionedUsers(str) {
return Promise.all(getUsernamesFromString(str).map((username) => {
return getUserByUsername(username).then((model) => model.key);
}));
}
A more readable version broken into two functions:
function getUserKeyByUsername(username) {
return getUserByUsername(username).then((user) => user.key);
}
function getMentionedUsers(str) {
const promises = getUsernamesFromString(str).map(getUserKeyByUsername);
return Promise.all(promises);
}

Use Promise.all.
const getMentionedUsers = str =>
Promise.all(
getUsernamesFromString(str).map(
username => getUserByUsername(username)
.then(model => model.key)
)
);
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

You can have the best of both. If you pass a next function, it will get called with the results. If not, your method will return a promise.
function getMentionedUsers(str, next){
var array = getUsernamesFromString(str); //['john','alex','jess'];
var promise = Promise.resolve([]); // default
var hasNext = typeof next === 'function';
if(array.length > 0){
promise = Promise.all(array.map(function(username){
return getUserByUsername(username);
}));
}
promise = promise.then(models => {
var users = models.map(model => model.key);
if (hasNext) next(null, users);
return users;
});
if (hasNext) promise.catch(next);
else return promise;
};
UPDATE: Though not part of your original question, this is still a good point and worth pointing out. Your existing code is using a non-standard callback technique. The standard callback technique expects an error as the first parameter and results as a second parameter:
next(new Error(...)); //-> when something fails
next(null, results); //-> when something succeeds
As such, I have updated my code to show the "standard" callback behavior alongside promises. Using the hybrid approach above allows for existing code to stay in place while allowing new code to use the new Promise technique. This would be considered a "non-breaking change".

using native ES6 promises, written in a functional style:
// Returns array of usernames
function getUsernamesFromString(str = '') {
return str.split(',').map(s => s.trim())
}
// returns promise of user
function getUserByUserName(username) {
// Lets say this is a slow async function and returns a promise
return Promise.resolve({
id: (Math.random() * 10 ** 10).toFixed(0),
username
});
}
function getMentionedUsers(str) {
return Promise.all(
getUsernamesFromString(str).map(getUserByUserName)
);
}
getMentionedUsers('kai, ava, mia, nova').then(console.log.bind(console))
However, there are also libraries like bluebird that can promisify objects and functions automatically as long as the follow the NODE convention of (err, result) as the callback arguments.
You can also just return a new Promise((resolve, reject) => { /* all your code */ }) and just call resolve(dataToResolveWith) if it succeeds and reject(new Error()) if it fails, but you rarely have to do that and in fact, it's an anti-pattern.

Related

Asyncronicity in a reduce() function WITHOUT using async/await

I am patching the exec() function to allow subpopulating in Mongoose, which is why I am not able to use async/await here -- my function will be chained off a db call, so there is no opportunity to call await on it, and within the submodule itself, there I can't add async/await outside of an async function itself.
With that out of the way, let's look at what I'm trying to do. I have two separate arrays (matchingMealPlanFoods and matchingMealPlanRecipeFoods) full of IDs that I need to populate. Both of them reside on the same array, foods. They each require a db call with aggregation, and the problem in my current scenario is that only one of the arrays populates because they are happening asynchronously.
What I am trying to do now is use the reduce function to return the updated foods array to the next run of reduce so that when the final result is returned, I can replace the entire foods array once on my doc. The problem of course is that my aggregate/exec has not yet returned a value by the time the reduce function goes into its next run. Is there a way I can achieve this without async/await here? I'm including the high-level structure here so you can see what needs to happen, and why using .then() is probably not viable.
EDIT: Updating code with async suggestion
function execute(model, docs, options, lean, cb) {
options = formatOptions(options);
let resolvedCount = 0;
let error = false;
(async () => {
for (let doc of docs) {
let newFoodsArray = [...doc.foods];
for (let option of options) {
const path = option.path.split(".");
// ... various things happen here to prep the data
const aggregationOptions = [
// // $match, then $unwind, then $replaceRoot
];
await rootRefModel
.aggregate(aggregationOptions)
.exec((err, refSubDocuments) => {
// more stuff happens
console.log('newFoodsArray', newFoodsArray); // this is to check whether the second iteration is using the updated newFoods Array
const arrToReturn = newFoodsArray.map((food) => {
const newMatchingArray = food[nests[1]].map((matchingFood) => {
//more stuff
return matchingFood;
});
const updatedFood = food;
updatedFood[`${nests[1]}`] = newMatchingArray;
return updatedFood;
});
console.log('arrToReturn', arrToReturn);
newFoodsArray = [...arrToReturn];
});
}
};
console.log('finalNewFoods', newFoodsArray); // this should log after the other two, but it is logging first.
const document = doc.toObject();
document.foods = newFoodsArray;
if (resolvedCount === options.length) cb(null, [document]);
}
})()
EDIT: Since it seems it will help, here is the what is calling the execute function I have excerpted above.
/**
* This will populate sub refs
* #param {import('mongoose').ModelPopulateOptions[]|
* import('mongoose').ModelPopulateOptions|String[]|String} options
* #returns {Promise}
*/
schema.methods.subPopulate = function (options = null) {
const model = this.constructor;
if (options) {
return new Promise((resolve, reject) => execute(model, [this], options, false, (err, docs) => {
if (err) return reject(err);
return resolve(docs[0]);
}));
}
Promise.resolve();
};
};
We can use async/await just fine here, as long as we remember that async is the same as "returning a Promise" and await is the same as "resolving a Promise's .then or .catch".
So let's turn all those "synchronous but callback-based" calls into awaitables: your outer code has to keep obeying the API contract, but since it's not meant to a return a value, we can safely mark our own version of it as async, and then we can use await in combination with promises around any other callback based function calls in our own code just fine:
async function execute(model, docs, options, lean, andThenContinueToThis) {
options = formatOptions(options);
let option, resolvedCount = 0;
for (let doc of docs) {
let newFoodsArray = [...doc.foods];
for (option of options) {
// ...things happen here...
const aggregationOptions = [/*...data...*/];
try {
const refSubDocuments = await new Promise((resolve, reject) => rootRefModel
.aggregate(aggregationOptions)
.exec((err, result) => err ? reject(err) : resolve(result));
// ...do some work based on refSubDocuments...
}
// remember to forward errors and then stop:
catch (err) {
return andThenContinueToThis(err);
}
}
// remember: bind newFoodsArray somewhere so it doesn't get lost next iteration
}
// As our absolutely last action, when all went well, we trigger the call forwarding:
andThenContinueToThis(null, dataToForward);
}

How to wait for the iteration to finish when pushing result of a callback function into an array

What is the correct way to implement array.push so that it "array_of_results" is returned after the forEach iteration if finished?
const postgres = require("./postgres");
function get_array(value) {
var array_of_results = []
value.forEach( item => {
postgres.query(item["id"],function(res){
console.log(res) //gives proper res after empty array
array_of_results.push(res);
})
});
console.log(array_of_results)// prints empty array
return array_of_results;
}
Edit:
and postgres.js looks like :
const { Pool } = require("pg");
const pool = new Pool();
var query_string = "select...."
function query(id, call) {
pool.query(query_string, [id], (err, res) => {
if (err) {
console.log(err.stack)
} else {
call(res.rows[0])
}
})
}
module.exports = {
query
}
There are a few ways to do this, but first you need to understand what is actually happening.
In postgres.query(item["id"],function(res){ you are calling postgres.query with (1) an item ID and (2) a callback function. That call happens and then immediately continues in your calling code. So now you've just sent a bunch of requests to your database, and then immediately return an empty array. Those callbacks (2) have not been called yet.
To get the data back to your calling function, you'll need to either pass a callback instead of using return, or change to async/await.
Using async/await in every iteration of your loop is not as efficient, as you're waiting for each call to return sequentially. For the most efficient method, you will need to fire the requests and wait for them all to complete. You can do this by using promises.
You can modify your code to push a promise into an array for each iteration of the loop, then call (and await) Promise.all on the array of promises.
Here's a basic rewrite for you:
postgres.js:
function query(id) {
return new Promise((resolve, reject) => {
pool.query(query_string, [id], (err, res) => {
if (err) {
console.log(err.stack)
reject(err)
} else {
resolve(res.rows[0])
}
})
})
}
module.exports = {
query
}
get_array implementation :
async function get_array(value) {
var array_of_promises = [], array_of_results = []
value.forEach( item => {
array_of_promises.push(postgres.query(item["id"]));
});
array_of_results = await Promise.all(array_of_promises);
console.log(array_of_results)// prints populated array
return array_of_results;
}
Note that when you call get_array you'll have to use await before the call, e.g. change var array = get_array(items) to var array = await get_array(items) and using await in a function requires it to be declared as an async function.
If you can't declare it as an async function, you may change the calling code to consume the promise:
var arrayPromise = get_array(items);
arrayPromise.then((results) => {
// do something with results
// but remember you cannot _return_ from within a callback, as discussed above
});

Sequential execution of Promise.all

Hi I need to execute promises one after the other how do I achieve this using promise.all any help would be awesome. Below is the sample of my code I am currently using but it executes parallel so the search will not work properly
public testData: any = (req, res) => {
// This method is called first via API and then promise is triggerd
var body = req.body;
// set up data eg 2 is repeated twice so insert 2, 5 only once into DB
// Assuming we cant control the data and also maybe 3 maybe inside the DB
let arrayOfData = [1,2,3,2,4,5,5];
const promises = arrayOfData.map(this.searchAndInsert.bind(this));
Promise.all(promises)
.then((results) => {
// we only get here if ALL promises fulfill
console.log('Success', results);
res.status(200).json({ "status": 1, "message": "Success data" });
})
.catch((err) => {
// Will catch failure of first failed promise
console.log('Failed:', err);
res.status(200).json({ "status": 0, "message": "Failed data" });
});
}
public searchAndInsert: any = (data) => {
// There are database operations happening here like searching for other
// entries in the JSON and inserting to DB
console.log('Searching and updating', data);
return new Promise((resolve, reject) => {
// This is not an other function its just written her to make code readable
if(dataExistsInDB(data) == true){
resolve(data);
} else {
// This is not an other function its just written her to make code readable
insertIntoDB(data).then() => resolve(data);
}
});
}
I looked up in google and saw the reduce will help I would appreciate any help on how to convert this to reduce or any method you suggest (Concurrency in .map did not work)
the Promises unfortunatelly does not allow any control of their flow. It means -> once you create new Promise, it will be doing its asynchronous parts as they like.
The Promise.all does not change it, its only purpose is that it checks all promises that you put into it and it is resolved once all of them are finished (or one of them fail).
To be able to create and control asynchronous flow, the easiest way is to wrap the creation of Promise into function and create some kind of factory method. Then instead of creating all promises upfront, you just create only one promise when you need it, wait until it is resolved and after it continue in same behaviour.
async function doAllSequentually(fnPromiseArr) {
for (let i=0; i < fnPromiseArr.length; i++) {
const val = await fnPromiseArr[i]();
console.log(val);
}
}
function createFnPromise(val) {
return () => new Promise(resolve => resolve(val));
}
const arr = [];
for (let j=0; j < 10; j++) {
arr.push(createFnPromise(Math.random()));
}
doAllSequentually(arr).then(() => console.log('finished'));
PS: It is also possible without async/await using standard promise-chains, but it requires to be implemented with recursion.
If anyone else cares about ESLint complaining about the use of "for" and the "no await in loop" here is a typescript ESLint friendly version of the above answer:
async function runPromisesSequentially<T>(promises: Array<Promise<T>>):Promise<Array<T>> {
if (promises.length === 0) return [];
const [firstElement, ...rest] = promises;
return [await firstElement, ...(await runPromisesSequentially(rest))];
}
You can then just replace Promise.all by runPromisesSequentially.
#lmX2015's answer is close but it's taking in promises that have already started executing.
A slight modification fixes it
export async function runPromisesSequentially<T>(functions: (() => Promise<T>)[]): Promise<T[]> {
if (functions.length === 0) {
return [];
}
const [first, ...rest] = functions;
return [await first(), ...(await runPromisesSequentially(rest))];
}

Async await with a forEach loop still running asynchronously [duplicate]

This question already has answers here:
Using async/await with a forEach loop
(33 answers)
Closed 4 years ago.
First of all I did read through similar questions, and still cannot see where I'm making my mistake.
Here's my code:
async function validateWebsites(website) {
var result = url.parse(`http://${website}`);
console.log(result.hostname);
return await fetch(`http://www.${result.hostname}`)
.then(() => console.log(true))
.catch(() => console.log(false));
}
var wrongWebsites = [];
var wrongWebsites = [];
var i = 0;
websites.forEach(website => {
i++;
if (validateWebsites(website) === false
) {
wrongWebsites.push(i);
}
});
console.log(wrongWebsites);
How it works:
The user passes an array of websites, and I want to validate if they're valid websites, not to waste resources and block other errors. Now to the console:
digitlead.com
google.com
georgiancollege.ca
youtube.com
[]
true
true
true
true
So as you see, it prints out first the websites array, and then the response. So it's still async. How do I make it wait? I changed the loop from a for to forEach as suggested by many posts, I used the await and I am returning a promise. So what else do I have to do?
Edit:
I tried to do this:
async function validateWebsites(website) {
var result = url.parse(`http://${website}`); // TODO figure out if filtering all the subpages is a good idea.
console.log(result.hostname);
return await fetch(`http://www.${result.hostname}`)
.then(()=>console.log(true))
.catch(()=>console.log(false));
}
But it doesn't change anything
I found a function called readFileSync. That's more or less what I'm looking for, but with the ability to call a different website.
Here is how you can get all valid websites.
Problem with your validateWebsites function is that it returns promise wchih is resolving to undefined thanks to promise chaining and your loging
Also using forEach to filter array is unnesesery.
But if you wanted you could do something like this
websites.forEach(async website => {
i++;
if (await validateWebsites(website) === false) { // now value is Boolean instead of Promise
wrongWebsites.push(i);
}
});
Also note that if you use global i with asyncronous functions to keep track of index this can lead to many errors.
However I think this soultion should satisfy you
async function validateWebsites(website) {
var result = url.parse(`http://${website}`)
return fetch(`http://www.${result.hostname}`)
.then(() => true) // async function returns promise
.catch(() => false)
}
const websites = ['digitlead.com',
'google.com',
'georgiancollege.ca',
'youtube.com',
'111.1',
'foobarbaz']
async function filter(array, func) {
const tmp = await Promise.all( // waits for all promises to resolve
array.map(func) // evecutes async function and stores it result in new array then returns array of promises
)
return array.filter((_, i) => tmp[i]) // removes invalid websites
}
const validWebsites = filter(websites, validateWebsites)
validWebsites.then(console.log)
Get the indexes of the non-valid sites
async function filter(array, func) {
const tmp = await Promise.all(array.map(func))
return tmp
.map((x, i) => !x && i) // flip true to false and asign index when x is false
.filter(x => x !== false) // return indexes
}
destoryeris saying you should do something like this:
websites.forEach(async website => {
i++;
if (await validateWebsites(website) === false
) {
wrongWebsites.push(i);
}
});
But that alone is problematic because you have wrap your async functions in a try/catch to handle their errors. So something more like this:
websites.forEach(async website => {
i++;
try {
const validSites = await validateWebsites(website);
if (validSites === false) {
wrongWebsites.push(i);
}
} catch(e) {
// handle e
}
})

getting object Parse from a loop

In a Parse server function, it's getting Matches and profiles.
From a query to get matches another function is called to get Profiles by id but the result is :
{"_resolved":false,"_rejected":false,"_reso resolvedCallbacks":[],"_rejectedCallbacks":[]}
Main Query :
mainQuery.find().then(function(matches) {
_.each(matches, function(match) {
// Clear the current users profile, no need to return that over the network, and clean the Profile
if(match.get('uid1') === user.id) {
match.set('profile2', _processProfile(match.get('profile2')))
match.unset('profile1')
}
else if (match.get('uid2') === user.id) {
var profileMatch = _getProfile(match.get('profile1').id);
alert(">>>"+JSON.stringify(profileMatch));
match.set('profile1', _processProfile(match.get('profile1')))
match.unset('profile2')
}
})
the function to get Profile info:
function _getProfile(id){
var promise = new Parse.Promise();
Parse.Cloud.useMasterKey();
var queryProfile = new Parse.Query(Profile);
return queryProfile.equalTo("objectId",id).find()
.then(function(result){
if(result){
promise.resolve(result);
alert("!!!!"+result);
}
else {
console.log("Profile ID: " + id + " was not found");
promise.resolve(null);
}
},
function(error){
promise.reject(error)
});
return promise;
}
Just found this a little late. You've probably moved on, but for future readers: the key to solving something like this is to use promises as returns from small, logical asynch (or sometimes asynch, as in your case) operations.
The whole _getProfile function can be restated as:
function _getProfile(id){
Parse.Cloud.useMasterKey();
var queryProfile = new Parse.Query(Profile);
return queryProfile.get(id);
}
Since it returns a promise, though, you cannot call it like this:
var myProfileObject = _getProfile("abc123");
// use result here
Instead, call it like this:
_getProfile("abc123").then(function(myProfileObject) { // use result here });
Knowing that, we need to rework the loop that calls this function. The key idea is that, since the loop sometimes produces promises, we'll need to let those promises resolve at the end.
// return a promise to change all of the passed matches' profile attributes
function updateMatchesProfiles(matches) {
// setup mainQuery
mainQuery.find().then(function(matches) {
var promises = _.map(matches, function(match) {
// Clear the current users profile, no need to return that over the network, and clean the Profile
if(match.get('uid1') === user.id) {
match.set('profile2', _processProfile(match.get('profile2'))); // assuming _processProfile is a synchronous function!!
match.unset('profile1');
return match;
} else if (match.get('uid2') === user.id) {
var profileId = match.get('profile1').id;
return _getProfile(profileId).then(function(profileMatch) {
alert(">>>"+JSON.stringify(profileMatch));
match.set('profile1', _processProfile(match.get('profile1')))
match.unset('profile2');
return match;
});
}
});
// return a promise that is fulfilled when all of the loop promises have been
return Parse.Promise.when(promises);
}

Categories

Resources