+12
−0
+2
−0
Loading
Signalé à l'usage : décrocher un appel laissait sonner l'autre navigateur.
La cause
La salle Socket.IO d'un joueur est `ex:{exerciseId}:p:{participantId}` — indexée
sur le PARTICIPANT, pas sur l'onglet. Le même lien joueur ouvert dans deux
navigateurs place donc les deux sockets dans la même salle, et `voice:incoming`
est diffusé à toute la salle : tous les onglets sonnent, ce qui est voulu.
Mais le décrochage n'était annoncé qu'à l'animateur, sur `call.staffSocketId`.
Rien ne revenait vers la salle du participant. L'onglet où l'on cliquait se
taisait bien — `stopRing()` est appelé en première ligne d'`accept()` — et les
autres n'avaient aucun moyen de savoir.
Sans filet, de surcroît : `answer()` désarme le minuteur d'abandon des 45 s
introduit par 1b339706, précisément pour ne pas couper une conversation décrochée.
Les autres onglets sonnaient donc pendant TOUTE la conversation, et non 45 s.
Le correctif
Un événement `voice:taken`, symétrique de `announceEnded` qui prévient déjà les
deux côtés d'une fin d'appel. C'est l'absence de cette symétrie au décrochage qui
faisait le défaut.
Émis depuis `client` et non depuis `this.server` : Socket.IO exclut alors
l'émetteur de sa propre diffusion. L'onglet qui vient de décrocher n'est donc pas
averti de son propre décrochage — sinon il raccrocherait l'appel qu'il vient de
prendre. Cela évite aussi de faire circuler un identifiant de socket dans la
charge pour que chaque client se reconnaisse.
Côté joueur, les autres onglets se taisent et rendent l'écran par
`closeWith('ANSWERED')`, chemin déjà existant : l'appel a bien été pris, et
l'historique local de ces onglets doit le lire ainsi. Leur `incomingRef` devient
nul, donc le `voice:ended` diffusé plus tard au raccrochage est ignoré par le
garde d'identifiant — pas de double écriture dans l'historique.
Tests
Deux tests de passerelle. Le premier tombe si l'annonce est retirée, et tombe
AUSSI si elle est émise depuis `this.server` au lieu de `client` — c'est ce
second cas qui compte, une diffusion trop large raccrochant l'appel de celui qui
vient de le prendre. Le second est une contre-épreuve : un décrochage refusé
(`NOT_RINGING`) n'annonce rien, ni au joueur ni à l'animateur.
Le harnais de test des sockets gagne un `to()`, la passerelle diffusant
désormais AUSSI depuis la socket et non seulement depuis le serveur.
Vérifications
NON EXÉCUTÉES : ni node ni pnpm n'étaient disponibles dans l'environnement de
travail, et node_modules n'y est pas installé. Tests, eslint et typechecks
restent donc à passer avant fusion, et le trajet complet à reprendre à deux
navigateurs. La relecture statique est faite : formatage prettier conforme,
`participantRoom` déjà importé dans la passerelle, aucun test existant
n'atteignait le chemin nominal d'`onVoiceAnswer`.
Défauts voisins relevés, NON traités
Micro laissé ouvert : `accept()` ne lit pas l'accusé de réception de
`voice:answer`. Si deux onglets décrochent au même instant, le perdant reçoit
`NOT_RINGING`, l'ignore, et affiche « appel en cours » avec un micro capté pour
rien. Ce correctif réduit la fenêtre sans la fermer.
Socket orpheline : dans Player.tsx, `connectSocket()` est appelé après plusieurs
`await` sans nouveau contrôle de `cancelled`. Un démontage pendant le chargement
laisse une socket jamais fermée, qui reste dans la salle du participant et gonfle
le décompte servant à `playerOnline`.
Co-Authored-By: Claude (RCA)