Async-await not working as I expected in forEach loop - javascript

I am using WebRTC to try and connect two users together. I call createOffer then I set the local description of the peer connection using what createOffer returns. I don't know why I am getting the following error:
Note: I am using firebase firestore as a signaling server
Uncaught (in promise) DOMException: Cannot set local offer when
createOffer has not been called.
Here is my code:
async function preformSignaling() {
let users = await negDoc.collection("users").get();
let newPeerConnection;
users.forEach(async (doc) => {
if (isNotAlreadyConnected(doc.id)) {
newPeerConnection = new UserConnection(servers, doc.id);
if (doc.id != sessionStorage.getItem("userID") && doc.id != "metadata") {
let connOfferDescription =
await newPeerConnection.userPeerConnection.createOffer();
await newPeerConnection.userPeerConnection.setLocalDescription(
connOfferDescription
);
await doc.collection("offer-candidates").doc("offer").set({
offer: newPeerConnection.userPeerConnection.localDescription,
});
}
peerConnections.push(newPeerConnection);
}
});
}
class UserConnection {
constructor(servers, remoteUserID) {
this.userPeerConnection = new RTCPeerConnection(servers);
this.remoteStream = new MediaStream();
this.remoteUserID = remoteUserID;
}
getRemoteUserID() {
return this.remoteUserID;
}
}

I'm taking a bit of a blind stab here but using async-await in a forEach (or map,filter,reduce etc.) is a common gotcha. It essentially just fires a bunch of async calls.
It won't wait for what's happening in that callback to finish before firing the next one. The internals of the forEach would need to await the callback and the callback must return a promise.
Because you have let newPeerConnection outside of the loop, you are probably writing that variable multiple times before any of the createOffer calls finish.
You could bring that variable inside the loop if none of these simultaneous calls will affect each other. Otherwise, just use a for-of loop if you'd like to run them one by one. That might look something like this:
async function preformSignaling() {
let users = await negDoc.collection('users').get();
let newPeerConnection;
for (let doc of users) {
if (!isNotAlreadyConnected(doc.id)) continue;
newPeerConnection = new UserConnection(servers, doc.id);
if (
doc.id !== sessionStorage.getItem('userID') &&
doc.id !== 'metadata'
) {
let connOfferDescription =
await newPeerConnection.userPeerConnection.createOffer();
await newPeerConnection.userPeerConnection.setLocalDescription(
connOfferDescription
);
await doc.collection('offer-candidates').doc('offer').set({
offer: newPeerConnection.userPeerConnection.localDescription,
});
}
peerConnections.push(newPeerConnection);
}
}
Note a couple of minor changes in there for better practices: !== not !=, and check the inverse of the bool and continue early to avoid the pyramid of doom.

Related

Javascript for loop does not wait for fetch request to complete and moves onto next iteration

