How to call a function after the promise has been resolved? - javascript

The following code is for my FCM function where I am listening to firestore the getting tokens before constructing payload to send. Every time its sent the system logs that the tokens are empty. How can I make sure its not empty when sending the fcm?
let functions = require('firebase-functions');
let admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendNotification =functions.firestore.document('chatrooms/{chatRoomId}/messages/{messageId}')
.onWrite((snap, context) => {
let message = snap.after.data().messageBody;
let messageSender = snap.after.data().senderName;
let messageUserId = snap.after.data().userId;
let chatRoomId = context.params.chatRoomId;
let tokens = [];
let chatRoomRef = admin.firestore().collection("chatrooms").doc(chatRoomId);
return admin.firestore().runTransaction(t => {
return t.get(chatRoomRef)
.then(chatroom => {
let usersArray = chatroom.data().chatMembers;
usersArray.forEach(user_id => {
let userIdRef = admin.firestore().collection("tokens").doc(user_id);
return t.get(userIdRef).then(doc => {
if (doc.exists) {
let user_token = doc.data().token;
functions.logger.log('token: ', token);
tokens.push(user_token);
}
}).catch(err => {
functions.logger.error(err);
})
});
});
}).then(() => {
//The transaction has run successfully, we expect tokens array to not be empty
functions.logger.log("Construction the notification message.");
const payload = {
data: {
data_type: "data_type_chat_message",
title: "Tuchat",
message: message,
sender_id: messageUserId,
sender_name: messageSender,
chatRoom_id: chatRoomId
}
};
const options = {
priority: "high",
timeToLive: 60 * 60 * 24
};
return admin.messaging().sendToDevice(tokens, payload).catch(err => {
functions.logger.error(err);
});
}).catch(err => {
functions.logger.error('Transaction error: ', err);
})
});
Also before trying transactions it was returning empty tokens.

The problem is because of the way you're dealing with promises inside the forEach loop. The loop will not wait for promises to be resolved for the code inside it. It is currently just iterating as fast as possible over each user ID and not waiting for the queries to complete inside it. That means the code will continue before tokens can get populated.
You should instead collect each of the promises inside the loop and use Promise.all() to wait for the entire batch, then return the promise from that to indicate when all the work is complete. That will ensure that tokens contains everything you want before the next then in the chain is executed.
The general form of the code will be like this:
.then(() => {
const promises = []
collection.forEach(item => {
const promise = doSomeWork()
promises.push(promise)
})
return Promise.all(promises)
})
.then(() => {
// this will continue only after all promises are resolved
})
See also:
How to use promise in forEach loop of array to populate an object
Node JS Promise.all and forEach

Related

Firestore slow queries are causing the entire logic to crash

