Compensación de la latencia

Sidebar 7.5

Porcentaje completado

En este capítulo:

  • Comprenderemos qué es la compensación de latencia.
  • Bajaremos la velocidad de la aplicación para ver qué ocurre.
  • Veremos cómo se llaman los métodos entre sí en Meteor.
  • En el último capítulo, se introdujo un nuevo concepto en el mundo Meteor: los métodos.

    Sin compensación de la latencia
    Sin compensación de la latencia

    Un método es una forma de ejecutar una serie de comandos en el servidor de una manera estructurada. En nuestro ejemplo, hemos utilizado un método porque queríamos asegurarnos que los nuevos posts se etiqueten con el nombre e identificador de su autor y con la hora actual del servidor.

    Sin embargo, tendríamos un problema si Meteor ejecutara los métodos de una forma más básica. Considera la siguiente secuencia de eventos (nota: las marcas de tiempo son valores aleatorios escogidos con fines ilustrativos):

    • +0ms: El usuario hace clic en un botón de envío y el navegador lanza una llamada al método.
    • +200ms: El servidor realiza cambios en la base de datos Mongo.
    • +500ms: El cliente recibe estos cambios y actualiza la interfaz de usuario para reflejarlos.

    Si fuera así, habría un breve retraso entre la acción y la visualización de los resultados (que se notaría más o menos dependiendo de lo cerca que estuviésemos del servidor).¡Esto no se puede permitir en una aplicación web moderna!

    Compensación de la latencia

    Con compensación de la latencia
    Con compensación de la latencia

    Para evitar este problema, Meteor introduce un concepto llamado Compensación de la Latencia. Cuando definimos nuestro método Post, lo colocamos dentro de un archivo en el directorio collections/. Esto implica que estará disponible tanto para el servidor como para el cliente - ¡y se ejecutará al mismo tiempo en ambos contextos!

    Cuando se hace una llamada a un método, el cliente no solo envía la llamada al servidor, sino que también simula la acción en su propia colección. De esta forma, el flujo de trabajo sería:

    • +0ms: El usuario hace clic en un botón de envío y el navegador lanza una llamada al método.
    • +0ms: El cliente simula la acción en su propia colección y cambia la interfaz de usuario para reflejar los cambios.
    • +200ms: El servidor realiza cambios en la base de datos Mongo..
    • +500ms: El cliente recibe esos cambios deshaciendo los que ha simulado y reemplazándolos con los del servidor (que, generalmente son los mismos) y la interfaz de usuario los refleja.

    Esto se traduce en que el usuario ve los cambios instantáneamente. Cuando llega la respuesta del servidor unos momentos más tarde, puede haber o no cambios notables. Por tanto, una cosa que tenemos que aprender es que debemos tratar de asegurar de que, en la medida de lo posible, los documentos simulados sean muy parecidos a los reales.

    Observando la compensación de la latencia

    Haciendo un pequeño cambio en el método post veremos todo esto en acción. Para ello, usaremos la útil función Meteor._sleepForMs() para retrasar la llamada de la función cinco segundos, pero (importante) solo en el servidor.

    Usaremos isServer para preguntar a Meteor si el método se está invocando en el cliente (como un “stub”) o en el servidor. Un stub es la simulación del método que ejecuta Meteor en el cliente, mientras el método “real” se está ejecutando en el servidor.

    Así que vamos a preguntar a Meteor si se está ejecutando el código en el servidor. Si es así, retrasaremos el proceso cinco segundos y añadiremos la cadena (server) al final del título de nuestro post. Si no, añadiremos (client):

    Posts = new Mongo.Collection('posts');
    
    Meteor.methods({
      postInsert: function(postAttributes) {
        check(this.userId, String);
        check(postAttributes, {
          title: String,
          url: String
        });
    
        if (Meteor.isServer) {
          postAttributes.title += "(server)";
          // wait for 5 seconds
          Meteor._sleepForMs(5000);
        } else {
          postAttributes.title += "(client)";
        }
    
        var postWithSameLink = Posts.findOne({url: postAttributes.url});
        if (postWithSameLink) {
          return {
            postExists: true,
            _id: postWithSameLink._id
          }
        }
    
        var user = Meteor.user();
        var post = _.extend(postAttributes, {
          userId: user._id, 
          author: user.username, 
          submitted: new Date()
        });
    
        var postId = Posts.insert(post);
    
        return {
          _id: postId
        };
      }
    });
    
    collections/posts.js

    Si nos detuviéramos aquí, la demostración no sería muy concluyente. En este punto, solo parece que el envío del formulario está siendo pausado por cinco segundos antes de redirigirte a la lista principal de posts, y nada más.

    Para entender porqué, volvamos a ver el ayudante de envío:

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        };
    
        Meteor.call('postInsert', post, function(error, result) {
          // display the error to the user and abort
          if (error)
            return alert(error.reason);
    
          // show this result but route anyway
          if (result.postExists)
            alert('This link has already been posted');
    
          Router.go('postPage', {_id: result._id});  
        });
      }
    });
    
    client/templates/posts/post_submit.js

    Hemos colocado nuestra llamada de enrutamiento Router.go() dentro de la función de retorno de la llamada. Lo que significa que el formulario está esperando a que la llamada al método concluya antes de redirigir.

    Normalmente, esto es el cauce normal de la acción. Después de todo, no podemos redirigir al usuario antes de saber si el post es válido o no, ya que sería extremadamente confuso redirigir, y, al momento, redirigir de nuevo hacia el formulario de envío original para corregir los datos, todo en cuestión de segundos.

    Pero para el propósito de este ejemplo, queremos ver el resultado de nuestras acciones inmediatamente. Por lo que cambiaremos la llamada a Router para redirigir a la ruta postsList (no podemos redirigir al post porque no sabemos su identificador fuera del método). Lo sacaremos fuera de la llamada, y veremos que pasa:

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        };
    
        Meteor.call('postInsert', post, function(error, result) {
          // display the error to the user and abort
          if (error)
            return alert(error.reason);
    
          // show this result but route anyway
          if (result.postExists)
            alert('This link has already been posted');
        });
    
        Router.go('postsList');  
    
      }
    });
    
    client/templates/posts/post_submit.js

    Commit 7-5-1

    Demostrar el orden en el que aparecen los posts usando un…

    Si ahora creamos un nuevo post, vemos claramente la compensación de la latencia. En primer lugar, se inserta el post con el título (cliente) (El primer post de la lista, apuntando hacia GitHub):

    Nuestro post insertado en la colección del cliente
    Nuestro post insertado en la colección del cliente

    Cinco segundos más tarde, se reemplaza con el documento real insertado por el servidor:

    Nuestro post una vez que el cliente recibe la actualización del servidor
    Nuestro post una vez que el cliente recibe la actualización del servidor

    Métodos en el lado del cliente

    Después de todo esto, puedes pensar que todo esto de los métodos, es complicado, pero en realidad pueden ser bastante simples. De hecho, ya hemos visto tres métodos muy sencillos: los métodos insert, update y remove de una colección.

    Cuando se define una colección 'posts', se están definiendo implícitamente tres métodos:posts/insert, posts/update y posts/delete. En otras palabras, cuando se llama Posts.insert() desde el cliente, se llama a un método compensado en latencia que hace dos cosas:

    1. Comprueba si podemos hacer el cambio llamando a los callbacks allow y deny (sin embargo, esto no tiene porque ser así en la simulación).
    2. Efectúa, de verdad, el cambio en el almacén de datos subyacente.

    Métodos que llaman a métodos

    Si has estado atento, te habrás dado cuenta de que el método post llama a su vez a otro método (posts/insert) cada vez que insertamos un nuevo post. ¿Y, cómo es que esto funciona?

    Mientras se está ejecutando la simulación (la versión del método en el cliente), ejecutamos un insert simulado (lo insertamos en nuestra colección cliente), pero realmente no llamamos al insert del servidor, porque esperamos que sea la versión de post que hay en el servidor la que lo haga.

    En consecuencia, cuando el método post del servidor llama a insert no tiene necesidad de preocuparse por la simulación, y la inserción se realiza sin problemas.

    Como anteriormente, no olvides deshacer los cambios antes de avanzar al siguiente capítulo.