rhysclay
rhysclay

Reputation: 1683

How to make a Firebase messaging function asynchronous

I have a firebase messaging function, but the return function seems to execute before the rest of the functions. Here is the code (sorry its long):

exports.newMessageNotification = functions.firestore
  .document(`messages/{messageId}`) // wildcard for the msg id
  .onCreate(async (change, context) => {
    const db = admin.firestore();
    const messageId: string = context.params.messageId;
    const messageRef = db.collection('messages').doc(messageId);
    const tokens = [];

    // get the message
    const message: Message = await messageRef
      .get()
      .then(q => q.data() as Message);

    const recipients: any = await message.recipients;

    const user: User = await db
      .collection('users')
      .doc(message.senderId)
      .get()
      .then(q => {
        return q.data() as User;
      });

    // Notification content
    const payload = await {
      notification: {
        title: `${user.name}`,
        body: `${message.message}`,
      },
    };

    console.log(payload);

    // loop though each recipient, get their devices and push to tokens
    Object.keys(recipients).forEach(async recipient => {
      const devicesRef = db
        .collection('devices')
        .where('userId', '==', recipient);
      const devices = await devicesRef.get();

      devices.forEach(res => {
        const token: string = res.data().token;
        console.log(token);
        tokens.push(token);
      });
    });

    console.log(tokens); // logs empty
    return await sendToCloud(tokens, payload);
  });

I think the issue is that the forEach is not asynchronous so the final line of code is executing before waiting for forEach to finish?

Upvotes: 0

Views: 294

Answers (1)

Brian Kung
Brian Kung

Reputation: 4277

Ugh. I had this problem somewhere recently. You are correct, at least in my experience: forEach does not seem to obey the async directive. I had to use the for ... in ... syntax to:

  1. Get it to obey async (from the parent scope)
  2. Process sequentially, as order was important to me at the time

In your case, it would probably look like for (const recipient in recipients) { ... }


Apparently, this is caused by forEach calling the callback on each item without awaiting a response. Even if the callback is asynchronous, forEach doesn't know to await its response on each loop.

Source: https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404


A solution from the comments on the blog linked above:

await Promise.all(
  Object.keys(recipients).map(async recipient => {
    const devicesRef = db
      .collection('devices')
      .where('userId', '==', recipient);
    const devices = await devicesRef.get();

    devices.forEach(res => {
      const token: string = res.data().token;
      console.log(token);
      tokens.push(token);
    });
  });
)

Note the forEach has been replaced by a map, and it's within Promise.all(...).

Upvotes: 2

Related Questions