I want my while loop to match 2 conditions in order to break the loop - res has to NOT be undefined, which is the case only when status code is 200, and obj.owner has to be equal to some value i set. It takes a couple of seconds for the owner on that page to update, so i need the while loop to run until the owner updates.
Here's the request function
const getOwner = () => {
const opts = {
url: 'https://example.com',
json: true,
simple: false,
resolveWithFullResponse: true
}
return request(opts)
.then(res => {
if (res.statusCode == 200) {
const obj = {
owner: res.body.owner
}
return obj
} else {
console.error(`Error: ${res.statusCode}`)
}
})
.catch(e => {
console.error(`Panic: ` + e.message)
})
}
And here's the while loop
let owner = await getOwner()
while (!owner && owner.owner != 'some value') {
owner = await getOwner()
}
console.log('Loop broken')
When I console.log(!owner, (owner.owner != 'some value')) after the first function call, before trying to enter into the loop, it returns false for the first condition, and true for the second.
When status code is other than 200, it doesnt return the object, so i need those 2 conditions together, as otherwise it crashes the program due to owner being undefined.
Now, when i execute this, it always skips the loop no matter what, im really lost at this point.
In my opinion, it's a bad idea to loop through a resource until you get the intended result you are seeking.
If you have control over the resource that you are calling, change it to return the result according to your expectation.
If you do not have control over the resource, in my opinion, the resource should not be return different results on different calls. If you get a different response on each call, then the resource is not idempotent. It's a bad design.
Imagine, you would be looping n times, with that you may or may not get the results you are looking for.
I would suggest looking into implementation and fixing the root of the problem, rather than work around.
getOwner returns a Promise. You will need to await for it. And I don't think you really need the while loop:
let owner = await getOwner();
if(owner && owner.owner != 'some value') {
/*do stuff with owner*/
}
console.log(owner);
Related
I'm having some issues adding some fields on to a Gatsby node. The real issue comes down to the fact that I just can't seem to wrap my head around the asynchronous situation, since I'm creating these fields from API call results. I'm still trying to learn about promises/async/etc.
I make one API call to an API to get location information and add it as a field (locationRequest, which is working just fine), and then run another call to get the orthodontists that work at that location.
When getOrthos runs, and it gets up to the console.log that should be spitting out an array of orthodontist entities, I'm getting this instead:
Created Ortho Node... [ Promise { <pending> }, Promise { <pending> } ]
What am I doing wrong? I've tried to go through some Promise tutorials, but I can't figure out the best way to do this where it returns the actual data rather than the promise.
Thank you for any guidance you can provide, and please excuse my ignorance.
const yextOrthos = node.acf.location_orthodontists;
const locationRequest = async () => {
const data = await fetch("https://FAKEURL.COM")
.then(response => response.json());
if( data && data.response && data.response.count === 1 ){
createNodeField({
node,
name: `yextLocation`,
value: data.response.entities[0]
});
} else {
console.log("NO LOCATIONS FOUND");
}
};
const getOrthos = async () => {
let orthodontists = await yextOrthos.map( async (ortho, i) => {
let orthoID = ortho.acf.yext_entity_ortho_id;
return await orthoRequest(orthoID);
});
if( orthodontists.length ){
createNodeField({
node,
name: `yextOrthos`,
value: orthodontists
});
console.log("Created Ortho Node...", orthodontists);
} else {
console.log("NO DOCTORS FOUND");
}
};
const orthoRequest = async (orthoID) => {
const dataPros = await fetch("https://FAKEURL.COM").then(response => response.json());
if( dataPros && dataPros.response && dataPros.response.count === 1 ){
return dataPros.response.entities[0];
} else {
return;
}
}
locationRequest();
getOrthos();
What you need to remember is that await should only stand before promise or something that returns promise. Array.prototype.map() returns array so you can't use await with it directly. Promise.all() on the other hand accepts an array and returns a promise. The example Jose Vasquez gave seems sufficient.
Good luck
You should use Promise.all() for arrays, on this line:
let orthodontists = await Promise.all(yextOrthos.map( async (ortho, i) => {...});
I hope it helps!
Edit:
A Promise which will be resolved with the value returned by the async
function, or rejected with an uncaught exception thrown from within
the async function.
If you wish to fully perform two or more jobs in parallel, you must
use await Promise.all([job1(), job2()]) as shown in the parallel
example.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
I've read MANY articles, but I'm totally a newbie to this whole async thing and am having a very hard time wrapping my brain around how it all works. I want to map the filtered array of objects, and inside that, I'd like to return the result of a function (an amount) and set that as the value of pmtdue. I tried this a bunch of ways, but always get zoneawarepromise or observable when it's logged, or a ton of errors. This is probably the closest I've gotten, but it's still not right.
async today(day = null, status = null) {
this.logger.log(`show today's ${status} appts ${day}`);
// filter master list for today
const filtered = [...this.apptList].filter(appt => {
if (!status) {
return (
appt.scheduled >= this.helperService.dayStart(day) &&
appt.scheduled <= this.helperService.dayEnd(day) &&
appt.status.status !== 'Checked Out' &&
appt.status.status !== 'Scheduled'
);
} else {
return (
appt.scheduled >= this.helperService.dayStart(day) &&
appt.scheduled <= this.helperService.dayEnd(day) &&
appt.status.status === status
);
}
});
// calculate due amount and map it to pmtdue field
const dueappts = await this.getTotalDue(filtered).then(
res => {
// console.log(res);
this.ApptModels = res;
},
err => {
console.log(err);
}
);
// send the data to ng2-smart-table
console.log(`filtered ApptModels`, this.ApptModels);
}
This is the function that does the mapping and has the functions I want to work
// a.pmtduedate returns the correct value as there is no http call
// a.pmtdue returns a zoneawarepromise but I don't know how to get the VALUE
getTotalDue(appts: Array<any>): Promise<any> {
return Promise.all(
appts.map(async (a: any) => {
a.pmtduedate = await this.helperService.getDueDate(a);
a.pmtdue = await this.dataService.sendTotalDue(a);
console.log(a.pmtdue); // logs undefined
return a;
})
);
}
My data service function (I know sometimes code matters that I think is insignificant):
async sendTotalDue(appt) {
this.logger.log(`fetch amount ${appt.patientID.nickname} owes`);
return await this.http.post(`${SERVER_URL}/sendtotaldue`, appt);
}
And finally, the backend function(minus details on data). It logs the correct amount on the backend, I just can't get it to display on the frontend:
module.exports.sendTotalDue = (req, res) => {
const appt = req.body;
// callback function that handles returning data
function done(err, results) {
const totaldue = parseInt(results, 10);
console.log(`API sendTotalDue CALLBACK done...totaldue: ${totaldue}`);
if (err) {
console.log('ERROR getting total due: callback error', err);
res.sendStatus(500).json(err); // server error; it'd be good to be more specific if possible
} else {
// end the request, send totaldue to frontend
console.log(`SUCCESS send totaldue to frontend ${totaldue}`);
res.status(200).json(totaldue);
}
}
// run first function
console.log(`1. getAmtDue:`);
this.getAmtDue(appt, done);
};
module.exports.getAmtDue(appt, callback) {
... function finds past visits, past payment and due totals
}
module.exports.getCurrentDue(appt, pastdueamt, callback) {
... function finds current visits and payments. calculates current due and adds the past due
callback(null, totaldue);
}
Can someone please help me understand what I'm doing wrong? Feel free to dumb it down for me, cause that's how I feel at this point.
EDITED TO FIX ERRORS like missing await and return. It is now to the point where I can see the value returned in the data service, but I get undefined in the map function section.
Ooooh!!! I GOT IT! I still don't totally understand WHY it works, but I changed the data service as follows:
async sendTotalDue(appt): Promise<any> {
this.logger.log(`fetch amount ${appt.patientID.nickname} owes`);
try {
const result = await this.http
.post(`${SERVER_URL}/sendtotaldue`, appt)
.toPromise();
return result as any[];
} catch (error) {
console.log(error);
}
}
Changing my service to the above finally got my values to appear exactly where I wanted them in the data table! :)
I found this article, which helped figure out how to work with Observable
Angular Tutorial with Async and Await
Currently, I am trying to get the md5 of every value in array. Essentially, I loop over every value and then hash it, as such.
var crypto = require('crypto');
function userHash(userIDstring) {
return crypto.createHash('md5').update(userIDstring).digest('hex');
}
for (var userID in watching) {
refPromises.push(admin.database().ref('notifications/'+ userID).once('value', (snapshot) => {
if (snapshot.exists()) {
const userHashString = userHash(userID)
console.log(userHashString.toUpperCase() + "this is the hashed string")
if (userHashString.toUpperCase() === poster){
return console.log("this is the poster")
}
else {
..
}
}
else {
return null
}
})
)}
However, this leads to two problems. The first is that I am receiving the error warning "Don't make functions within a loop". The second problem is that the hashes are all returning the same. Even though every userID is unique, the userHashString is printing out the same value for every user in the console log, as if it is just using the first userID, getting the hash for it, and then printing it out every time.
Update LATEST :
exports.sendNotificationForPost = functions.firestore
.document('posts/{posts}').onCreate((snap, context) => {
const value = snap.data()
const watching = value.watchedBy
const poster = value.poster
const postContentNotification = value.post
const refPromises = []
var crypto = require('crypto');
function userHash(userIDstring) {
return crypto.createHash('md5').update(userIDstring).digest('hex');
}
for (let userID in watching) {
refPromises.push(admin.database().ref('notifications/'+ userID).once('value', (snapshot) => {
if (snapshot.exists()) {
const userHashString = userHash(userID)
if (userHashString.toUpperCase() === poster){
return null
}
else {
const payload = {
notification: {
title: "Someone posted something!",
body: postContentNotification,
sound: 'default'
}
};
return admin.messaging().sendToDevice(snapshot.val(), payload)
}
}
else {
return null
}
})
)}
return Promise.all(refPromises);
});
You have a couple issues going on here. First, you have a non-blocking asynchronous operation inside a loop. You need to fully understand what that means. Your loop runs to completion starting a bunch of non-blocking, asynchronous operations. Then, when the loop finished, one by one your asynchronous operations finish. That is why your loop variable userID is sitting on the wrong value. It's on the terminal value when all your async callbacks get called.
You can see a discussion of the loop variable issue here with several options for addressing that:
Asynchronous Process inside a javascript for loop
Second, you also need a way to know when all your asynchronous operations are done. It's kind of like you sent off 20 carrier pigeons with no idea when they will all bring you back some message (in any random order), so you need a way to know when all of them have come back.
To know when all your async operations are done, there are a bunch of different approaches. The "modern design" and the future of the Javascript language would be to use promises to represent your asynchronous operations and to use Promise.all() to track them, keep the results in order, notify you when they are all done and propagate any error that might occur.
Here's a cleaned-up version of your code:
const crypto = require('crypto');
exports.sendNotificationForPost = functions.firestore.document('posts/{posts}').onCreate((snap, context) => {
const value = snap.data();
const watching = value.watchedBy;
const poster = value.poster;
const postContentNotification = value.post;
function userHash(userIDstring) {
return crypto.createHash('md5').update(userIDstring).digest('hex');
}
return Promise.all(Object.keys(watching).map(userID => {
return admin.database().ref('notifications/' + userID).once('value').then(snapshot => {
if (snapshot.exists()) {
const userHashString = userHash(userID);
if (userHashString.toUpperCase() === poster) {
// user is same as poster, don't send to them
return {response: null, user: userID, poster: true};
} else {
const payload = {
notification: {
title: "Someone posted something!",
body: postContentNotification,
sound: 'default'
}
};
return admin.messaging().sendToDevice(snapshot.val(), payload).then(response => {
return {response, user: userID};
}).catch(err => {
console.log("err in sendToDevice", err);
// if you want further processing to stop if there's a sendToDevice error, then
// uncomment the throw err line and remove the lines after it.
// Otherwise, the error is logged and returned, but then ignored
// so other processing continues
// throw err
// when return value is an object with err property, caller can see
// that that particular sendToDevice failed, can see the userID and the error
return {err, user: userID};
});
}
} else {
return {response: null, user: userID};
}
});
}));
});
Changes:
Move require() out of the loop. No reason to call it multiple times.
Use .map() to collect the array of promises for Promise.all().
Use Object.keys() to get an array of userIDs from the object keys so we can then use .map() on it.
Use .then() with .once().
Log sendToDevice() error.
Use Promise.all() to track when all the promises are done
Make sure all promise return paths return an object with some common properties so the caller can get a full look at what happened for each user
These are not two problems: the warning you get is trying to help you solve the second problem you noticed.
And the problem is: in Javascript, only functions create separate scopes - every function you define inside a loop - uses the same scope. And that means they don't get their own copies of the relevant loop variables, they share a single reference (which, by the time the first promise is resolved, will be equal to the last element of the array).
Just replace for with .forEach.
I am trying to use Mongoose's built in promise support to write some clean Javascript code for a user sending a friend request to another. However, when I try to ensure proper error handling and sequentiality, I still end up with a (slightly smaller than normal) pyramid of doom.
Here, I first ensure that the friend request is valid, then save the target's Id to the requester's sent requests then, if that save was successful, save the requester's Id to the target's friend requests.
Do I need to use a third party library like q in order to do this as cleanly as possible? How can I structure this such that I can use the traditional single error handler at the end?
function _addFriend (requesterId, targetId) {
// (integer, integer)
User.findById(requesterId)
.exec((requester) => {
if (!(targetId in requester.friends
|| targetId in requester.sentfriendRequests
|| targetId in requester.friendRequests)) {
requester.sentfriendRequests = requester.sentfriendRequests.concat([targetId])
requester.save()
.then((err) => {
if (err) throw err;
User.findById(targetId)
.exec((err, target) => {
if (err) throw err;
target.friendRequests = target.friendRequests.concat([requesterId])
target.save().then(err => {if (err) throw err})
})
})
}
})
}
You will need some nesting to do conditionals in promise code, but not as much as with callback-based code.
You seem to have messed up a bit of the if (err) throw err; stuff, you should never need that with promises. Just always use .then(result => {…}), and don't pass callbacks to exec any more.
If you always properly return promises from your asynchronous functions (including then callbacks for chaining), you can add the single error handler in the end.
function _addFriend (requesterId, targetId) {
// (integer, integer)
return User.findById(requesterId).exec().then(requester => {
if (targetId in requester.friends
|| targetId in requester.sentfriendRequests
|| targetId in requester.friendRequests) {
return;
}
requester.sentfriendRequests = requester.sentfriendRequests.concat([targetId])
return requester.save().then(() => {
return User.findById(targetId).exec()
}).then(target => {
target.friendRequests = target.friendRequests.concat([requesterId])
return target.save()
});
});
}
_addFriend(…).catch(err => {
…
})
In English, the way to do this is to use the promises returned by exec() have then blocks return promises, un-indent, then add then to those. Much easier to say in code...
EDIT thanks (again) to #Bergi for making me read and understand the app logic. #Bergi is right that there must be a little nesting to get the job done, but the real point isn't about reducing nesting, but about improving clarity.
Better clarity can come from factoring into logical parts, including some that return in promises.
These few functions conceal the promise nesting that's required by the logic. This doesn't specify (because the OP doesn't indicate how the app should handle) what addFriend should return when it refuses to do so due to an existing request...
function _addFriend (requesterId, targetId) {
// note - pass no params to exec(), use it's returned promise
return User.findById(requesterId).exec().then((requester) => {
return canAddFriend(requester, targetId) ? addFriend(requester, targetId) : null;
});
}
function canAddFriend(requester, targetId) {
return requester && targetId &&
!(targetId in requester.friends
|| targetId in requester.sentfriendRequests
|| targetId in requester.friendRequests);
}
function addFriend(requester, targetId) {
requester.sentfriendRequests = requester.sentfriendRequests.concat([targetId]);
return requester.save().then(() => {
return User.findById(targetId).exec();
}).then((target) => {
target.friendRequests = target.friendRequests.concat([requesterId]);
return target.save();
});
}
Once you realise that .exec() returns a promise, you can :
achieve the desired flattening and make the code more readable.
avoid the need to handle errors amongst the "success" code.
handle errors in a terminal .then() or .catch().
As a bonus you can also (more readily) throw meaningful errors for each of those x in y conditions.
Straightforwardly, you could write :
function _addFriend(requesterId, targetId) {
return User.findById(requesterId).exec().then(requester => {
if (targetId in requester.friends) {
throw new Error('target is already a friend');
}
if (targetId in requester.sentfriendRequests) {
throw new Error('friend request already sent to target');
}
if (targetId in requester.friendRequests) {
throw new Error('target already sent a friend request to requester');
}
requester.sentfriendRequests = requester.sentfriendRequests.concat([targetId]); // or just .push()?
return requester.save();
}).then(() => {
return User.findById(targetId).exec().then(target => {
target.friendRequests = target.friendRequests.concat([requesterId]); // or just .push()?
return target.save();
});
});
}
Note the need for returns to control flow.
But you could do even better. As writtten above, the requested stuff could succeed then the target stuff fail, resulting in a db disparity. So what you really want is a db transaction to guarantee that both happen or neither. Mongoose undoubtedly provides for transactions however you can do something client-side to give you something transaction-like with partial benefit.
function _addFriend(requesterId, targetId) {
return Promise.all([User.findById(requesterId).exec(), User.findById(targetId).exec()]).then(([requester, target]) => { // note destructuring
if (targetId in requester.friends) {
throw new Error('target is already a friend');
}
if (targetId in requester.sentfriendRequests) {
throw new Error('friend request already sent to target');
}
if (targetId in requester.friendRequests) {
throw new Error('target already sent a friend request to requester');
}
requester.sentfriendRequests = requester.sentfriendRequests.concat([targetId]);
target.friendRequests = target.friendRequests.concat([requesterId]);
return requester.save().then(() => {
return target.save();
});
});
}
Here, you could still get the (unlikely) situation that the first save is successful and the second save fails, but at least you have the assurance that absolutely nothing happens unless both the requester and target exist.
In both cases, call as follows :
_addFriend(requesterId, targetId).then(function() {
// do whatever on success
}, function(error) {
// do whatever on error
});
Even if you don't use the error messages in the live environment, they could be very useful when testing/debugging. Please check them - I may have gotten them wrong.
Can you recommend how to correctly deal with a control flow with many if/switch and promises? All the tutorials on the Internet that I've found tend to deal with simple control flow, without many (any?) different processing branches. Any suggested reading or at least search terms?
The way I do it now is to encapsulate if/switch logic in a function that returns a Promise after evaluating the conditions and returns to the main process loop. Any way to do it better, nicer?
Sample code:
// Check if argument is a valid URL
Promise.promisify(checkUrl)().then(() => {
// Delete all query parameters from URL if present
return sanitizer.cleanAsync(argv.url)
}).then(_cleanUrl => {
cleanUrl = _cleanUrl;
logger.warn(`URL: ${cleanUrl}`);
// Validate Google Analytics view id supplied as '--gaId=<id>' command line argument or exit if it is not present
return Promise.promisify(checkGaId)()
}).then(() => {
// Check if DB exists, if not create it
return db.checkIfDatabaseExistsAsync()
}).then(() => {
// Check if all tables exist, if not create them
return db.checkTablesAsync()
}).then(() => {
// Check DB integrity (possiblDelete all query parameters from URL if presente to turn off in the config)
if (config.database.checkIntegrity) {
return db.integrityChecksAsync();
}
}).then(() => {
// Check if URL already exists in DB, if not insert it
return db.getOrCreateEntryUrlIdAsync(cleanUrl)
}).then(_entryId => {
entryId = _entryId;
// Check if any previous executions for the entry point exist and if so whether the last one completed
return db.getLastExecutionDataAsync(entryId);
}).then(lastExecution => {
// If last execution was not completed prompt for user action
return processLastExecution(entryId, lastExecution)
}).then(_pages => {
... more code follows here...
And psuedo-code for processLasExecution function:
function processLastExecution(entryId, lastExecution) {
return new Promise(
function (resolve, reject) {
// No previous executions found or all was okay
if (lastExecution == null || (lastExecution != null && lastExecution.is_completed == 'Y')) {
...resolves with A;
} else {
Promise.promisify(selectRunOption)().then(option => {
switch (option) {
case 'resume':
...resolves with B;
break;
case 'ignore':
...resolves with C;
break;
case 'delete':
...resolves with D;
break;
default:
...rejects
}
});
}
}
)
}
Any way of having the if/switch logic better/more clearly encapsulated or served?
Oh, if anyone wonders this is a command line script, not a web application, and this not exactly what Node.js was intended for.
I think it is better to use generator, then you can write sync like codes:
co(function* () {
// Check if argument is a valid URL
if (yield checkUrl) {
var cleanUrl = yield sanitizer.cleanAsync(argv.url);
...
}
...
}, ...
co can cooperation with callback and promise, see https://github.com/tj/co