Cloud function not executing ref.once on realtime db - javascript

My cloud function was working fine this morning. But now it suddenly stopped working. So I added a few more logs and I noticed that rootRef.once was not executing. Why could this be happening? The function keeps on timing out.
exports.assignTeams = functions.https.onCall((data, context) => {
const judgeId = data.judgeId;
console.log("Assigning teams to judge ", judgeId);
var db = admin.database();
var rootRef = db.ref("/");
rootRef.once(
"value",
function(snapshot) {
console.log("Passed value ", snapshot);
processAssignedQueue(snapshot, judgeId);
},
function(error) {
console.log("ERROR:\n" + error);
return { teams: [] };
}
);
});
The console logs from Firebase:

The problem is that you're not returning a promise from the function that resolves with the data to send to the caller. Right now, your function is actually returning nothing, because once() is asynchronous and returns immediately before the query is complete. When a callable function returns without a promise, it shuts down immediately, an any asynchronous work will not complete.
Also, you should note that the return statements inside the callbacks you passed to once() are only return values from those individual function callbacks, not the entire function.
What you should do instead is make use of the promise returned by once(), and use that to determine what to return to the client. Here's a simple example:
return rootRef.once("value")
.then(snapshot => {
console.log("Passed value ", snapshot);
return { data: snapshot.val() }
})
.catch(error => {
console.log("ERROR:\n" + error);
return { teams: [] };
});
In order to write Cloud Functions effectively, you will need to fully understand how javascript promises work. If you don't work with them correctly, your functions will fail in mysterious ways.

Related

Return the previous value with Cloud functions (firestore)

My application (React) uses the service of Firestore. When a user changes his username, I must verify that this username is unique. For that, I want to use a Cloud function for more reliability.
However, my function always goes into an infinite loop. I don't see how I can get out of it.
Here is my function.
Any help would be much appreciated.
exports.checkUsername = functions.firestore
.document('/users/{userId}')
.onUpdate((change, context) => {
const before = change.before.data();
const after = change.after.data();
if (after.username === before.username) {
return null;
}
db.collection('users').where('username', '==', after.username).get()
.then((query) => {
if (!query.empty) {
// Username not available
return change.before.ref.update({ username: before.username });
}
// Username available
return null;
})
.catch(() => {
return change.before.ref.update({ username: before.username });
})
});
You're not dealing with promises correctly. Your function needs to return a promise that resolves after all the asynchronous work is complete. That's how Cloud Functions knows it's safe to terminate and clean up. Right now your function returns nothing in the case where the before and after usernames are not equal.
Minimally, you should return the final promise from the chain:
return db.collection('users').where('username', '==', after.username).get()
.then(...)
.catch(...)

Firebase Cloud Functions and Firestore onCreate Listener Problem

I am trying to make a chat app and using firestore and cloud functions.
My firestore structure is
Users -> name,username,email,etc...
Conversations -> members:[memberId1,memberId2]
and when there is a new conversation created I am adding a conversation collection to Users/{memberId}/Conversation collection but either its not getting created or it takes long time to gets created
this is my cloud function to add Conversation info to Users collection
functions.firestore
.document('Conversations/{conversationId}')
.onCreate((snapshot:any, context:any) => {
const data = snapshot.data();
const conversationId = context.params.conversationId;
if (data) {
const members = data.members;
if(!Array.isArray(members)){
console.log("Members is not an array")
return null;
}
for ( const member of members) {
const currentUserId = member;
const remainingUserIDs = members.filter((u:string) => u !== currentUserId);
remainingUserIDs.forEach(async (m:string) => {
return admin
.firestore()
.collection('Users')
.doc(m)
.get()
.then((_doc) => {
const userData = _doc.data();
if (userData) {
return admin
.firestore()
.collection('Users')
.doc(currentUserId)
.collection('Conversations')
.doc(m)
.create({
conversationId: conversationId,
image: userData.profilePic,
username: userData.username,
unseenCount: 0,
userId: m,
});
}
return null;
})
.catch((err) => {
console.log(err);
return null;
});
});
}
}
return null;
});
and i see in the logs its gets called without error but no data getting added or it takes long to gets created.
what am I doing wrong ?
and another interesting thing is I have tested the function on emulator and it works fine but on production there is a problem
Your code is not waiting for each of the promises in the foreach loop to resolve before returning from the overall function. The function is just returning null immediately. Calling then and catch on a promise doesn't make the code pause.
Your function must return a promise that resolves only after all other async work is finished. If you don't, the function will terminate early and clean up before the work is complete. This means you have to track all the promises that you kick off in the foreach loop. I suggest reading these to better understand the problem and get some ideas on how to manage this:
Node JS Promise.all and forEach
How to use promise in forEach loop of array to populate an object
How to call promise inside forEach?

