refactor: re-key FederatedCallEntry by federatedId, add ringedUserIds
This commit is contained in:
@@ -60,13 +60,14 @@ export interface DmRoomMeta {
|
|||||||
|
|
||||||
/** In-memory registry for federated calls on REMOTE instances. */
|
/** In-memory registry for federated calls on REMOTE instances. */
|
||||||
export interface FederatedCallEntry {
|
export interface FederatedCallEntry {
|
||||||
dmChannelId: string;
|
dmChannelId: string | null; // null for Path B (no local DM), late-bound when DM created mid-call
|
||||||
federatedId: string;
|
federatedId: string; // primary key — cross-instance stable
|
||||||
callerId: string; // local stub userId of the caller
|
callerId: string; // local stub userId of the caller
|
||||||
callerHomeUserId: string;
|
callerHomeUserId: string;
|
||||||
federatedCallHost: string; // peer origin of the host instance
|
federatedCallHost: string; // peer origin of the host instance
|
||||||
livekitUrl: string;
|
livekitUrl: string;
|
||||||
tokens: Map<string, string>; // homeUserId → LiveKit token
|
tokens: Map<string, string>; // homeUserId → LiveKit token
|
||||||
|
ringedUserIds: string[]; // local userIds that received dm_call_incoming
|
||||||
state: 'ringing' | 'active';
|
state: 'ringing' | 'active';
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
}
|
}
|
||||||
@@ -98,7 +99,7 @@ class ConnectionManager {
|
|||||||
private pendingOfflineTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
private pendingOfflineTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
||||||
// roomId → Timeout for ringing DM rooms (60s auto-cleanup)
|
// roomId → Timeout for ringing DM rooms (60s auto-cleanup)
|
||||||
private ringingTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
private ringingTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
||||||
/** Federated calls where this instance is NOT the host. Keyed by local dmChannelId. */
|
/** Federated calls where this instance is NOT the host. Keyed by federatedId. */
|
||||||
private federatedCalls: Map<string, FederatedCallEntry> = new Map();
|
private federatedCalls: Map<string, FederatedCallEntry> = new Map();
|
||||||
private federatedCallTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
private federatedCallTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
||||||
// Space-muted/deafened users (moderator action)
|
// Space-muted/deafened users (moderator action)
|
||||||
@@ -445,53 +446,76 @@ class ConnectionManager {
|
|||||||
|
|
||||||
/** Register a federated call received via S2S. Adds 60s ringing timeout. */
|
/** Register a federated call received via S2S. Adds 60s ringing timeout. */
|
||||||
createFederatedCall(entry: FederatedCallEntry): void {
|
createFederatedCall(entry: FederatedCallEntry): void {
|
||||||
// Clear any existing entry + timeout to avoid leaked timers
|
this.clearFederatedCall(entry.federatedId);
|
||||||
this.clearFederatedCall(entry.dmChannelId);
|
this.federatedCalls.set(entry.federatedId, entry);
|
||||||
this.federatedCalls.set(entry.dmChannelId, entry);
|
|
||||||
|
|
||||||
// 60s ringing timeout — mirrors host behavior
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
this.federatedCallTimeouts.delete(entry.dmChannelId);
|
this.federatedCallTimeouts.delete(entry.federatedId);
|
||||||
const call = this.federatedCalls.get(entry.dmChannelId);
|
const call = this.federatedCalls.get(entry.federatedId);
|
||||||
if (call && call.state === 'ringing') {
|
if (call && call.state === 'ringing') {
|
||||||
this.federatedCalls.delete(entry.dmChannelId);
|
this.federatedCalls.delete(entry.federatedId);
|
||||||
this.sendToDmMembers(entry.dmChannelId, {
|
const endEvent = {
|
||||||
type: 'dm_call_ended',
|
type: 'dm_call_ended',
|
||||||
dmChannelId: entry.dmChannelId,
|
dmChannelId: call.dmChannelId,
|
||||||
});
|
federatedCallId: call.federatedId,
|
||||||
|
};
|
||||||
|
for (const uid of call.ringedUserIds) {
|
||||||
|
this.sendToUser(uid, endEvent as ServerEvent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
this.federatedCallTimeouts.set(entry.dmChannelId, timeout);
|
this.federatedCallTimeouts.set(entry.federatedId, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get a federated call entry by local dmChannelId. */
|
/** Get a federated call entry by federatedId (primary lookup). */
|
||||||
getFederatedCall(dmChannelId: string): FederatedCallEntry | undefined {
|
getFederatedCall(federatedId: string): FederatedCallEntry | undefined {
|
||||||
return this.federatedCalls.get(dmChannelId);
|
return this.federatedCalls.get(federatedId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a federated call entry by local dmChannelId (convenience reverse lookup). */
|
||||||
|
getFederatedCallByDmChannel(dmChannelId: string): FederatedCallEntry | undefined {
|
||||||
|
for (const entry of this.federatedCalls.values()) {
|
||||||
|
if (entry.dmChannelId === dmChannelId) return entry;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Transition a federated call from ringing → active. */
|
/** Transition a federated call from ringing → active. */
|
||||||
activateFederatedCall(dmChannelId: string): boolean {
|
activateFederatedCall(federatedId: string): boolean {
|
||||||
const call = this.federatedCalls.get(dmChannelId);
|
const call = this.federatedCalls.get(federatedId);
|
||||||
if (!call || call.state !== 'ringing') return false;
|
if (!call || call.state !== 'ringing') return false;
|
||||||
call.state = 'active';
|
call.state = 'active';
|
||||||
const timeout = this.federatedCallTimeouts.get(dmChannelId);
|
const timeout = this.federatedCallTimeouts.get(federatedId);
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
this.federatedCallTimeouts.delete(dmChannelId);
|
this.federatedCallTimeouts.delete(federatedId);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove a federated call entry and clear its timeout. */
|
/** Remove a federated call entry and clear its timeout. */
|
||||||
clearFederatedCall(dmChannelId: string): void {
|
clearFederatedCall(federatedId: string): void {
|
||||||
this.federatedCalls.delete(dmChannelId);
|
this.federatedCalls.delete(federatedId);
|
||||||
const timeout = this.federatedCallTimeouts.get(dmChannelId);
|
const timeout = this.federatedCallTimeouts.get(federatedId);
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
this.federatedCallTimeouts.delete(dmChannelId);
|
this.federatedCallTimeouts.delete(federatedId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Late-bind a dmChannelId onto a Path B FederatedCallEntry. */
|
||||||
|
lateBindFederatedCall(federatedId: string, dmChannelId: string): void {
|
||||||
|
const call = this.federatedCalls.get(federatedId);
|
||||||
|
if (call && call.dmChannelId === null) {
|
||||||
|
call.dmChannelId = dmChannelId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Expose federated calls for ready payload assembly. */
|
||||||
|
getAllFederatedCalls(): Map<string, FederatedCallEntry> {
|
||||||
|
return this.federatedCalls;
|
||||||
|
}
|
||||||
|
|
||||||
/** Add a user to a room. Enforces one-room-per-user invariant. Returns the room or null if room doesn't exist. */
|
/** Add a user to a room. Enforces one-room-per-user invariant. Returns the room or null if room doesn't exist. */
|
||||||
joinRoom(roomId: string, userId: string): VoiceRoom | null {
|
joinRoom(roomId: string, userId: string): VoiceRoom | null {
|
||||||
const room = this.voiceRooms.get(roomId);
|
const room = this.voiceRooms.get(roomId);
|
||||||
@@ -730,6 +754,19 @@ class ConnectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Send event to users involved in a federated call. Works for both Path A (DM exists) and Path B (no local DM). */
|
||||||
|
sendToFederatedCallUsers(federatedId: string, event: ServerEvent): void {
|
||||||
|
const call = this.federatedCalls.get(federatedId);
|
||||||
|
if (!call) return;
|
||||||
|
if (call.dmChannelId) {
|
||||||
|
this.sendToDmMembers(call.dmChannelId, event);
|
||||||
|
} else {
|
||||||
|
for (const uid of call.ringedUserIds) {
|
||||||
|
this.sendToUser(uid, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Send to a room — routes to sendToSpace (space rooms) or sendToDmMembers (DM rooms). */
|
/** Send to a room — routes to sendToSpace (space rooms) or sendToDmMembers (DM rooms). */
|
||||||
sendToRoom(roomId: string, event: ServerEvent, excludeUserId?: string): void {
|
sendToRoom(roomId: string, event: ServerEvent, excludeUserId?: string): void {
|
||||||
const room = this.voiceRooms.get(roomId);
|
const room = this.voiceRooms.get(roomId);
|
||||||
@@ -1321,18 +1358,20 @@ function buildReadyPayload(userId: string): {
|
|||||||
const myHomeUserId = readyUser?.homeUserId || userId;
|
const myHomeUserId = readyUser?.homeUserId || userId;
|
||||||
|
|
||||||
// Also include federated calls (this instance is NOT the host)
|
// Also include federated calls (this instance is NOT the host)
|
||||||
for (const dm of dmMemberships) {
|
for (const [_fedId, fedCall] of connectionManager.getAllFederatedCalls()) {
|
||||||
const fedCall = connectionManager.getFederatedCall(dm.dmChannelId);
|
const isParticipant = fedCall.ringedUserIds.includes(userId);
|
||||||
if (fedCall) {
|
const isDmMember = fedCall.dmChannelId && dmMemberships.some(dm => dm.dmChannelId === fedCall.dmChannelId);
|
||||||
|
if (isParticipant || isDmMember) {
|
||||||
activeCalls.push({
|
activeCalls.push({
|
||||||
dmChannelId: dm.dmChannelId,
|
dmChannelId: fedCall.dmChannelId,
|
||||||
|
federatedCallId: fedCall.federatedId,
|
||||||
callerId: fedCall.callerId,
|
callerId: fedCall.callerId,
|
||||||
participants: [],
|
participants: [],
|
||||||
startedAt: fedCall.startedAt,
|
startedAt: fedCall.startedAt,
|
||||||
state: fedCall.state,
|
state: fedCall.state,
|
||||||
federatedCallHost: fedCall.federatedCallHost,
|
federatedCallHost: fedCall.federatedCallHost,
|
||||||
livekitUrl: fedCall.livekitUrl,
|
livekitUrl: fedCall.livekitUrl,
|
||||||
livekitToken: fedCall.tokens.get(myHomeUserId), // only this user's token
|
livekitToken: fedCall.tokens.get(myHomeUserId),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -274,7 +274,8 @@ export interface Embed {
|
|||||||
// ─── Active Call Types ───────────────────────────────────────────────────────
|
// ─── Active Call Types ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface ActiveCallInfo {
|
export interface ActiveCallInfo {
|
||||||
dmChannelId: string;
|
dmChannelId: string | null;
|
||||||
|
federatedCallId?: string;
|
||||||
callerId: string;
|
callerId: string;
|
||||||
participants: string[];
|
participants: string[];
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user