Los productos en tiempo real tienen una tensión:
- esperar al servidor después de cada interacción se siente lento;
- confiar en el cliente vuelve poco fiable el estado compartido.
La respuesta útil le da al cliente permiso para predecir y al servidor autoridad para decidir.
Una predicción no es la verdad
En Real-Time Chess, el navegador mantiene una instancia local de chess.js. Cuando un jugador arrastra una pieza, el cliente comprueba la jugada y actualiza su tablero de inmediato.
Ese resultado local tiene una sola finalidad: reducir la latencia percibida.
El servidor comprueba por su cuenta que:
- la sala existe;
- el socket pertenece a un jugador;
- el turno actual corresponde al color de ese jugador;
- la jugada es legal en el tablero del servidor;
- se puede guardar la nueva instantánea de la sala.
Solo después de esas comprobaciones, el servidor difunde la jugada.
El cliente predice. El servidor confirma el cambio.
Diseña la reversión antes del optimismo
Una interfaz optimista está incompleta si no puede recuperarse de un rechazo.
El cliente de ajedrez conserva la posición FEN sincronizada de la instantánea de la sala. Cuando el servidor emite invalid-move, el navegador restaura ese FEN. Si no hay una instantánea disponible, deshace la jugada local.
El patrón se aplica a otros productos:
| Producto | Acción optimista | Fuente para revertir |
|---|---|---|
| Ajedrez | Mover una pieza | Última versión del tablero del servidor |
| Kanban | Mover una tarjeta | Columna y posición anteriores |
| Chat | Mostrar un mensaje pendiente | Registro local del mensaje pendiente |
| Editor | Aplicar una operación local | Registro de operaciones o una instantánea nueva del documento |
La fuente para revertir debería estar definida antes de publicar la animación optimista.
Relaciona la predicción con la decisión
Un protocolo básico puede identificar el socket emisor y permitirle ignorar su propia difusión. Eso funciona con una sola acción pendiente, pero se vuelve ambiguo cuando hay reintentos o comandos rápidos.
Una petición más completa lleva un identificador generado por el cliente:
socket.emit("move", {
clientMoveId: "01J...",
roomId: "abc123",
move: { from: "e2", to: "e4" },
});
El evento de aceptación devuelve ese identificador:
{
clientMoveId: "01J...",
version: 18,
move: { from: "e2", to: "e4", san: "e4" }
}
Ahora el cliente puede:
- marcar como confirmada la acción optimista exacta;
- medir la latencia de ida y vuelta;
- reintentar sin duplicar el comando;
- rechazar o reconciliar una acción sin adivinar cuál era.
Asigna versiones al estado compartido
Los identificadores del emisor responden «¿lo originé yo?». Las versiones responden «¿sigo sincronizado?».
Cada comando aceptado debería incrementar la versión de la sala o del documento de forma monotónica:
snapshot v17 → accepted move → event v18
Si un cliente recibe v19 mientras conserva v17, puede dejar de aplicar eventos y pedir una instantánea nueva. Un salto silencioso se convierte en una vía de recuperación observable.
Sin versiones, la interfaz puede parecer correcta aunque su historial difiera del que guarda el servidor.
El texto del rechazo importa
Un sistema cuya autoridad reside en el servidor rechaza acciones durante su funcionamiento normal:
- estado desactualizado;
- permisos perdidos;
- turno incorrecto;
- plaza en la sala vencida;
- actualización concurrente;
- comando de dominio inválido.
No reduzcas todos esos casos a «Algo salió mal».
El usuario necesita información suficiente para recuperarse:
Not your turn. Wait for your opponent.
This board changed. We restored the latest position.
Your room seat expired. Rejoin the game.
Es decir: «No es tu turno. Espera a tu oponente», «El tablero cambió. Restauramos la última posición» o «Tu plaza en la sala venció. Vuelve a unirte a la partida».
El protocolo puede usar códigos legibles por máquinas mientras la interfaz ofrece un mensaje concreto.
La autoridad del servidor no exige un solo servidor
La autoridad implica un único punto lógico de decisión, no necesariamente un único proceso.
Cuando el sistema se distribuye entre varias instancias, las mismas reglas necesitan coordinación compartida:
- almacenamiento coherente de las salas;
- un adaptador de Socket.IO para difundir entre instancias;
- escrituras transaccionales o con versiones;
- claves de idempotencia;
- una cola por sala cuando los comandos deben procesarse en serie.
Si dos servidores pueden aceptar transiciones distintas desde la misma versión del estado, la arquitectura ya no conserva una autoridad única.
Cuándo no usar una interfaz optimista
El optimismo es una decisión de producto, no una opción predeterminada.
Espera la confirmación cuando:
- la acción sea destructiva y difícil de revertir;
- la tasa de rechazo sea alta;
- la predicción local no pueda representar la regla del servidor;
- otro usuario pudiera observar el estado provisional;
- el coste de una reversión visible supere el beneficio de reducir la latencia.
Para esas acciones, muestra el estado pendiente y deja que el servidor decida primero.
La regla de trabajo
Para interfaces compartidas rápidas:
predict locally
identify the command
validate centrally
commit once
broadcast the decision
confirm or roll back
recover from version gaps
Predice en el cliente, identifica el comando, valida en el servidor, confirma una sola vez, difunde la decisión, confirma o revierte la predicción y recupérate de los saltos de versión.
La autoridad del servidor necesita una separación clara entre lo que el navegador puede predecir y lo que el sistema acepta como verdadero. No necesita una interfaz lenta.
Un pequeño voto de confianza
¿Valió la pena leer esta nota?