I am currently designing a todo app with react and firebase without any node js server code. Everything was fine when inserting data and authenticating, but when I tried to query all the tasks where uid equals a certain value(that means all the tasks that belongs to a particular user), somehow the query gets skipped and returns null. But then, after a second or so, the array got retrieved and printed out.
The code:
function retrieveTasksOfUser(uid) {
// get all tasks where the uid equals the given value
return db.collection("tasks").where("uid", "==", uid).get();
}
function retrieveTasks() {
let res = [];
retrieveTasksOfUser(currentUser["uid"])
.then((snapshot) => {
snapshot.forEach((doc) => {
// include the doc id in each returned object
let buffer = doc.data();
buffer["id"] = doc.id;
res.push(buffer);
});
console.log(res);
return res;
})
.catch((err) => {
console.error("Error retrieving tasks: ", err);
return res;
});
}
let tasks = retrieveTasks();
console.log({ tasks });
EDIT:
Inspired by Frank's answer, I modified my code and got:
async function retrieveTasks() {
let res = [];
const snapshot = await retrieveTasksOfUser(currentUser["uid"]);
snapshot.forEach((doc) => {
let buffer = doc.data();
buffer["id"] = doc.id;
res.push(buffer);
});
return res;
}
let tasks = [];
let promise = retrieveTasks();
promise.then((res) => {
console.log("Tasks are");
tasks = res;
console.log(res);
});
console.log(tasks);
But the result turns out to be an empty array
Thanks a bunch in advance.
You're not returning anything from the top-level code in retrieveTasks, so that means that tasks will always be undefined.
The simplest fix is to return the result of the query, which then means that your return res will be bubbled up:
return retrieveTasksOfUser(currentUser["uid"])
...
But this means you're returning a promise, as the data is loaded asynchronously. So in the calling code, you have to wait for that promise to resolve with then:
retrieveTasks().then((tasks) => {
console.log({ tasks });
})
You can make all of this a lot more readable (although it'll function exactly the same) by using the async and await keywords.
With those, the code would become:
async function retrieveTasks() {
let res = [];
const snapshot = await retrieveTasksOfUser(currentUser["uid"]);
snapshot.forEach((doc) => {
let buffer = doc.data();
buffer["id"] = doc.id;
res.push(buffer);
});
return res;
}
let tasks = await retrieveTasks();
console.log({ tasks });
Or with a bit more modern JavaScript magic, we can make it even shorter:
async function retrieveTasks() {
const snapshot = await retrieveTasksOfUser(currentUser["uid"]);
return snapshot.docs.map((doc) => { ...doc.data, id: doc.id });
}
let tasks = await retrieveTasks();
console.log({ tasks });

File upload async function doesn't wait [duplicate]

This question already has answers here:
Using async/await with a forEach loop
(33 answers)
Closed 2 years ago.
I am trying to create a function that handles file uploads in bulk. I have what I call a floor, and each floor has rooms. Every room can have an audio file. This function should handle the upload of these files. My problem is that the updated "floor" gets written before the files have been uploaded. Meaning I can't make it work asynchronously properly.
This is my function:
- tracks is an object with the keys being the index of the room and the values the files themselves (using an object and not an array because this is an updating funtction so I might not have all the rooms in this object).
export const saveFloor = (floor, tracks, cb) => async () => {
const newFloor = { ...floor };
if (tracks) {
await Object.keys(tracks).forEach(async (trackKey) => {
const track = tracks[trackKey];
const ref = storage.ref(
`/audio/floor_tracks/${floor.id}/${trackKey}/${track.name}`
);
const upload = await ref.put(track);
if (!upload) return;
const downloadUrl = await ref.getDownloadURL();
if (!downloadUrl) return;
newFloor.rooms[trackKey].track = { name: track.name, file: downloadUrl };
console.log("this is the floor", "Just finished one track");
});
console.log("this is the floor", "Just finished all tracks");
}
console.log("this is the floor", newFloor.rooms);
db.collection("floors")
.doc(floor.id)
.set(newFloor)
.then(() => {
cb();
})
.catch((e) => {
console.log("Saving to db failed", e);
});
};
I get Just finished all tracks and newFloor.rooms printed way before Just finished one track.
Edit after the answer from RikkusRukkus, this is my current function, and I am getting the error
Unhandled Rejection (TypeError): undefined is not iterable (cannot read property Symbol(Symbol.iterator))
export const saveFloor = (floor, logoFile, tracks, cb) => async () => {
const newFloor = { ...floor };
if (logoFile) {
const ref = storage.ref(`/images/floor_logos/${floor.id}`);
const upload = await ref.put(logoFile);
if (!upload) return;
const downloadUrl = await ref.getDownloadURL();
if (!downloadUrl) return;
newFloor.logo = downloadUrl;
}
if (tracks) {
await Promise.all(
Object.keys(tracks).forEach(async (trackKey) => {
const track = tracks[trackKey];
const ref = storage.ref(
`/audio/floor_tracks/${floor.id}/${trackKey}/${track.name}`
);
const upload = await ref.put(track);
if (!upload) return;
const downloadUrl = await ref.getDownloadURL();
if (!downloadUrl) return;
newFloor.rooms[trackKey].track = {
name: track.name,
file: downloadUrl,
};
console.log("this is the floor", "Just finsihed one track");
})
);
console.log("this is the floor", "Just finsihed all tracks");
}
console.log("this is the floor", newFloor.rooms);
db.collection("floors")
.doc(floor.id)
.set(newFloor)
.then(() => {
cb();
})
.catch((e) => {
console.log("Saving to db failed", e);
});
};
await Object.keys(tracks).forEach(async (trackKey) => {
Array.prototype.forEach returns undefined, this will not throw an error because you can await any kind of value, not just Promises. So what happens is:
Your array is looped over
Async processes are started
undefined is returned by forEach
It's not a promise so it immediately resolves and with almost no delay, the next line is executed ("finished all tracks")
Some time later, the promises from #2 resolve. Nothing is done with their resolved values (there are no values in the return statement, so the promises will resolve to undefined).
You can await an array of promises like so:
await Promise.all( Object.keys(tracks).map(async (trackKey) => { ... }) )
Array.prototype.map does return values, in this case Promises. Promise.all takes an array of Promises & resolves when all resolve or rejects ("throws") when any one rejects.
_
If you were to include return values in your async functions then Promise.all will resolve to an array of these returned values, in order.

How to avoid nesting promises with firebase transaction?

In an onDelete trigger I'm running a transaction to update some object. I now need to do some cleanup and delete some other objects before running that transaction. After adding the cleanup code I'm getting a warning about nesting promises which I don't know how to get rid of. Here is a snippet:
exports.onDeleteAccount = functions.firestore
.document('accounts/{accountID}')
.onDelete((account, context) => {
// First do the cleanup and delete addresses of the account
const query = admin.firestore().collection('account_addresses').where('accountID', '==', account.id);
return query.get().then(addresses => {
var promises = [];
addresses.forEach(address=>{
promises.push(address.ref.delete());
})
return Promise.all(promises);
}).then(()=> {
// Then run the transaction to update the account_type object
return runTransaction(transaction => {
// This code may get re-run multiple times if there are conflicts.
const acc_type = account.data().type;
const accountTypeRef = admin.firestore().doc("account_types/"+acc_type);
return transaction.get(accountTypeRef).then(accTypeDoc => {
// Do some stuff and update an object called users
transaction.update(accountTypeRef, {users: users});
return;
})
})
})
.catch(error => {
console.log("AccountType delete transaction failed. Error: "+error);
});
})
I don't think the problem comes from the Transaction but from the forEach loop where you call delete(). You should use Promise.all() in order to return a single Promise that fulfills when all of the promises (returned by delete()) passed to the promises array have been fulfilled, see below.
In addition, you do runTransaction(transaction => {...}) but runTransaction is a method of Firestore. You should do admin.firestore().runTransaction(...).
Therefore, the following should do the trick:
exports.onDeleteAccount = functions.firestore
.document('accounts/{accountID}')
.onDelete((account, context) => {
// First do the cleanup and delete addresses of the account
const query = admin.firestore().collection('account_addresses').where('accountID', '==', account.id);
return query.get()
.then(addresses => {
const promises = [];
addresses.forEach(address => {
promises.push(address.ref.delete());
})
return Promise.all(promises);
}).then(() => {
// Then run the transaction to update the account_type object
return admin.firestore().runTransaction(transaction => {
// This code may get re-run multiple times if there are conflicts.
const acc_type = account.data().type;
const accountTypeRef = admin.firestore().doc("account_types/" + acc_type);
return transaction.get(accountTypeRef).then(accTypeDoc => {
// Do some stuff and update an object called users
transaction.update(accountTypeRef, { users: users });
})
})
})
.catch(error => {
console.log("AccountType delete transaction failed. Error: " + error);
});
})

Param didn't pass to local function in firebase cloud functions

I have a firebase cloud function as follows:
exports.foo = functions.database
.ref("/candidates/{jobTrack}/{candidateId}")
.onCreate((snap, context) => {
const candidate = snap.val().candidate;
const jobTrack = context.params.jobTrack;
const jobsRef = admin.database().ref("jobs");
return jobsRef
.child(jobTrack)
.once("value")
.then(jobs => {
const promises = [];
jobs.forEach(job => {
promises.push(job.val());
});
return Promise.all(promises);
})
.then(jobs => {
return jobs.forEach(job => {
var percent = getMatchedPercent(candidate, job);
if (percent >= 0.9) {
admin
.database()
.ref("feeds")
.child(job.feedId)
.child("upcomingWeek")
.push(candidate); // add to team's feed
}
});
})
.catch(err => {
console.log("firebase got an error: ", err);
});
});
In function foo, I call a local non-cloud function getMatchedPercent which is defined as below:
const getMatchedPercent = (candidate, job) => {
console.log("In get percent: ", candidate, job);
// do something
};
The problem is when I checked job.val() in foo before calling getMatchedPercent, I can see valid data got printed from console for job.val(). When once get in getMatchedPercent, I tried to print job, it complains it's undefined.
Is there anything I missed? Why the information of job can be lost during calling a function? Thanks!
Your problem is caused by these lines:
const promises = [];
jobs.forEach(job => {
promises.push(job.val());
});
return Promise.all(promises);
job.val() returns an object (of the data) not a promise, so Promise.all() incorrectly interprets it as a resolved promise with no value. In your next block of code, the array jobs is an array of undefined values rather than the data you were expecting.
To fix this, you would instead return the array of values rather than using Promise.all().
const jobValues = [];
jobs.forEach(job => {
jobValues.push(job.val());
});
return jobValues;
But because no asyncronous work is taking place here you can flatten your Promise chain. By doing so, you will use less memory because you won't need an array containing of all of your job.val() objects at once.
exports.foo = functions.database
.ref("/candidates/{jobTrack}/{candidateId}")
.onCreate((snap, context) => {
const candidate = snap.val().candidate;
const jobTrack = context.params.jobTrack;
const jobsRef = admin.database().ref("jobs");
return jobsRef
.child(jobTrack)
.once("value")
.then(jobs => {
const promises = []; // will contain any team feed update promises
jobs.forEach(jobSnapshot => { // This is DataSnapshot#forEach
const job = jobSnapshot.val();
const percent = getMatchedPercent(candidate, job);
if (percent >= 0.9) {
promises.push(
admin
.database()
.ref("feeds")
.child(job.feedId)
.child("upcomingWeek")
.push(candidate) // add to team's feed
);
}
});
return Promise.all(promises);
})
.catch(err => {
console.log("Failed to update team feeds: ", err);
});
});
However, this still has another problem where some of the feed updates may succeed and others may fail which leaves your database in an unknown state. So instead you might want to consider writing to the database atomically (all data is written, or nothing at all).
This could be achieved using:
exports.foo = functions.database
.ref("/candidates/{jobTrack}/{candidateId}")
.onCreate((snap, context) => {
const candidate = snap.val().candidate;
const jobTrack = context.params.jobTrack;
const jobsRef = admin.database().ref("jobs");
return jobsRef
.child(jobTrack)
.once("value")
.then(jobs => {
const pendingUpdates = {}; // "path: value" pairs to be applied to the database
const feedsRef = admin.database().ref("feeds");
jobs.forEach(jobSnapshot => { // This is DataSnapshot#forEach
const job = jobSnapshot.val();
const percent = getMatchedPercent(candidate, job);
if (percent >= 0.9) {
const pushId = feedsRef.push().key; // push() without arguments doesn't write anything to the database, it just generates a new reference with a push ID we can use.
const path = job.feedId + "/upcomingWeek/" + pushId;
pendingUpdates[path] = candidate; // queue add to team's feed
}
});
// apply all updates in pendingUpdates object,
// relative to feedsRef as an all-or-nothing operation.
// e.g. pendingUpdates["feed001/upcomingWeek/9jksdfghsdjhn"] = "someUserId"
// will be written to "feeds/feed001/upcomingWeek/9jksdfghsdjhn"
return feedsRef.update(pendingUpdates); // commit changes
})
.catch(err => {
console.log("Failed to apply all feed updates: ", err);
});
});

Javascript promise chain hell

I'm having a problem with promise chains that I don't know how to resolve. Summary of my code: I do a mongoose query for a specific user, fetch his CarIds and then query each car for his details and return these details via JSON response.
let carsDetails = [];
User.findById(userId)
.then(user => {
const carIds = user.carsDetails;
carIds.forEach((carId) => {
Car.findById(carId)
.then(car => {
console.log(car);
carsDetails.push(car);
})
.catch(err => { throw err; });
});
return res.status(200).json({ data: carsDetails });
})
.catch(error => {
throw error;
});
The problem is that no cars get actually pushed onto carsDetails array on the carsDetails.push(car); line, because it jumps to return statement before it manages to fill up the array. Is there a workaround that could do those queries and return a result in a form of an array, object...? I tried writing everything in async await form too with self-made async forEach statement, but it doesn't help me. Any suggestions? I already tried with Promise.all(), but didn't manage to fix the issue. Thanks!
You'll need to collect the promises of your Car.findById(carId) calls and use Promise.all() to wait for all of them before responding. You can use array.map() to map each ID to a promise from Car.findById().
User.findById(userId)
.then(user => {
const carIds = user.carsDetails;
const carPromises = carIds.map(carId => Car.findById(carId))
return Promise.all(carPromises)
})
.then(cars => {
res.status(200).json({ data: cars })
})
.catch(error => {
throw error
})
If you can have a lot of cars to find, you may want to do your query in a single request, no need to stack multiple promises :
User.findById(userId)
.then(user => {
const carIds = user.carsDetails;
// if carsDetails is not an array of objectIds do this instead :
// const carIds = user.carsDetails.map(id => mongoose.Types.ObjectId(id));
return Car.find({ _id: { $in: carIds });
})
.then(userCars => {
res.status(200).json({ data: userCars })
})
await/async is the way to go, with await/async you use regular for ... of loops instead of forEach.
async function getCarDetails() {
let carsDetails = [];
let user = await User.findById(userId);
const carIds = user.carsDetails;
for (let carID of carIds) {
let car = await Car.findById(carId)
console.log(car);
carsDetails.push(car);
}
return res.status(200).json({
data: carsDetails
});
}
Or you use Promise.all and map instead of for ... of
async function getCarDetails() {
let user = await User.findById(userId);
const carIds = user.carsDetails;
let carsDetails = await Promise.all(carIds.map(carID => Car.findById(carID)));
return res.status(200).json({
data: carsDetails
});
}
Those two solutions are slightly different. The second version with the map will send all requests to the DB at once, and then waits until they are all resolved. The first one will send the request one after another. Which one is better depends on the use-case, the second one could lead to request peeks, and might be easier be abused for DDoS.
Try to use async/await to solve this problem. It will be more readable and clean
async function getCarDetail() {
let carsDetails = []
try {
const user = await User.findById(userId)
const carIds = user.carsDetails
for (let i = 0; i < carIds.length; i++) {
const carId = carIds[i]
const car = await Car.findById(carId)
carsDetails.push(car)
}
return res.status(200).json({ data: carsDetails })
} catch(error) {
console.log(error)
}
}

Categories

Resources