From 9ad240495f3e11b377dddd41034ae8a10047e636 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:21:27 +0200 Subject: [PATCH] fix: prevent auto-connect on dm_call_accepted for non-caller instances Three fixes for multi-instance call state consistency: 1. Client dm_call_accepted handler only auto-connects to LiveKit if the user was the caller (outgoingCall was set). Other instances of the same user just clear ringing state without connecting. 2. Server processDmCallAcceptEvent remote path skips duplicate broadcast when FederatedCallEntry is already active (prevents state conflicts from host fan-out arriving after local accept). 3. Ready payload handler clears stuck incomingCall when restoring an already-active call after page refresh. --- packages/server/src/routes/federation.ts | 16 +++++++++++----- packages/web/src/hooks/useWebSocket.ts | 12 +++++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/server/src/routes/federation.ts b/packages/server/src/routes/federation.ts index ae7c7fda..40f2cc1f 100644 --- a/packages/server/src/routes/federation.ts +++ b/packages/server/src/routes/federation.ts @@ -3614,12 +3614,18 @@ function processDmCallAcceptEvent( // We're a REMOTE instance receiving fan-out — transition local state const fedCall = connectionManager.getFederatedCall(event.federatedId); if (fedCall) { + // Only broadcast if transitioning from ringing → active. + // If already active (e.g., we initiated the accept and the host is fanning out back), + // skip the duplicate broadcast to avoid state conflicts on the client. + const wasRinging = fedCall.state === 'ringing'; connectionManager.activateFederatedCall(event.federatedId); - connectionManager.sendToFederatedCallUsers(event.federatedId, { - type: 'dm_call_accepted', - dmChannelId: fedCall.dmChannelId, - federatedCallId: event.federatedId, - } as ServerEvent); + if (wasRinging) { + connectionManager.sendToFederatedCallUsers(event.federatedId, { + type: 'dm_call_accepted', + dmChannelId: fedCall.dmChannelId, + federatedCallId: event.federatedId, + } as ServerEvent); + } } } diff --git a/packages/web/src/hooks/useWebSocket.ts b/packages/web/src/hooks/useWebSocket.ts index ed170b5e..de3d790d 100644 --- a/packages/web/src/hooks/useWebSocket.ts +++ b/packages/web/src/hooks/useWebSocket.ts @@ -322,6 +322,8 @@ function handleEvent(origin: string, event: ServerEvent): void { const isParticipant = call.participants.includes(myId) || !!call.livekitToken; if (call.state === 'active' && isParticipant) { const callDmId = call.dmChannelId || call.federatedCallId || ''; + // Clear any stuck ringing UI from a ringing→active transition during refresh + setIncomingCall(null); setActiveDmCall({ dmChannelId: callDmId }); // Store federated call data if present (server already filtered to this user's token) if (call.livekitUrl && call.livekitToken) { @@ -797,13 +799,17 @@ function handleEvent(origin: string, event: ServerEvent): void { } case 'dm_call_accepted': { - const { setIncomingCall, setOutgoingCall, setActiveDmCall, connectFn, isLiveKitConnected } = useVoiceStore.getState(); + const { setIncomingCall, setOutgoingCall, outgoingCall, setActiveDmCall, connectFn, isLiveKitConnected } = useVoiceStore.getState(); + // Track whether WE are the caller before clearing state + const wasOutgoingCall = !!outgoingCall; setIncomingCall(null); setOutgoingCall(null); const callDmId = event.dmChannelId || event.federatedCallId || ''; setActiveDmCall({ dmChannelId: callDmId }); - // Only connect if not already connected (federated acceptor connects immediately in handleAccept) - if (connectFn && !isLiveKitConnected && callDmId) { + // Only auto-connect if we're the CALLER waiting for acceptance. + // The callee who accepted connects in the click handler (handleAccept). + // Other instances of the same user should NOT auto-connect. + if (connectFn && !isLiveKitConnected && wasOutgoingCall && callDmId) { connectFn(callDmId, true).catch((err: unknown) => { console.error('[WS] DM call connect failed:', err); });