Async Await function not awaiting - asynchronous

I'm trying to hash a password before adding it to a database, but my async hash function isn't allowed to finish before attempting to add to my database.
calling the await function:
const hashedPassword = await hashPassword(newPassword)
console.log({ hashedPassword })
console.log({hashedPassword}) returns undefined
hashPassword function:
const hashPassword = async (password) => {
bcrypt.genSalt(saltRounds, function (err, salt) {
if (err) {
return res.status(422).send({ error: err.message })
}
bcrypt.hash(password, salt, function (err, hash) {
if (err) {
return res.status(422).send({ error: err.message })
}
console.log({hash})
return hash
})
})
}
console.log({hash}) returns a hashed password (as expected).

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.

Returning a value from an asynchronous callback (Node.JS)

So, I have my route which console.logs 'undefined':
router.get("/validate-pin", async (req, res) => {
// restrict when done
try {
const { userId, pin } = req.query;
const isActivePin = await pinsDB.compareActivePin(userId, pin);
console.log(isActivePin)
return res.status(200).json(isActivePin);
} catch (error) {
console.log(error);
res.status(500).json({ error: "db error: ", error });
}
});
I have my compareActivePin method, which logs out the 'res' parameter, but for some reason doesn't return it:
async function compareActivePin(userId, received_pin) {
const active_pin = await db("account_pins").where({ userId, isActive: true });
const pinIsValidated = bcrypt.compareSync(
received_pin,
active_pin[0].account_pin
);
if (pinIsValidated) {
let skLocation = await db("sks").where({ userId }).select("url");
await readKey(skLocation[0].url, (res) => {
// console.log(res);
return res;
});
} else return false;
}
And I have my readKey method, which actually grabs the data I want my compareActivePin to return. This works like a charm.
const readKey = async (key, callback) => {
const aws = require("aws-sdk");
aws.config.update({
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
region: "us-east-2",
});
const s3 = new aws.S3();
const getParams = { Bucket: process.env.SK_BUCKET, Key: `${key}.txt` };
await s3.getObject(getParams, (err, data) => {
if (err) return err;
return callback(data.Body.toString());
});
};
So, just to recap. When I hit my endpoint, I pass in a userId and pin (strings). This calls the compareActivePin method which validates the pin and then, if the pin is valid, it then calls readKey, which grabs the file from S3 and returns the text within the file.
Like I said, I'm able to log it out to the console from within the readKey callback, but when I try to log it out as the returned value from the route, it comes back undefined.
Hoping someone could point me in the right direction.
Thanks...
I ended up answering my own question. I don't think it's possible to get a return value from the callback, so I ended up paring down the call from the database and sending the response from the readKey function using the router response object, like so:
//CompareActivePin Function
async function compareActivePin(userId, received_pin) {
const active_pin = await db("account_pins").where({ userId, isActive: true });
const pinIsValidated = bcrypt.compareSync(
received_pin,
active_pin[0].account_pin
);
return pinIsValidated;
}
//Router Call
router.get("/validate-pin", async (req, res) => {
// restrict when done
try {
const { userId, pin } = req.query;
const isActivePin = await pinsDB.compareActivePin(userId, pin);
if (isActivePin) {
let skLocation = await skDB.findUrl(userId);
readKeyFunc(skLocation[0].url, (result) => {
return res.status(200).json({ confirmed: isActivePin, key: result });
});
} else return res.status(401).json({ confirmed: isActivePin, key: null });
} catch (error) {
res.status(500).json({ error: "db error: ", error });
}
});
This also goes a long way toward keeping my database methods pure and separating my concerns.
Thanks, StackOverflow!

Redux Saga not waiting for Firebase service to return

