0

I try to make a notification server in node.js which get the notifications from a database, edit their payload, send them via Firebase Cloud Messaging, and then edit their status in database.

Javascript is not my main language, so I hope their is not a lot of misconceptions in this code.

To do this, I use some Promises and a Promise.all.

Currently, the problem is when I call firebase.admin.messaging().sendToDevice, my app never ends its execution.

Here is the code :

main.js :

'use strict';
global.basePath = __dirname + '/';

  const  conf   = require('./config/config'),
         db     = require('./lib/database'),
         builder = require('./notify/builder');

const gender  = conf.genderId[process.argv[2]],
      type    = conf.typeId[process.argv[3]],
      confSql = conf.inc.prod ? conf.mysql.prod : conf.mysql.dev,
      database = new db.database(confSql);

const notify = new Promise(
    (resolve, reject) => {
        if (typeof(gender) !== 'number' && typeof(type) !== 'number') {
            return reject('Error: gender and type are mandatory');
        }
        resolve();
    }
);


function main () {

    notify
    //Get the notifications from DB - They are already group by user
    .then( () => { return database.getNotifications(gender, type); })
    //Set the payload, send to Firebase, and update the status in DB
    // <-- Inside it is the call to Firebase
    .then( rows => { return Promise.all(rows.map(builder.handleNotification)); } 
        , err => {
            return database.close().then( () => {
                return Promise.reject(err)
            } );
        }
    )
    .then(() => {
        console.log('Success ! The DB and the app must close.');
        database.close();
    })
    .catch(console.log.bind(console))
    ;
}


main();

builder.js :

'use strict';
const conf = require('./../config/config'),
      sender = require('./sender'),
      database = require('./../lib/database');


//This is called inside an array.map
//It is a chain of Promises that are resolved or rejected in a Promise.all
function handleNotification( notification){  
    let notif = notification;

    return Promise.resolve(setGroupPayload(notification))
        .then(sender.send)
        .then(console.log)
        .catch(error => {
            return Promise.reject(error);
        });
}


function setGroupPayload (notification){

    //Do some change on notifications
    // (...)

    return notification;
}

module.exports = {
    handleNotification: handleNotification
};

database.js :

const mysql = require( 'mysql' );



function Database(config) {
    this.connection = mysql.createConnection( config );
}

Database.prototype.query = function query( sql, args ) {
    return new Promise( ( resolve, reject ) => {
        this.connection.query( sql, args, ( err, rows ) => {
            if ( err )
                return reject( err );
            resolve( rows );
        } );
    } );
};


Database.prototype.ping = function ping(){
    return new Promise( ( resolve, reject) => {
        this.connection.ping( err => {
            if ( err )
                return reject( err );
            resolve('Server responded to ping');
        } );
    } );
};

Database.prototype.close = function close() {
    console.log('close connection');
    return new Promise( ( resolve, reject ) => {
        this.connection.end( err => {
            if ( err )
                return reject( err );
            console.log('connection closed');
            resolve();
        } );
    } );
};


Database.prototype.getNotifications = function getNotifications (gender, type) {
    const query = `(...)`;
    const params = [gender, type];

    return this.query(query, params);
};

module.exports = {
    database: Database
};

And finally, the sender.js :

'use strict';
const firebase = require('./../lib/firebase-admin');

/**
 *
 * @param notification
 * @returns {Promise}
 */
function send (notification) {

    if (notification.message === false) {
        return Promise.reject(["payload is empty"]);
    }
    if (!(notification.token && notification.token.length > 0)) {
        return Promise.reject(["target is empty."]);
    }

    const options = {
        contentAvailable: true
    };

    //When this is called here, the app never ends
    return firebase.admin.messaging().sendToDevice(notification.token, notification.message, options);  /
}


module.exports = {
    send: send
};

I have from firebase.admin.messaging().sendToDevice(notification.token, notification.message, options) the following response, which is a Promise.resolve :

[ { error: { [Error: The provided registration token is not registered. A previously valid registration token can be unregistered for a variety of reasons. See the error documentation for more details. Remove this registration token and stop using it to send messages.] errorInfo: [Object], codePrefix: 'messaging' } } ]

This is right because the token is not valid. And I want to handle this response. But what I don't understand, is why my app never end ? It looks like their is a neverending promise in the Promise.all that prevent the app to end.

I tried also to handle the response from Firebase and send a Promise.reject to the promise chain, but without success...

So... Where am I wrong ? Thanks a lot to anybody that can help me to resolve this bug.

Edit :

I have added a .then() before the catch in the builder.js as @JimWright asked.

And here is the result :

Here is the result:

{ results: [ { error: [Object] } ],
  canonicalRegistrationTokenCount: 0,
  failureCount: 1,
  successCount: 0,
  multicastId: 6057186106180078000 }
Success ! The DB and the app must close.
close connection
connection closed
MrZog
  • 31
  • 3
  • Try adding a `then()` before the catch in `builder.js` - do you get the result there? Also log the error to see if you get anything there. – Jim Wright Dec 12 '17 at 11:09
  • Is that from `then()` or `catch()`? – Jim Wright Dec 12 '17 at 11:14
  • Thank you for your answer @JimWright. I have edited the post. sendToDevice sends a Promise.resolve(). So even if it sends an error, it is still a resolve. – MrZog Dec 12 '17 at 11:19