I have below code in javascript in which some asynchronous task is being performed:
async function fetchData(id){
for(let i=1;;++i){
res = await fetch(`https://some-api/v1/products/${id}/data?page=${i}`,{//headers here});
res = await res.json();
if(res.length==0) break;
else{ //do some work here and continue for next iteration}
}
}
async function callApi(){
var arr = [//list of id's here to pass to api one by one, almost 100 id's here];
await Promise.all(arr.map(async(e)=>{
await fetchData(e);
}));
}
callApi();
The above code looks fine to me, except that it doesn't work as expected. Ideally, what should happen is that unless one id's call is not completed( unless break condition not satisfies for one id), the for loop should not proceed to next iteration. Rather, I am getting totally different results. The api calls are happening in random order because the loop is not waiting the iteration to complete. My hard requirement is that unless one iteration is not complete, it should not move to next one.
await seems to have no effect here. Please guide me how can I achieve this. I am running out of ideas.
Thank You!
Your arr.map(...) is not awaiting the different fetchData calls before the next map call, so I'd turn this into a specific for loop to be sure it waits:
async function callApi(){
const arr = [...];
for(let i = 0; i < arr.length; i++){
await fetchData(arr[i]);
}
}
or alternatively use a for of
async function callApi(){
const arr = [...];
for(let a of arr){
await fetchData(a);
}
}
The fetchData function also looks like it could use some improvements with error handling, but since you shortened your code quite a bit, I'm assuming there is something like that going on there, too, and your issue is actually with the callApi() code instead, as the fetch and await looks good to me there.
You should decide either to use promises or async await. Don't mix them.
With promises you can always use funky abstractions but with a simple recursive approach you can do like
function fetchData(hds, id, page = 1, pages = []){
return fetch(`https://some-api/v1/products/${id}/data?page=${page}`,hds)
.then(r => r.ok ? r.json() : Promise.reject({status:r.status,pages})
.then(j => fetchData(hds, id, ++page, pages.push(doSomethingWith(j))))
.catch(e => (console.log(e.status), e.pages));
}
So we use recursion to fetch indefinitelly until the API says enough and r.ok is false.
At the callApi side you can use reduce since we have an ids array.
const ids = [/* ids array */],
hds = { /* headers object */ };
function callApi(ids){
return ids.reduce( (p,id) => p.then(_ => fetchData(hds,id))
.then(pages => process(pages))
, Promise.resolve(null)
)
.catch(e => console.log(e));
}
So now both accesses to the id and page data are working asynchronously but only fired once the previous one finishes. Such as
(id=1,page=1) then (id=1,page=2) then (id=1,page=3) then (process 3 pages of id=1) then
(id=2,page=1) then (id=2,page=2) then (process 2 pages of id=2) etc...
While I love the promises, you can also implement the same functionality with the asyc await abstraction. I believe the idea behind the invention of the async await is to mimic sync imperative code. But keep in mind that it's an abstraction over an abstraction and I urge you to learn promises by heart before even attemting to use async await. The general rule is to never mix both in the same code.
Accordingly the above code could have been written as follows by using async await.
async function fetchData(hds, id){
let page = 1,
pages = [],
res;
while(true){
res = await fetch(`https://some-api/v1/products/${id}/data?page=${page++}`,hds);
if (res.ok) pages.push(await res.json())
else return pages;
}
}
Then the callApi function can be implemented in a similar fashion
const ids = [/* ids array */],
hds = { /* headers object */ };
async function callApi(ids){
let pages;
for(let i = 0; i < ids.length; i++){
try {
pages = await fetchData(hds,ids[i]);
await process(pages); // no need for await if the process function is sync
}
catch(e){
console.log(e);
}
}
}

How to stop running async function on node.js from react application?

React app can run node.js function which preparing data and sending information to the database in batches.
It takes a lot of time and I would like to add the ability to stop this function right from react app.
const getShopifyOrders = require('./shopify');
const getTrack = require('./tracking');
const Order = require('./model');
async function addOrdersToDB(limit) {
try {
// Get latest order from DB
let latestOrd = await Order.findOne().sort('-order_number');
do {
// Get Shopify Orders
let orders = await getShopifyOrders(
latestOrd ? latestOrd.order_id : 0,
limit
);
latestOrd = orders[0] ? orders[orders.length - 1] : undefined;
// Update array with tracking status
let fullArray = await getTrack(orders);
// Add to DB
let ins = await Order.insertMany(fullArray, { ordered: false });
console.log(`Added ${ins.length} entries`);
} while (latestOrd);
} catch (err) {
console.log(err);
}
}
module.exports = addOrdersToDB;
I tried a lot of things to include in this function including:
while loop: added the variable outside the function - if 'true' - run code, if not - return - it just doesn't work (variable was changed from react using socket.IO)
setTimeout (also setInterval), triger clearTimeout function from react: this doesn't work as setTimeout and setInterval doesn't work in async function
after that:
made (actually fond here on stackoverflow) new function to promisify setTimeout to be able to use in async function:
const setTimeout2 = (callback, ms) => {
return new Promise(
resolve =>
(to = setTimeout(() => {
callback();
resolve();
}, ms))
);
};
async function addOrdersToDB(limit) {
do {
await setTimeout2(async () => {
try {
// some code here
} catch (err) {
console.log(err);
}
}, 400);
} while (latestOrderExist);
}
function clearTO() {
setTimeout(() => {
console.log('clearTO');
clearTimeout(to);
}, 3000);
}
This for some reason doesn't iterate.
Is there solution for this?
Thanks!
To abort the do/while loop, you will need to add an additional test to that loop that is some variable that can be modified from the outside world. Also, note that the additional test only works here because you're using await inside the loop. If there was no await inside the loop, then the loop would be entirely synchronous and there would be no ability to change a variable from outside the loop while the loop was running (because of nodejs' single-threadedness).
Since this is a server (and globals are generally bad), I will assume we should not use a global. So instead, I would restructure addOrdersToDB() to return a data structure that contains both the promise the existing version returns and an abort() function the caller can call to stop the current processing. This also permits multiple separate calls to addOrdersToDB() to be running, each with their own separate abort() method.
function addOrdersToDB(limit) {
let stop = false;
function abort() {
stop = true;
}
async function run() {
try {
// Get latest order from DB
let latestOrd = await Order.findOne().sort('-order_number');
do {
// Get Shopify Orders
let orders = await getShopifyOrders(
latestOrd ? latestOrd.order_id : 0,
limit
);
latestOrd = orders[0] ? orders[orders.length - 1] : undefined;
// Update array with tracking status
let fullArray = await getTrack(orders);
// Add to DB
let ins = await Order.insertMany(fullArray, { ordered: false });
console.log(`Added ${ins.length} entries`);
} while (!stop && latestOrd);
// make resolved value be a boolean that indicates
// whether processing was stopped with more work still pending
return !!(latestOrd && stop);
} catch (err) {
// log error and rethrow so caller gets error propagation
console.log(err);
throw err;
}
}
return {
promise: run(),
abort: abort
}
}
So, to use this, you would have to change the way you call addOrdersToDB() (since it no longer returns just a promise) and you would have to capture the abort() function that it returns. Then, some other part of your code can call the abort() function and it will then flip the internal stop variable that will cause your do/while loop to stop any further iterations.
Note, this does not stop the asynchronous processing inside the current iteration of the do/while loop - it just stops any further iterations of the loop.
Note, I also changed your catch block so that it rethrows the error so that the caller will see if/when there was an error.
And, the resolved value of the function is the internal stop variable so the caller can see if the loop was aborted or not. A true resolved value means the loop was aborted and there was more work to do.
Here's an additional version of the function that creates more opportunities for it to stop between await operations within your function and within the loop. This still does not abort an individual database operation that may be in progress - you'd have to examine whether your database supports such an operation and, if so, how to use it.
function addOrdersToDB(limit) {
let stop = false;
function abort() {
stop = true;
}
async function run() {
try {
// Get latest order from DB
let latestOrd = await Order.findOne().sort('-order_number');
if (!stop) {
do {
// Get Shopify Orders
let orders = await getShopifyOrders(
latestOrd ? latestOrd.order_id : 0,
limit
);
latestOrd = orders[0] ? orders[orders.length - 1] : undefined;
if (stop) break;
// Update array with tracking status
let fullArray = await getTrack(orders);
if (stop) break;
// Add to DB
let ins = await Order.insertMany(fullArray, { ordered: false });
console.log(`Added ${ins.length} entries`);
} while (!stop && latestOrd);
}
// make resolved value be a boolean that indicates
// whether processing was stopped with more work still pending
return !!(latestOrd && stop);
} catch (err) {
// log and rethrow error so error gets propagated back to cller
console.log(err);
throw err;
}
}
return {
promise: run(),
abort: abort
}
}

Async/Promise issues when adding a Gatsby node field

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

Firebase functions for loop async await

I'm trying to do a relatively simple, in theory, function using Firebase Functions.
Specifically:
Add + 1 to a realtime database variable for all users
Send a notification to all users
I'm still trying to understand async/await which is probably why I'm struggling so much with this.
Here is what I'm doing:
exports.gcIncrement = functions.database
.ref('gthreads/{threadId}/messages/{messageId}')
.onCreate(async (snapshot, context) => {
const data = snapshot.val();
const threadId = context.params.threadId;
const uid = context.auth.uid;
adb.ref('gchats/' + threadId).once('value').then(async (gchatData) => {
const parent = gchatData.val();
incrementUser(parent.users, uid, threadId); //parent.users is an object with 1-30 users.
sendGCNotification(parent.users, data);
return true;
}).catch(error => console.log(error))
});
And then I have the function incrementUser:
function IncrementUser(array, uid, threadId) {
for (const key in array) {
if (key != uid) {
const gcMessageRef =
adb.ref('users/' + key + '/gthreads/' + threadId + '/' + threadId+'/unread/');
gcMessageRef.transaction((int) => {
return (int || 0) + 1;
}
}
}
and the function sendGCNotification:
function sendGCNotification(array, numbOfMsg, data) {
let payload = {
notification: {
title: 'My App - ' + data.title,
body: "This is a new notification!",
}
}
const db = admin.firestore()
for (const key in array) {
if (!data.adminMessage) {
if (array[key] === 0) {
const devicesRef = db.collection('devices').where('userId', '==', key)
const devices = await devicesRef.get();
devices.forEach(result => {
const tokens = [];
const token = result.data().token;
tokens.push(token)
return admin.messaging().sendToDevice(tokens, payload)
})
}
}
}
}
I currently get the error:
'await' expression is only allowed within an async function.
const devices = await devicesRef.get();
But even when I get it error-free, it doesn't seem work. The Firebase Functions log says:
4:45:26.207 PM
gcIncrement
Function execution took 444 ms, finished with status: 'ok'
4:45:25.763 PM
gcIncrement
Function execution started
So it seems to run as expected but not fulfill the code as expected. Any ideas? Thank you!
All uses of await have to occur within the main body of a function that's marked async. Your function sendGCNotification is not async. You'll have to mark it async, and also make sure that any promises within it have been awaited, or return a promise that resolves when all the async work is done.
Also, in IncrementUser you are not handling the promise returned by gcMessageRef.transaction(). You need to handle every promise that you generate from all the async work, and make sure they are all a part of the final promise that you return or await from your top-level function.
If you want to learn more about promises and async/await in Cloud Functions code, I suggest you use my video series. Specifically, the one titled "How does async/await work with TypeScript and ECMAScript 2017?". Even if you aren't using TypeScript, async/await work the same way.

Node.js for loop using previous values?

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.

Categories