Firestore took too long to fetch data therefore I couldn't display it

I have a function which fetch a data from Firestore :
getLastTime(collectionName: string) {
const docRef = this.afs.firestore.collection(collectionName).doc(this.User).collection('lastTime').doc('lastTime');
docRef.get().then(doc => {
if (doc.exists) {
this.get = doc.data().lastTime;
} else {
this.get = 'Never done';
}
}).catch(error => {
console.log('Error getting document:', error);
});
return this.get;
}
For my test I actually have a string value inside the doc 'lastTime' which is a string.
Inside ngOnInit(), I called my function and console.log the result
this.InjuredLastTime = this.getLastTime('INJURY');
console.log(this. this.InjuredLastTime);
Normally I should have my string printed inside the console but I got undefined...
Maybe it's because Firestore do not fetch fast enough my data, but I am quiet surprised since Firestore is quiet fast normally...
You don't actually wait for the promise that is created by docRef.get() before you return from getLastTime(). So, unless the call to firebase is instant (e.g. never) this won't work.
The correct solution really depends on what you are doing with this.InjuredLastTime. But one approach would just be to return a promise and set it after it is ready:
getLastTime(collectionName: string) {
const docRef = this.afs.firestore.collection(collectionName).doc(this.User).collection('lastTime').doc('lastTime');
return docRef.get().then(doc => {
if (doc.exists) {
return doc.data().lastTime;
} else {
return 'Never done';
}
}).catch(error => {
console.log('Error getting document:', error);
return null;
});
}
Then, instead of the assignment synchronously, do it asynchronously:
this.getLastTime('INJURY').then(result => { this.InjuredLastTime = result });
Data is loaded from Firestore asynchronously, since it may take some time before the data comes back from the server. To prevent having to block the browser, your code is instead allowed to continue to run, and then your callback is called when the data is available.
You can easily see this with a few well-placed log statements:
const docRef = this.afs.firestore.collection(collectionName).doc(this.User).collection('lastTime').doc('lastTime');
console.log('Before starting to get data');
docRef.get().then(doc => {
console.log('Got data');
});
console.log('After starting to get data');
If you run this code, you'll get:
Before starting to get data
After starting to get data
Got data
This is probably not the order that you expected the logging output in, but it is actually the correct behavior. And it completely explains why you're getting undefined out of your getLastTime function: by the time return this.get; runs, the data hasn't loaded yet.
The simplest solution in modern JavaScript is to mark your function as async and then await its result. That would look something like this:
async function getLastTime(collectionName: string) {
const docRef = this.afs.firestore.collection(collectionName).doc(this.User).collection('lastTime').doc('lastTime');
doc = await docRef.get();
if (doc.exists) {
this.get = doc.data().lastTime;
} else {
this.get = 'Never done';
}
return this.get;
}
And then call it with:
this.InjuredLastTime = await this.getLastTime('INJURY');

How to fix "Function returned undefined, expected Promise or value" when creating a function for Firebase?

