fix: Fix users unable to see or hear each other in voice channels due to

stale server sockets, passive non-initiators, and race conditions
during peer connection setup.

Fix users unable to see or hear each other in voice channels due to
stale server sockets, passive non-initiators, and race conditions
during peer connection setup.

Server:
- Close stale WebSocket connections sharing the same oderId in
  handleIdentify instead of letting them linger up to 45s
- Make user_joined/user_left broadcasts identity-aware so duplicate
  sockets don't produce phantom join/leave events
- Include serverIds in user_left payload for multi-room presence
- Simplify findUserByOderId now that stale sockets are cleaned up

Client - signaling:
- Add fallback offer system with 1s timer for missed user_joined races
- Add non-initiator takeover after 5s when the initiator fails to send
  an offer (NON_INITIATOR_GIVE_UP_MS)
- Scope peerServerMap per signaling URL to prevent cross-server
  collisions
- Add socket identity guards on all signaling event handlers
- Replace canReusePeerConnection with hasActivePeerConnection and
  isPeerConnectionNegotiating with extended grace periods

Client - peer connections:
- Extract replaceUnusablePeer helper to deduplicate stale peer
  replacement in offer and ICE handlers
- Add stale connectionstatechange guard to ignore events from replaced
  RTCPeerConnection instances
- Use deterministic initiator election in peer recovery reconnects
- Track createdAt on PeerData for staleness detection

Client - presence:
- Add multi-room presence tracking via presenceServerIds on User
- Replace clearUsers + individual userJoined with syncServerPresence
  for atomic server roster updates
- Make userLeft handle partial server removal instead of full eviction

Documentation:
- Add server-side connection hygiene, non-initiator takeover, and stale
  peer replacement sections to the realtime README
This commit is contained in:
2026-04-04 02:47:58 +02:00
parent ae0ee8fac7
commit de2d3300d4
24 changed files with 1128 additions and 164 deletions

View File

@@ -95,6 +95,7 @@ export class SignalingTransportHandler<TMessage> {
sendRawMessage(message: Record<string, unknown>): void {
const targetPeerId = typeof message['targetUserId'] === 'string' ? message['targetUserId'] : null;
const messageType = typeof message['type'] === 'string' ? message['type'] : 'unknown';
if (targetPeerId) {
const targetSignalUrl = this.dependencies.signalingCoordinator.getPeerSignalUrl(targetPeerId);
@@ -102,6 +103,11 @@ export class SignalingTransportHandler<TMessage> {
if (targetSignalUrl && this.sendRawMessageToSignalUrl(targetSignalUrl, message)) {
return;
}
this.dependencies.logger.warn('[signaling] Missing peer signal route for outbound raw message', {
targetPeerId,
type: messageType
});
}
const serverId = typeof message['serverId'] === 'string' ? message['serverId'] : null;
@@ -118,12 +124,19 @@ export class SignalingTransportHandler<TMessage> {
if (connectedManagers.length === 0) {
this.dependencies.logger.error('[signaling] No active signaling connection for outbound message', new Error('No signaling manager available'), {
type: typeof message['type'] === 'string' ? message['type'] : 'unknown'
type: messageType
});
return;
}
this.dependencies.logger.warn('[signaling] Broadcasting raw message to all signaling managers due to unresolved route', {
connectedSignalUrls: connectedManagers.map(({ signalUrl }) => signalUrl),
serverId,
targetPeerId,
type: messageType
});
for (const { manager } of connectedManagers) {
manager.sendRawMessage(message);
}