SignUpSaga is broken. It doesn't wait for "yield put(updateUserAction(user)); " to return a value, it just returns undefined breaking the app then, sign up service returns after.
How do I make it wait? I thought this is what yielding does.
Parts of redux package:
export const updateUserAction = (user: User | null): UpdateUserActionType => ({
type: UPDATE_USER,
user,
});
...
export const userReducer = (
state: StateSlice = initialState.user,
action: UpdateUserActionType
): StateSlice => {
switch (action.type) {
case UPDATE_USER:
return updateHandler(state, action);
case SIGN_OUT:
return signOutHandler();
default:
return state;
}
};
Interactor
export class SignUpInteractor {
signUpService: SignUpService;
constructor(signUpService: SignUpService) {
this.signUpService = signUpService;
}
async signUp(
firstName: string,
lastName: string,
credential: Credential
): Promise<User> {
const user = new User(firstName, lastName, credential.email);
return this.signUpService.signUpUser(user, credential);
}
}
saga
function* signUpSaga(action: SignUpActionType) {
const { firstName, lastName, credential } = action;
const service = new FirebaseLogin();
const interactor = new SignUpInteractor(service);
const user = yield call(() => {
interactor.signUp(firstName, lastName, credential);
});
yield put(updateUserAction(user));
}
Finally, sign up service:
export class FirebaseLogin implements SignUpService {
async signUpUser(user: User, credential: Credential): Promise<User> {
var user: User;
try {
db.app
.auth()
.createUserWithEmailAndPassword(credential.email, credential.password)
.then((authData) => {
db.app
.database()
.ref("users/" + authData.user.uid)
.set({
uid: authData.user.uid,
email: credential.email,
firstName: user.firstName,
lastName: user.lastName,
})
.then(() => {
db.app
.database()
.ref("users/" + authData.user.uid)
.once("value")
//do this to make sure data was returned as values, not resolved later by firebase.
.then((snapchat) => {})
.then(() => {
console.log("returning");
return user;
});
})
.catch((error) => {
return new User("error", "error", "err#err.com");
});
})
.catch((error) => {
console.log("error here");
return new User("error", "error", "err#err.com");
});
} catch (error) {
console.log("error here");
return new User("error", "error", "err#err.com");
}
}
I think the primary problem is how you're using call:
const user = yield call(() => {
interactor.signUp(firstName, lastName, credential);
});
You're passing a synchronous function that will start an async call and return immediately. In addition, that function returns nothing, hence the undefined.
What you really want is to tell the middleware to make the async call, and thus wait for the promise to be resolved:
const user = yield call(interactor.signUp, firstName, lastName, credential);
Also, it looks like your code is wayyyy more complicated than it needs to be: writing Redux code by hand, extra functions in the reducers, defining separate TS types for action objects, and also this "interactor" and "service" bit. Even using sagas is overkill here for this example. You should be using our official Redux Toolkit package and following our recommendations for using Redux with TS. That will simplify your code dramatically.
If I was writing this myself, I'd have the signUp part be a standalone async function, and use RTK's createAsyncThunk:
async function signUpUser(user: User, credential: Credential): Promise<User> {
// omit Firebase code
return user;
}
interface SignUpArgs {
firstName: string;
lastName: string;
credential: Credential;
}
const signUp = createAsyncThunk(
'users/signUp',
async ({firstName, lastName, credential}: SignUpArgs) => {
const user = new User(firstName, lastName, credential.email);
return signUpUser(user, credential);
}
)
type UserState = User | null;
const initialState : UserState = null;
const userSlice = createSlice({
name: 'users',
initialState, // whatever your state actually is,
reducers: {
// assorted reducer logic here
signOut(state, action) {
return null;
}
},
extraReducers: builder => {
builder.addCase(signUp.fulfilled, (state, action) => {
return action.payload
});
}
})
Much less code in the end, especially if you're doing any actual meaningful state updates in the reducers.

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,
});
});

Dispatch Action Inside A Promise Then Function In A Saga

I have a saga (using redux-saga) that calls a function that POSTs to an API endpoint (using axios). The deeper API call through axios returns a promise. I'd like to dispatch actions inside the then() method of the promise, but I obviously can't use a yield. A put() doesn't seem to put anything. What's the right way to do this?
Here's the saga:
export function* loginFlow(action) {
try {
const {username, password} = action.payload;
const responsePromise = yield call(login, {username, password, isRegistering: false});
yield responsePromise
.then(result => {
console.log('loginFlow responsePromise result', result);
put(creators.queueLoginSucceededAction()); // doesn't work
put(push('/')); // doesn't work
})
.catch(err => {
console.log('loginFlow responsePromise err', err);
put(creators.queueLoginFailedAction()); // doesn't work
});
}
catch(err) {
console.log(err);
yield put(creators.queueLoginFailedAction());
}
}
Here's the function being called:
export function* login(options) {
try {
// if we are already logged in, via token in local storage,
// then skip checking against server
if( store.get('token') ) {
return Promise.resolve('Already logged in.');
}
// query server for valid login, returns a JWT token to store
const hash = yield bcrypt.hashSync(options.password, 10);
yield put(creators.queueLoginHttpPostedAction());
return axios.post('/auth/local', {
params: {
username: options.username,
password: hash,
hash: true,
}
})
.then(result => {
console.log('api>auth>login>result', result);
put(creators.queueLoginHttpSucceededAction()); // doesn't work
return Promise.resolve('Login successful');
})
.catch(err => {
console.log('api>auth>login>err', err);
put(creators.queueLoginHttpFailedAction()); // doesn't work
return Promise.reject(err.message);
});
}
catch (err) {
yield put(creators.queueLoginHttpFailedAction());
return Promise.reject('Login could not execute');
}
}
A saga 'yield call' will wait for a returned promise to complete. If it fails, it throws an error, so instead of using 'then' and 'catch' for promises, you can just use a normal try-catch instead.
The Redux Saga docs explain this in more detail - definitely worth a read:
https://redux-saga.js.org/docs/basics/ErrorHandling.html
In other words, the login(options) function below will execute the HTTP request and so long as it doesn't send back a rejected promise, it will continue to queue the succeeded action and redirect the user back. If it does send back a rejected promise, it will instead immediately jump to the 'catch' block instead.
Corrected for the login flow:
export function * loginFlow(action) {
try {
const {username, password} = action.payload;
const responsePromise = yield call(login, {username, password, isRegistering: false});
console.log('loginFlow responsePromise result', result);
yield put(creators.queueLoginSucceededAction());
yield put(push('/'));
}
catch(err) {
console.log('loginFlow responsePromise err', err);
yield put(creators.queueLoginFailedAction());
}
}
And for the actual login process:
export function * login(options) {
// if we are already logged in, via token in local storage,
// then skip checking against server
if( store.get('token') ) {
return Promise.resolve('Already logged in.');
}
// query server for valid login, returns a JWT token to store
const hash = yield bcrypt.hashSync(options.password, 10);
yield put(creators.queueLoginHttpPostedAction());
try {
yield call(
axios.post,
'/auth/local',
{ params: { username: options.username, password: hash, hash: true } }
)
console.log('api>auth>login>result', result);
yield put(creators.queueLoginHttpSucceededAction()); // doesn't work
return Promise.resolve('Login successful');
} catch(err) {
console.log('api>auth>login>err', err);
yield put(creators.queueLoginHttpFailedAction()); // doesn't work
return Promise.reject(err.message);
}
}

Resources