From 5265117d29dcc3a4e0a3206f6807b740d71db146 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:53:58 +0200 Subject: [PATCH] fix: three root causes for federated DM call failures 1. Accept/reject/end from remote instance now resolves federatedId to local dmChannelId via DB lookup, so the host can find its VoiceRoom when the event arrives with only a federatedCallId. Previously silently failed with "No active call" error. 2. Batch all dm_call_incoming state updates into a single useVoiceStore.setState() call. Prevents SoundController from starting multiple ringtone instances (async playSound guard race when 4 separate set() calls each triggered the subscription). 3. Always overwrite callOrigin/federatedCallId (with null if absent) on dm_call_incoming. Prevents stale values from a previous federated call routing local accepts to the wrong instance. --- packages/server/src/ws/events.ts | 38 ++++++++++++++++++++++++-- packages/web/src/hooks/useWebSocket.ts | 28 +++++++++---------- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/packages/server/src/ws/events.ts b/packages/server/src/ws/events.ts index 8748e84d..571476dd 100644 --- a/packages/server/src/ws/events.ts +++ b/packages/server/src/ws/events.ts @@ -1442,7 +1442,7 @@ function handleDmCallStart(event: Record, userId: string, usern } function handleDmCallAccept(event: Record, userId: string, ws: WebSocket): void { - const dmChannelId = (event.dmChannelId as string) || null; + let dmChannelId = (event.dmChannelId as string) || null; const federatedCallId = (event.federatedCallId as string) || null; if (!dmChannelId && !federatedCallId) { @@ -1450,6 +1450,18 @@ function handleDmCallAccept(event: Record, userId: string, ws: return; } + // Resolve federatedCallId → dmChannelId when only federatedCallId is provided. + // This happens when a remote instance accepts via callOrigin and the DM doesn't + // exist there (Path B), but the HOST has the VoiceRoom keyed by dmChannelId. + if (!dmChannelId && federatedCallId) { + const db = getDb(); + const ch = db.select({ id: schema.dmChannels.id }) + .from(schema.dmChannels) + .where(eq(schema.dmChannels.federatedId, federatedCallId)) + .get(); + if (ch) dmChannelId = ch.id; + } + // Path 1: Local room (we're the host) — only possible with dmChannelId if (dmChannelId) { if (!isDmMember(dmChannelId, userId)) { @@ -1530,11 +1542,21 @@ function handleDmCallAccept(event: Record, userId: string, ws: } function handleDmCallReject(event: Record, userId: string): void { - const dmChannelId = (event.dmChannelId as string) || null; + let dmChannelId = (event.dmChannelId as string) || null; const federatedCallId = (event.federatedCallId as string) || null; if (!dmChannelId && !federatedCallId) return; + // Resolve federatedCallId → dmChannelId for host VoiceRoom lookup + if (!dmChannelId && federatedCallId) { + const db = getDb(); + const ch = db.select({ id: schema.dmChannels.id }) + .from(schema.dmChannels) + .where(eq(schema.dmChannels.federatedId, federatedCallId)) + .get(); + if (ch) dmChannelId = ch.id; + } + // Path 1: Local room (we're the host) if (dmChannelId) { if (!isDmMember(dmChannelId, userId)) return; @@ -1587,11 +1609,21 @@ function handleDmCallReject(event: Record, userId: string): voi } function handleDmCallEnd(event: Record, userId: string): void { - const dmChannelId = (event.dmChannelId as string) || null; + let dmChannelId = (event.dmChannelId as string) || null; const federatedCallId = (event.federatedCallId as string) || null; if (!dmChannelId && !federatedCallId) return; + // Resolve federatedCallId → dmChannelId for host VoiceRoom lookup + if (!dmChannelId && federatedCallId) { + const db = getDb(); + const ch = db.select({ id: schema.dmChannels.id }) + .from(schema.dmChannels) + .where(eq(schema.dmChannels.federatedId, federatedCallId)) + .get(); + if (ch) dmChannelId = ch.id; + } + // Path 1: Local room (we're the host) if (dmChannelId) { if (!isDmMember(dmChannelId, userId)) return; diff --git a/packages/web/src/hooks/useWebSocket.ts b/packages/web/src/hooks/useWebSocket.ts index de3d790d..65e1f211 100644 --- a/packages/web/src/hooks/useWebSocket.ts +++ b/packages/web/src/hooks/useWebSocket.ts @@ -779,22 +779,20 @@ function handleEvent(origin: string, event: ServerEvent): void { // ─── DM call events (all origins) ────────────────────────────────────── case 'dm_call_incoming': { - const { setIncomingCall, setFederatedCallData, setFederatedCallId, setCallOrigin } = useVoiceStore.getState(); - setIncomingCall({ - dmChannelId: event.dmChannelId ?? null, - callerId: event.callerId, - callerName: event.callerName, + // Batch ALL call state into a single set() to prevent: + // 1. Ringtone multiplication (multiple subscription triggers from separate set() calls) + // 2. Stale callOrigin/federatedCallId from previous calls (always overwritten) + useVoiceStore.setState({ + incomingCall: { + dmChannelId: event.dmChannelId ?? null, + callerId: event.callerId, + callerName: event.callerName, + }, + federatedCallToken: event.livekitToken ?? null, + federatedCallUrl: event.livekitUrl ?? null, + federatedCallId: event.federatedCallId ?? null, + callOrigin: event.callOrigin ?? null, }); - // Store federated call data if present (remote LiveKit URL + token) - if (event.livekitUrl && event.livekitToken) { - setFederatedCallData(event.livekitToken, event.livekitUrl); - } - if (event.federatedCallId) { - setFederatedCallId(event.federatedCallId); - } - if (event.callOrigin) { - setCallOrigin(event.callOrigin); - } break; }