No results
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.
🏥 QueueMed
Navigation
📖 Général
👥 Guides Utilisateur
⚙️ Technique
📊 Fonctionnalités
🔗 Liens Rapides
🏥 QueueMed — Gestion intelligente de file d'attente médicale | Développé par tarzzan | git.cosmolan.fr