Conexión y flujo con el backend
El widget usa tres constantes derivadas de CRM_ORIGIN:
| Constante | Valor |
|---|---|
API_BASE | CRM_ORIGIN + '/api/v1' |
SOCKET_NS | CRM_ORIGIN + '/chat' |
SOCKET_CLIENT_SRC | CRM_ORIGIN + '/socket.io/socket.io.js' |
El módulo widget del backend expone los endpoints HTTP (API_PATHS.WIDGET = 'widget', versión v1) y el gateway chat (namespace chat) atiende el socket.
Endpoints HTTP
| Método y ruta | Uso en el widget |
|---|---|
GET /widget/config?token= | Al arrancar (boot). Del resultado solo se usa requiresAuth; devuelve data.results[0] |
GET /widget/validate?token= | Pre-check no bloqueante tras montar; si el estado no es active solo emite un console.warn |
GET /widget/messages?token=&externalId= | Historial del ticket abierto, tras cada authenticated |
POST /widget/files/upload?token=&externalId= | Subida de archivos (multipart/form-data, campo file); responde data.id |
Todos validan el token y el Origin/Referer (ver Seguridad y orígenes).
Carga de socket.io-client
ensureSocketIo inyecta un script con SOCKET_CLIENT_SRC en head. Mantiene una cola de callbacks, un token por intento y un timeout de 15 s (SOCKET_IO_LOAD_TIMEOUT_MS); si falla, muestra el mensaje de sistema "No se pudo cargar el chat. Intenta más tarde." y permite reintentar.
Conexión
socket = window.io(SOCKET_NS, {
transports: ['websocket'],
autoConnect: true,
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 30000,
});
Una bandera connecting evita abrir dos sockets si init() y open() se llaman seguidos.
Autenticación
En connect el widget emite authenticate:
| Campo | Origen |
|---|---|
sendType | siempre 'contact' |
token | config.token |
externalId, username | identidad del visitante |
lang | VISITOR.lang, luego config.lang, luego idioma del navegador |
email, phone, fullName | solo si están presentes (el backend los persiste en el Contact; los ausentes no borran) |
Si no llega authenticated en 10 s (AUTHENTICATE_TIMEOUT_MS), el widget hace disconnect() y connect() por su cuenta (el servidor corta a los 30 s).
Al recibir authenticated: se marca como autenticado, se limpia la lista de mensajes, se muestra el skeleton de carga, se llama loadHistory() (recupera lo recibido durante una caída) y se habilita el composer. El authenticated de un contacto no trae ticketId; el widget lo toma de los eventos new_message o del historial.
La presencia online la deriva el backend del propio authenticate; el widget no emite un evento extra.
Eventos
Emitidos por el widget
| Evento | Payload | Cuándo |
|---|---|---|
authenticate | ver arriba | En cada connect |
send-message | { content, parentId? } | Envío de texto (con parentId si responde a un mensaje) |
send-file | { fileId, parentId? } | Tras subir el archivo por HTTP |
message-delivered | { ticketId, messageIds } | Al recibir mensajes de agente o al cargar historial con ack === 'sent' |
mark-read | { ticketId } | Al enfocar el campo de escritura |
contact-leaving | sin payload | Cierre intencional (ver abajo) |
Recibidos
| Evento | Efecto |
|---|---|
authenticated | Ver arriba |
new_message | Pinta el mensaje; sendType === 'contact' es el lado propio |
new-file-message | Pinta imagen, audio o enlace de archivo |
message_ack_changed | Actualiza ✓ / ✓✓ solo en las filas propias indicadas en messageIds (si coincide el ticket activo) |
ticket_closed | Mensaje de sistema "Esta conversación fue cerrada por el agente.", deshabilita el composer y estado "Conversación cerrada" |
exception | Muestra err.message como mensaje de sistema |
disconnect | Ver reconexión |
Si el widget no está visible (panel cerrado, pestaña oculta o sin foco), cada mensaje de agente incrementa el badge, reproduce el sonido (si está habilitado) y lanza una notificación del navegador.
Subida de archivos
- Validación local (
attachments.js): documentos hasta 10 MB, imágenes hasta 5 MB, audio de 1 a 120 s. POST /widget/files/uploadporXMLHttpRequestcon barra de progreso.- Con la respuesta (
data.id) emitesend-filepor el socket. - Si
destroy()ocurre durante la subida, la request se aborta para no filtrar el archivo a otra sesión.
Reconexión
| Situación | Comportamiento |
|---|---|
Caída de red u otro motivo distinto de io server disconnect | Estado "Conexión perdida. Reconectando…" y reintentos automáticos de socket.io (infinitos, backoff de 1 a 30 s) |
reconnect_failed | Estado "Sin conexión" |
io server disconnect | Hasta 3 reintentos propios, con espera de n x 3000 ms; agotados, "Sesión finalizada. Tocá para reintentar." y el estado pasa a ser pulsable para reintentar |
El contador de reintentos solo se reinicia si la conexión se mantiene autenticada 5 s (STABLE_CONNECTION_MS), para evitar bucles infinitos silenciosos.
Aviso de salida (contact-leaving)
presence.js decide si se emite: solo con origen pagehide o destroy, y únicamente si el widget está autenticado y el socket conectado. Una caída de red u origen desconocido nunca lo emite, de modo que el backend lo clasifica como network-loss. Se envía por el mismo socket (no sendBeacon) y es de mejor esfuerzo. El evento coincide con CHAT_EVENTS.CONTACT_LEAVING del backend.