diff --git a/Temps-Reel.-.md b/Temps-Reel.-.md new file mode 100644 index 0000000..a9d9897 --- /dev/null +++ b/Temps-Reel.-.md @@ -0,0 +1,285 @@ +# ⚡ 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 + +```typescript +// 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. + +--- + +