Tema representativo de entrevista

Entrevista Frontend: ¿Cómo diseñarías un cliente de reconexión WebSocket robusto?

FrontendDifícil
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Una página de colaboración en tiempo real a menudo se congela después de un cambio de red y solo se recupera tras refrescarla. ¿Cómo diseñarías la detección, la reconexión, la recuperación de estado y la retroalimentación al usuario para el cliente WebSocket?

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

text
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ónElecciónPor qué
Retraso de reconexiónRetroceso exponencial truncado más jitterReduce los picos sincronizados
RecuperaciónReproducción por cursor, instantánea (snapshot) cuando sea necesarioEvita requerir una recarga de la página
ConfiabilidadNotificaciones descartables, comandos confirmados idempotentesIguala el costo con el valor del negocio
Pestañas en segundo planoLimitar o pausar, luego sincronizar al volverAhorra 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.

Fuentes públicas

Preguntas relacionadas