简体   繁体   中英

Meteor: Proper use of Meteor.wrapAsync on server

Background

I'm trying to integrate stripe payments into my site. I need to create a stripe user using my private stripe key. I'm storing this key on my server, and calling a server method to create the user. Maybe there's another way of accomplishing this? Here's the stripe api (copied below for convenience): https://stripe.com/docs/api/node#create_customer

//stripe api call
var Stripe = StripeAPI('my_secret_key');

Stripe.customers.create({
  description: 'Customer for test@example.com',
  card: "foobar" // obtained with Stripe.js
}, function(err, customer) {
  // asynchronously called
});

My attempts and results

I've been using the same client code with different server code. All attempts immediately give undefined on the client's console.log(...) but give the proper response on the server console.log(...):

//client
Meteor.call('stripeCreateUser', options, function(err, result) {
  console.log(err, result);
});

//server attempt 1
var Stripe = StripeAPI('my_secret_key');

Meteor.methods({
    stripeCreateUser: function(options) {  
        return Meteor.wrapAsync(Stripe.customers.create({
            description: 'Woot! A new customer!',
            card: options.ccToken,
            plan: options.pricingPlan
        }, function (err, res) {
            console.log(res, err);
            return (res || err);
        }));
    }
});

//server attempt 2
var Stripe = StripeAPI('my_secret_key');

Meteor.methods({
    stripeCreateUser: function(options) {  
        return Meteor.wrapAsync(Stripe.customers.create({
            description: 'Woot! A new customer!',
            card: options.ccToken,
            plan: options.pricingPlan
        }));
    }
});

I've also tried both without Meteor.wrapAsync.

EDIT - I'm also using this package: https://atmospherejs.com/mrgalaxy/stripe

From the Meteor.wrapAsync http://docs.meteor.com/#meteor_wrapasync you can see that you need to pass it a function and optionally a context, whereas on your two attempts you are passing the RESULT of calling the async version of Stripe.customers.create .

Meteor.methods({
  stripeCreateUser: function(options) {
    // get a sync version of our API async func
    var stripeCustomersCreateSync=Meteor.wrapAsync(Stripe.customers.create,Stripe.customers);
    // call the sync version of our API func with the parameters from the method call
    var result=stripeCustomersCreateSync({
      description: 'Woot! A new customer!',
      card: options.ccToken,
      plan: options.pricingPlan
    });
    // do whatever you want with the result
    console.log(result);
  }
});

Meteor.wrapAsync transforms an async function into a convenient synchronous-looking function which allows to write sequentially looking code. (underhood everything is still executed within the asynchronous Node.js event loop).

We need to pass to Meteor.wrapAsync our API function ( Stripe.customers.create ) along with the function context, ie this inside the body of the API func, which in this case is Stripe.customers .

EDIT :

How to retrieve errors ?

Traditional node style API functions usually take a callback as last argument that will get called ultimately when the required task is completed. This callback takes 2 arguments : error and data, either one will be null according to the result of the call.

How do we access the error object using the synchronous wrapped functions returned by Meteor.wrapAsync ?

We have to rely on using try/catch blocks, because in case of error, it will be thrown by the sync function instead of being passed as first argument of the async function callback.

try{
  var result=syncFunction(params);
  console.log("result :",result);
}
catch(error){
  console.log("error",error);
}
// is the equivalent of :
asyncFunc(params,function(error,result){
  if(error){
    console.log("error",error);
    return;
  }
  console.log("result :",result);
});

why doesn't Stripe need to be passed?

JavaScript has no "namespace" concept, so API developers use the common trick of defining a global object acting as API namespace, properties defined on this object are "sub-modules" of the API. It means that Stripe.customers is a submodule of the Stripe API exposing customers-related funcs, and as such these funcs this context is Stripe.customers , not Stripe .

You can test it yourself by copy pasting this mocking code in your browser console :

Stripe={
  customers:{
    create:function(){
      console.log(this==Stripe.customers);
    }
  }
};

And then calling the stub function in your browser console like this :

> Stripe.customers.create();
true

Another option is this package which achieves the similar goals.

meteor add meteorhacks:async

From the package README:

Async.wrap(function)

Wrap an asynchronous function and allow it to be run inside Meteor without callbacks.

//declare a simple async function
function delayedMessge(delay, message, callback) {
  setTimeout(function() {
    callback(null, message);
  }, delay);
}

//wrapping
var wrappedDelayedMessage = Async.wrap(delayedMessge);

//usage
Meteor.methods({
  'delayedEcho': function(message) {
    var response = wrappedDelayedMessage(500, message);
    return response;
  }
});

First of all, thanks to @saimeunt for his answer, which makes some difficult concepts clear. However I had the problem of wanting a classic async callback(err, result) showing both the error and the result back on the client, so that I can give informative messages in the browser.

I solved it this way:

Server code:

var Stripe = StripeAPI(STRIPE_SECRET_KEY);

Meteor.methods({
    createCust: Meteor.wrapAsync(Stripe.charges.create, Stripe.charges)
});

Client code:

var stripeCallOptions = {
    description: 'Woot! A new customer!',
    card: ccToken,
    plan: pricingPlan
};


Meteor.call('createCust', stripeCallOptions, function(error, result){
    console.log('client error', error);
    console.log('client result', result);
});

Looks neat. However unfortunately wrapAsync has an open bug, (see https://github.com/meteor/meteor/issues/2774 ) because it doesn't restore the proper error to the caller. A genius called Faceyspacey has written a replacement called Meteor.makeAsync() which you will find on the bug page I mentioned, which however returns either the result OR the error to the 'result' variable, leaving the 'error' variable undefined. That's fine by me for now, at least I've got a hook on the proper error object.

If you use makeAsync() you will need to import Futures like this:

Meteor.startup(function () {
    //this is so that our makeAsync function works
    Future = Npm.require('fibers/future');
});

因为你几乎需要在Async中包含所有功能,所以你应该使用这个包https://atmospherejs.com/copleykj/stripe-sync它使用WrapAsync预先包装所有条带功能,让你的生活更轻松,代码更清晰。

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM