Proper way to make callbacks async by wrapping them using `co`?

1.9k Views Asked by At

It is 2016, Node has had nearly full ES6 support since v4, and Promises have been around since 0.12. It's time to leave callbacks in the dust IMO.

I'm working on a commander.js-based CLI util which leverages a lot of async operations - http requests and user input. I want to wrap the Commander actions in async functions so that they can be treated as promises, and also to support generators (useful for the co-prompt library I'm using for user input).

I've tried wrapping the CB with co in two ways:

1) program.command('myCmd') .action(program => co(function* (program) {...}) .catch(err => console.log(err.stack)) );

and

2) program.command('myCmd').action(co.wrap(function* (program) { .. }));

The problem with 1) is that the program parameter isn't passed

The problem with 2) is that errors are swallowed...

I'd really like to get this working as it yields much nicer code in my use case - involving a lot of http requests and also waiting for user input using the co-prompt library..

Is it a better option altogether perhaps to wrap program.Command.prototype.action somehow?

thanks!

2

There are 2 best solutions below

0
On

I've used a bespoke version of something like co to get a db.exec function which uses yield to do database request. You can pass parameters into a generator function (I pass in a connection object - see the comment where I do it).

Here is by db.exec function that is very similar to what co does

exec(generator) {
  var self = this;
  var it;
  debug('In db.exec iterator');
  return new Promise((accept,reject) => {
    debug('In db.exec Promise');
    var myConnection;
    var onResult = lastPromiseResult => {
      debug('In db.exec onResult');
      var obj = it.next(lastPromiseResult);
      if (!obj.done) {
        debug('db.exec Iterator NOT done yet');
        obj.value.then(onResult,reject);
      } else {
        if (myConnection) {
          myConnection.release();
          debug('db.exec released connection');
        }
        accept(obj.value);
        debug('db.exec Promise Resolved with value %d',obj.value);
      }
    };
    self._connection().then(connection => {
      debug('db.exec got a connection');
      myConnection = connection;
      it = generator(connection); //This passes it into the generator
      onResult();  //starts the generator
    }).catch(error => {
      logger('database', 'Exec Function Error: ' + error.message);
      reject(error);
    });
  });
}

the connection object also wraps by database connection object and provides a generator function ability to process the rows of the results from the database, but I won't post that here (although the example below is using it to process the rows).

Here is an example of using the exec function to run a sequence of sql

    db.exec(function*(connection) {
      if (params.name === ADMIN_USER) {
        debug('Admin Logon');
        user.name = ADMIN_DISPLAY;
        user.keys = 'A';
        user.uid = 0;
        let sql = 'SELECT passwordsalt FROM Admin WHERE AdminID = 0';
        connection.request(sql);
        yield connection.execSql(function*() {
          let row = yield;
          if (row) {
            user.nopass = (row[0].value === null);
          } else {
            user.nopass = false;
          }
          debug('Admin Password bypass ' + user.nopass.toString());
        });
      } else {
        debug('Normal User Logon');
        let sql = `SELECT u.UserID,PasswordSalt,DisplayName,AccessKey,l.LogID FROM Users u
          LEFT JOIN UserLog l ON u.userID = l.userID AND DATEDIFF(D,l.LogDate,GETDATE()) = 0
          WHERE u.UserName = @username`;
        let request = connection.request(sql);
        request.addParameter('username',db.TYPES.NVarChar,params.name);
        let count = yield connection.execSql(function*() {
          let row = yield;
          if (row) {
            user.uid = row[0].value;
            user.name = row[2].value;
            user.keys = (row[3].value  === null) ? '' : row[3].value;
            user.nopass = (row[1].value === null) ;
            user.lid = (row[4].value === null) ? 0 : row[4].value;
            debug('Found User with uid = %d and lid = %d, keys = %s',
              user.uid, user.lid, user.keys);
          }
        });
        if (count === 0) {
          debug('Not Found User');
          // couldn't find name in database
          reply(false,false);
          return;
        }
      }
      if (!user.nopass) {
        debug('Need a Password');
        //user has a password so we must check it
        passGood = false; //assume false as we go into this
        let request = connection.request('CheckPassword');
        request.addParameter('UserID',db.TYPES.Int,user.uid);
        request.addParameter('password',db.TYPES.VarChar,params.password);
        yield connection.callProcedure(function*() {
          let row = yield;
          if (row) {
            //got a valid row means we have a valid password
            passGood = true;

          }
        });
      } else {
        passGood = true;
      }
      if (!passGood) {
        debug('Not a Good Pasword');
        reply(false,true);
      } else {
        if (user.uid !== 0 && user.lid === 0) {
          let sql = `INSERT INTO UserLog(UserID,LogDate,TimeOn,UserName) OUTPUT INSERTED.logID
           VALUES(@uid,GETDATE(),GETDATE(),@username)`;
          let request = connection.request(sql);
          request.addParameter('uid',db.TYPES.Int,user.uid);
          request.addParameter('username',db.TYPES.NVarChar,user.name);
          yield connection.execSql(function*() {
            let row = yield;
            if (row) {
              user.lid = row[0].value;
              debug('Users Log Entry = %d',user.lid);
            }
          });
        }
        reply(true,user);
      }
    })
    .catch((err) => {
      logger('database','Error on logon: ' + err.message);
      reply(false,false);
    });
  });
0
On

There is a quite simple way to do async function in Commander.js

async function run() { 
  /* code goes here */  
}

program
  .command('gettime')
  .action(run);
  
program.parse(process.argv);