How to subscribe a firebase function so that it executes via a pub/sub trigger - firebase

I have been asked to create a firebase (fb) function that triggers onUpdate. Then I have to gather a bunch of information from the fb database and publish a message so that another function triggers at that point.
fb update triggers functionA
functionA publishes a message
functionB is a subscriber to that topic and is triggered after the message publishes.
I have the basics of the onUpdate trigger below:
const functions = require("firebase-functions"),
Promise = require("promise"),
PubSub = require(`#google-cloud/pubsub`),
admin = require("firebase-admin");
const pubsub = new PubSub();
exports.checkInOrder = functions.database
.ref("/orders/{id}")
.onUpdate((change, context) => {
const after = change.after.val();
// check the status: "pending-pickup" or "fulfilled" TODO
if (after.status === "new") {
console.log("ended because package is new.");
return null;
}
let dsObj = {};
const orderId = context.params.id;
const topicName = 'check-in-order';
const subscriptionName = 'check-in-order';
return // how would I send the message to the pubsub here?
});
So to summarize:
How do I send a message to pubsub
How do I subscribe a firebase function to trigger when a topic receives a message?
If it sounds very confusing I'm sorry - I am completely lost here. Thanks!

So I finally figured it out. Pretty straight forward, just felt overwhelmed with learning all of this stuff at once and in a rushed time frame. Below is my code. I included the whole page so in case this might help someone else out there in the future.
const functions = require("firebase-functions"),
Promise = require("promise"),
PubSub = require(`#google-cloud/pubsub`),
admin = require("firebase-admin");
const init = () => {
const topicName = "check-in-order";
pubsub
.createTopic(topicName)
.then(results => {
const topic = results[0];
console.log(`Topic ${topicName} created.`);
return;
})
.catch(err => {
console.error("ERROR on init:", err);
return;
});
};
const pubsub = new PubSub();
exports.orderCreated = functions.database
.ref("/orders/{id}")
.onUpdate((change, context) => {
console.log("it's happening!");
const after = change.after.val();
console.log("after::::>", after)
if (after.status === "new") {
console.log('stopped because status is new');
return null;
}
if (after.status === "pending-pickup" || after.status === "fulfilled") {
const orderId = context.params.id;
const topicName = "check-in-order";
let { assignedSlot, userId, parcelLockerId, carrier, trackingNumber, orderInDate, pickUpCode, status, } = after;
const dsObj = {
order: {
orderId,
userId,
parcelLockerId,
carrier,
trackingNumber,
orderInDate,
pickUpCode,
status,
}
};
const dataBuffer = new Buffer.from(dsObj.toString());
// publish to trigger check in function
return pubsub
.topic(topicName)
.publisher()
.publish(dataBuffer)
.then(messageId => {
console.log(`:::::::: Message ${messageId} has now published. :::::::::::`);
return true;
})
.catch(err => {
console.error("ERROR:", err);
throw err;
});
}
return false;
});
exports.checkInOrder = () => {
}
exports.checkIn = functions.pubsub.topic('check-in-order').onPublish((message) => {
console.log("everything is running now", message);
return true;
});
init();

Related

FCM very slow and unreliable when sending to a group of recipients through Cloud Function