I created a Firebase function to trigger push notifications, but I get the following register on Firebase,"Function returned undefined, expected Promise or value" and "snapshot.docs is not iterable at admin.firestore.collection.get.then(/srv/index.js:20:38)".Do you guys have any Idea how to fix it?
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
var msgData;
exports.destaquesTrigger = functions.firestore.document(
'destaques/{destaquesId}'
).onCreate((snapshot, context) => {
msgData = snapshot.data();
admin.firestore().collection('pushtokens').get().then((snapshots) =>
{
var tokens = [];
if (snapshots.empty) {
console.log('No Devices');
} else {
for (var token of snapshot.docs) {
tokens.push(token.data().devtoken);
}
var payload = {
"notification": {
"title": msgData.notif1,
"body": msgData.notif2,
"sound": "default"
},
"data": {
"sendername": msgData.notif3,
"message": msgData.notif4,
}
}
return admin.messaging().sendToDevice(tokens,
payload).then((response) => {
console.log('Pushed them all');
}).catch((err) => {
console.log(err);
})
}
})
})
the error:
6:22:54.175 PM destaquesTrigger Function execution started
6:22:54.175 PM destaquesTrigger Billing account not configured.
External network is not accessible and quotas are severely
limited. Configure billing account to remove these restrictions
6:22:54.676 PM destaquesTrigger Function returned undefined,
expected Promise or value
6:22:57.168 PM destaquesTrigger Function execution took 2993 ms,
finished with status: 'ok'
6:23:23.652 PM destaquesTrigger Unhandled rejection
6:23:23.752 PM destaquesTrigger TypeError: snapshot.docs is not
iterable at admin.firestore.collection.get.then
(/srv/index.js:20:38) at <anonymous> at
process._tickDomainCallback (internal/process/next_tick.js:229:7)
You're not returning any value from the top-level function definition. This means that the Google Cloud Functions environment has no way to know when your function is done, so it has no way to know when to stop charging you for its use.
To make sure Cloud Functions knows when your code is done, you need to either return a value from the top-level function (in the case that all work happens synchronously), or return a promise from the top-level function (in the case that some work continues after the closing } of the function).
Right now the top-level code in your Cloud Function is not returning anything, which means that the result of calling this function is undefined. And that's what the Cloud Functions environment complains about.
In your case, you're loading data from Firestore, which is an asynchronous operation, so you will need to return a promise:
exports.destaquesTrigger = functions.firestore.document('destaques/{destaquesId}').onCreate((snapshot, context) => {
msgData = snapshot.data();
return admin.firestore().collection('pushtokens').get().then((snapshots) =>
{
var tokens = [];
if (snapshots.empty) {
console.log('No Devices');
return false;
} else {
for (var token of snapshot.docs) {
tokens.push(token.data().devtoken);
}
var payload = {
"notification": {
"title": msgData.notif1,
"body": msgData.notif2,
"sound": "default"
},
"data": {
"sendername": msgData.notif3,
"message": msgData.notif4,
}
}
return admin.messaging().sendToDevice(tokens,
payload).then((response) => {
console.log('Pushed them all');
}).catch((err) => {
console.log(err);
})
}
})
})
The changes I made:
Added a return to return admin.firestore().collection('pushtokens').get(). This is needed so that your return admin.messaging().sendToDevice( can "bubble up" to be returned to Cloud Functions.
Added a return false in case there are no snapshots, as otherwise you're not returning anything.
I am assuming that Mr. Kanitz has solved his issue. But since no one answered the latter part of his question, I may have a possible solution. Change the following snippet:
(var token of snapshot.docs)
to:
(var token of snapshots.docs)
I think you used the wrong variable to get docs from! Hope that helps anyone else following Raja Yogan's FCM tutorial!
Your branch under if (snapshots.empty) does not return a defined value. The error message indicates it expects you to return a Promise value. Try returning Promise.resolve() in that branch.

Firebase Cloud Functions sometimes not executing until the end of the function

If I have a cloud function like this, will the function setData() always execute until the end of the function(console either "successfully set data!" or "could not set data")? Because I have a function set up in a similar manner as this and it sometimes seems to stop executing in the middle of its execution.
function setData() {
admin.database().ref(`someLocation`).set(true)
.then(() => {
return admin.database().ref(`anotherLocation`).set(true)
}).then(() => {
console.log("successfully set data!")
}).catch(err => {
console.log("could not set data", err)
})
}
exports.newPotentialMember = functions.database.ref('listenLocation')
.onCreate(event => {
setData()
})
You're not returning any value from your newPotentialMember right now. That means that Cloud Functions can stop execution of your code as soon as newPotentialMember returns, which is straight after setData starts. Since writing to the database happens asynchronously, you have to return a promise from newPotentialMember that resolves after all writes have completed.
function setData() {
return admin.database().ref(`someLocation`).set(true)
.then(() => {
return admin.database().ref(`anotherLocation`).set(true)
})
}
exports.newPotentialMember = functions.database.ref('listenLocation')
.onCreate(event => {
return setData()
})
I recommend you carefully read the Firebase documentation on synchronous functions, asynchronous functions, and promises.

Categories