I have been trying to wrap my head around Promises. For basic concepts I understand, but once it gets nested, I am a little bit confused. Any feedback is appreciated
Here is the code that I am trying to refactor into Promises (bluebird)
var getIndividualData = function(url, doneGetIndividualData) {
var $, data;
request(url, function(err, res, body) {
if (!err && res.statusCode === 200) {
$ = cheerio.load(body);
data = {
title: $("#itemTitle").children()["0"].next.data,
condition: $("#vi-itm-cond").text(),
price: $("#prcIsum_bidPrice").text(),
imgUrl: $("#icImg")[0].attribs.src,
createdAt: chance.date(),
likes: chance.integer({min: 0, max: 1000})
};
doneGetIndividualData(null, data);
} else {
doneGetIndividualData(err);
}
});
};
var getListing = function(url, doneGetListing) {
var $;
var links = [];
request(url, function(err, res, body) {
if (!err && res.statusCode === 200) {
$ = cheerio.load(body);
$('.vip').each(function(i, el) {
if (i < 15) {
links.push(el.attribs.href);
}
});
async
.concat(links, getIndividualData, function(err, result) {
return doneGetListing(null, result);
});
} else {
doneGetListing(err);
}
});
};
var putToMongo = function(err, result) {
if (devConfig.seedDB) {
mongoose.connect(devConfig.mongo.uri);
Item.find({}).remove(function(err, items) {
Item.create(result, function(err, items) {
console.log('done');
process.kill();
});
});
}
};
async
.concat(urls, getListing, putToMongo);
The first thing to do is wrap request in something that returns a promise. Many promise libraries have utilities for "promisifying" async functions, but I don't think that'll work here because request passes two success values into its callback:
var requestAsync = function(url) {
return new Promise(function (resolve, reject) {
request(function (err, res, body) {
if (err) {
reject(err);
}
resolve({ res: res, body: body});
});
});
};
Once that's done, it gets a lot easier:
var getIndividualData = function(url) {
return requestAsync(url).then(function (result) {
if (result.res.statusCode === 200) {
var $ = cheerio.load(result.body);
return {
title: $("#itemTitle").children()["0"].next.data,
condition: $("#vi-itm-cond").text(),
price: $("#prcIsum_bidPrice").text(),
imgUrl: $("#icImg")[0].attribs.src,
createdAt: chance.date(),
likes: chance.integer({min: 0, max: 1000})
};
}
throw new Error("Individual data status code: " + result.res.statusCode);
});
};
var getListing = function(url, doneGetListing) {
return requestAsync(url).then(function (result) {
if (result.res.statusCode === 200) {
var $ = cheerio.load(result.body),
promises = $('.vip').filter(function (i) {
return i < 15;
}).map(function (i, el) {
return getIndividualData(el.attribs.href);
});
return Promise.all(promises);
}
throw new Error("Listing status code: " + result.res.statusCode);
});
};
var putToMongo = function(result) {
if (devConfig.seedDB) {
mongoose.connect(devConfig.mongo.uri);
Item.find({}).remove(function(err, items) {
Item.create(result, function(err, items) {
console.log('done');
process.kill();
});
});
}
};
Promise.all(urls.map(getListing))
.then(putToMongo)
.catch(function (err) {
// handle error
});
Related
I'm currently writing unit tests for an API. One of the helper functions is the async.waterfall to avoid "callback hell". I made this method async and await where the method is called.
The test is exiting saying Number of calls: 0 in Node.js. I'm using Jest as my test framework.
Method being tested:
outletInfoController.fetchAllOutletsByUserIdAndCityId = async function (req, res) {
var userId = req.body.userId;
var cityId = req.body.cityId;
await outletInfoHelper.fetchAllOutletsByUserIdAndCityId(userId, cityId, function (error, result) {
if (error || !result) {
responseUtils.buildAndRespond(commonConst.CODE.FAILURE_RESPONSE_CODE, commonConst.MESSAGE.FAILURE_RESPONSE_MESSAGE,
error, null, res);
} else {
responseUtils.buildAndRespond(commonConst.CODE.SUCCESS_RESPONSE_CODE, commonConst.MESSAGE.SUCCESS_RESPONSE_MESSAGE,
null, result, res);
}
});
};
Method which is using async.waterfall:
outletInfoHelper.fetchAllOutletsByUserIdAndCityId = async function (userId, cityId, outerCallback) {
async.waterfall(
[function getRestaurantsByUserId(callback) {
//do something
}, function getRestaurantsDetails(restIds, callback) {
//do something
}, function getRestaurantsUserCount(restaurants, callback) {
//do something
}], function (error, results) {
//do something
});
}
Test code snippet:
test("outletInfo_success_response", async () => {
let req = {
body: {
userId: 8881,
cityId: 211
}
};
let restIds = [];
await factory.restaurantListFactory((result) => {
restIds = result;
});
await fetchUserRestaurantObjFromUserId.mockImplementation((userId, cb) => {
cb(null, restIds);
});
let mockResponse = [];
await factory.restaurantObjectsFactory(function (result) {
mockResponse = result;
});
let successResp = {
statusMessage: 'success',
data: mockResponse,
errorMessage: null,
statusCode: 0
}
await fetchRestaurantsFromIdsAndCityId.mockImplementation((restIds, cityId, cb) => {
cb(null, mockResponse);
});
let response;
await factory.getUserRestMap((result) => {
response = result;
});
await fetchUsersFromOutletId.mockImplementation((restIds, cb) => {
cb(null, response);
});
let res = await getResponseObj();
await outletInfoController.fetchAllOutletsByUserIdAndCityId(req, res);
expect(res.send).toBeCalledWith(successResp);
});
Been trying everything to get this to work specifically the async each method. for (const element of resultsHistory) didn't work either.
I'm trying to modify the result array from a previous query by running a foreach over it and doing a mysql query.
However this needs to wait for the query to complete.
Is there a way to access these results without the setTimeout(function() { } I put manually in order to wait for the query to finish?
function getUserLikes(params, callback) {
var usersArrayCat = [];
console.log(`length of array ${params.length}`)
// 1 here means 1 request at a time
async.eachLimit(params, 1, function (element, cb) {
element.liked = 0;
var queryLiked = `SELECT * from users_likes WHERE user_id = \"${req.body.userid}\" AND product_id = \"${element.product_id}\"`;
connectionPromise.query(queryLiked, function (err, result) {
if (!result) {
} else if (result.length == 0) {
} else {
element.liked = result[0].userlike;
usersArrayCat.push(element);
// console.log(usersArrayCat);
cb();
}
})
}, function (err) {
if (err) return callback(err);
callback(null, usersArrayCat)
});
};
getUserLikes(resultsHistory, function (e) {
console.log(e);
});
if(!res.headersSent) {
setTimeout(function() {
res.send(JSON.stringify({"status": 200 ,"error": null, "top3":resultsHistory}));
}, 150);
}
Managed to get it working like this, but still not sure if this is the right way.
async function getUserLikes(resultsHistory) {
for (const element of resultsHistory) {
element.liked = 0;
let queryLiked = `SELECT * from users_likes WHERE user_id = \"${req.body.userid}\" AND product_id = \"${element.product_id}\"`;
let liked = await conn2.query(queryLiked);
if (liked[0]) {
element.liked = liked[0].userlike;
}
}
if(!res.headersSent) {
conn2.release();
let newres = resultsHistory.sort(
firstBy(function (v1, v2) { return v2.rating - v1.rating; })
.thenBy(function (v1, v2) { return v2.dranktimes - v1.dranktimes; })
).filter( function(history) {
return history.event === eventName;
}).slice(0, 3);
res.send({"status": 200 ,"error": null, "top3":newres});
}
}
getUserLikes(resultsHistory);
See my answer:
function getUserLikes(params, callback) {
var usersArrayCat = [];
console.log(`length of array ${params.length}`)
// 1 here means 1 request at a time
async.eachLimit(params, 1, function (element, cb) {
element.liked = 0;
var queryLiked = `SELECT * from users_likes WHERE user_id = \"${req.body.userid}\" AND product_id = \"${element.product_id}\"`;
connectionPromise.query(queryLiked, function (err, result) {
if (!result) {
} else if (result.length == 0) {
} else {
element.liked = result[0].userlike;
usersArrayCat.push(element);
// console.log(usersArrayCat);
}
cb();
})
}, function (err) {
if (err) return callback(err);
callback(null, usersArrayCat)
});
};
getUserLikes(resultsHistory, function (e, usersArrayCat) {
if (e) {
console.log(e);
return res.send({status: 400, error: e}); // your error response
}
console.log(usersArrayCat); // your usersArrayCat with liked property
res.send(JSON.stringify({ // why your need return a string intead of json object ???
"status": 200,
"error": null,
"top3": usersArrayCat // I think return `usersArrayCat` is a right way
}));
});
Let's say i need to constantly collecting some data from a lot of clients and in parallel running some complex loop that solving some stuff with this data. How can i do it? Should i just write this in my piece of code:
app.get('/', function(req, res) {
res.sendFile(__dirname + '/public/views/index0.html');
});
io.sockets.on('connection', function(socket) {
// SOME STUFF WITH THE SOCKET
socket.on('disconnect', function(data) {
//SOME OTHER STUFF
});
});
while(...) {
//THE LOOP STUFF
}
Or i need to use the setTimeout() and setInterval() functions? How can i do the loop on the server that runs in parallel with the callbacks' stuff?
Don’t use while for make it, this block a thread. setTimeout() will run only once. You need to use setInterval() function.
You can use the async module to handle the async operation with the callback or use promise to avoid callback.
Here how I handle a complex async for each operation, that might helpfull to you get idea handeling ayncs forach
var cond = { _schedule: schedule_id }; // find curse by schedule id
Course.find(cond, function (err, courses) {
if (err) {
callback({ "success": false, "message": "Not able to update" });
} else {
async.forEachLimit(courses, 1, function (course, coursesCallback) {
async.waterfall([
function (callback) {
var schedule_date = moment(change_data.date).format('YYYY-MM-DD') + "T00:00:00.000Z"
var Assignmentcond = {
assignment_schedule_order: {
$gte: schedule_date
},
_course: course._id,
_schedule: schedule_id,
_user: userid
};
Assignment.find(Assignmentcond)
.populate({
path: '_course',
})
.lean()
.sort({ assignment_schedule_order: 1 })
.exec(function (err, AssignmentList) {
if (err) {
callback(null, '');
} else {
//console.log("------------------AssignmentList---------------------------");
//console.log(AssignmentList);
async.forEachLimit(AssignmentList, 1, function (ThisAssignmentCell, ThisAssignmentCellCallback) {
async.waterfall([
function (callback) {
var SearchObj = items;
var lebelObject = {};
for (var i = 0, flag = 0, insert = 0; i < SearchObj.length; i++) {
if (SearchObj[i].date == ThisAssignmentCell.assignment_schedule_date) {
flag = 1;
}
if (flag == 1 && SearchObj[i].label != "") {
if (ThisAssignmentCell.day == SearchObj[i].day_index) {
insert = 1;
var lebelObject = SearchObj[i];
break;
}
}
}
callback(null, ThisAssignmentCell, lebelObject, insert);
},
function (ThisAssignmentCell, SearchObj, insert, callback) {
console.log('----------------------');
console.log('ThisAssignmentCell', ThisAssignmentCell);
console.log('SearchObj', SearchObj);
console.log('----------------------');
if (insert > 0) {
var query = { _id: ThisAssignmentCell._id },
fields = {
assignment_date: moment(SearchObj.date).format('MM/DD/YYYY'),
assignment_schedule_date: moment(SearchObj.date).format('YYYY-MM-DD'),
assignment_schedule_order: new Date(SearchObj.date),
day: SearchObj.day_index,
dayNum: SearchObj.weekday_num
},
options = { upsert: false };
Assignment.update(query, fields, options, function (err, affected) {
callback(null, '');
});
} else {
// var cond = { _id: ThisAssignmentCell._id};
// Assignment.remove(cond)
// .exec(function (err, cnt) {
// callback(null, '');
// });
}
}
], function (err, result) {
// result now equals 'done'
console.log('done')
ThisAssignmentCellCallback();
});
}, function (err) {
console.log("Assignment For Loop Completed");
callback(null, AssignmentList);
});
}
});
}
], function (err, result) {
// result now equals 'done'
console.log('done')
coursesCallback();
});
}, function (err) {
console.log("courses For Loop Completed");
});
}
});
I have read other related posts and still am not understanding correctly how to use promises.
router.get('/', ensureAuthenticated, function(req, res) {
let promiseToGetResponses = new Promise(function(resolve, reject) {
var indexData = new getIndexData();
resolve(indexData);
console.log('received ' + indexData.length);
});
promiseToGetResponses.then(function(data) {
console.log('then data length ' + data.length);
res.render('index', {rsvpsIn: data});
}).catch(function() { });
});
Console shows this:
received undefined
then data length undefined
returned 1 *** this is from a console.log inside the getIndexData().
The function is getting the data, but my promise usage is not waiting for it.
Thanks.
P.S. I didn't know the getIndexData function was needed. Here it is:
function getIndexData(){
RSVP.find({response: 'in'}, function (err, data) {
if (err) throw err;
// This will be a list of all responses to show in the view
var rsvpsIn = [];
if (data.length > 0) {
// Use because the foreach loop below has async calls.
var responseCounter = data.length;
data.forEach(function(response) {
var foundUser = User.getUserById(response.userId, function(err, user) {
var newRSVP = {userName: user.username, notes: response.notes};
rsvpsIn.push(newRSVP);
// decrement and if we are done, return list
responseCounter -= 1;
if (responseCounter == 0) {
console.log('returned ' + rsvpsIn.length);
return rsvpsIn;
}
});
});
} else {
return rsvpsIn;
}
});
}
Not familiar with RSVP but it appears to be callback based API, so you should wrap it in a promise and just use the promise directly:
function getIndexData(){
return new Promise((resolve, reject) => {
RSVP.find({response: 'in'}, function (err, data) {
if (err) reject(err);
// This will be a list of all responses to show in the view
var rsvpsIn = [];
if (data.length > 0) {
// Use because the foreach loop below has async calls.
var responseCounter = data.length;
data.forEach(function(response) {
var foundUser = User.getUserById(response.userId, function(err, user) {
var newRSVP = {userName: user.username, notes: response.notes};
rsvpsIn.push(newRSVP);
// decrement and if we are done, return list
responseCounter -= 1;
if (responseCounter == 0) {
console.log('returned ' + rsvpsIn.length);
resolve(rsvpsIn);
}
});
});
} else {
resolve(rsvpsIn);
}
});
});
}
then just use it where you use the promise:
router.get('/', ensureAuthenticated, function(req, res) {
getIndexData().then(function(data) {
console.log('then data length ' + data.length);
res.render('index', {rsvpsIn: data});
}).catch(function() { });
});
I am writing an service, where I retrieve a list of items from a another service, then iterate over result performing keystone.list operation(s).
I am loosing the return status in the find/exec operation. I have tried promises, async, etc.
If someone could point out the correct way to implement this, I would appreciate it.
general implementation:
exports = module.exports = function (req, res) {
var rtn = {
added: 0,
count: 0
}
service(params)
.then(function(svcResult) {
svcResult.forEach(function(item) {
rtn.count++; // <-- correctly seen in apiresponse
Artifact.model.find()
.where({ artifactId: item.id})
.exec(function(err, result) {
if (result.length == 0) {
result = new Artifact.model({
... populate from item ....
});
result.save();
rtn.added++; // <-- not seen in api response
});
});
res.apiResponse(rtn);
});
}
for starters, exec is an async call, which you are ignoring in your res.apiResponse and thus count is incremented and not added, to make life easy, I am moving the exec call outside and wrapping it with promise:
function pExec(id){
return new Promise(function(resolve, reject){
Artifact.model.find()
.where({ artifactId: id})
.exec(function(err, result){
console.log('result: ', result); // there is a possibility that this is not empty array, which seems to be the only case when you increment added value
err? reject(err): resolve(result);
});
});
}
exports = module.exports = function(req, res){ // I think it is 'exports' not 'exposts'
service(params)
.then(function(svcResult) {
var promises = svcResult.map(function(item){
rtn.count++;
return pExec(item.id).then(function(result){
if (result.length == 0) {
result = new Artifact.model({
//... populate from item ....
});
result.save(); // again this might be an async call whose response you might need before incrementing added...
rtn.added++; // <-- not seen in api response
};
});
});
Promise.all(promises).then(function(){
res.apiResponse(rtn);
});
});
}
Thanks... Here is what I have come up with so far....
function getArtifact(id) {
return new Promise(function (resolve, reject) {
Artifact.model.findOne()
.where({artifactId: id})
.exec(function (err, artifact) {
err ? resolve(null) : resolve(artifact);
});
});
}
function createArtifact(item) {
return new Promise(function (resolve, reject) {
var artifact = new Artifact.model({
// ... populate from item ....
});
artifact.save(function (err, artifact) {
err ? resolve(null) : resolve(artifact);
});
});
}
exports = module.exports = function (req, res) {
var rtn = {
success: false,
count: 0,
section: '',
globalLibrary: {
Added: 0,
Matched: 0
},
messages: [],
};
if (!req.user || !req.user._id) {
rtn.messages.push("Requires Authentication");
return res.apiResponse(rtn);
}
if (!req.params.section) {
rtn.messages.push("Invalid parameters");
return res.apiResponse(rtn);
}
var userId = req.user._id;
var section = req.params.section;
rtn.section = section;
service(section)
.then(function (svcResult) {
if (svcResult.length == 0 || svcResult.items.length == 0) {
rtn.messages.push("Retrieved empty collection");
return;
}
rtn.messages.push("Retrieved collection");
var artifacts = svcResult.items(function (item) {
rtn.count++;
return getArtifact(item.objectid)
.then(function (artifact) {
if (!artifact || artifact.length == 0) {
rtn.messages.push("Global Library Adding: " + item.name['$t']);
rtn.globalLibrary.Added++;
artifact = createArtifact(item);
} else {
rtn.globalLibrary.Matched++;
}
return artifact;
})
});
Promise.all(artifacts)
.then(function () {
rtn.success = true;
res.apiResponse(rtn);
});
});
}