-
Notifications
You must be signed in to change notification settings - Fork 30
/
07s-latency-compensation.md.erb
187 lines (134 loc) · 9.22 KB
/
07s-latency-compensation.md.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
---
title: Compensación de la latencia
slug: latency-compensation
date: 0007/01/02
number: 7.5
points: 5
sidebar: true
photoUrl: http://www.flickr.com/photos/ikewinski/9473352049/
photoAuthor: Mike Lewinski
contents: 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.
paragraphs: 28
---
En el último capítulo, se introdujo un nuevo concepto en el mundo Meteor: **los métodos**.
<%= diagram "latency1", "Sin compensación de la latencia", "pull-right" %>
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
<%= diagram "latency2", "Con compensación de la latencia", "pull-right" %>
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](http://docs.meteor.com/#methods_header) 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)`:
~~~js
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
};
}
});
~~~
<%= caption "collections/posts.js" %>
<%= highlight "11~17" %>
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:
~~~js
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});
});
}
});
~~~
<%= caption "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:
~~~js
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');
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= highlight "20" %>
<%= scommit "7-5-1", "Demostrar el orden en el que aparecen los posts usando un sleep." %>
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):
<%= screenshot "s5-1", "Nuestro post insertado en la colección del cliente" %>
Cinco segundos más tarde, se reemplaza con el documento real insertado por el servidor:
<%= screenshot "s5-2", "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.