How do I properly use an NPM module that returns a Promise?


#1

I’ve been trying to figure this out for a bit now and am not getting too far, I keep getting different wrong results. I’m not too familiar with Node Promises so maybe I am just overlooking something.

I am using an NPM module quickbase-npm which returns a promise, I need to chain two functions (auth and do_query) and return the result to another function who processes and inserts the data into a Meteor collection.

Conceptually I get the problem and understand how Promises solves the async issue. I can run the chained functions in Meteor (server side) but the process/insert function fails.

Here’s that bit…

  var ticketCall = function () {
    var qid = 1000402
    var username = Meteor.settings.quickbase.username;
    var password = Meteor.settings.quickbase.password;
    var domain = Meteor.settings.quickbase.url;
    var database = Meteor.settings.quickbase.database;
    var realm = Meteor.settings.quickbase.realm;
    var QuickBase = Meteor.npmRequire('quickbase')

    var quickbase = new QuickBase({
      realm: realm,
      username: username,
      password: password
    });

    quickbase.api('API_Authenticate', {
      username: username,
      password: password
    }).then(function(result) {
        return quickbase.api('API_DoQuery', {
          dbid: database,
          qid: qid,
          fmt: "none"
        }).then(function(result) {
          return result;
        });
      }).then(function(result){
        console.log("result: " +result);
        processTickets(result.records)
      }).catch(function(err){
        console.log("error " +err);
      });
    }

    var processTickets = function (tickets) {
      log.info(">>> Adding " + tickets.length + " tickets to collection")
      TempCollection.insert(tickets)
      return tickets.length
    }

This results in:

I20150903-11:04:48.164(-4)? result: [object Object]
I20150903-11:04:48.165(-4)? info: >>> Adding 279 tickets to collection
I20150903-11:04:48.167(-4)? error Error: Meteor code must always run within a Fiber. Try wrapping callbacks that you pass to non-Meteor libraries with Meteor.bindEnvironment.

So this tells me my authenticate and doQuery worked and I passed my tickets to processTickets, but I am unable to insert the tickets because I am not in a fiber (I think).

I tried taking the console’s advice and wrapped the functions in Meteor.bindEnvironment, didn’t seem to resolve. I tried wrapping the processTickets function and the functions in the initial promise chain.

I then tried to wrap the a function around the promise block using Async.runSync, but wasn’t getting the data out properly.

After some testing I thought I had it with the following, but - I am not returning the function so it just keeps running (the data makes it though).

var loadTickets = function() {  
  var qid = 1000402
  log.info("Request to pull tickets")
  var username = Meteor.settings.quickbase.username;
  var password = Meteor.settings.quickbase.password;
  var domain = Meteor.settings.quickbase.url;
  var database = Meteor.settings.quickbase.database;
  var realm = Meteor.settings.quickbase.realm;

  var QuickBase = Meteor.npmRequire('quickbase')
  var quickbase = new QuickBase({
    realm: realm,
    username: username,
    password: password
  });

  var response = Async.runSync(function(done) {
    var auth = quickbase.api('API_Authenticate', {
      username: username,
      password: password
    }).then(function(result) {
      return quickbase.api('API_DoQuery', {
        dbid: database,
        qid: qid,
        fmt: "none",        
      }).then(function(result) {
        return result
      });
    }).then(function(result) {
      done(null, result)
      return result
    }).catch(function(err) {
      log.error(err)
    });
  })
  wrappedProcess(response.result.records)
}

var processTickets = function (tickets) {
  log.info("Count of docs in collection " +TempCollection.find().count() )
  log.info(">> Adding " + tickets.length + " tickets to collection")  

  tickets.forEach(function (doc) {
    TempCollection.insert(doc)
  })

  log.info("Count of docs in collection " +TempCollection.find().count() )  

  return tickets.length
}

var wrappedProcess = Async.wrap(processTickets);

results in

I20150903-11:56:20.225(-4)? info: Request to pull tickets
I20150903-11:56:24.445(-4)? info: Count of docs in collection 1
I20150903-11:56:24.446(-4)? info: >> Adding 282 tickets to collection
I20150903-11:56:28.743(-4)? info: Count of docs in collection 283
I20150903-11:56:59.137(-4)? info: Request to pull tickets
I20150903-11:57:03.729(-4)? info: Count of docs in collection 283
I20150903-11:57:03.729(-4)? info: >> Adding 282 tickets to collection
I20150903-11:57:08.091(-4)? info: Count of docs in collection 565

But in the Meteor shell and when run from SyncedCron it just kind of “runs” and never “ends”

Maybe I am going about the wrong or wrapping the wrong function / call back?

Thoughts?


3rd party npm module method returns a Promise. Recommendations?
#2
  1. have you got the meteor promise package in your app ? It automatically wraps server-side then statements in fibers.

  2. You don’t need to nest then statements except in rare circumstances, this not appearing to be one of them… I suggest making a linear promise chain

  3. The result of a chain of promises is still a single promise, so you can wrap the entire thing in Promise.await

Just some suggestions, it’d be too hard to type out the code, but if you get the concepts, I’m sure you can do it. Good luck!


#3

Thanks for the reply -

Pete over at http://blog.east5th.co/ pointed me in the right direction. Simplified the code, works perfect - here is a summary of the concept.

Here is how I fixed it

Promise.await(quickbase.api('API_Authenticate', ...));
return Promise.await(quickbase.api('API_DoQuery', ...));

Which is the best promise package right now?
#4

Very nice, good stuff… Of course, Promise.await won’t work on the client, but with grigio:babel, which includes experimental ES7 features…

Template.example3Demo.events({
  'click #triggerPromise': async function (event, template) {
    var output = await Meteor.promise("add", "1", "2");
    template.$("#promiseOutput").html(output);
  }
});