1 Temps-Reel
tarzzan edited this page 2026-05-20 03:04:24 +00:00

Architecture Temps Réel

WebSocket via Socket.io : rooms, événements, reconnexion et synchronisation.


Pourquoi le Temps Réel ?

QueueMed repose sur la mise à jour instantanée de 3 interfaces simultanément :

┌──────────────────────────────────────────────────────────────────────┐
│                                                                      │
│  Médecin appelle le suivant                                          │
│         │                                                            │
│         │  < 100ms                                                   │
│         │                                                            │
│         ├──────────────────────────────────────────────┐             │
│         │                                              │             │
│         ▼                                              ▼             │
│  ┌──────────────────┐                    ┌──────────────────┐       │
│  │ 📱 Patient #13    │                    │ 📺 Écran salle    │       │
│  │                  │                    │                  │       │
│  │ "C'est votre    │                    │  ┌────────────┐  │       │
│  │  tour !"        │                    │  │    #13     │  │       │
│  │                  │                    │  │  APPELÉ    │  │       │
│  └──────────────────┘                    │  └────────────┘  │       │
│                                          └──────────────────┘       │
│         │                                              │             │
│         ▼                                              ▼             │
│  ┌──────────────────┐                    ┌──────────────────┐       │
│  │ 📱 Patient #14    │                    │ 📱 Patient #15    │       │
│  │                  │                    │                  │       │
│  │ Position: 1er   │                    │ Position: 2ème   │       │
│  │ (était 2ème)    │                    │ (était 3ème)     │       │
│  └──────────────────┘                    └──────────────────┘       │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

Architecture Socket.io

Initialisation

Le serveur Socket.io est intégré au serveur Express principal :

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│  server/_core/index.ts                                          │
│                                                                 │
│  const httpServer = createServer(app);                          │
│  const io = new Server(httpServer, {                            │
│    cors: { origin: "*" },                                       │
│    transports: ["websocket", "polling"]                         │
│  });                                                            │
│                                                                 │
│  // Exposé globalement pour accès depuis les procédures tRPC    │
│  (global as any).__socketIo = io;                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Accès depuis les Procédures

// Dans routers.ts
function getIo(): Server {
  return (global as any).__socketIo;
}

// Utilisation dans une procédure
const io = getIo();
io.to(`clinic:${clinicId}`).emit("queue:updated", data);

Système de Rooms

Socket.io utilise des rooms pour isoler les communications par cabinet et par patient :

┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│  ROOMS SOCKET.IO                                                    │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  clinic:{clinicId}                                           │   │
│  │                                                             │   │
│  │  Membres : Médecin + Écran d'affichage                      │   │
│  │  Événements reçus : queue:updated, patient:called           │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  patient:{patientToken}                                      │   │
│  │                                                             │   │
│  │  Membres : 1 patient (navigateur unique)                    │   │
│  │  Événements reçus : position:updated, turn:called           │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  display:{clinicId}                                          │   │
│  │                                                             │   │
│  │  Membres : Écran(s) d'affichage du cabinet                  │   │
│  │  Événements reçus : display:update, display:called          │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Connexion Client

Patient ouvre /queue/{token}
         │
         ▼
┌─────────────────────────────────────────┐
│ socket.emit("join:patient", {           │
│   patientToken: "abc123",               │
│   clinicId: 5                           │
│ });                                     │
├─────────────────────────────────────────┤
│ Serveur :                               │
│ socket.join(`patient:abc123`);          │
│ socket.join(`clinic:5`);                │
└─────────────────────────────────────────┘

Événements WebSocket

Événements Émis par le Serveur

Événement Room cible Payload Déclencheur
queue:updated clinic:{id} { entries, stats } Tout changement de file
patient:called patient:{token} { ticketNumber, message } Médecin appelle
position:updated patient:{token} { position, estimatedWait } Recalcul positions
display:update display:{id} { calledNumber, waiting, next } Changement d'état
display:called display:{id} { ticketNumber, animation } Nouveau patient appelé
queue:closed clinic:{id} { message } File fermée
queue:opened clinic:{id} { message } File ouverte