2 Answers2

2

Are you calling app.delete() function after using firebase admin? It must be called to close connections and background tasks.

In your main function you should do something like this (I didn't find were your call to firebase.initializeApp() is so I am assuming it is in the main.js file):

const firebase = require('firebase-admin');
const firebaseApp = FirebaseAdmin.initializeApp()

function main () {
    notify
    //Get the notifications from DB - They are already group by user
    .then( () => { return database.getNotifications(gender, type); })
    //Set the payload, send to Firebase, and update the status in DB
    // <-- Inside it is the call to Firebase
    .then( rows => { return Promise.all(rows.map(builder.handleNotification)); } 
        , err => {
            return database.close().then( () => {
                return Promise.reject(err)
            } );
        }
    )
    .then(() => {
        console.log('Success ! The DB and the app must close.');
        database.close();
        firebaseApp.delete(); // Add this to finish firebase background tasks
    })
    .catch(console.log.bind(console))
    ;
}

References:

How to properly exit firebase-admin nodejs script when all transaction is completed

https://github.com/firebase/firebase-admin-node/issues/91

Roshana Pitigala
  • 8,437
  • 8
  • 49
  • 80
mwebler
  • 41
  • 4
1

You should be throwing the error that is returned.

 sender.js

function send (notification) {

    if (notification.message === false) {
        return Promise.reject(new Error("payload is empty"));
    }
    if (!(notification.token && notification.token.length > 0)) {
        return Promise.reject(new Error("target is empty."));
    }

    const options = {
        contentAvailable: true
    };

    //When this is called here, the app never ends
    const response = firebase.admin.messaging().sendToDevice(notification.token, notification.message, options);
    if ('error' in response[0] and response[0]['error']) {
        return Promise.reject(response[0]['error']);
    );
    
    return response;
}

EDIT:

From the logs it looks like your code is executing to the final point. You should use .finally() to close all of your connections as this closure will run whether or not the promise is resolved or rejected.

main.js

function main () {

    notify
    //Get the notifications from DB - They are already group by user
    .then( () => { return database.getNotifications(gender, type); })
    //Set the payload, send to Firebase, and update the status in DB
    // <-- Inside it is the call to Firebase
    .then( rows => { return Promise.all(rows.map(builder.handleNotification)); } 
        , err => {
            return database.close().then( () => {
                return Promise.reject(err)
            } );
        }
    )
    .then(() => {
        console.log('Success!');
        // database.close();
    })
    .catch(console.log.bind(console))
    .finally(() => {
        console.log('Closing all connections...');
        database.close();
        console.log('All connections closed.');
        // Execution should stop here
    });
}
Community
  • 1
  • 1
Jim Wright
  • 5,905
  • 1
  • 15
  • 34
  • Never reject non `Error` objects in JavaScript it will obliterate your production stack traces. – Benjamin Gruenbaum Dec 12 '17 at 13:14
  • I only added the last few lines - I believe that comment is one for OP. – Jim Wright Dec 12 '17 at 13:31
  • `response` is just a Pending Promise, so it must call a `.then()` in order to access the results and error property. – MrZog Dec 12 '17 at 13:53
  • I tried with the `throw`. It correctly exit the Promise.all, but the program still does not exit. :( – MrZog Dec 12 '17 at 13:54
  • Ok @BenjaminGruenbaum, so I suppose I must add `new Error` on lines like this one : `return Promise.reject(["payload is empty"]);` ? – MrZog Dec 12 '17 at 15:01
  • @MrZog Correct, I added the errors in my answer. From the logs it seems that the code is executing until the end. Do you have any connections that are left open? – Jim Wright Dec 12 '17 at 15:22
  • @MrZog I've updated my answer. Could you let me know if `console.log('All connections closed.');` is executed? – Jim Wright Dec 12 '17 at 15:28
  • @JimWright I can not use finally because it is not available on my node version. I have done some more check and the end of the program is executed. The connection is correctly closed. But I still have this problem with the call of firebase... – MrZog Dec 12 '17 at 16:21
  • Have you thought of using bluebird or Q? They are fully featured promise libraries. As the execution is getting to the end of your main function I'm not sure why it is not ending. – Jim Wright Dec 12 '17 at 16:48
  • 1
    Yes, sorry @JimWright I missed that it was in the original post and then couldn't undownvote. I just did. I recommend not using bluebird or Q (I'm a maintainer of both) today unless you have a compelling reason - we've been working hard on adding debugging features to native promises in Node and browsers. – Benjamin Gruenbaum Dec 13 '17 at 08:57
  • @BenjaminGruenbaum Thanks! It's interesting that you suggest not using third party promise libraries. I have a few questions but I suspect they are not suited for comments :P – Jim Wright Dec 13 '17 at 10:18
  • 1
    Feel free to hit me up in the JavaScript chatroom at http://chat.stackoverflow.com/rooms/17/javascript and ping me @BenjaminGruenbaum – Benjamin Gruenbaum Dec 13 '17 at 13:01