fix: cross-instance event contamination in federated DM calls
Root cause: sendToFederatedCallUsers used sendToDmMembers when dmChannelId was set, which broadcast to ALL DM members including the caller's replicated stub. The caller's multi-instance WS received dm_call_accepted with the REMOTE instance's dmChannelId, causing token request for a non-existent channel (403) and preventing the caller from connecting. Fix 1: sendToFederatedCallUsers always uses ringedUserIds (exact recipients) instead of sendToDmMembers (all members including caller stub). Fix 2: dm_call_accepted handler only sets activeDmCall if the client is the caller (wasOutgoingCall) or already connected to LiveKit. Other instances of the same user just clear ringing without entering stuck "Connecting..." state.
This commit is contained in:
@@ -754,16 +754,15 @@ class ConnectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Send event to users involved in a federated call. Works for both Path A (DM exists) and Path B (no local DM). */
|
/** Send event to users who were ringed for a federated call.
|
||||||
|
* ALWAYS uses ringedUserIds, never sendToDmMembers — sendToDmMembers would
|
||||||
|
* also reach the caller's replicated stub, causing cross-instance event contamination
|
||||||
|
* (the caller's multi-instance WS gets dm_call_accepted with the wrong dmChannelId). */
|
||||||
sendToFederatedCallUsers(federatedId: string, event: ServerEvent): void {
|
sendToFederatedCallUsers(federatedId: string, event: ServerEvent): void {
|
||||||
const call = this.federatedCalls.get(federatedId);
|
const call = this.federatedCalls.get(federatedId);
|
||||||
if (!call) return;
|
if (!call) return;
|
||||||
if (call.dmChannelId) {
|
for (const uid of call.ringedUserIds) {
|
||||||
this.sendToDmMembers(call.dmChannelId, event);
|
this.sendToUser(uid, event);
|
||||||
} else {
|
|
||||||
for (const uid of call.ringedUserIds) {
|
|
||||||
this.sendToUser(uid, event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -802,15 +802,19 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
|||||||
|
|
||||||
case 'dm_call_accepted': {
|
case 'dm_call_accepted': {
|
||||||
const { setIncomingCall, setOutgoingCall, outgoingCall, 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;
|
const wasOutgoingCall = !!outgoingCall;
|
||||||
setIncomingCall(null);
|
setIncomingCall(null);
|
||||||
setOutgoingCall(null);
|
setOutgoingCall(null);
|
||||||
|
|
||||||
|
// Only enter active call state if:
|
||||||
|
// - We're the caller (wasOutgoingCall) → will connect via connectFn below
|
||||||
|
// - We already connected to LiveKit (clicked accept in handleAccept)
|
||||||
|
// Other instances of the same user must NOT enter call state — they'd show
|
||||||
|
// "Connecting..." forever with no actual LiveKit connection.
|
||||||
const callDmId = event.dmChannelId || event.federatedCallId || '';
|
const callDmId = event.dmChannelId || event.federatedCallId || '';
|
||||||
setActiveDmCall({ dmChannelId: callDmId });
|
if (wasOutgoingCall || isLiveKitConnected) {
|
||||||
// Only auto-connect if we're the CALLER waiting for acceptance.
|
setActiveDmCall({ dmChannelId: callDmId });
|
||||||
// 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) {
|
if (connectFn && !isLiveKitConnected && wasOutgoingCall && callDmId) {
|
||||||
connectFn(callDmId, true).catch((err: unknown) => {
|
connectFn(callDmId, true).catch((err: unknown) => {
|
||||||
console.error('[WS] DM call connect failed:', err);
|
console.error('[WS] DM call connect failed:', err);
|
||||||
|
|||||||
Reference in New Issue
Block a user