In our last article, we took a look at the basic concepts of latency compensation.

But as is often the case, real-world usage can be different from theory. So in this second part, let’s look at a few practical use cases and the issues they can trigger.

Last But Not Least

Let’s go back to the Meteor documentation’s definition of latency compensation for a second:

Latency Compensation. On the client, Meteor prefetches data and simulates models to make it look like server method calls return instantly.

This raises a question: when exactly does this simulation happen?

You might think that calling a Meteor method (i.e. Meteor.call('insertPost', post)) triggers both the client-side (in other words, the simulation) and the server-side components of the method at the same time.

But as it turns out, Meteor runs the simulation first, and only then runs the method server-side. We can prove this by adding a simple alert() call to the client-side part of our method:

Meteor.methods({
  insertPost: function (post) {
    if (Meteor.isClient) {
      alert("I'm the client!")
    }
    Posts.insert(post);
  }
});

Can you guess what will happen?

Blocking the server method from the client.

The alert() call is holding up the simulation. But the server-side component of the method can’t run until the simulation is finished! For that reason, our post won’t appear until the alert is cleared.

isSimulation vs isClient

We’ve just used Meteor.isClient to determine if the method was running on the client. But you may be aware of another boolean, Meteor.isSimulation. So what’s the difference?

Strictly speaking, the more correct boolean to use when dealing with latency compensation and simulations is isSimulation. This is because in some cases, the simulation might be running on a server, and isClient then becomes useless.

That being said, in the context of this article, isSimulation and isClient are equivalent. So in order to keep things simple (after all, everybody knows what “client” and “server” mean, while “simulation” is not as obvious), we’ve decided to use isClient after all.

Can’t Stop Won’t Stop

This brings us to a small gripe with the way Meteor methods work. We just saw that a method’s simulation runs first, and then triggers the server-side call on completion.

Sadly, there is no way to prevent that server-side call, no matter what happens in the simulation. Even if we throw an error during the simulation:

Meteor.methods({
  insertPost: function (post) {
    if (Meteor.isClient) {
      throw new Meteor.Error("Stop everything!!")
    }
    if (Meteor.isServer) {
      console.log("I'm the server!")
    }
    Posts.insert(post);
  }
});

The “I’m the server!” line will still get logged 100% of the time, no matter what.

Having a way to prevent the server-side call from being executed altogether would be handy for preventing unnecessary round trips (for example, in cases where you already know on the client that the method will end up failing on the server).

Latency Compensation Gone Wrong

Now so far we’ve assumed that client and server are more or less identical environments, and that simulating an operation on the client will give the same result as actually performing it on the server.

But of course, that’s not always the case. For example, we could try and insert our post, only to see the operation be denied by the server because, for example, we are over our post quota for the day.

Yet in the meantime, the client’s simulation will have succeeded because the client is not aware of this quota. So we end up with a conflict: the client says the post is good to go, but the server says no way.

In these cases, the server always gets the last word: after a few hundred milliseconds (or however long it takes to get a response back from the server), our brand new client-side post will go back to the void whence it came.

Of course, the scenario I just described would make for a pretty rough user experience: “Here’s your post! Whoops, now it’s gone!”. So there are clearly some cases where latency compensation is worse than the old-fashioned, wait-for-the-server approach.

Latency Showdown

We can illustrate this by going back to our insertPost method and modifying it a bit:

Meteor.methods({
  insertPost: function (post) {
    if (Meteor.isServer) {
      post.title += " (server)";
      Meteor._sleepForMs(5000); // wait for 5 seconds
    } else {
      post.title += " (client)";
    }
    Posts.insert(post);
  }
});

If we’re on the server, we’ll add the string (server) at the end of each new post’s title. If we’re on the client, we’ll add the string (client). This means we’ll end up with a showdown between two conflicting post titles, and we’ll find out which one will triumph!

Conflicting latency compensation