I have the following Function that:
Listens for document (text message) creation
Grab IDs of members of a group chat
Get the FCM Tokens for each member
With a for-loop, send messages to group members
exports.sendChatMessage = functions.firestore
.document("chats/{mealID}/messages/{messageID}")
.onCreate((snap, context) => {
const data = snap.data();
const mealID = context.params.mealID;
const senderID = data.senderID;
const senderName = data.senderName;
const messageContent = data.content;
var docRef = db.collection("chats").doc(mealID);
docRef
.get()
.then((doc) => {
if (doc.exists) {
const docData = doc.data();
const mealName = docData.name;
const userStatus = docData.userStatus;
var users = docData.to;
var eligibleUsers = users.filter(
(user) => userStatus[user] == "accepted"
);
eligibleUsers.push(docData.from);
// get fcmTokens from eligibleUsers and send the messagme
db.collection("users")
.where("uid", "in", eligibleUsers)
.get()
.then((snapshot) => {
var fcmTokens = [];
var thumbnailPicURL = "";
// get thumbnailpic of the sender and collect fcmTokens
snapshot.forEach((doc) => {
if (doc.data().uid == senderID) {
thumbnailPicURL =
doc.data().thumbnailPicURL == null
? "https://i.imgur.com/8wSudUk.png"
: doc.data().thumbnailPicURL;
} else {
fcmTokens.push(doc.data().fcmToken);
}
});
// send the message fcmTokens
fcmTokens.forEach((token) => {
if (token != "") {
const fcmMessage = {
message: {
token: token,
notification: {
title: mealName,
body: senderName + ": " + messageContent,
image: thumbnailPicURL,
},
apns: {
payload: {
aps: {
category: "MESSAGE_RECEIVED",
},
MEAL_ID: mealID,
},
},
},
};
tokenManger.sendFcmMessage(fcmMessage);
}
});
return true;
});
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
return false;
}
})
.catch((error) => {
console.log("Error getting document:", error);
return false;
});
return true;
});
My send function comes from a helper file that uses the HTTP V1 protocol to build the send-request:
const { google } = require("googleapis");
const https = require("https");
const MESSAGING_SCOPE = "https://www.googleapis.com/auth/firebase.messaging";
const SCOPES = [MESSAGING_SCOPE];
const PROJECT_ID = MY_PROJECT_ID;
const HOST = "fcm.googleapis.com";
const PATH = "/v1/projects/" + PROJECT_ID + "/messages:send";
exports.getAccessToken = () => {
return new Promise(function (resolve, reject) {
const key = require("./service-account.json");
var jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
SCOPES,
null
);
jwtClient.authorize(function (err, tokens) {
if (err) {
reject(err);
return;
}
resolve(tokens.access_token);
});
});
};
//send message
exports.sendFcmMessage = (fcmMessage) => {
this.getAccessToken().then(function (accessToken) {
var options = {
hostname: HOST,
path: PATH,
method: "POST",
headers: {
Authorization: "Bearer " + accessToken,
},
// … plus the body of your notification or data message
};
var request = https.request(options, function (resp) {
resp.setEncoding("utf8");
resp.on("data", function (data) {
console.log("Message sent to Firebase for delivery, response:");
console.log(data);
});
});
request.on("error", function (err) {
console.log("Unable to send message to Firebase");
console.log(err);
});
request.write(JSON.stringify(fcmMessage));
request.end();
});
};
It worked all fine in the emulator but once deployed, there're significant delays (~3 mins):
I also noticed that the console says the cloud function finishes execution BEFORE sendFcmMessage logs success messages.
I did some research online, it appears that it might have something to do with the usage of Promise but I wasn't sure if that's the sole reason or it has something to do with my for-loop.
The Problem
To summarize the issue, you are creating "floating promises" or starting other asynchronous tasks (like in sendFcmMessage) where you aren't returning a promise because they use callbacks instead.
In a deployed function, as soon as the function returns its result or the Promise chain resolves, all further actions should be treated as if they will never be executed as documented here. An "inactive" function might be terminated at any time, is severely throttled and any network calls you make (like setting data in database or calling out to FCM) may never be executed.
An indicator that you haven't properly chained the promises is when you see the function completion log message ("Function execution took...") before other messages you are logging. When you see this, you need to look at the code you are running and confirm whether you have any "floating promises" or are using callback-based APIs. Once you have changed the callback-based APIs to use promises and then made sure they are all chained together properly, you should see a significant boost in performance.
The fixes
Sending the message data to FCM
In your tokenManger file, getAccessToken() could be reworked slightly and sendFcmMessage should be converted to return a Promise:
exports.getAccessToken = () => {
return new Promise(function (resolve, reject) {
const key = require("./service-account.json");
const jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
SCOPES,
null
);
jwtClient.authorize(
(err, tokens) => err ? reject(err) : resolve(tokens.access_token)
);
});
};
//send message
exports.sendFcmMessage = (fcmMessage) => {
// CHANGED: return the Promise
return this.getAccessToken().then(function (accessToken) {
const options = {
hostname: HOST,
path: PATH,
method: "POST",
headers: {
Authorization: "Bearer " + accessToken,
},
// … plus the body of your notification or data message
};
// CHANGED: convert to Promise:
return new Promise((resolve, reject) => {
const request = https.request(options, (resp) => {
resp.setEncoding("utf8");
resp.on("data", resolve);
resp.on("error", reject);
});
request.on("error", reject);
request.write(JSON.stringify(fcmMessage));
request.end();
});
});
};
However, the above code was built for googleapis ^52.1.0 and google-auth-library ^6.0.3. The modern versions of these modules are v92.0.0 and v7.11.0 respectively. This means you should really update the code to use these later versions like so:
// Import JWT module directly
const { JWT } = require('google-auth-library');
// FIREBASE_CONFIG is a JSON string available in Cloud Functions
const PROJECT_ID = JSON.parse(process.env.FIREBASE_CONFIG).projectId;
const FCM_ENDPOINT = `https://fcm.googleapis.com/v1/projects/${PROJECT_ID}/messages:send`;
const FCM_SCOPES = ["https://www.googleapis.com/auth/firebase.messaging"];
exports.sendFcmMessage = (fcmMessage) => {
const key = require("./service-account.json"); // consider moving outside of function (so it throws an error during deployment if its missing)
const client = new JWT({
email: key.client_email,
key: key.private_key,
scopes: FCM_SCOPES
});
return client.request({ // <-- this uses `gaxios`, Google's fork of `axios` built for Promise-based APIs
url: FCM_ENDPOINT,
method: "POST",
data: fcmMessage
});
}
Better yet, just use the messaging APIs provided by the Firebase Admin SDKs that handle the details for you. Just feed it the message and tokens as needed.
import { initializeApp } from "firebase-admin/app";
import { getMessaging } from "firebase-admin/messaging";
initializeApp(); // initializes using default credentials provided by Cloud Functions
const fcm = getMessaging();
fcm.send(message) // send to one (uses the given token)
fcm.sendAll(messagesArr) // send to many at once (each message uses the given token)
fcm.sendMulticast(message) // send to many at once (uses a `tokens` array instead of `token`)
The Cloud Function
Updating the main Cloud Function, you'd get:
exports.sendChatMessage = functions.firestore
.document("chats/{mealID}/messages/{messageID}")
.onCreate((snap, context) => {
const mealID = context.params.mealID;
const { senderID, senderName, content: messageContent } = snap.data();
const docRef = db.collection("chats").doc(mealID);
/* --> */ return docRef
.get()
.then((doc) => {
if (!doc.exists) { // CHANGED: Fail fast and avoid else statements
console.log(`Could not find "chat:${mealID}"!`);
return false;
}
const { userStatus, to: users, name: mealName, from: fromUser } = doc.data();
const eligibleUsers = users.filter(
(user) => userStatus[user] == "accepted"
);
eligibleUsers.push(fromUser);
// get fcmTokens from eligibleUsers and send the message
/* --> */ return db.collection("users")
.where("uid", "in", eligibleUsers) // WARNING: This will only work for up to 10 users! You'll need to break it up into chunks of 10 if there are more.
.get()
.then(async (snapshot) => {
const fcmTokens = [];
let thumbnailPicURL = "";
// get thumbnailpic of the sender and collect fcmTokens
snapshot.forEach((doc) => {
if (doc.get("uid") == senderID) {
thumbnailPicURL = doc.get("thumbnailPicURL"); // update with given thumbnail pic
} else {
fcmTokens.push(doc.get("fcmToken"));
}
});
const baseMessage = {
notification: {
title: mealName,
body: senderName + ": " + messageContent,
image: thumbnailPicURL || "https://i.imgur.com/8wSudUk.png", // CHANGED: specified fallback image here
},
apns: {
payload: {
aps: {
category: "MESSAGE_RECEIVED",
},
MEAL_ID: mealID,
},
}
}
// log error if fcmTokens empty?
// ----- OPTION 1 -----
// send the message to each fcmToken
const messagePromises = fcmTokens.map((token) => {
if (!token) // handle "" and undefined
return; // skip
/* --> */ return tokenManger
.sendFcmMessage({
message: { ...baseMessage, token }
})
.catch((err) => { // catch the error here, so as many notifications are sent out as possible
console.error(`Failed to send message to "fcm:${token}"`, err);
})
});
await Promise.all(messagePromises); // wait for all messages to be sent out
// --------------------
// ----- OPTION 2 -----
// send the message to each fcmToken
await getMessaging().sendAll(
fcmTokens.map((token) => ({ ...baseMessage, token }))
);
// --------------------
return true;
})
.catch((error) => {
console.log("Error sending messages:", error);
return false;
});
})
.catch((error) => {
console.log("Error getting document:", error);
return false;
});
});
I found out that the culprit is my queries to db. Like #samthecodingman commented, I was creating floating Promises.
Originally, I have codes like:
db.collection("users")
.where("uid", "in", eligibleUsers)
.get()
.then((snapshot) => {...}
All I needed to do is to return that call:
return db.collection("users")
.where("uid", "in", eligibleUsers)
.get()
.then((snapshot) => {...}
Although it's still not instant delivery, it's much faster now.

How to get all items from subcollection Firebase Firestore Vue

How do I get all the comments from the subcollection?
This is mine reusable function to get comments collection.
import { ref, watchEffect } from 'vue';
import { projectFirestore } from '../firebase/config';
const getCollection = (collection, id, subcollection) => {
const comments = ref(null);
const error = ref(null);
// register the firestore collection reference
let collectionRef = projectFirestore
.collection(collection)
.doc(id)
.collection(subcollection);
const unsub = collectionRef.onSnapshot(
snap => {
let results = [];
snap.docs.forEach(doc => {
doc.data().createdAt && results.push(doc.data());
});
// update values
comments.value = results;
error.value = null;
},
err => {
console.log(err.message);
comments.value = null;
error.value = 'could not fetch the data';
}
);
watchEffect(onInvalidate => {
onInvalidate(() => unsub());
});
return { error, comments };
};
export default getCollection;
And this is mine Comments.vue where i passing arguments in setup() function (composition API)
const { comments } = getAllComments('posts', props.id, 'comments');
When i console.log(comments) its null, in snapshot doc.data() is good but somehow results too is empty array even if i push doc.data() to results array and pass it to comments.value.
Can someone help me how to get that subcollection?
This is my Comment.vue component
export default {
props: ['id'],
setup(props) {
const { user } = getUser();
const content = ref('');
const { comments } = getAllComments('posts', props.id, 'comments');
const ownership = computed(() => {
return (
comments.value && user.value && user.value.uid == comments.value.userId
);
});
console.log(comments.value);
}
return { user, content, handleComment, comments, ownership };
},
};
const getCollection = (collection, id, subcollection) => {
const comments = ref(null);
const error = ref(null);
// Firestore listener
return { error, comments };
}
The initial value of comments here is null and since Firebase operations are asynchronous, it can take a while before the data loads and hence it'll log null. If you are using comments in v-for then that might throw an error.
It'll be best if you set initial value to an empty array so it'll not throw any error while the data loads:
const comments = ref([]);
Additionally, if you are fetching once, use .get() instead of onSnapshot()

Function execution took 60002 ms, finished with status: 'timeout' for callable function

I am experiencing an issue with Firebase callable functions and Auth triggers. You can see the callable function below. When it works it usually takes less than 1 second to finish but it started give frequent timeout errors since yesterday. Same thing for the Auth trigger, I was simply returning a Promise that writes user email to the Firestore in that case.
exports.respondToInvite = functions.https.onCall(async (data, context) => {
if (!context.auth) {
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
'while authenticated.');
}
const uid = context.auth.token.uid;
const inviteId = data.inviteId;
const groupId = data.groupId;
const accepted: boolean = data.accepted;
try {
const batch = admin.firestore().batch();
const inviteRef = admin.firestore().collection("invites").doc(inviteId);
batch.update(inviteRef, {
userId: uid,
status: accepted ? "accepted" : "rejected",
})
if (accepted) {
const groupUsersRef = admin.firestore().collection("groups").doc(groupId).collection("users").doc(context.auth.uid);
batch.set(groupUsersRef, {
createdAt: admin.firestore.Timestamp.now()
})
const userRef = admin.firestore().collection("users").doc(uid);
batch.set(userRef, {
"groupId": groupId
});
}
await batch.commit();
return "invitation accepted";
} catch (error) {
console.error(error);
throw new functions.https.HttpsError('failed-precondition', 'invite response failed',error);
}
});
Edit:
Here is the Auth trigger function
exports.newUser = functions.auth.user().onCreate((user) => {
const userRef = admin.firestore().collection("users").doc(user.uid);
return userRef.create({
"email": user.email,
});
});

