Planteamiento y contexto
Esta pregunta evalúa si un ingeniero frontend trata a WebSocket como una conexión no confiable. Los cambios de Wi-Fi, la suspensión del equipo, los reinicios de proxy, los reinicios del servidor y los sockets semiabiertos silenciosos son normales. Llamar a connect() inmediatamente desde onclose crea una tormenta de reconexión y no puede recuperar los eventos perdidos mientras se estaba desconectado. Cubre el estado del cliente, el retroceso (backoff), el latido (heartbeat), la reproducción (replay), la autenticación y el ciclo de vida de la página.
Qué evalúa el entrevistador
Las respuestas sólidas definen la semántica de los mensajes y los límites de recuperación, y luego imponen las transiciones mediante una máquina de estados explícita. Utilizan retroceso exponencial con jitter, un latido a nivel de aplicación para la detección de conexiones semiabiertas, reproducción por secuencia o cursor para los eventos perdidos, y colas que distinguen las notificaciones descartables de los comandos confirmados. También discuten pestañas en segundo plano, cambios de red, tokens expirados, limitación de tasa (throttling) del servidor y el estado visible para el usuario.
Preguntas para clarificar
- ¿Es un mensaje una notificación descartable, un evento reproducible o un comando que debe procesarse? ¿El servidor admite la reproducción basada en cursores?
- ¿Cómo se actualiza la autenticación? ¿Qué sucede cuando un token, un permiso o una versión del protocolo cambia durante la reconexión?
- ¿Quién envía y confirma los latidos? ¿Durante cuánto tiempo puede un proxy descartar paquetes de forma silenciosa?
- ¿Debe el cliente admitir múltiples pestañas, paso a segundo plano en móviles, suspensión del navegador y transiciones a modo sin conexión?
- ¿Pueden los usuarios seguir editando mientras están desconectados? ¿Cómo se gestionan los conflictos, el orden y los envíos duplicados?
Estructura de respuesta en 30 segundos
“Modelaría el cliente con los estados idle, connecting, open, suspect, backoff y closed. Tras abrirse, un ping/pong a nivel de aplicación con un tiempo límite detecta fallas silenciosas. Un cierre anormal o el vencimiento del latido entra en retroceso exponencial con jitter para que los clientes no se reconecten todos al mismo tiempo. Cada evento tiene una secuencia monotónica; tras reconectarse, el cliente reanuda desde su último cursor contiguo antes de consumir eventos en vivo. Los comandos llevan claves de idempotencia y requieren confirmaciones, mientras que las notificaciones pueden descartarse. Los estados offline, oculto y de token expirado pausan o reautentican con retroalimentación clara al usuario.”
Respuesta detallada paso a paso
Paso 1: Definir la máquina de estados de conexión
Centraliza el estado y las transiciones válidas en lugar de permitir que los callbacks muten isConnected. Una ruta típica es connecting -> open -> suspect -> backoff -> connecting; un cierre por parte del usuario entra en closed y deshabilita la reconexión automática. Registra la razón, el conteo de intentos y el id de conexión para cada transición.
Paso 2: Distinguir entre sockets cerrados, con error y semiabiertos
Los eventos error del navegador pueden no contener una razón accionable, y close podría no llegar nunca en una ruta semiabierta. El latido registra el tiempo de envío, el tiempo límite del pong y el último mensaje recibido. Al expirar el tiempo de espera, cierra el socket antiguo de forma explícita antes de programar uno nuevo para que dos conexiones no puedan entregar eventos duplicados.
Paso 3: Implementar retroceso y jitter
La RFC 6455 advierte que las reconexiones inmediatas y persistentes pueden convertirse en una tormenta similar a una denegación de servicio. Utiliza min(cap, base * 2^attempt) + random(0, jitter) y reinicia el conteo de intentos solo después de una conexión estable. Respeta las indicaciones de mantenimiento o limitación del servidor, tales como Retry-After.
Paso 4: Recuperar un cursor de eventos
Los eventos llevan una secuencia de flujo monotónica. El cliente persiste la última secuencia contigua y la envía en el saludo (handshake) de reanudación. El servidor devuelve el rango faltante y luego cambia a la entrega en vivo. Si el cursor expiró, devuelve una versión de instantánea (snapshot); el cliente la carga y reanuda después de la secuencia de la instantánea.
Paso 5: Diseñar la cola de envío y la idempotencia
Las notificaciones de presencia y escritura pueden descartarse; las ediciones y las intenciones de pago requieren confirmación. Los comandos llevan un clientMessageId, y el servidor deduplica mediante esa clave y almacena el resultado. Mantén una cola acotada para el modo sin conexión; cuando esté llena, deja de aceptar más comandos y explica el estado en lugar de ocultar una cola en memoria no acotada.
Paso 6: Manejar la autenticación y los cambios de protocolo
Verifica si un token está cerca de expirar antes de reconectar y renuévalo cuando sea necesario. Un código de cierre no autorizado detiene los reintentos ciegos y entra en el flujo de inicio de sesión. Incluye una versión de protocolo en el handshake; negocia una versión compatible o muestra una ruta de actualización en lugar de entrar en un bucle infinito.
Paso 7: Integrar el ciclo de vida de la página y de la red
visibilitychange, online/offline y el paso a segundo plano en móviles alteran la estrategia. Una página oculta puede reducir la frecuencia de latidos o pausar la entrega en vivo; al volver, ejecuta una comprobación de estado y sincronización por cursor. El estado offline detiene el marcado inmediatamente y el estado online inicia un cronograma de retroceso en lugar de iterar intentos fallidos.
Paso 8: Observar la experiencia de usuario y la presión del servidor
Registra el tiempo de conexión, los intentos de reconexión, los tiempos de espera del latido, el conteo de reproducciones, las brechas de cursor y los elementos descartados de la cola. El servidor observa sockets concurrentes, fallos de handshake, picos de reconexión y mensajes duplicados por tenant y versión del cliente. Nunca registres tokens ni cuerpos de mensajes en los logs. Muestra “reconectando” y “al día” en lugar de códigos de error de transporte.
Pseudocódigo mínimo de la máquina de estados
on_open(socket):
state = OPEN
send({type: "resume", lastSeen: cursor})
on_heartbeat_timeout():
socket.close()
state = BACKOFF
delay = min(MAX, BASE * 2 ** attempts) + random(0, JITTER)
schedule(connect, delay)Compensaciones y límites
| Decisión | Elección | Por qué |
|---|---|---|
| Retraso de reconexión | Retroceso exponencial truncado más jitter | Reduce los picos sincronizados |
| Recuperación | Reproducción por cursor, instantánea (snapshot) cuando sea necesario | Evita requerir una recarga de la página |
| Confiabilidad | Notificaciones descartables, comandos confirmados idempotentes | Iguala el costo con el valor del negocio |
| Pestañas en segundo plano | Limitar o pausar, luego sincronizar al volver | Ahorra energía y conexiones inactivas |
WebSocket proporciona mensajes ordenados, no semántica de exactamente-una-vez a nivel de negocio, colas sin conexión o sincronización de estado. Esas semánticas pertenecen al protocolo de aplicación. Si un producto solo necesita envío unidireccional tolerante a pérdidas desde el servidor, evalúa SSE, pero no confundas la elección del transporte con las garantías de recuperación.
Plan de implementación y evidencia
Despliega primero la máquina de estados, el latido y el retroceso con jitter, luego agrega la recuperación por cursor y los comandos idempotentes. Realiza simulacros de cambios de red, reinicios de servidor, suspensión del navegador, expiración de tokens y una desconexión sincronizada de muchos clientes. La RFC 6455 recomienda un retraso inicial aleatorio y un retroceso creciente tras un cierre anormal; MDN documenta los eventos de WebSocket del navegador y readyState.
Criterios de salida de la prueba piloto
La página se recupera sin refrescar; los comandos confirmados no se duplican ni se pierden; los picos de reconexión no sobrecargan el servidor; las páginas ocultas no retienen sockets innecesarios; y los usuarios pueden ver el estado de la conexión y la hora de la última sincronización. Cualquier falla implica corregir primero el protocolo o la política de ciclo de vida.
Cómo demostrar que la mejora es real
Compara el éxito de la recuperación, el tiempo de recuperación p95, la tasa de eventos duplicados, las brechas de cursor, los picos de handshake y la energía móvil antes y después. Segmenta por red, navegador y visibilidad de la página para que un promedio no oculte los fallos de una plataforma en particular.
Errores comunes y preguntas de seguimiento
Reconectar inmediatamente en onclose
Cuando un servidor se reinicia, todos los clientes intentan conectarse a la vez. Utiliza retroceso, jitter e indicaciones de limitación del servidor, y reinicia los intentos solo después de alcanzar estabilidad.
Depender únicamente de close
Una ruta semiabierta puede no emitir nunca close. Utiliza un latido y un tiempo límite, y luego cierra tú mismo el socket obsoleto.
Asumir que una reconexión exitosa significa datos completos
La recuperación de la conexión no reproduce los eventos perdidos mientras se estaba desconectado. Utiliza un cursor del último visto, una versión de instantánea y comprobaciones de secuencias contiguas.
¿Cómo evitas comandos duplicados?
Asigna a cada comando una clave de idempotencia del cliente, persiste el resultado en el servidor y elimina el elemento de la cola solo tras la confirmación o tras consultar el resultado.
¿Qué pasa si el token expira durante la reconexión?
Renuévalo y realiza el handshake nuevamente. Un cierre por falta de autorización detiene los reintentos y entra en el flujo de inicio de sesión; no es un error de red transitorio.
¿Cómo controlas las conexiones entre múltiples pestañas?
Utiliza BroadcastChannel o SharedWorker para que una pestaña sea la dueña del socket y las demás se suscriban. Transfiere la propiedad cuando la dueña desaparezca y revalida el cursor.