I recently took some time to completely refactor one of the core Meteor methods in Telescope, namely the one used to submit a new post to the app.

This might sound like a simple task, but it’s one that actually involves many of the trickiest aspects of building Meteor apps: security, latency compensation, and client/server interactions.

So here’s a quick break-down of the method, and hopefully you’ll be able to learn a thing or two from the decisions I made.

This is the third part of our series on latency compensation.

The Code

If you’d like to follow along with the Telescope repo, you’ll find the relevant code in:

Calling the Method

As usual, everything starts on the client.

Telescope used to employ a fairly standard Meteor.call('submitPost', post) method call complete with checks and callbacks, but I was able to completely refactor all of it thanks to the Autoform package.

Now, Autoform’s quickform helper not only defines which schema to build the form from (collection="Posts") and which form template to use (template="telescope"), but also which method to submit the form’s contents to (type="method" and meteormethod="submitPost"):

{{> quickForm collection="Posts" id="submitPostForm" template="telescope" 
  type="method" meteormethod="submitPost"}}

In other words, we’re getting rid of Meteor.call() completely. This not only keeps our code lightweight, but also forces us to build more flexible methods and decouple them from the client-side method call, since we don’t control it anymore.

Finally, Autoform hooks also take care of any pre-submission checks or post-submission callbacks. This can be useful for things like adding loading indicators and showing relevant errors.

A nice touch is that thanks to Autoform’s addHooks method, you can also define hooks once, and then reuse them across multiple forms if it makes sense (although Telescope isn’t quite there yet).

The submitPost Meteor Method

The submitPost method is shared between client and server. Its main job is performing a few important checks, and generally acting as a wrapper for the submitPost function.

Here’s a rough outline of the method’s structure:

Meteor.methods({

  submitPost: function(post){

    // basic checks

    // rate limiting

    // properties checks

    // call submitPost function
  }
}

We’ll first make a basic check for posting permissions, as well as a rate limiting check. Since the method’s code is shared, these checks will first be simulated on the client, then actually run on the server.

Triggering an error on the client won’t stop the method from being executed on the server, but it will at least pop an error in the console which can be helpful for troubleshooting purposes.

if (!user || !canPost(user))
  throw new Meteor.Error(601, i18n.t('you_need_to_login_or_be_invited_to_post_new_stories'));

Now it’s important to keep in mind that as far as Meteor is concerned, there is no difference between a post submitted via a form, and one inserted right from the browser console.

So we have to take care of removing any property which the user is not supposed to have access to:

  if (!hasAdminRights) {
    delete post.status;
    delete post.postedAt;
    delete post.userId;
    delete post.sticky;
  }

After a few more checks, the method’s final task is calling the submitPost function, which will do the rest of the work.

The submitPost Function

You might be wondering why we’re “stepping out” of our Meteor method at this point. And in fact, the post submission operation used to be entirely encapsulated in the postSubmit method until I ran into a new use case: I needed to submit posts from the server.

In Telescope’s case, this happened because of a new package that lets you automatically parse an RSS feed at regular intervals and create posts from it.

At this point I couldn’t just call the submitPost method: it had been written to be called from the client, so it assumed the existence of a logged-in user (by calling Meteor.user()) and performed extra checks that weren’t necessary in a trusted server environment. So my solution was to extract the core of the method code into a separate function that can be safely called from the server.

Note that Meteor consultancy (and day job of one half of the Discover Meteor team) Percolate also mentions this pattern on their wiki :

We define the actual mutators on the CollectionName.mutate namespace so that they can be called directly without enforcing security constraints. Finally, we wrap this namespace in method definitions that do enforce the security constraints and these are what we call from the client.

In other words, one tier for trusted (server) code, and one for untrusted (client) code.

Additional Checks

Now even in a trusted environment, you might still want to make a few checks. In Telescope’s case for example, we still check for the presence of a title, and that the link has not already been posted:

// check that a title was provided
if(!post.title)
  throw new Meteor.Error(602, i18n.t('please_fill_in_a_title'));

// check that there are no posts with the same URL
if(!!post.url)
  checkForPostsWithSameUrl(post.url, user);

If all checks are cleared, we then finally insert our post:

post._id = Posts.insert(post);

The Post-Submission Hook

The Collection.insert() method returns the newly inserted post’s _id, which we can in turn use to perform various extra operations, such as:

  • Upvoting the post.
  • Updating the user’s post count.
  • Notifying other users by email.

None of these operations impact the experience of the user who just submitted the post. So there’s no reason why they should hold up the method call: instead, these should all be background tasks.

As we saw in our advanced latency compensation article We can accomplish this with Meteor.defer():

if (Meteor.isServer) {
  Meteor.defer(function () { // use defer to avoid holding up client
    // run all post submit server callbacks on post object successively
    post = postAfterSubmitMethodCallbacks.reduce(function(result, currentFunction) {
        return currentFunction(result);
    }, post);
  });
}

You’ll also notice that to make Telescope easier to extend, we don’t perform these operations directly: instead, we loop over the postAfterSubmitMethodCallbacks array (I know, naming is not my best skill…), which itself contains the various functions that need to be executed on the post object.

Adding a new operation then becomes a simple matter:

postAfterSubmitMethodCallbacks.push(function (post) {

  var userId = post.userId,
      postAuthor = Meteor.users.findOne(userId);

  // increment posts count
  Meteor.users.update({_id: userId}, {$inc: {postCount: 1}});
  upvoteItem(Posts, post, postAuthor);

  return post;

});

Client & Server

Although I stated that the postSubmit function primarily expected to run on the server, it will run on the client in two situations.

First, when called from the postSubmit method as part of that method’s client-side simulation. In that case, Meteor will perform the simulation, insert the post in the client-side database, and then finally trigger the server-side postSubmit method call.

The other use case is when someone calls the postSubmit function from the browser console directly. If that happens, the Posts.insert() call will fail because we aren’t allowing client-side inserts, and nothing will happen.

Note that allow/deny doesn’t affect code executed from within a Meteor method, which is why simulations don’t fail even if you didn’t declare an allow/deny policy.

Conclusion

As you’re probably aware of by now, Meteor apps require you to be constantly aware of what’s running on the client, what’s running on the server, and what’s running on both.

This two-tiered pattern gives you fine-grained control over all of that, and hopefully you’ll find it useful enough to use in your own apps!