firestore cloud functions onCreate/onDelete sometimes immediately triggered twice

I have observed this behavior occasionally with both onCreate and onDelete triggers.
Both the executions happened for the same document created in firestore. There's only one document there so I don't understand how it could trigger the handler twice. the handler itself is very simple:
module.exports = functions.firestore.document('notes/{noteId}').onCreate((event) => {
const db = admin.firestore();
const params = event.params;
const data = event.data.data();
// empty
});
this doesn't happen all the time. What am I missing?
See the Cloud Firestore Triggers Limitations and Guarantees:
Delivery of function invocations is not currently guaranteed. As the
Cloud Firestore and Cloud Functions integration improves, we plan to
guarantee "at least once" delivery. However, this may not always be
the case during beta. This may also result in multiple invocations
for a single event, so for the highest quality functions ensure that
the functions are written to be idempotent.
There is a Firecast video with tips for implementing idempotence.
Also two Google Blog posts: the first, the second.
Based on #saranpol's answer we use the below for now. We have yet to check if we actually get any duplicate event ids though.
const alreadyTriggered = eventId => {
// Firestore doesn't support forward slash in ids and the eventId often has it
const validEventId = eventId.replace('/', '')
const firestore = firebase.firestore()
return firestore.runTransaction(async transaction => {
const ref = firestore.doc(`eventIds/${validEventId}`)
const doc = await transaction.get(ref)
if (doc.exists) {
console.error(`Already triggered function for event: ${validEventId}`)
return true
} else {
transaction.set(ref, {})
return false
}
})
}
// Usage
if (await alreadyTriggered(context.eventId)) {
return
}
In my case I try to use eventId and transaction to prevent onCreate sometimes triggered twice
(you may need to save eventId in list and check if it exist if your function actually triggered often)
const functions = require('firebase-functions')
const admin = require('firebase-admin')
const db = admin.firestore()
exports = module.exports = functions.firestore.document('...').onCreate((snap, context) => {
const prize = 1000
const eventId = context.eventId
if (!eventId) {
return false
}
// increment money
const p1 = () => {
const ref = db.doc('...')
return db.runTransaction(t => {
return t.get(ref).then(doc => {
let money_total = 0
if (doc.exists) {
const eventIdLast = doc.data().event_id_last
if (eventIdLast === eventId) {
throw 'duplicated event'
}
const m0 = doc.data().money_total
if(m0 !== undefined) {
money_total = m0 + prize
}
} else {
money_total = prize
}
return t.set(ref, {
money_total: money_total,
event_id_last: eventId
}, {merge: true})
})
})
}
// will execute p2 p3 p4 if p1 success
const p2 = () => {
...
}
const p3 = () => {
...
}
const p4 = () => {
...
}
return p1().then(() => {
return Promise.all([p2(), p3(), p4()])
}).catch((error) => {
console.log(error)
})
})
Late to the party, I had this issue but having a min instance solved the issue for me
Upon looking #xaxsis attached screenshot, my function took almost the amount of time about 15 seconds for the first request and about 1/4 of that for the second request

