How can I turn a javascript callback thread into a Promise?
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
});
};
I am doing this now:
getMentionedUsers(model.body, function(users){
console.log("Mentions", users);
});
However, I would like to turn getMentionedUsers
into a promise. How can i do this? I am new to Promises
source to share
You can use Promise.all
and Array#map
:
function getMentionedUsers(str) {
return Promise.all(getUsernamesFromString(str).map((username) => {
return getUserByUsername(username).then((model) => model.key);
}));
}
The more readable version is split 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);
}
source to share
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
source to share
You can get the best out of both. If you pass a function next
, it will be 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: While not part of your original question, it is still a good point and worth noting. The existing code uses a non-standard callback technology. The standard callback method assumes an error as the first parameter and the result as the second parameter:
next(new Error(...)); //-> when something fails
next(null, results); //-> when something succeeds
So I updated my code to show the "standard" callback behavior along with promises. Using the above hybrid approach allows existing code to be leveraged by allowing new code to use the new Promise method. This will be considered an "unchanged change".
source to share
using native ES6 promises written in 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, such as bluebird , that can automatically both encode objects and functions if you follow the NODE convention (err, result)
as callback arguments.
You can also just return new Promise((resolve, reject) => { /* all your code */ })
and just call resolve(dataToResolveWith)
if it succeeds, and reject(new Error())
if it doesn't work, but you rarely have to do that and is actually an anti-pattern.
source to share