# Tempo reale (WebSocket)

> Il WebSocket della piattaforma - il ticket monouso, il sottoprotocollo memberfy-realtime, le stanze automatiche e quelle richieste, gli avvisi invalidate, i limiti e i codici di chiusura.

L'app riceve i cambiamenti subito tramite un WebSocket: la campanella, la chat, l'"Online adesso", chi c'è in ogni schermata, il banner dell'evento in diretta, i numeri delle campagne. Qualsiasi membro della community, di qualsiasi ruolo, apre il socket, **con la chat attiva o disattivata**.

## Aprire la connessione

1. Chiedi un ticket con `POST /api/realtime/ticket`, con il token e l'`X-CommunityId` (vedi [Autenticazione](/api/autenticacao) e [X-CommunityId](/api/x-community-id)). Il ticket è **monouso**, vale **60 secondi** ed è legato alla persona, alla community e alla sessione.
2. Apri `wss://<api>/api/realtime/socket` (il `data.path`) passando il ticket come **secondo sottoprotocollo**, dopo `memberfy-realtime` (il `data.protocols`). Il server risponde con `memberfy-realtime`.

```js
const { data } = await api.post('/api/realtime/ticket'); // con Authorization e X-CommunityId
const ws = new WebSocket(`wss://api.memberfy.net${data.path}`, data.protocols);
```

Il ticket non va mai nell'URL (nella query viene ignorato). Senza un ticket valido, la connessione viene rifiutata prima di diventare WebSocket (401). Cambiare community chiude il socket: chiedi un altro ticket. Senza il servizio di tempo reale, il ticket restituisce `503`, e l'app continua a leggere tramite le rotte.

`POST /api/chat/ticket` e `/api/chat/socket` continuano a funzionare come **alias** di questi per un deploy e poi spariscono: usa i nuovi.

## Stanze

Ogni connessione entra già in tre stanze:

| Stanza | Chi |
|---|---|
| `me` | La persona stessa |
| `community` | Tutti nella community |
| `staff` | Solo il team (proprietario, amministratore, moderatore e finanza) |

Le altre le chiede il client, con la stessa regola del `GET` corrispondente, sempre in questa community:

| Stanza | Chi entra |
|---|---|
| `space:<id>` | Chi può aprire lo spazio |
| `item:event:<id>` | Chi può aprire l'evento |
| `item:post:<id>` | Chi può aprire il post |

```json
{ "type": "subscribe", "rooms": ["space:5b0e7c1a-…", "item:event:9d2f…"] }
{ "type": "unsubscribe", "rooms": ["space:5b0e7c1a-…"] }
```

Ci sono anche stanze per gli editor, solo per i ruoli che possono salvare lì: `item:course:<id>`, `item:form:<id>` (il team e chi gestisce il modulo; un membro quando è pubblicato e apre lo spazio), `item:campaign:<id>`, `item:email:<id>` (e-mail della libreria), `item:workflow:<id>`, `item:survey:<id>` (proprietario e amministratore), `item:certificateTemplate:<id>` (chi emette certificati nello spazio), `item:emailOverride:<emailKey>` (e-mail predefinita, proprietario e amministratore). E le stanze di area, una per schermata di sezione: `area:home` (tutti), `area:members`, `area:campaigns`, `area:emails`, `area:workflows` (team), `area:moderation` (proprietario, amministratore e moderatore), `area:analytics`, `area:dashboard`, `area:settings` (proprietario e amministratore) e `area:finance` (proprietario, amministratore e finanza).

Al massimo **20 stanze** per connessione (quelle automatiche non contano). La risposta è `subscribed` con le stanze accettate e un `subscribe.denied` per stanza rifiutata, **senza motivo**. Quando l'accesso della persona cambia (è uscita, nuovo ruolo, gruppo, accesso allo spazio), il server rivaluta le stanze, manda `subscribe.denied` per quelle perse e poi `access.changed`.

## Avvisi di cambiamento (`invalidate`)

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

L'avviso porta **solo gli id e l'argomento**, mai titolo, testo, importo o nome: quando lo ricevi, rifai il `GET` di ciò che è sullo schermo tramite le rotte di sempre.

| Argomento | Stanza | Quando |
|---|---|---|
| `event.live` | `community`, `space:<id>`, `item:event:<id>` | Un evento ha iniziato o finito la trasmissione |
| `events` | `community`, `space:<id>`, `item:event:<id>` | Un evento è stato creato, modificato o rimosso |
| `analytics.online` | `staff` (proprietario e amministratore) | L'"Online adesso" è cambiato (al massimo 1 volta ogni 5 secondi) |
| `campaigns.stats` | `staff` | Un invio di campagna è terminato |
| `menu.changed` | `community` | Il menu è cambiato: sezione o spazio creato, modificato (nome, icona, indirizzo, sezione, accesso, visibilità), rimosso o riordinato. Senza `ids` e senza nessun dato del menu: ogni persona rifà il `GET /api/sections` con i propri permessi. Modifiche in sequenza diventano un solo avviso, entro 1 secondo |

Gli avvisi degli eventi arrivano solo a chi vede quello spazio; un evento privato o in moderazione, solo al team.

## Chi c'è qui

La presenza di area (i volti in cima a ogni schermata, vedi [Chi c'è qui e modifica in team](/conceitos/presenca-e-edicao-em-equipe)) usa le stesse stanze.

```json
{ "type": "presence.join", "room": "area:campaigns", "editing": false }
{ "type": "presence.leave", "room": "area:campaigns" }
{ "type": "presence.active" }
```

- `presence.join` all'apertura della schermata: la stanza viene valutata come in `subscribe` (e sottoscritta, se non lo era), al massimo **3 posti** per connessione. La risposta è `presence.joined` (`room`, `editing`) seguito da un'istantanea `presence`, oppure `presence.denied` (`room`), senza motivo.
- `presence.leave` all'uscita. Non c'è heartbeat: il ping del server mantiene il posto, che sparisce 45 secondi dopo l'ultima risposta; chiudere la connessione lo toglie subito.
- `presence.active` quando c'è interazione (tasto, clic, focus), al massimo ogni 10 secondi. Entrare conta come attività.

A ogni cambiamento (al massimo 1 al secondo per stanza), la stanza riceve un'istantanea costruita per ogni connessione:

```json
{
  "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"
}
```

- Tu **non** sei mai nella tua lista. `members` contiene al massimo 5 persone (prima chi modifica ed è attivo, poi gli attivi, poi gli inattivi); `count` è il totale delle altre persone nella stanza.
- **Chi legge soltanto:** nelle stanze di spazio e di elemento, chi non sta modificando lì riceve solo `memberId`, `name`, `avatarUrl` e `state`, senza `editing`, `lastActiveAt` né `editingCount`, per la privacy di chi modifica. Chi modifica lo stesso elemento e le stanze di area del team ricevono l'istantanea completa.
- `state: "idle"`: connessa, senza attività da 30 secondi o più. Resta nella lista.
- **Privacy:** chi ha disattivato **Disponibile a chattare** non compare né conta per gli altri, ma vede chi c'è. Un blocco nella chat nasconde le due persone l'una all'altra.
- **Chat della community disattivata:** le stanze di area e chi legge soltanto non ricevono nessuno (`members` vuoto, `count` 0), team compreso. Solo chi modifica lo stesso elemento continua a vedere gli altri editor; `item.saved` e il `409` di versione non cambiano.

### Modifica e `item.saved`

In un editor (evento, campagna, e-mail della libreria, workflow, corso, modulo, sondaggio, modello di certificato, e-mail predefinita e le impostazioni, in `area:settings`), entra con `"editing": true`. Vale solo per chi può salvare lì (il team e, nel modulo, chi lo gestisce). Chi modifica compare con `editing: true` agli **altri editor** della stanza, anche con lo stato della chat disattivato; chi legge solo la pagina non lo sa. `editingCount` conta solo chi modifica ed è attivo.

A ogni salvataggio, la stanza riceve, per il team e per chi sta modificando:

```json
{ "type": "item.saved", "topic": "item.saved", "room": "item:campaign:<id>", "itemType": "campaign", "itemId": "<id>", "by": { "memberId": "…", "name": "Maria" }, "updatedAt": "<nuova versione>", "at": "…" }
```

Confronta `updatedAt` con la versione che hai per ignorare il tuo salvataggio. `itemType`: `event`, `campaign`, `email`, `workflow`, `course` (inclusi moduli e lezioni), `form`, `survey`, `certificateTemplate`, `emailOverride` (`itemId` = `emailKey`) e, nella stanza `area:settings` (`itemId` = la community), `settings`, `chatSettings`, `notificationSettings`, `messagingSettings` (SMS + WhatsApp), `creditSettings`, `businessInformation`, `payoutSettings`.

I salvataggi accettano la versione letta e rispondono `409` quando un altro è arrivato prima: vedi [Conflitto di versione](/api/respostas-e-erros#conflitto-di-versione).

## Altri eventi

Dal server, in JSON `{ type, data }`: `ready` (con `profileId`, `communityId`, `chatAvailable`, `chatStatus` (lo stato scelto in chat), `chatEnabled`, `rooms`, `maxRooms` e `pingIntervalMs`), `notification.new`, `notification.read` e `notification.removed` (la campanella, in ogni community), `chat.settings`, `session.revoked` e `pong`. Gli eventi della chat arrivano solo con la chat **attiva**; con la chat disattivata, il socket resta in piedi e si fermano solo quelli.

Dal client: `subscribe`, `unsubscribe`, `presence.join`, `presence.leave`, `presence.active`, `presence.idle` (`{ "type": "presence.idle", "idle": true }` quando la scheda resta inattiva per 10 minuti, `false` quando torna: la persona risulta **Assente** finché tutte le sue schede sono inattive), `typing` (solo con la chat attiva) e `ping`.

## Limiti e codici di chiusura

- Messaggi fino a **4 KB**, niente binario, al massimo **5 al secondo**.
- Il server manda un `ping` ogni 25 secondi.
- Per riconnetterti, aspetta un tempo casuale e crescente e chiedi un **ticket nuovo**.

| Codice | Cosa significa | Cosa fare |
|---|---|---|
| `4004` | Sessione terminata | Non riconnetterti con la stessa sessione |
| `4009` | Sostituita: la persona ha aperto più di 10 connessioni e si è chiusa la più vecchia | Niente, se è la scheda vecchia |
| `4029` | Server pieno | Riconnettiti più tardi e usa le rotte nel frattempo |
| `1008` | Troppi messaggi | Rallenta |
| `1001` | Server in riavvio | Riconnettiti |

## Correlati

- [Autenticazione](/api/autenticacao)
- [X-CommunityId](/api/x-community-id)
- [Messaggi](/membros/mensagens)
