Temps réel — intégration client¶
Guide d'intégration de la passerelle websocket pour un client frontend. Pour les décisions d'architecture et la contrainte d'instance unique, voir Temps réel.
Une seule connexion porte tout : la cloche de notifications et les fils d'entretien. Il n'y a rien à ouvrir par fonctionnalité.
1. Se connecter¶
La passerelle parle socket.io et écoute à la racine de l'API — pas sous /api, qui ne préfixe que les routes HTTP.
import { io } from 'socket.io-client';
const socket = io('https://api.ikloze.com', {
query: { token: accessToken },
transports: ['websocket'],
});
Le jeton d'accès voyage en paramètre de requête et non en en-tête : l'API WebSocket du navigateur ne permet pas d'en poser un sur la poignée de main. C'est le même jeton que Authorization: Bearer en HTTP.
Une connexion sans jeton valide est fermée immédiatement par le serveur. Le client reçoit disconnect et non une erreur applicative.
Aucun en-tête X-Workspace-Id n'est lu, et aucun workspace actif n'est résolu : la connexion est cadrée sur la personne. Un closer reçoit donc les messages des espaces dont il n'est pas membre, ce qui est exactement le propos de la marketplace.
2. Attendre ready, pas connect¶
connect se déclenche avant que le serveur ait fini d'authentifier et d'inscrire le client dans sa salle. Émettre ou compter sur une réception entre connect et ready fait manquer des messages. ready est le seul signal fiable.
3. La cloche¶
L'inscription est automatique : rien à demander.
La charge utile est celle de GET /api/notifications (un élément de items).
Rupture pour les clients existants
GET /api/notifications/stream (SSE) n'existe plus. Un client encore branché dessus n'a plus de cloche temps réel du tout — il faut basculer sur cette connexion.
4. Le fil d'entretien d'une candidature¶
Une salle par candidature, adhésion explicite.
const { joined } = await socket
.timeout(5000)
.emitWithAck('room:join', { room: `application:${applicationId}` });
if (joined) {
socket.on('application-message', (message) => { /* … */ });
}
// en quittant l'écran
await socket.emitWithAck('room:leave', { room: `application:${applicationId}` });
| Émission | Charge utile | Accusé |
|---|---|---|
room:join |
{ room: "application:<id>" } |
{ joined: boolean } |
room:leave |
{ room: "application:<id>" } |
{ left: boolean } |
Un refus est un { joined: false }, pas une erreur. Candidature inconnue, appartenant à quelqu'un d'autre, ou hors de portée d'un manager : la réponse est la même, délibérément — elle n'apprend rien sur ce qui existe.
L'événement application-message porte exactement la réponse de POST /api/announcement-applications/:id/messages, métadonnées de pièces jointes et URLs signées comprises. Le type généré depuis l'OpenAPI (ApplicationMessageDto) s'applique tel quel — il n'y a pas de second schéma à écrire.
5. Ce qu'il faut savoir¶
Le socket accélère, il ne fait pas foi. Un message manqué — reconnexion, onglet en veille — se retrouve par le listing HTTP, qui reste la source de vérité. Aucun rejeu n'est livré : la pagination fait déjà ce travail.
Après une reconnexion, réadhérer. La salle personnelle est rejointe automatiquement à chaque poignée de main ; les salles de candidature, non. Un client qui a perdu la connexion doit rejouer ses room:join après le ready suivant, sinon il est connecté et silencieux.
Les URLs signées des pièces jointes expirent au bout de 5 minutes. Sur un socket resté ouvert longtemps, une URL reçue plus tôt sera périmée ; recharger le fil en HTTP la régénère. Il n'y a pas de protocole de rafraîchissement d'URL.
Plusieurs onglets fonctionnent. Chaque connexion du même utilisateur reçoit la diffusion ; il n'y a rien à coordonner côté client.
La salle personnelle n'est pas manipulable. room:join et room:leave refusent tout nom commençant par user: — la rejoindre serait s'abonner aux notifications d'autrui, la quitter éteindrait silencieusement sa propre cloche.
L'origine doit être autorisée. La passerelle applique la même liste que le HTTP (CORS_ALLOWED_ORIGINS, séparée par des espaces). Une origine absente échoue à la poignée de main.
6. Récapitulatif des événements¶
| Sens | Nom | Contenu |
|---|---|---|
| serveur → client | ready |
Aucun — signal d'inscription effective |
| serveur → client | notification |
Une notification de cloche |
| serveur → client | application-message |
Un message d'entretien |
| client → serveur | room:join |
{ room } → { joined } |
| client → serveur | room:leave |
{ room } → { left } |