Async callback from Meteor.methods


#1

Hi!
I have a Meteor based backend and native iOS app. I’m using restivus as API endpoint and in-app-purchase libs for validating receipt.
The problem is I can’t figure out how to get response from Meteor.methods back asynchronous.
Here is my code.
method.js

Meteor.methods({
    'validateReceipt': function(appleReceipt) {
        var iap = require('in-app-purchase'); 
        var receipt = [];
        iap.config({
            applePassword: 'my_super_secret_password'
        }); 
    
        iap.setup()
            .then(() => {
                iap.validate(appleReceipt).then(onSuccess).catch(onError);
            }) 
            .catch((error) => {
                console.log('Error: ' + error);
            });
               
        function onSuccess(response, validatedData) {
            var options = {
                ignoreCanceled: true, 
                ignoreExpired: true 
            };
            console.log('RECEIPT: ' + JSON.stringify(response.receipt, null, 2));
            return response.receipt; // === here what I want to call back
        }                  
                           
        function onError(error) {
            console.log('ERROR: ' + error);
            return error;  
        }                  
    }                      
});

Here is how I call validateReceipt method in restAPI.js. When I call this endpoint thru POST request to http://../validateReceipt address it executes function defined in action: section

var Api = new Restivus({
    prettyJson: true,
    useDefaultAuth: true
});

// Validate receipt
Api.addRoute('validateReceipt', {authRequired: false}, {
    post: {
        authRequired: false,
        action: function() {
            var receipt = this.bodyParams.receiptData;
            var data = []; 
            Meteor.call('validateReceipt', receipt, function(error, result) {
                if (error) {
                    console.log('Error: ' + error);
                } else {
                    console.log('Result: ' + result);
                    data = result;
                }
                console.log('============= RESULT: ', data); //=== data is `undefined`
            });
            return {status: 'success', data: data};
        }   
    }   
}); 

I receive undefined because of function return immediately and doesn’t wait while method return. But I can’t understand how to rewrite this async to get result.
Please help.
Thanks.


How to return async server call results from meteor method
#2

I do mine as such:

  const nameHere = await new Promise((resolve, reject) =>
      Meteor.call("validateReceipt", name, (error, result) => {
        if (error) return reject(error);
        resolve(result);
      })
    );

I can now access this variable in an event. Be sure to put async at the top of the event.


#3

What you want to do is wait for the callback to finish first.

There’s three ways you can do this with Meteor (Examples from a different API but should translate exactly the same)

1. Use Meteor.wrapAsync

// Wrap the function you want to call
// The second parameter sets `this` in the wrapped function. 
// I have no idea if the GoogleApi module you're using needs this, but I'm including it just in case
const wrappedGoogleApiGet = Meteor.wrapAsync(GoogleApi.get, GoogleApi);
// (...)
    'fetchMessages'() {
        const messages = [];
        const user = Meteor.users.findOne(Meteor.userId());
        const email = user.services.google.email;

        let url = '/gmail/v1/users/' + encodeURIComponent(email) + '/messages?q=is:unread';
        // Now you can call the wrapped function as though it was synchronous
        const data = wrappedGoogleApiGet(url);
        (...)
        messages.push(message);
        (...)
        return messages;
    }

Here’s the docs on Meteor.wrapAsync:

Long before Promises and async functions, Meteor provided sync style async calls on the server using Fibers. If you’re curious, you can get a rundown here: https://benjamn.github.io/goto2015-talk/#/

2. Return a Promise:

'fetchMessages'() {
    const messages = [];
    const user = Meteor.users.findOne(Meteor.userId());
    const email = user.services.google.email;

    let url = '/gmail/v1/users/' + encodeURIComponent(email) + '/messages?q=is:unread';
    // Return the promise, that will *eventually* resolve with the value you want to send
    return new Promise(function (resolve, reject) { 
        GoogleApi.get(url, function (error, data) {
            (...)
            messages.push(message);
            (...)
            // Resolve the promise with the value of messages
            resolve(messages);
        });
    });
}

This works because Meteor checks if you’re returning a promise from a method and will automatically await the result before sending it to the client

3. Use Async/Await

async functions and async/await work best in when the library you’re using already returns promises or if you can promisify the function in question.
I’ll use the pify module to promisify the function in the example