Have you figured out what’s going on here?

  1. We call the insertPost method on the client.
  2. The client runs the simulation, adding (client) to the post’s title.
  3. The server runs the method, adding (server) to the post’s title.
  4. The server waits five seconds.
  5. The server inserts data in the database.
  6. Finally, the new data change is reactively propagated to the client.

Callback Conundrum

Up to now we’ve been calling the insertPost method and letting it run on its own. But in many case, you’ll want to make use of a Meteor method’s return value.

With Collection.insert(), we were able to simply write:

var postId = Posts.insert(post);

But that won’t work with a Meteor method, even if we were to give it a return value:

Meteor.methods({
  insertPost: function (post) {
    return Posts.insert(post);
  }
});

In an ideal world – and if the Meteor Development Group is reading this, let’s hope they’ll consider adding this feature –, calling Meteor.call('insertPost', post) would at least return the result of the simulation on the client. But as things stand now, Meteor methods always return undefined client-side.

This means we need another way to access the return value, and that’s where callbacks come in. So our method calls become:

Meteor.call('insertPost', post, function (error, postId) {
  alert('Just inserted a post with id '+postId);
});

It’s important to note that by nature, the contents of a callback are not latency compensated.

It makes sense when you think about it: a callback is something that happens on the client specifically after waiting for the method to be done on the server.

As usual, a practical example will make things clearer. Here’s our insertPost method again, this time with the five seconds delay as well as an added return:

Meteor.methods({
  insertPost: function (post) {
    if (Meteor.isServer) {
      Meteor._sleepForMs(5000); // wait for 5 seconds
    }
    return Posts.insert(post);
  }
});

And let’s add a callback to the event handler over on the client:

Template.posts.events({
  'click .submit': function (event, instance) {
    event.preventDefault();
    Meteor.call('insertPost', {title: Fake.sentence(6), body: Fake.paragraph(3)}, function (error, postId) {
      alert('Inserted a post with id '+postId);
    });
  }
});

Now let’s watch what happens:

Latency compensation with a callback.

As you can see, the method itself is latency compensated, and the post pops up instantly. Yet the callback (which contains the alert()) has to wait until the full five seconds have elapsed and the server is ready to return its value.

Time Out!

This can be a problem. What if part of your method requires a time-intensive operation, such as a call to an external API or maybe sending out email notifications?

If you’re making use of the result of this operation in the callback, I’m afraid you’re out of luck. But in many cases (such as the “sending out email notifications” example), you’d rather not wait for the task to be done before giving the client the green light to move ahead.

Thankfully, there’s an easy way to take any code block out of the main flow: Meteor.defer().

We’ll keep our five second delay as a stand-in for any time-consuming server-side operation, but magically “cut it out” using Meteor.defer, which is equivalent to using a 0 millisecond setTimeout:

Meteor.methods({
  insertPost: function (post) {
    if (Meteor.isServer) {
      Meteor.defer(function () {
        Meteor._sleepForMs(5000); // wait for 5 seconds
        console.log("Time-consuming server-side operation done!")
      });
    }
    return Posts.insert(post);
  }
});
Using defer.

Thanks to defer() the delay is now gone, and our callback triggers immediately.

Conclusion

As we’ve seen, latency compensation can sometimes be hard to fit in, and trying to make it work will impact both code architecture and user experience.

For example, redirecting to a post’s page after inserting it requires knowing its _id, which requires waiting for the callback to trigger. But redirecting to a list of posts doesn’t, which means we can do it immediately.

Then again, if the insert operation has a high chance of being denied server-side, maybe we don’t want to immediately show the user their post, possibly giving them false information.

At the end of the day, it will be up to you to determine where to use latency compensation, and where not to. But hopefully, this article will have given you the tools to make this difficult decision!

Stay tuned for the third and final part of this latency compensation series, where we’ll take a look at how to put in practice all these principles in a real-world app (hint: it’s a famous Meteor open-source project!).