Tiempo real y estado del chat
Cómo la pantalla de Chat de tickets mantiene sincronizados la lista, el hilo y las notificaciones: el slice NgRx tickets, los consumidores de ChatSocketService dentro de la feature y las reglas de reconexión. La tabla completa de eventos y la mecánica del socket (autenticación, reintentos, payloads) está en Sockets y el patrón de estado en Estado; aquí solo se describe el uso propio de esta feature. Contraparte en el backend: Chat y Ticket.
Archivos
| Archivo | Rol |
|---|---|
core/services/ui/chat-socket.service.ts | Socket del chat: estado de conexión, emits y observables de eventos |
core/helpers/global/chat-socket.constants.ts | SOCKET_EVENTS |
core/interfaces/ui/chat-socket.interface.ts | Payloads de emisión y de recepción |
core/states/ticket/* | Slice tickets (acciones, efectos, reducer, selectores) |
core/states/scheduled-message/* | Slice scheduledMessages |
core/services/ui/notification-center.service.ts | Centro de notificaciones del encabezado |
core/services/ui/audio-notification.service.ts | Sonido de aviso |
core/helpers/global/presence.helper.ts | PRESENCE_STALE_MS e isPresenceFresh |
Ambos slices se registran en core/states/app.state.ts (tickets: ticketReducer, scheduledMessages: scheduledMessageReducer) junto con TicketEffects y ScheduledMessageEffects.
Conexión
ContentComponent (el layout autenticado) llama connect(user._id) de ChatSocketService cuando carga el perfil y disconnect() al destruirse. ChatListComponent.initSocketConnection y el botón de reintento de ChatPageComponent vuelven a llamar a connect, que es idempotente. app-connection-status-bar refleja el estado (idle, connecting, authenticated, error); mientras no esté authenticated los emits (join-ticket, send-message, send-file, send-note, mark-read, message-delivered) no hacen nada.
Estado tickets
TicketState guarda tickets, totalCount, currentPage, hasMore, isLoading, isInitialLoad, isLoadingMore, error y activeFilter (el filtro de la última carga, que usan los reducers para decidir si una fila sigue perteneciendo a la pestaña actual). Selectores (ticket.selectors.ts): selectTickets, selectTicketsTotalCount, selectTicketsCurrentPage, selectTicketsHasMore, selectTicketsIsLoading, selectTicketsIsInitialLoad, selectTicketsIsLoadingMore, selectTicketsError y selectTicketsActiveFilter.
Los reducers comparten dos ayudantes:
removeIfFilterMismatch(tickets, id, nuevoEstado, activeFilter, extra): si el filtro tiene un estado y no coincide con el nuevo, quita la fila; si coincide (o no hay estado), actualiza el estado y los campos extra.removeIfUnassignedMismatch: para devolver a la cola; quita la fila si la pestaña no esopeno si la vista está acotada a otro operador.
El catálogo de las 54 acciones, con su efecto en el reducer, está en Acciones del ticket.
Quién escucha qué
| Consumidor | Evento u observable | Reacción |
|---|---|---|
ChatSocketService | ticket_updated, ticket_assigned, ticket_status_changed, ticket_reactivated, ticket_deleted, contact_presence_changed, ticket_lock_changed | Despacha la acción ...FromSocket de TicketActions (y expone también un Subject salvo presencia y bloqueo) |
ChatSocketService | scheduled_message_sent, _cancelled, _failed, _created, _updated | Despacha las acciones ...FromSocket de ScheduledMessageActions |
ChatListComponent | ticketAssigned$ | Toast "Asignación automática" si el usuario es el asignado o es administrador; si el ticket asignado al usuario no está en la lista, recarga |
ChatListComponent | ticketCreated$ | Recarga la lista si la pestaña es open (o no hay) |
ChatListComponent | ticketStatusChanged$ | Recarga si el ticket no está en la lista y coincide con la pestaña (permite que aparezca un ticket que cambió de estado) |
ChatListComponent | ticketReactivated$ | Recarga si no está en la lista y la pestaña es open |
ChatListComponent | ticketUpdated$ (solo sendType === 'contact') | Si no es el ticket abierto y el usuario es administrador o el ticket está en su lista: suena el aviso y ticketIncrementUnread |
ChatListComponent | messageAckChanged$ | ticketLastMessageAckChanged; si el acuse es read, ticketClearUnread |
ChatListComponent | newNote$ | Invalida la caché de notas del contacto abierto y recarga si la pestaña de notas está visible |
ChatListComponent | reconnected$ | Recarga la lista (loadTickets) |
ChatConversationComponent | connectionStatus$ | Se une a la sala del ticket en cada transición a authenticated; actualiza isConnectionLive |
ChatConversationComponent | newMessage$, newFileMessage$, newNote$ | Agrega al hilo si el contactId coincide; confirma entrega de los del contacto |
ChatConversationComponent | messageAckChanged$ | applyAck sobre los mensajes del ticket |
ChatConversationComponent | reconnected$ | Vacía y recarga el hilo |
NotificationCenterService | ticketUpdated$ (contacto) | Agrega a las notificaciones, muestra toast (salvo ticket activo) y notificación del navegador; un agente solo recibe las de tickets de su lista |
ConnectionsTableComponent, ConnectionWhatsappModalComponent | whatsappQr$, whatsappSessionStatus$ | Ver Conexiones |
El centro de notificaciones recibe setActiveTicket al seleccionar o cerrar un ticket: al abrir uno marca como leídas sus notificaciones y descuenta el contador. Carga las no leídas recientes al iniciar con GET /messages/recent-unread (paginado).
Salas y confirmaciones
- Al abrir un ticket el hilo emite
join-tickety, en la primera carga,mark-readymessage-deliveredpara los mensajes del contacto que seguían ensent. Al cerrar emiteleave-ticket. - Al llegar un mensaje del contacto con el hilo abierto se emite
message-deliveredcon sudbId. isPaused(modo pausa del agente,set-agent-status) se reenvía al autenticar; lo gestiona el encabezado, no esta feature.
Presencia del contacto
contact_presence_changed llega con { contactId, isOnline, lastSeen, disconnectReason }; el reducer parchea todas las filas de ese contacto. Como un contacto puede caer en estado "conectado" sin recibir el evento de desconexión, isPresenceFresh considera desconectado a quien no haya refrescado lastSeen en 90 s (PRESENCE_STALE_MS) y la lista se rehidrata cada 30 s para reevaluarlo sin esperar un evento.
Reconexión
Los eventos perdidos durante una caída no se reenvían. Por eso reconnected$ (emite en cada reautenticación posterior a la primera) hace que la lista vuelva a pedir la página 1 con el filtro vigente y el hilo abierto vuelva a cargar su historial; el texto que el agente tenía escrito se conserva. Si el servidor corta con io server disconnect, el servicio reintenta hasta cinco veces con espera creciente de 1 s (ver Sockets).
Pruebas
| Spec | Cubre |
|---|---|
core/states/ticket/ticket.reducers.spec.ts | Reducers de posposición, reactivación, borrado por socket idempotente, no leído, presencia, bloqueo, estado terminal, devolver con filtro de operador, asignación y transferencia |
core/states/ticket/ticket.effects.spec.ts, ticket.actions.spec.ts | Efectos y creadores de posposición, bloqueo y conflicto 409 |
core/states/scheduled-message/*.spec.ts | Efectos y reducer del slice de mensajes programados |
chat-list.component.spec.ts | Tick de presencia (se mantiene en línea dentro de 90 s, pasa a desconectado al cruzar el umbral, se detiene al destruir), recarga tras reconexión, invalidación por newNote$ |
chat-conversation.component.spec.ts | Unirse a la sala en cada reautenticación y recarga del hilo tras reconectar |
No hay specs de chat-socket.service.ts ni de notification-center.service.ts dentro de esta feature.
Gotchas
ticket_createdno tiene acción NgRx: solo hace que la lista se vuelva a cargar.- Tickets bloqueados no usa este slice: llama a
TicketService.getLockedTicketsdirectamente, de modo que sus filas no se actualizan con los eventos de socket descritos aquí. - El contador "no leídos" de una tarjeta se incrementa en el cliente con cada
ticket_updateddel contacto y solo se pone a cero al abrir el ticket o al llegar un acuse de lectura; no se recalcula contra el backend hasta la siguiente recarga.