Événements Reçus par le Serveur

Événement Émetteur Payload Action
join:patient Patient { patientToken, clinicId } Rejoint les rooms
join:clinic Médecin { clinicId } Rejoint room clinic
join:display Écran { clinicId } Rejoint room display
disconnect Tous Nettoyage rooms

Flux de Données Complet

Appel du Suivant

┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│  1. Médecin → tRPC mutation queue.callNext({ clinicId: 5 })         │
│                                                                     │
│  2. Serveur :                                                       │
│     ├── UPDATE queue_entries SET status='called' WHERE id=42        │
│     ├── UPDATE queue_entries SET position=position-1 (tous après)   │
│     ├── INSERT analytics_events (patient_called)                    │
│     │                                                               │
│     ├── io.to("patient:abc123").emit("patient:called", {            │
│     │     ticketNumber: 13, message: "C'est votre tour !"           │
│     │   })                                                          │
│     │                                                               │
│     ├── io.to("display:5").emit("display:called", {                 │
│     │     ticketNumber: 13, animation: "pulse"                      │
│     │   })                                                          │
│     │                                                               │
│     ├── io.to("clinic:5").emit("queue:updated", {                   │
│     │     entries: [...], stats: { waiting: 4, called: 1 }          │
│     │   })                                                          │
│     │                                                               │
│     └── Pour chaque patient restant :                               │
│         io.to("patient:{token}").emit("position:updated", {         │
│           position: newPos, estimatedWait: newWait                  │
│         })                                                          │
│                                                                     │
│  3. Résultat : Toutes les interfaces se mettent à jour < 100ms     │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Patient Rejoint la File

┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│  1. Patient → tRPC mutation queue.join({ token, name? })            │
│                                                                     │
│  2. Serveur :                                                       │
│     ├── Valide le token QR (non expiré, file ouverte)               │
│     ├── INSERT queue_entries (nouveau patient)                      │
│     ├── INSERT analytics_events (patient_joined)                    │
│     │                                                               │
│     ├── io.to("clinic:5").emit("queue:updated", { ... })            │
│     ├── io.to("display:5").emit("display:update", { ... })          │
│     │                                                               │
│     └── Retourne { ticketNumber, position, estimatedWait }          │
│                                                                     │
│  3. Patient : socket.emit("join:patient", { patientToken, clinicId})│
│     → Rejoint la room pour recevoir les mises à jour                │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Reconnexion Automatique

Côté Client

┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│  Socket.io gère automatiquement la reconnexion :                    │
│                                                                     │
│  Connexion ──── Perte réseau ──── Tentative 1 (1s) ──── Échec      │
│                                         │                           │
│                                    Tentative 2 (2s) ──── Échec      │
│                                         │                           │
│                                    Tentative 3 (4s) ──── ✅ Reconnecté│
│                                         │                           │
│                                    Re-join rooms                    │
│                                    Sync état actuel                 │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Indicateur de Connexion

L'écran d'affichage montre un indicateur visuel :

État Indicateur Description
🟢 Connecté Pastille verte WebSocket actif
🟡 Reconnexion Pastille orange clignotante Tentative en cours
🔴 Déconnecté Pastille rouge Connexion perdue

Performance

Métrique Valeur Description
Latence < 100ms Temps entre action et mise à jour
Connexions simultanées ~1000 Par instance serveur
Taille message ~200 bytes Payload JSON compressé
Reconnexion < 5s Délai max de reconnexion
Transports WebSocket + Polling Fallback automatique

Scalabilité

Pour un déploiement multi-instances, Socket.io supporte un adapter Redis :

Instance 1 ──┐
              │
Instance 2 ──┼──── Redis Pub/Sub ──── Synchronisation rooms
              │
Instance 3 ──┘

Note v1.3 : Le déploiement actuel est mono-instance. L'adapter Redis sera ajouté en v2.0 si nécessaire.