Firebase Deploy Error: Failed to configure trigger

I have following sample function from this tutorial: Asynchronous Programming (I Promise!) with Cloud Functions for Firebase - Firecasts
exports.emailEmployeeReport = functions.database
.ref('/employees/${eid}/reports/${rid}')
.onWrite(event => {
const eid = event.params.eid;
const report = event.data.val().report;
const root = event.data.ref.root;
const mgr_promise = root.child(`/employees/${eid}/manager`).once('value');
const then_promise = mgr_promise.then(snap => {
const mgr_id = snap.val();
const email_promise = root.child(`/employees/${mgr_id}/email`).once('value');
return email_promise;
}).catch(reason => {
// Handle the error
console.log(reason);
});;
const then_promise2 = then_promise.then(snap => {
const email = snap.val();
const emailReportPromise = sendReportEmail(email, report);
return emailReportPromise;
}).catch(reason => {
// Handle the error
console.log(reason);
});
return then_promise2;
});
var sendReportEmail = function (email, report) {
const myFirstPromise = new Promise((resolve, reject) => {
// do something asynchronous which eventually calls either:
//
setTimeout(function () {
try {
var someValue = "sendReportEmail";
console.log(someValue);
// fulfilled
resolve(someValue);
}
catch (ex) {
// rejected
reject(ex);
}
}, 2000);
});
return myFirstPromise;
}
once I run firebase deploy command, eventually I am getting following error:
functions[emailEmployeeReport]: Deploy Error: Failed to configure
trigger
providers/google.firebase.database/eventTypes/ref.write#firebaseio.com
(emailEmployeeReport)
I also have a simple hello-world method and a similar trigger method, and they deploy fine.
Am I missing something here?
The syntax for wildcards in the database reference does not have "$".
Try the following:
exports.emailEmployeeReport = functions.database
.ref('/employees/{eid}/reports/{rid}')

Resources