import pify from 'pify'
// promisify the function you want to call
const GoogleApiGetPromise = pify(GoogleApi.get);
(...)
// Change the function declaration to an async function
'fetchMessages': async function() {
    const messages = [];
    const user = Meteor.users.findOne(Meteor.userId());
    const email = user.services.google.email;

    let url = '/gmail/v1/users/' + encodeURIComponent(email) + '/messages?q=is:unread';
    // Now when we run the promisified function it returns a promise that we 
    // can wait for the value of with `await`
    const data = await GoogleApiGetPromise(url);
    (...)
    messages.push(message);
    (...)
    return messages;
}

Note that await is only available inside an async function. async functions always return a promise.

This one is most similar to the Meteor example, except that it uses pure javascript.
One notable difference is that when Meteor sees an async function it acts as though you ran this.unblock() inside the function, and so the order in which methods are called results are returned is not guaranteed (unlike with wrapAsync).


#4

Just to clarify this point: it’s the order in which results are returned which is not guaranteed. For more background on this, check out Fun with Meteor Methods.


#5

Thank you for reply!
Now I can access to result of method function validateReceipt, but

// Validate receipt
    Api.addRoute('validateReceipt', {authRequired: false}, {
        post: {        
          authRequired: false,
          action: async function() {
          var receipt = this.bodyParams.receiptData;
          var resultData;
          var validatedReceipt = new Promise (function(resolve, reject) {
              Meteor.call('validateReceipt', receipt, function(error, result) {
                  if (error) {
                      reject(error);
                  } else {
                      resultData = result; // Now I can access `result` and make an assign `result` to variable `resultData`
                      resolve(result);
                  }  
              });    
          });
          return {status: 'success', data: resultData}; // But here I can't access `resultData` = undefined 
       }   
   }   

#6

Looks like restivus endpoints are very different to meteor calls.

The good news is that they are similar to node / express middleware functions, so we can take a leaf from their book for this.

I skimmed the docs for restivus and found under addRoute that each endpoint is given access to the request and response objects under this.request and this.response. This is useful because then you can control exactly when the endpoint will return a result to the client (ie. after waiting for async functions or callbacks to execute)

This also means there’s no need for the async function or promises here (although you can use them):

// Validate receipt
Api.addRoute('validateReceipt', {authRequired: false}, {
    post: {        
        authRequired: false,
        action: function() {
            var receipt = this.bodyParams.receiptData;
            var response = this.response;

            Meteor.call('validateReceipt', receipt, function(error, result) {
                if (error) {
                    // Send an error header and message to the client
                    response.writeHead(500, 'BAD THINGS: ' + error.message); 
                    response.end();
                }
                // Replace with whatever status code makse sense
                response.writeHead(200, { 'Content-Type': 'application/json' }); 
                // Now I can access `result` and send it back to the client
                response.end(JSON.stringify({ status: 'success', data: result }));
            });
        }
    }
});

#7

@coagmano unfortunately this is not helped. I still trying to deal with this problem.
Now i’m rewrites it like this.

Here is my method:

Meteor.methods({
    'validateReceipt': function(appleReceipt) {
     var iap = require('in-app-purchase');
       var receipt = {};
       var error = {};
     iap.config({
        applePassword: '#############################',
     });
       return new Promise(function(resolve, reject) {
          iap.setup()
             .then(() => {
                iap.validate(appleReceipt).then(onSuccess).catch(onError);
             })
             .catch((error) => {
                // error...
                console.log('Error: ' + error);
             });

          function onSuccess(validatedData) {
             var options = {
                ignoreCanceled: true,
                ignoreExpired: true
             };
             receipt = validatedData;
             resolve(receipt);
          }
          function onError(error) {
             error = error;
             reject(error);
          }
       });
    }
});

And here my code there I’m trying to catch result:

// My async function
function asyncMethodCall(methodName, args) {
     return new Promise((resolve, reject) => {
        Meteor.call(methodName, args, (error, result) => {
           if (error) {
              reject(error);
           } else {
              resolve(result);
              resolve(result);
           }
        });
     });
 };

 Api.addRoute('validateReceipt', {authRequired: false}, {
     post: {
        authRequired: false,
        action: async function() {
           var receiptData = this.bodyParams.receiptData;
           let result = await asyncMethodCall('validateReceipt', receiptData);
           let json = JSON.stringify(result); // Here I have a result from 'asyncMethodCall'
           return {status: "success", data: json)}; // But this code returns nothing.
       }
    }
});

Looks like I get response before receiving result from acyncMethodCall.
This is not very critical issue, but I would like to solve it finally :slight_smile:
Thank you for your help!


#8

Are you sure restivus can correctly expect/use an async function as the action object?

If it correctly expects an async function as an action, are you sure async function() {...} returns the correct type? Are you sure it’s not supposed to return a new Promise?

Are you sure you don’t want to try using wrapAsync, and don’t use promises at all?


#9

I think @doctorpangloss is right to ask if restivus can use an async function as an action.

The restivus docs don’t say anything about supporting async functions or promises, so it’d be safe to say that they do not.

Remember that functions and async functions are fundamentally different in that an async function will always immediately return a promise, not the return value of the function.

If you want to get an asynchronous function to synchronously wait for the result, you can use the magical Meteor.wrapAsync on the server side.

Though in this case, Meteor.call already has the Meteor magic so you can use it’s non-callback form (again, only on the server):

Api.addRoute('validateReceipt', {authRequired: false}, {
    post: {
        authRequired: false,
        action: function() {
            var receiptData = this.bodyParams.receiptData;
            // Used on the server without a callback will run in a fiber,
            // blocking the enclosing function until it returns
            let result = Meteor.call('validateReceipt', receiptData); 
            let json = JSON.stringify(result);
            return {status: "success", data: json};
        }
    }
});

#10

Also note that Meteor methods can use async functions and will automatically await the function before returning the result to the caller

This really simplifies your method code:

Meteor.methods({
    'validateReceipt': async function(appleReceipt) {
        var iap = require('in-app-purchase');
        iap.config({
            applePassword: '#############################',
        });
        await iap.setup();
        // Returning the promise from validate will still await the result before returning
        // ref: https://eslint.org/docs/rules/no-return-await
        return iap.validate(appleReceipt);
    }
});

And you can most likely load iap and it’s setup outside the method so it doesn’t have to run every time


#11

@doctorpangloss and @coagmano thank you very much! Finally I got it. Here is my final code. Maybe it helps somebody else to deal with Restivus and async function.

// Validate receipt
Api.addRoute('validateReceipt', {
  authRequired: false
}, {
  post: {
    authRequired: false,
    action: function() {
      var receipt = this.bodyParams.receiptData;
      let result = Meteor.call('validateReceipt', receipt);
      return {status: 'success', data: result};
    }
  }
});
Meteor.methods({
  'validateReceipt': async function(receipt) {
    var iap = require('in-app-purchase');
    let secret = "#########################";
    await iap.config({
        applePassword: secret, 
        test: true, 
        verbose: false
    });
    return iap.validate(receipt);
  }
})

The key moments are:

  1. Method validateReceipt as async function without callback
  2. Restivus endpoint action validateReceipt is normal sync function.

#12

Writting my approach just in case someone needs it

In my case I wanted to call another validated method from a validated method from server (getRandomAds)

...
...
import { getRandomAds } from '../../Ads/server/methods';
...
...
export const getByIdArray = new ValidatedMethod({
  name: 'devices.getByIdArray',
  validate: new SimpleSchema({
    devicesArray: { type: Array },
    'devicesArray.$': {
      type: String,
    },
    enableAds: {
      type: Boolean,
      optional: true,
    },
    limit: {
      type: Number,
      optional: true,
    },
  }).validator(),
  async run({ devicesArray, enableAds, limit }) {
    let ads = null;
    const devices = Devices.find(
      {
        state: 1,
        _id: { $in: devicesArray },
      },
      {
        fields: {
          _id: 1,
          name: 1,
          image: 1,
          routines: 1,
        },
        sort: { name: 1 },
      },
    ).fetch();

    if (enableAds) {
      ads = await new Promise((resolve, reject) =>
        getRandomAds.call(
          {
            limit,
          },
          (error, result) => {
            if (error) return reject(error);
            resolve(result);
          },
        ),
      );
    }

    return { devices, ads };
  },
});