We have written code that sends emails to a user and their contacts, when a new node is added to a specific path in Firebase realtime database.
The average time to send the emails is 4 minutes. We think the problem is due to awaiting for some needed promises. We would like to get the run time down. Do you have any advice? Thanks in advance!
This is our code:
const functions = require("firebase-functions");
const nodemailer = require('nodemailer');
require('dotenv').config()
//for fire store
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
const { SENDER_EMAIL, SENDER_PASSWORD } = process.env;
exports.sendEmails = functions.database.ref("/devices/{device_ID}/history/{alert_ID}")
.onWrite(
(snapshot, context) => {
sendMail(snapshot, context);
return true;
}
);
async function sendMail(snapshot, context){
const { before, after } = snapshot;
// new alert created
if (before.val() == null) {
console.log('DEBUG:: NEW ALERT');
// get owners uID from device ID
const deviceRef = db.collection('deviceToUid').doc(context.params.device_ID);
const uidDoc = await deviceRef.get();
if(!uidDoc.exists){
functions.logger.info("No such document!");
return;
}
// get users email from uID
const userRef = db.collection('users').doc(uidDoc.data()[context.params.device_ID]).collection('user-info');
// get users contact
const contactRef = db.collection('users').doc(uidDoc.data()[context.params.device_ID]).collection('contacts');
const [userInfo, contactList] = await Promise.all([userRef.get(), contactRef.get()]);
if(userInfo.empty){
functions.logger.info("No such collection!");
return;
}
const email = userInfo.docs[0].id; // owners email
let contacts = []; // initialize contact list
contactList.forEach(
(doc) => {
if(doc.data().confirmed){
contacts.push(doc.id);
}
}
)
const mailTransport = nodemailer.createTransport({
service: 'gmail',
auth: {
user: SENDER_EMAIL,
pass: SENDER_PASSWORD,
},
});
const mailOptions = {
from: 'ALERT <noreply@firebase.com>',
to: email,
bcc: contacts,
subject: `...Motion detected`,
html: `<p dir=ltr>New Alert...</p>`
};
mailTransport.sendMail(mailOptions, function (error, info) {
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
}
}
I'd also recommend learning a bit about list comprehensions, as this:
let contacts = []; // initialize contact list
contactList.forEach(
(doc) => {
if(doc.data().confirmed){
contacts.push(doc.id);
}
}
)
Can be reduced to a more concise:
let contacts = contactList.docs
.filter((doc) => doc.data().confirmed)
.map((doc) => doc.id);
You were getting pretty close, but were missing an await
in the top-level function, and one inside sendMail
for the call to mailTransport.sendMail
.
I think this should be it:
exports.sendEmails = functions.database.ref("/devices/{device_ID}/history/{alert_ID}")
.onWrite(
async (snapshot, context) => {
await sendMail(snapshot, context);
return true;
}
);
async function sendMail(snapshot, context){
const { before, after } = snapshot;
// new alert created
if (before.val() == null) {
console.log('DEBUG:: NEW ALERT');
// get owners uID from device ID
const deviceRef = db.collection('deviceToUid').doc(context.params.device_ID);
const uidDoc = await deviceRef.get();
if(!uidDoc.exists){
functions.logger.info("No such document!");
return;
}
// get users email from uID
const userRef = db.collection('users').doc(uidDoc.data()[context.params.device_ID]).collection('user-info');
// get users contact
const contactRef = db.collection('users').doc(uidDoc.data()[context.params.device_ID]).collection('contacts');
const [userInfo, contactList] = await Promise.all([userRef.get(), contactRef.get()]);
if(userInfo.empty){
functions.logger.info("No such collection!");
return;
}
const email = userInfo.docs[0].id; // owners email
let contacts = []; // initialize contact list
contactList.forEach(
(doc) => {
if(doc.data().confirmed){
contacts.push(doc.id);
}
}
)
const mailTransport = nodemailer.createTransport({
service: 'gmail',
auth: {
user: SENDER_EMAIL,
pass: SENDER_PASSWORD,
},
});
const mailOptions = {
from: 'ALERT <noreply@firebase.com>',
to: email,
bcc: contacts,
subject: `...Motion detected`,
html: `<p dir=ltr>New Alert...</p>`
};
await mailTransport.sendMail(mailOptions, function (error, info) {
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
return true;
}
}
Since you were not using await
in the top-level call, the Cloud Functions contains will/may shut down the container before the asynchronous calls have completed. For more on this, see the documentation on sync, async and promises - and how Cloud Functions are terminated.