Temps réel : la passerelle websocket¶
Le temps réel d'ikloze passe par une passerelle websocket unique (socket.io), RealtimeGateway (web/realtime/realtime.gateway.ts). Elle porte la cloche de notifications aujourd'hui, et les canaux de ressource dès qu'un module en déclare un.
Elle remplace le flux SSE GET /api/notifications/stream, supprimé : deux transports temps réel auraient signifié deux authentifications de connexion, deux modèles de diffusion et deux endroits où chercher quand un message n'arrive pas.
Côté client
Cette page couvre les décisions et les contraintes d'exploitation. Pour brancher un frontend — événements, salles, accusés, pièges —, voir Temps réel — intégration client.
Connexion¶
Le client se connecte à la racine de l'API. Le jeton d'accès voyage en paramètre de requête, l'API WebSocket du navigateur ne permettant pas de poser d'en-tête Authorization sur la poignée de main :
const socket = io('https://api.ikloze.com', {
query: { token: accessToken },
transports: ['websocket'],
});
socket.on('ready', () => {
/* la salle personnelle est rejointe — les notifications arrivent */
});
socket.on('notification', (notification) => { /* … */ });
L'événement connect de socket.io se déclenche avant que le serveur ait fini d'authentifier et d'inscrire le client dans sa salle. Attendre ready est ce qui garantit de ne rien manquer.
Une connexion sans jeton valide est fermée immédiatement, avant toute adhésion à une salle.
Les gardes ne s'exécutent pas sur la poignée de main
Dans Nest, @UseGuards sur une passerelle ne décore que les handlers de messages. Une passerelle protégée par une garde accepterait donc toutes les connexions et n'authentifierait qu'au premier message — jamais, pour un client qui se contente d'écouter. L'authentification a lieu dans handleConnection, via HandshakeAuthenticator, qui réutilise la vérification JWT d'AuthenticatedGuard. Ne pas « simplifier » cela en y remettant une garde.
Aucun workspace actif n'est résolu à la connexion : la cloche est cadrée sur l'utilisateur.
Salles¶
| Salle | Adhésion | Contenu |
|---|---|---|
user:<id> |
Automatique à la connexion | La cloche (notification) |
<ressource>:<id> |
Explicite, via room:join / room:leave |
Selon le module propriétaire |
Un même utilisateur peut tenir plusieurs connexions — plusieurs onglets — et chacune reçoit la diffusion.
La salle personnelle n'est pas gérée par le client : room:join et room:leave refusent le préfixe user:. La rejoindre par son nom serait le moyen de s'abonner aux notifications d'autrui ; la quitter éteindrait silencieusement sa propre cloche.
Ouvrir une salle de ressource¶
La passerelle ne connaît aucune ressource métier. L'autorisation d'adhérer est portée par le module propriétaire, qui déclare un RoomAuthorizer :
@Injectable()
export class ApplicationRoomAuthorizer extends RoomAuthorizer {
readonly prefix = 'application';
async canJoin(resourceId: string, user: User): Promise<boolean> {
/* la décision appartient au service du module */
}
}
puis l'enregistre auprès de RoomAuthorizerRegistry (exporté par RealtimeModule). Une salle dont le préfixe n'est réclamé par aucun module est refusée — le défaut est le refus.
⚠️ Une seule instance¶
Un socket appartient à un processus. Une notification créée sur une seconde instance n'atteindrait jamais un client connecté à la première.
Le déploiement actuel tient sur un processus unique : entrypoint.sh se termine par un node web/main, sans mode cluster ni gestionnaire de processus, et docker-compose.yml déclare un seul service web dont la liaison de port fixe 3000:3000 interdit les réplicas en l'état. La contrainte n'est donc pas rencontrée, et aucun adaptateur de diffusion partagé n'est livré : sur un processus unique, il transporterait une diffusion vers lui-même.
Avant de passer à plusieurs réplicas
Monter le nombre d'instances sans adaptateur partagé casse le temps réel silencieusement : la moitié des messages disparaît selon l'instance sur laquelle le client est tombé, et le symptôme est impossible à reproduire en développement. Brancher l'adaptateur Redis de socket.io (@socket.io/redis-adapter) est un préalable, pas un correctif ultérieur. Redis est déjà présent dans la pile déployée (REDIS_URL, utilisé par BullMQ quand QUEUE_BACKEND=bull).
Diffuser depuis le code applicatif¶
La couche app/ ne connaît aucun transport. Elle dépend du port NotificationRealtimePublisher (app/notifications/notification-realtime.publisher.ts), que la passerelle implémente ; NotificationsModule fait la liaison.
Une passerelle indisponible ne fait jamais échouer la création d'une notification : publish avale et journalise l'échec de diffusion. La notification reste en base et la cloche la rattrape au prochain GET /api/notifications.