Start using async/await instead of promises and callbacks


#62

That’s certainly possible: do you have a subscription to your Store collection which should make this available on the client?

The other piece I’m confused about is getClosestStore - is this a method for a reason? It looks like it could be completely replaced with Store.findOne()._id;. (I also can’t see why that will return the closest store, but maybe you’ve just simplified that for the post).


#63

@robfallows yes there is suppose to be a logic before sending in the store, so I’ve just simplified the code to make it easy.

That I don’t have, I am new to Meteor and I thought the method will return that for me like REST would do. So should I do a subscription in the client then huh? But everything works by enclosing the method inside Meteor.isServer though, but I’m just not sure if it’s a good practice or what.


#64

No. A method is used for “give me this data right now”: https://guide.meteor.com/methods.html

If you’re using Collection.find() you need to subscribe (on the client) to a publish method on the server. That pub/sub gives you a “live” connection on the client to the database on the server - or a small subset of the database: https://guide.meteor.com/data-loading.html


#65

Hi guys,

Interesting read about the async and promises. I tried to use callPromise from deanius but with a modern Typescript project that is not going to work since you can’t amend the Meteor object typing to add the definition for callPromise.

I tried to use await but I am not sure if this is going to work since my target is es5 (needs to run on IE 10+) and I am not sure if await is going to be polyfilled.

In the end I just used the promise wrapper

export const callWithPromise = ( method, myParameters ) => {
  return new Promise( ( resolve, reject ) => {
    Meteor.call( method, myParameters, ( err, res ) => {
      if ( err ) reject( err );
      resolve( res );
    } );
  } );
}

so that at least I can use the .then function.

Does anyone have a nice example of this await setup with an async method call using typescript?


#67

thanks @robfallows for your many interventions. I have read a couple of your posts on async/await and though it still seems a bit above my head, I have some spaghetti code that sometime runs into race conditions and I believe this async/await will help resolve it.
this answer right here explained how I can setup my server methods ( I am thinking only converting the methods called on the client and involved in this call back from h**)

so on the client I have the following: how do I rewrite it to take advantage of async/await, to be more predictable and cleaner after converting the server side methods as you have described here?

