Saltar al contenido

Tiempo real (WebSocket)

El WebSocket de la plataforma - el ticket de un solo uso, el subprotocolo memberfy-realtime, las salas automáticas y las pedidas, los avisos invalidate, los límites y los códigos de cierre.

La app recibe los cambios al momento por un WebSocket: la campana, el chat, el "En línea ahora", quién está en cada pantalla, el banner del evento en directo, los números de las campañas. Cualquier miembro de la comunidad, de cualquier rol, abre el socket, con el chat activado o desactivado.

Abrir la conexión

  1. Pide un ticket con POST /api/realtime/ticket, con el token y el X-CommunityId (ver Autenticación y X-CommunityId). El ticket es de un solo uso, vale 60 segundos y queda ligado a la persona, a la comunidad y a la sesión.
  2. Abre wss://<api>/api/realtime/socket (el data.path) pasando el ticket como segundo subprotocolo, después de memberfy-realtime (el data.protocols). El servidor responde con memberfy-realtime.
const { data } = await api.post('/api/realtime/ticket'); // con Authorization y X-CommunityId
const ws = new WebSocket(`wss://api.memberfy.net${data.path}`, data.protocols);

El ticket nunca va en la URL (en la query, se ignora). Sin un ticket válido, la conexión se rechaza antes de convertirse en WebSocket (401). Cambiar de comunidad cierra el socket: pide otro ticket. Sin el servicio de tiempo real, el ticket devuelve 503, y la app sigue consultando por las rutas.

POST /api/chat/ticket y /api/chat/socket siguen funcionando como alias de estos durante un despliegue y después desaparecen: usa los nuevos.

Salas

Toda conexión ya entra en tres salas:

SalaQuién
meLa propia persona
communityTodos en la comunidad
staffSolo el equipo (propietario, administrador, moderador y finanzas)

Las demás las pide el cliente, con la misma regla del GET correspondiente, siempre en esta comunidad:

SalaQuién entra
space:<id>Quien puede abrir el espacio
item:event:<id>Quien puede abrir el evento
item:post:<id>Quien puede abrir la publicación
{ "type": "subscribe", "rooms": ["space:5b0e7c1a-…", "item:event:9d2f…"] }
{ "type": "unsubscribe", "rooms": ["space:5b0e7c1a-…"] }

También hay salas para los editores, solo para los roles que pueden guardar ahí: item:course:<id>, item:form:<id> (el equipo y quien gestiona el formulario; un miembro cuando está publicado y abre el espacio), item:campaign:<id>, item:email:<id> (e-mail de la biblioteca), item:workflow:<id>, item:survey:<id> (propietario y administrador), item:certificateTemplate:<id> (quien emite certificados en el espacio), item:emailOverride:<emailKey> (e-mail predeterminado, propietario y administrador). Y las salas de área, una por pantalla de sección: area:home (todos), area:members, area:campaigns, area:emails, area:workflows (equipo), area:moderation (propietario, administrador y moderador), area:analytics, area:dashboard, area:settings (propietario y administrador) y area:finance (propietario, administrador y finanzas).

Como máximo 20 salas por conexión (las automáticas no cuentan). La respuesta es subscribed con las salas aceptadas y un subscribe.denied por sala rechazada, sin motivo. Cuando el acceso de la persona cambia (salió, rol nuevo, grupo, acceso al espacio), el servidor vuelve a evaluar las salas, envía subscribe.denied de las que perdió y después access.changed.

Avisos de cambio (invalidate)

{ "type": "invalidate", "room": "community", "topic": "event.live", "ids": ["9d2f…"], "at": "2026-10-23T14:05:00.000Z" }

El aviso trae solo los ids y el tema, nunca título, texto, importe ni nombre: al recibirlo, repite el GET de lo que está en pantalla por las rutas de siempre.

TemaSalaCuándo
event.livecommunity, space:<id>, item:event:<id>Un evento empezó o terminó la transmisión
eventscommunity, space:<id>, item:event:<id>Un evento se creó, se editó o se eliminó
analytics.onlinestaff (propietario y administrador)El "En línea ahora" cambió (como máximo 1 vez cada 5 segundos)
campaigns.statsstaffTerminó un envío de campaña
menu.changedcommunityEl menú cambió: sección o espacio creado, editado (nombre, icono, dirección, sección, acceso, visibilidad), eliminado o reordenado. Sin ids y sin ningún dato del menú: cada persona vuelve a hacer el GET /api/sections con sus propios permisos. Los cambios seguidos se convierten en un solo aviso, en hasta 1 segundo

Los avisos de evento solo llegan a quien ve ese espacio; un evento privado o en moderación, solo al equipo.

Quién está aquí

La presencia de área (las caras en la parte superior de cada pantalla, ver Quién está aquí y edición en equipo) usa las mismas salas.

