
# Abonnements en temps réel

Les composants de page liés aux données peuvent rester à jour sans rechargement complet en s'abonnant aux modifications d'enregistrements. Une source de données déclare un `refreshMode` — `static` (par défaut), `poll` ou `realtime` — et Sovrium fait le reste : le mode poll récupère à nouveau les données à intervalle régulier, le mode realtime ouvre un WebSocket (avec un repli SSE) et applique les événements `insert`/`update`/`delete` à mesure qu'ils arrivent. Les intégrations externes peuvent s'abonner programmatiquement via les mêmes points de terminaison.

| Point de terminaison                       | Transport                            |
| ------------------------------------------ | ------------------------------------ |
| `GET /api/tables/:tableSlug/subscribe`     | Mise à niveau WebSocket (par table)  |
| `GET /api/tables/:tableSlug/subscribe/sse` | Repli Server-Sent Events (par table) |
| `GET /api/realtime/subscribe`              | WebSocket global                     |

## refreshMode sur une source de données

Définissez `refreshMode` sur la `dataSource` de n'importe quel composant. Le `filter` et le `sort` existants du composant délimitent également le flux en direct — les clients ne reçoivent que les modifications qui correspondent toujours aux critères de la source de données.

```yaml
pages:
  - name: Live Dashboard
    path: /dashboard
    components:
      - type: dataTable
        dataSource:
          table: orders
          filter:
            - field: status
              operator: eq
              value: processing
          sort:
            - field: createdAt
              direction: desc
          refreshMode: realtime
```

## Mode poll

Le mode poll récupère périodiquement à nouveau la source de données — aucun WebSocket nécessaire. Utilisez-le lorsqu'un quasi-temps-réel suffit et que la surcharge de gestion de connexion est indésirable.

```yaml
dataSource:
  table: inventory
  refreshMode: poll
  pollIntervalMs: 10000 # Re-fetch every 10 seconds
```

| Propriété        | Défaut         | Contrainte                                            |
| ---------------- | -------------- | ----------------------------------------------------- |
| `pollIntervalMs` | `30000` (30 s) | Minimum `1000` (1 s) pour éviter d'inonder le serveur |

## Mode temps réel (WebSocket)

`refreshMode: realtime` ouvre un WebSocket vers `/api/tables/:tableSlug/subscribe`. Le serveur pousse les événements de modification à mesure que les enregistrements sont créés, mis à jour ou supprimés — depuis **n'importe quelle** source (API REST, interface administrateur, écritures directes en base de données, automatisations).

```
GET /api/tables/orders/subscribe
Upgrade: websocket
```

Le serveur diffuse alors des messages tels que :

```json
{ "type": "change", "event": "insert", "record": { "id": "42", "fields": {  } } }
{ "type": "change", "event": "update", "record": {  }, "oldRecord": {  } }
{ "type": "change", "event": "delete", "recordId": 42 }
{ "type": "heartbeat", "timestamp": "2026-04-05T12:00:00Z" }
```

Les événements insert/update portent l'enregistrement complet `record` (filtré côté serveur selon les permissions de champ) ; les événements delete ne portent que `recordId`. La connexion requiert une authentification (`401` lors d'une mise à niveau non authentifiée) et un slug de table valide (`404` sinon).

## Filtrage des abonnements

Les abonnements respectent les `filter` et `sort` courants de la source de données, de sorte que les clients ne sont pas inondés d'événements non pertinents. Une modification qui ne correspond plus au filtre est livrée comme la transition appropriée (par ex. une mise à jour qui fait sortir une ligne de l'ensemble filtré est exposée afin que le client puisse l'abandonner).

## Contrat de message

Tous les messages temps réel — les trames WebSocket et les événements SSE `data:` utilisent le format **identique** — sont une union discriminée indexée sur `type`.

| `type`              | Objet                                                                                                    |
| ------------------- | -------------------------------------------------------------------------------------------------------- |
| `change`            | Mutation `insert`/`update`/`delete` ; `record`/`oldRecord` utilisent la charge utile `{ id, fields }`    |
| `conflict`          | Une modification concurrente a écrasé un changement optimiste en attente (déclenche le toast de conflit) |
| `heartbeat`         | Maintien de connexion émis tous les `heartbeatIntervalMs`                                                |
| `subscribed`        | Confirmation de handshake ; renvoie les `filter`/`fields`/`subscriptionId` résolus                       |
| `unsubscribed`      | Confirmation de désabonnement                                                                            |
| `join` / `leave`    | Un utilisateur a ouvert/quitté une page `presence: true` (délimité par `pagePath`)                       |
| `presence-sync`     | Instantané complet de présence envoyé une fois à la connexion                                            |
| `connection-status` | Connectivité du transport (`connected` / `reconnecting`)                                                 |

La charge utile `record` de `change` reflète l'enveloppe `{ id, fields }` de l'API des enregistrements, de sorte qu'un client peut appliquer un événement à un cache lié aux données sans aucune traduction de forme. Les `fields` sont déjà filtrés selon les permissions de champ.

:::callout
**Constantes de transport figées.** Le client et le serveur partagent `REALTIME_TRANSPORT_CONFIG` : recul de reconnexion `[1000, 2000, 4000, 8000]` ms, délai de reconnexion maximal `30000` ms, battement de cœur `30000` ms, maximum `10` connexions par utilisateur, délai d'inactivité `300000` ms, délai d'obsolescence de présence `60000` ms, maximum `50` entrées de présence par page.
:::

## Conscience de présence

Lorsqu'une page définit `presence: true`, le serveur diffuse des messages `join`/`leave`/`presence-sync` afin que les équipes puissent voir qui d'autre consulte la même page ou table — réduisant les conflits accidentels. La présence est délimitée par `pagePath`.

## Résolution de conflit et verrouillage optimiste

Le temps réel pilote un flux d'interface optimiste : le client applique sa modification immédiatement, puis se réconcilie avec le serveur. Si une modification concurrente a écrasé un changement optimiste en attente, le serveur émet un message `conflict` et la valeur canonique (le serveur l'emporte) prévaut.

Un `PATCH` d'enregistrement peut porter un jeton `updatedAt` de premier niveau. Lorsqu'il est présent, le serveur rejette une écriture obsolète avec `409 Conflict`. Les tables qui y participent déclarent un champ système `updated-at` afin que la colonne soit automatiquement incrémentée à chaque écriture — voir [CRUD et upsert](records-crud).

## Gestion de la connexion

Le client se reconnecte automatiquement avec le recul configuré, envoie/attend des battements de cœur et se replie sur SSE lorsque le WebSocket n'est pas disponible — de sorte que les fonctionnalités temps réel fonctionnent de manière fiable dans diverses conditions réseau.

## Abonnement programmatique

Les clients externes ouvrent `/api/tables/:tableSlug/subscribe` (ou le `/api/realtime/subscribe` global) et envoient un handshake nommant la `table` (et, en option, `subscriptionId`, `filter`, `fields`). Le serveur répond par une confirmation `subscribed`, puis diffuse les événements `change` ; un désabonnement produit une confirmation `unsubscribed`.

## Pages associées

- [CRUD et upsert](records-crud) — le contrat de verrouillage optimiste `updatedAt`
- [Filtrage, tri et pagination](records-filtering-sorting) — le filtre/tri qui délimite un abonnement
- [Vue d'ensemble des enregistrements](records-overview) — l'enveloppe `{ id, fields }` que le flux reflète
- [Vue d'ensemble des pages](pages-overview) — composants liés aux données et configuration de `dataSource`