AutoForm.hooks({
  addOrganizationForm:{
      onSubmit: function (insertDoc, updateDoc, currentDoc) {
        this.event.preventDefault();
        var self = this;
        // TODO: create a record of this in orders before continuing, NB approve instantly
        // 1. setup orders object
        // 2. create order and on sucess
        // 3. create organization
        let insertOrdersArgs = {
          orderType: 'registerOrganization',
          orderDetails: insertDoc,
          orderedByEntityType: Meteor.user().currentType ? Meteor.user().currentType : Meteor.user().initialType,   // for first run there is no currentType 
          orderStatus: "Approved", // for now, regeistering a organization should be aproved immediately, may change in future
          pending: false,
          approved: true,
          approvedAt: new Date(),
          // approvedBy: Meteor.userId() not necessary, handled in db         
        };
        Meteor.call('orders.insert', insertOrdersArgs, function(error, result) {           
          if (error) {
            // if error regestirng the order 
            Bert.alert({
              title: error.error,
              message: error.reason,
              type: 'danger',
              style: 'growl-top-right',
              icon: 'fa-frown-o',
              hideDelay: 5000,
              });
            return false;
            
          } 
          // else {
          if(result){ // if order is created,
            // actually create the organization now
            Meteor.call('organizations.insert', insertDoc, function(error, result) {              
              if (error) {
                Bert.alert({
                    title: error.error,
                    message: error.reason,
                    type: 'danger',
                    style: 'growl-top-right',
                    icon: 'fa-frown-o',
                    hideDelay: 5000,
                });
              } 
              if(result){ // if organization is created,
                // assign Creator role of this lincesor (group) to this user
                // Roles.addUsersToRoles(joesUserId, ['manage-team','schedule-game'], 'manchester-united.com')
                // Roles.addUsersToRoles(Meteor.userId(), ['Creator','Assignee'], licensorId);
                let updateRoleArgs = {
                    group: result, // saving organization._id as group name
                    roles: ['Creator'] //   roles: ['Creator','Assignee']
                  };
        
                // update user, 
                Meteor.call('users.assignUserRoles', updateRoleArgs , function(error, result){       
                    if(error){
                        Bert.alert({
                          title: error.error,
                          message: error.reason,
                          type: 'danger',
                          style: 'growl-top-right',
                          icon: 'fa-frown-o',
                          hideDelay: 5000,
                      });
                    } 
                    else {
                          // if(result){ // if role is assigned, doenst retunr a valid result
                          // update user's currentType, associatedOrganizations and currentOrganization, 
                          // 1. update user's currentType
                          // 2. update user's associatedOrganizations with new associatedOrganization (organization) id
                          // 3. set user's currentOrganization
                          // which will reactively update the reactiveVars currentUserType, currentOrganization
                          let userUpdateArgs = {
                            _id: Meteor.userId(),
                            modifier: {
                              // for cases where currentType not set to assignee, eg user later decided to setup a organization, long after first run stuff
                              $set:{currentType: "Creator", currentOrganization: updateRoleArgs.group}, 
                              $push: {'associatedOrganizations': updateRoleArgs.group} // for when using a simple array to store ids
                              }
                          };
              
                          // update, 
                          Meteor.call('users.update', userUpdateArgs , function(error, result){
                              if(error){
                                    Bert.alert({
                                    title: error.error,
                                    message: error.reason,
                                    type: 'danger',
                                    style: 'growl-top-right',
                                    icon: 'fa-frown-o',
                                    hideDelay: 5000,
                                });
                              } 
                             if(result){
                                  Meteor.call('subscriptions.insert', {
                                  subscriptionType: "Follower",
                                  subscriber: Meteor.userId(),
                                  subscriberType: "User",
                                  subscribedTo: Meteor.user().currentOrganization,
                                  subscribedToType: "Organisation"  
                                } , function(error, result) {   
                                if(error){
                                    console.log(error)        
                                }
                                console.log(result);
                              });
                                  
                                  // Modal.hide();
                                  FlowRouter.go('/dashboard');
                              }
                          });
                      
                      }
                });                  

                self.done();
              }
            });           
          }
        });     
        return false;
      },

#68

You could use my async package! https://atmospherejs.com/npdev/async-proxy

There is a note in there about legacy browsers, which is out of date. Just use my wrapper package for that, and get a good enough proxy polyfill in your legacy bundle, with no overhead in your main bundle: https://atmospherejs.com/npdev/legacy-proxy-polyfill


#69

What you do in principle for each Meteor.call is:

  1. Ensure the closest enclosing function is async. That looks like onSubmit: function (insertDoc, updateDoc, currentDoc) { becomes async onSubmit(insertDoc, updateDoc, currentDoc) { in your example.

  2. Assign the appropriate result variable to await each call and use try/catch for the error, so:

    Meteor.call('abc', p1, p2, (error, result) => {
      //callback code
    });
    

    becomes:

    try {
      const result = await callWithPromise('abc', p1, p2);
    } catch (error) {
      // handle error
    }
    
  3. Flatten the nesting.

    const result1 = await callWithPromise('abc', p1, p2);
    const result2 = await callWithPromise('def', result1);
    const result3 = await callWithPromise('def', result2, p3);
    // ...
    

Using the npdev:async-proxy package means callWithPromise('abc', ...) becomes callAsync('abc', ...) or Methods.abc(...). Similarly, if you were to use the deanius:promise package, you’d use Meteor.callPromise('abc',...).

Note, there is a gotcha when using const (and let, if you’re not careful), which makes it difficult to try/catch each call separately. Your code suggests you should use something like:

let result1;
try {
  result1 = await callWithPromise('abc', p1, p2);
} catch (error) {
  // ...
}
let result2;
try {
  result2 = await callWithPromise('def', result1);
} catch (error) {
  // ...
}
let result3;
try {
  result3 = await callWithPromise('def', result2, p3);
} catch (error) {
  // ...
}

Good luck!


#70

This is what my company did:

export class VivaMethod extends ValidatedMethod {
  asyncCall(args) {
    return new Promise((resolve, reject) => {
      this.connection.apply(this.name, [args], this.applyOptions, (err, res) => {
        if (err) {
          if (Meteor.isClient && !Meteor.isProduction) console.error({ ...err, details: `Error calling '${this.name}'` });
          reject(err);
        }
        resolve(res);
      });
    });
  }
}