{ "type": "presence.join", "room": "area:campaigns", "editing": false }
{ "type": "presence.leave", "room": "area:campaigns" }
{ "type": "presence.active" }
  • presence.join al abrir la pantalla: la sala se evalúa como en subscribe (y se suscribe si aún no lo estaba), como máximo 3 lugares por conexión. La respuesta es presence.joined (room, editing) seguido de una instantánea presence, o presence.denied (room), sin motivo.
  • presence.leave al salir. No hay heartbeat: el ping del servidor mantiene el lugar, que desaparece 45 segundos después de la última respuesta; cerrar la conexión lo quita al instante.
  • presence.active cuando hay interacción (tecla, clic, foco), como máximo cada 10 segundos. Entrar cuenta como actividad.

En cada cambio (como máximo 1 por segundo por sala), la sala recibe una instantánea montada para cada conexión:

{
  "type": "presence", "room": "area:campaigns", "area": "area:campaigns",
  "members": [{ "memberId": "…", "name": "Maria", "avatarUrl": "…", "editing": false, "state": "active", "lastActiveAt": "…" }],
  "count": 3, "editingCount": 1, "at": "2026-10-28T14:05:00.000Z"
}
  • Tú nunca estás en tu propia lista. members trae como máximo 5 personas (editando y activas primero, luego las activas, luego las inactivas); count es el total de las otras personas en la sala.
  • Quien solo lee: en las salas de espacio y de elemento, quien no está editando ahí recibe solo memberId, name, avatarUrl y state, sin editing, lastActiveAt ni editingCount, para preservar a quien edita. Quien edita el mismo elemento y las salas de área del equipo reciben la instantánea completa.
  • state: "idle": conectada, sin actividad desde hace 30 segundos o más. Sigue en la lista.
  • Privacidad: quien desactivó Disponible para conversar no aparece ni cuenta para los demás, pero ve quién está. Un bloqueo en el chat oculta a las dos personas entre sí.
  • Chat de la comunidad desactivado: las salas de área y quien solo lee no reciben a nadie (members vacío, count 0), incluido el equipo. Solo quien edita el mismo elemento sigue viendo a los otros editores; item.saved y el 409 de versión no cambian.

Edición e item.saved

En un editor (evento, campaña, e-mail de la biblioteca, workflow, curso, formulario, encuesta, plantilla de certificado, e-mail predeterminado y la configuración, en area:settings), entra con "editing": true. Solo vale para quien puede guardar ahí (el equipo y, en el formulario, quien lo gestiona). Quien edita aparece con editing: true para los otros editores de la sala, incluso con el estado del chat desactivado; quien solo lee la página no se entera. editingCount cuenta solo a quien edita activo.

En cada guardado, la sala recibe, para el equipo y para quien está editando:

{ "type": "item.saved", "topic": "item.saved", "room": "item:campaign:<id>", "itemType": "campaign", "itemId": "<id>", "by": { "memberId": "…", "name": "Maria" }, "updatedAt": "<nueva versión>", "at": "…" }

Compara updatedAt con la versión que tienes para ignorar tu propio guardado. itemType: event, campaign, email, workflow, course (incluidos módulos y lecciones), form, survey, certificateTemplate, emailOverride (itemId = emailKey) y, en la sala area:settings (itemId = la comunidad), settings, chatSettings, notificationSettings, messagingSettings (SMS + WhatsApp), creditSettings, businessInformation, payoutSettings.

Los guardados aceptan la versión leída y responden 409 cuando otro llegó antes: ver Conflicto de versión.

Otros eventos

Del servidor, en JSON { type, data }: ready (con profileId, communityId, chatAvailable, chatStatus (el estado elegido en el chat), chatEnabled, rooms, maxRooms y pingIntervalMs), notification.new, notification.read y notification.removed (la campana, en toda comunidad), chat.settings, session.revoked y pong. Los eventos del chat solo llegan con el chat activado; con el chat desactivado, el socket sigue en pie y solo esos se detienen.

Del cliente: subscribe, unsubscribe, presence.join, presence.leave, presence.active, presence.idle ({ "type": "presence.idle", "idle": true } cuando la pestaña lleva 10 minutos inactiva, false cuando vuelve: la persona aparece Ausente mientras todas sus pestañas están inactivas), typing (solo con el chat activado) y ping.

Límites y códigos de cierre

  • Mensajes de hasta 4 KB, sin binario, como máximo 5 por segundo.
  • El servidor envía ping cada 25 segundos.
  • Para reconectar, espera un tiempo aleatorio y creciente, y pide un ticket nuevo.
CódigoQué significaQué hacer
4004Sesión terminadaNo reconectes con la misma sesión
4009Reemplazada: la persona abrió más de 10 conexiones y se cerró la más antiguaNada, si es la pestaña antigua
4029Servidor llenoConéctate más tarde y usa las rutas mientras tanto
1008Demasiados mensajesBaja el ritmo
1001Servidor reiniciándoseReconecta

Relacionados