Promise executes then function before previous then execution is completed - javascript

I'm try to chain a couple of then functions which execute sequentially, but the last .then() is being executed before the previous is done executing and as a result it sends an empty payload. Following is the snippet:
router.get("/selectedHotels", function(req, res) {
let payload = [];
return collectionRef
.where("isOwner", "==", true)
.get() //fetches owners
.then(snapshot => {
snapshot.forEach(user => {
console.log("User", user);
collectionRef
.doc(user.id)
.collection("venues")
.get() // fetches hotels from owners
.then(snapshot => {
snapshot.forEach(doc => {
if (
doc.data().location.long == req.query.long &&
doc.data().location.lat == req.query.lat
) {
console.log(doc.id, "=>", doc.data());
payload.push({
id: doc.id,
data: doc.data()
});
}
});
})
.catch(err => {
console.log("No hotels of this user", err);
});
});
})
.then(() => {
console.log("Payload", payload);
response(res, 200, "Okay", payload, "Selected hotels");
})
.catch(err => {
console.log("Error getting documents", err);
response(res, 404, "Data not found", null, "No data available");
});
});
Any suggestions? Thanks

Your main mistake is that you have a non-promise returning function, forEach, in the middle of your nested promise chain.
router.get('/selectedHotels',function(req,res){
let payload = [];
return collectionRef.where(...).get()
.then((snapshot)=>{
snapshot.forEach(user => {
// ^^^^^^^^^^^^^^^^^ this means the outer promise doesn't wait for this iteration to finish
// ...
The easiest fix is to map your array of promises, pass them into Promise.all and return them:
router.get('/selectedHotels',function(req,res){
let payload = [];
return collectionRef.where(...).get()
.then((snapshot)=> {
return Promise.all(snapshot.map(
// ...
return collectionRef.doc(user.id).collection('venues').get()
.then(...)
))
That being said, nesting promises like this is an anti-pattern. A promise chain allows us to propagate values through the then callbacks so there's no need to nest them.
Instead, you should chain them vertically.
Here's an example of how you can do that:
router.get("/selectedHotels", function(req, res) {
return collectionRef
.where("isOwner", "==", true)
.get() //fetches owners
// portion of the chain that fetches hotels from owners
// and propagates it further
.then(snapshot =>
Promise.all(
snapshot.map(user =>
collectionRef
.doc(user.id)
.collection("venues")
.get()
)
)
)
// this portion of the chain has the hotels
// it filters them by the req query params
// then propagates the payload array
// (no need for global array)
.then(snapshot =>
snapshot
.filter(
doc =>
doc.data().location.long == req.query.long &&
doc.data().location.lat == req.query.lat
)
.map(doc => ({ id: doc.id, data: doc.data() }))
)
// this part of the chain has the same payload as you intended
.then(payload => {
console.log("Payload", payload);
response(res, 200, "Okay", payload, "Selected hotels");
})
.catch(err => {
console.log("Error getting documents", err);
response(res, 404, "Data not found", null, "No data available");
});
});

Your using firestore so you need to give all documents to map and you also need to return some values to next then. I hope this will help you to solve your problem.
router.get('/selectedVenues',function(req,res){
return collectionRef.where('isOwner', '==', true).get()
.then(snapshot => {
let venues = [];
snapshot.docs.map(user => {
venues.push(collectionRef.doc(user.id).collection('venues').get());
});
return Promise.all(venues);
}).then(snapshots => {
let payload = [];
snapshots.forEach(venues => {
venues.docs
.filter(doc =>
doc.data().longitude == req.query.lng &&
doc.data().latitude == req.query.lat
)
.map(doc =>
payload.push({
id: doc.id,
data: doc.data()
})
)
});
return payload ;
}).then(payload => {
console.log('Payload', payload);
response(res, 200, "Okay", payload, "Selected hotels");
}).catch(err => {
console.log('Error getting documents', err);
response(res, 404, 'Data not found', null, 'No data available');
});
});

You're not returning a promise from within your first then, so there's no way for the code to know that it should wait for an asynchronous result.
router.get('/selectedHotels',function(req,res){
let payload = [];
return collectionRef.where('isOwner', '==', true).get() //fetches owners
.then((snapshot)=>{
var userVenuesPromises = [];
snapshot.forEach(user => {
userVenuesPromises.push(collectionRef.doc(user.id).collection('venues').get());
})
return Promise.all(userVenuesPromises);
})
.then((snapshots) => {
snapshots.forEach((snapshot) => {
snapshot.forEach((doc)=> {
if (doc.data().location.long == req.query.long && doc.data().location.lat == req.query.lat){
console.log(doc.id, '=>', doc.data());
payload.push({
id: doc.id,
data: doc.data()
});
}
});
});
return payload;
})
.then((payload) => {
...
In addition to using Promise.all() to ensure all nested loads are done before continuing to the next step, this also removes the nested promise, instead unpacking the values from the snapshots in a an additional step.

When chaining .then with asynchronous work, you need to return the promise you want to resolve before the next .then is executed. Something like this :
return Promise.all(snapshot.map(user => {
console.log("User", user);
return collectionRef.doc(user.id).collection('venues').get() // fetches hotels from owners
.then(snapshot => {
snapshot.forEach((doc)=> {
if (doc.data().location.long == req.query.long && doc.data().location.lat == req.query.lat){
console.log(doc.id, '=>', doc.data());
payload.push({
id: doc.id,
data: doc.data()
});
}
});
}).catch((err)=>{
console.log('No hotels of this user', err);
});
});
)
You can see it in action in this sample snippet :
function asyncStuff() {
return new Promise(resolve => {
setTimeout(() => {
console.log('async')
resolve();
}, 100)
});
}
function doStuff() {
console.log('started');
asyncStuff()
.then(() => {
return Promise.all([0,1,2].map(() => asyncStuff()));
})
.then(() => {
console.log('second then');
})
.then(() => console.log('finished'));
}
doStuff();
And see that without the return it gives your initial behaviour :
function asyncStuff() {
return new Promise(resolve => {
setTimeout(() => {
console.log('async')
resolve();
}, 100)
});
}
function doStuff() {
console.log('started');
asyncStuff()
.then(() => {
Promise.all([0,1,2].map(() => asyncStuff()));
})
.then(() => {
console.log('second then');
})
.then(() => console.log('finished'));
}
doStuff();

Related

Firebase cloud function to update a record and send a notification

Evening All
Having a few problems performing two actions when a document is created
the below worked until i added the last "then" in the createDocument function where i attempt to send a notification to inform the use via fcm.
exports.createRequest = functions.firestore
.document('requests/{requestId}')
.onCreate((snap, context) => {
var email = snap.data().requestedFromEmail;
checkUserInFirebase(email)
.then((user) => {
//get user profile
return getUserProfile(user.user.uid);
})
.then(userProfile => {
return snap.ref.set({ requestedFromName: userProfile.data().fullName, requestedFromId: userProfile.id }, {merge:true});
})
.then(value=> {
return sendNotification(snap.data().requestedFromId, snap.data().requestedByName);
})
.catch(error => {return error;})
}
)
can anyone see where im going wrong, all the examples im finding send the fcm explicitly from using a exports. Ideally id like to pass the userProfile object through to the send notification function but um not sure how to do that and still set the changes to the document. Full code is below
async function checkUserInFirebase(email) {
return new Promise((resolve) => {
admin.auth().getUserByEmail(email)
.then((user) => {
return resolve({ isError: false, doesExist: true, user });
})
.catch((err) => {
return resolve({ isError: true, err });
});
});
}
async function getUserProfile(uid) {
return admin.firestore()
.collection("users")
.doc(uid)
.get();
}
async function sendNotification(uid, requestedByName) {
const querySnapshot = await db
.collection('users')
.doc(uid)
.collection('tokens')
.get();
const tokens = querySnapshot.docs.map(snap => snap.token);
console.info(tokens);
const payload = {
notification: {
title: 'New Request!',
body: `You received a new request from ${requestedByName}`,
icon: 'your-icon-url',
click_action: 'FLUTTER_NOTIFICATION_CLICK'
}
};
return fcm.sendToDevice(tokens, payload);
}
exports.createRequest = functions.firestore
.document('requests/{requestId}')
.onCreate((snap, context) => {
var email = snap.data().requestedFromEmail;
checkUserInFirebase(email)
.then((user) => {
//get user profile
return getUserProfile(user.user.uid);
})
.then(userProfile => {
return snap.ref.set({ requestedFromName: userProfile.data().fullName, requestedFromId: userProfile.id }, {merge:true});
})
.then(value=> {
return sendNotification(snap.data().requestedFromId, snap.data().requestedByName);
})
.catch(error => {return error;})
}
)
Your funciton needs to return a promise that resolves when all the async work is complete. Right now it returns nothing, which means that Cloud Functions might terminate it up before the work is done. You should return the promise chain:
return checkUserInFirebase(email)
.then((user) => {
//get user profile
return getUserProfile(user.user.uid);
})
.then(userProfile => {
return snap.ref.set({ requestedFromName: userProfile.data().fullName, requestedFromId: userProfile.id }, {merge:true});
})
.then(value=> {
return sendNotification(snap.data().requestedFromId, snap.data().requestedByName);
})
.catch(error => {return error;})
}
Note the return at the start of the whole thing.
See the documentation for more information.

Call an async function in foreach loop and return array once loop is done

I'm trying to request data from an API in a foreach loop and push that data to an array and then return that array at the end. Here is my code:
db
.collection('posts')
.orderBy('createdAt', 'desc')
.limit(10)
.get()
.then((data) => {
let posts = [];
data.forEach((doc) => {
db
.doc(`/users/${doc.data().userHandle}`)
.get()
.then((userDoc) => {
posts.push({
postId: doc.data().id,
userHandle: doc.data().userHandle,
userImageUrl: userDoc.data().imageUrl,
imageUrl: doc.data().imageUrl,
});
})
})
return res.json(posts);
})
.catch((err) => {
console.error(err);
res.status(500).json({ error: err.code});
});
From this, the posts array return en empty array or object, even when i replace return res.json(posts) with
.then(() => {
return res.json(posts);
})
Any help is awesome!!!
The array is empty because by the time the response is sent, promises with posts are still pending resolution.
In order to fix this issue, you can collect all the promises in an array using .map(), wait for them to resolve with help of Promise.all() and send the response after that:
db
.collection('posts')
.orderBy('createdAt', 'desc')
.limit(10)
.get()
.then((data) => {
const promises = data.map((doc) => {
return db
.doc(`/users/${doc.data().userHandle}`)
.get()
.then((userDoc) => {
return {
postId: doc.data().id,
userHandle: doc.data().userHandle,
userImageUrl: userDoc.data().imageUrl,
imageUrl: doc.data().imageUrl,
};
})
});
Promise.all(promises).then(posts => {
res.json(posts);
})
})
.catch((err) => {
console.error(err);
res.status(500).json({ error: err.code});
});

Can't send request in componentDidMount React Native

I have an issue with sending a request to backend from my componentDidMount(). Basically I need to do two things before rendering screen:
Obtain data from API call and save it to state
Send that obtained data to backend and take response values from backend.
The problem I've faced on first step is that setState() is async, and even though my array is not empty (I see it's elements in render() and componentDidUpdate fucntion) in componentDidMount() when I console.log() array it will be empty. Now, the issue is: I still need to send that state array to backend before showing the screen. But how can I do it, when it appears empty there?
I have everything working fine if I send the request from the Button element in my render function, but that's not exactly what I need. Any suggestions?
this.state = {
ActivityItem: [],
}
componentDidMount() {
this.getDataFromKit(INTERVAL); //get data from library that does API calls
this.sendDataToServer(); //sending to backend
}
componentDidUpdate() {
console.log("componentDidUpdate ", this.state.ActivityItem) // here array is not empty
}
getDataFromKit(dateFrom) {
new Promise((resolve) => {
AppleKit.getSamples(dateFrom, (err, results) => {
if (err) {
return resolve([]);
}
const newData = results.map(item => {
return { ...item, name: "ItemAmount" };
});
this.setState({ ActivityItem: [...this.state.ActivityItem, ...newData] })
})
});
And last one:
sendDataToServer() {
UserService.sendActivityData(this.state.ActivityItem).then(response => {
}).catch(error => {
console.log(error.response);
})
And here it works as expected:
<Button
title='send data!'
onPress={() => this.sendDataToServer()
} />
UPDATE
If I have like this (wrapped inside initKit function this will return undefined.
AppleKit.initKit(KitPermissions.uploadBasicKitData(), (err, results) => {
if (err) {
return;
}
return new Promise((resolve) => {
AppleKit.getSamples(dateFrom, (err, results) => {
if (err) return resolve([]);//rest is the same
you have to wait for the promise to resolve. You need something like this:
componentDidMount() {
this.getDataFromKit(INTERVAL).then(result => {
this.sendDataToServer(result); //sending to backend
}).catch(e => console.error);
}
and you can update your other function that fetches data to return it:
getDataFromKit(dateFrom) {
return new Promise((resolve) => {
AppleKit.getSamples(dateFrom, (err, results) => {
if (err) return resolve([]);
const newData = results.map(item => {
return { ...item, name: "ItemAmount" };
});
const allData = [ ...this.state.ActivityItem, ...newData ];
this.setState({ ActivityItem: allData });
resolve(allData);
});
});
}
finally, you need the 'sendData' function to not depend on state, but get a param passed to it instead:
sendDataToServer(data) {
UserService.sendActivityData(data).then(response => {
// ... do response stuff
}).catch(error => {
console.log(error.response);
});
}
Handling Multiple Requests
if the requests don't depend on each other:
componentDidMount() {
Promise.all([
promise1,
promise2,
promise3,
]).then(([ response1, response2, response3 ]) => {
// do stuff with your data
}).catch(e => console.error);
}
if the requests do depend on each other:
componentDidMount() {
let response1;
let response2;
let response3;
promise1().then(r => {
response1 = r;
return promise2(response1);
}).then(r => {
response2 = r;
return promise3(response2);
}).then(r => {
response3 = r;
// do stuff with response1, response2, and response3
}).catch(e => console.error);
}
as far as your update, it seems like you wrapped your async request in another async request. I'd just chain it instead of wrapping it:
make the initKit a function that returns a promise
function initKit() {
return new Promise((resolve, reject) => {
AppleKit.initKit(
KitPermissions.uploadBasicKitData(),
(err, results) => {
if (err) reject({ error: 'InitKit failed' });
else resolve({ data: results });
}
);
});
}
make get samples a separate function that returns a promise
function getSamples() {
return new Promise((resolve) => {
AppleKit.getSamples(dateFrom, (err, results) => {
if (err) resolve([]); //rest is the same
else resolve({ data: results });
});
});
}
chain 2 promises back to back: if initKit fails, it will go in the .catch block and getSamples wont run
componentDidMount() {
initKit().then(kit => {
return getSamples();
}).then(samples => {
// do stuff with samples
}).catch(e => console.log);
}

How to ensure function with promise continues execution

I have a function in a react-native application that is executed on a button press, it calls an async action, and then resets the navigation stack. the function looks something like this:
confirm = () => {
this.props.addEvent(args);
this.props.loading != true ? this.props.navigation.dispatch(popAction) : null;
}
the addEvent() action looks like this:
export const addEvent = (event, msgId, convoId) => {
return (dispatch) => {
dispatch({ type: types.UPDATING });
console.log('Firestore Write: (actions/agenda) => addEvent()');
return firebase
.firestore()
.collection('events')
.add({
date: event.date,
token: event.token,
withName: event.sender
})
.then((success) => {
updateReqToScheduled(msgId, { uid: event.schedulerId, convoId: convoId });
dispatch({ type: types.EVENT_ADD_SUCCESS });
})
.catch((err) => {
console.log('ERROR => addEvent()' + '\n' + err.message);
dispatch({ type: types.EVENT_ADD_FAIL, info: err.message });
});
};
};
the pre-firebase log statement executes, the document is added, and the updateReqToScheduled function execution begins. it looks like:
const updateReqToScheduled = (id, reader) => {
console.log('Firestore Write: (actions/agenda) => updateReqToScheduled()');
return firebase
.firestore()
.collection('messages')
.doc(id)
.update({ read: true })
.then((success) => {
return updateConvoUnread(reader);
})
.catch((err) => {
console.log(err.message);
});
};
this function also executes completely. the updateConvoUnread() function looks like:
const updateConvoUnread = (reader) => {
console.log('Firestore Read: (actions/agenda) => updateConvoUnread( 1 )');
return firebase
.firestore()
.collection('messages')
.where('userConvos', 'array-contains', reader.convoId)
.where('sender', '!=', reader.uid)
.where('read', '==', false)
.get()
.then((querySnapshot) => {
console.log('PRECONDITION');
if (querySnapshot.empty == true) {
console.log('Firestore Write: (actions/agenda) => updateConvoUnread( 2 )');
return firebase
.firestore()
.collection('user-conversations')
.doc(reader.convoId)
.update({ unread: false });
} else {
console.log('ELSE CLAUSE');
//return;
}
console.log('POST - IF');
})
.catch((err) => {
console.log('ERROR => updateConvoUnread(): ' + err.message);
});
};
the very first pre-firebase log statement executes, but no other log statement executes, not in the then(), not in the catch() and not in the conditional statement, thus the function execution halts and the navigation stack reset isn't executed.
Can anyone advise on the situation?
Use async on your function declarations and await on the firebase calls
Something was wrong with the firebase query. a name of the field was incorrect. it halted everything. once corrected the function finished as expected and the route was reset.

Promises are not behaving as I expect them to

I'm using Express for routing and Sequelize for DB management.
app.get('/api/users/:username', (req, res) => {
let username = req.params.username;
findChattersPerRole()
.then(chattersPerRole => {
console.log('instakbot should\'ve been added by now...');
});
});
The function findChattersPerRole returns an object with each user's username and role as another object.
const findChattersPerRole = () => {
return fetch('https://tmi.twitch.tv/group/user/instak/chatters')
.then(parseJSON)
.then(r => {
let chatters = r.chatters;
let chattersPerRole = Object.keys(chatters).map(role => {
return chatters[role].map(username => {
console.log('findOrCreateViewer will be executed after this');
findOrCreateViewer(username, role);
return {
username: username,
role: role
};
});
});
return Promise.resolve(flattenDeep(chattersPerRole));
}).catch(err => {
console.log(`Error in fetch: ${err}`);
});
};
The problem is, in my route, I expect the console.log('instakbot should\'ve been added by now...'); to be executed AFTER my viewers got inserted into the database because in my function findChattersPerRole I already insert them with the function findOrCreateViewer. I expect this to happen because in my route I write the console.log when findChattersPerRole() is resolved...
const findOrCreateViewer = (username, role) => {
return Viewer.findOrCreate({
where: {
username
},
defaults: {
instakluiten: 5,
role
}
}).spread((unit, created) => {
console.log('unit is: ', unit.dataValues.username);
if(created){
return `created is ${created}`;
}else{
return unit;
}
});
};
However, in my terminal you can see that this is not the way it's happening... Why aren't my promises being executed at the expected time?
Screenshot of my terminal
The return {username: ...} after findOrCreateViewer(username, role); happens immediately after the function is called and before any data has been inserted. That also means that return Promise.resolve(flattenDeep(chattersPerRole)); happens before any data has been inserted, etc.
You said findOrCreateViewer returns a promise, so you need to wait until that promise is resolved (i.e. wait until after the data was inserted) before continuing with something else.
You want chattersPerRole to be an array of (arrays of) promises and only proceed after all the promises are resolved.
This is easy to do with Promise.all:
const findChattersPerRole = () => {
return fetch('https://tmi.twitch.tv/group/user/instak/chatters')
.then(parseJSON)
.then(r => {
let chatters = r.chatters;
let chattersPerRole = Object.keys(chatters).map(
role => chatters[role].map(username => {
console.log('findOrCreateViewer will be executed after this');
return findOrCreateViewer(username, role).then(
() => ({username, role})
);
});
);
return Promise.all(flattenDeep(chattersPerRole));
}).catch(err => {
console.log(`Error in fetch: ${err}`);
});
};
Now the promise returned by findChattersPerRole will be resolved after all the promises returned by findOrCreateViewer are resolved.
Promises are doing no magic. Returning a promise doesn't mean that calling the function will block, but rather that you can easily chain callbacks to do something with the result. You'll need to use
function findChattersPerRole() {
return fetch('https://tmi.twitch.tv/group/user/instak/chatters')
.then(parseJSON)
.then(r => {
let chatters = r.chatters;
let chattersPerRole = Object.keys(chatters).map(role => {
return chatters[role].map(username => {
console.log('findOrCreateViewer will be executed after this');
return findOrCreateViewer(username, role).then(() => {
// ^^^^^^ ^^^^^
return {
username: username,
role: role
};
});
});
});
return Promise.all(flattenDeep(chattersPerRole));
// ^^^ get a promise for an array of results from an array of promises
}).catch(err => {
console.log(`Error in fetch: ${err}`);
});
}

Categories

Resources