feat(server): fanOutCallEvent returns failures; surface to host-side caller
This commit is contained in:
@@ -12,6 +12,8 @@ import { getDb, getRawDb, schema } from '../db/index.js';
|
|||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
import { connectionManager } from '../ws/handler.js';
|
import { connectionManager } from '../ws/handler.js';
|
||||||
import type { FederatedCallEntry, DmRoomMeta } from '../ws/handler.js';
|
import type { FederatedCallEntry, DmRoomMeta } from '../ws/handler.js';
|
||||||
|
import { mapCallReasonToEventReason, type CallFanoutFailure } from '../ws/events.js';
|
||||||
|
import type { DmCallUndeliverableFailure } from '@backspace/shared';
|
||||||
import { sanitizeUser } from '../utils/sanitize.js';
|
import { sanitizeUser } from '../utils/sanitize.js';
|
||||||
import { deleteAttachmentFiles, deleteUploadFile } from '../utils/fileCleanup.js';
|
import { deleteAttachmentFiles, deleteUploadFile } from '../utils/fileCleanup.js';
|
||||||
import { tombstoneUser, collectDeletionBroadcastTargets, collectProfileBroadcastTargetIds } from '../utils/userDeletion.js';
|
import { tombstoneUser, collectDeletionBroadcastTargets, collectProfileBroadcastTargetIds } from '../utils/userDeletion.js';
|
||||||
@@ -4464,10 +4466,14 @@ function processDmCallAcceptEvent(
|
|||||||
|
|
||||||
// Fan out to ALL other remote instances (exclude the one that sent the accept)
|
// Fan out to ALL other remote instances (exclude the one that sent the accept)
|
||||||
const normalizedSource = sourceInstance.startsWith('http') ? sourceInstance : `https://${sourceInstance}`;
|
const normalizedSource = sourceInstance.startsWith('http') ? sourceInstance : `https://${sourceInstance}`;
|
||||||
fanOutCallEvent(dmChannelId!, event.federatedId, 'dm_call_accept', {
|
const hostCallerId = (room.metadata as DmRoomMeta).callerId;
|
||||||
|
const localDmId = dmChannelId!;
|
||||||
|
void fanOutCallEvent(localDmId, event.federatedId, 'dm_call_accept', {
|
||||||
call: { acceptor: event.call.acceptor },
|
call: { acceptor: event.call.acceptor },
|
||||||
}, normalizedSource, db).catch(err =>
|
}, normalizedSource, db).then(failures => {
|
||||||
console.error('[federation] Fan-out dm_call_accept failed:', err)
|
emitHostFanoutUndeliverable(hostCallerId, localDmId, event.federatedId!, 'accept', failures);
|
||||||
|
}).catch(err =>
|
||||||
|
console.error('[federation] Fan-out dm_call_accept threw:', err),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// We're a REMOTE instance receiving fan-out — transition local state
|
// We're a REMOTE instance receiving fan-out — transition local state
|
||||||
@@ -4517,19 +4523,23 @@ function processDmCallRejectEvent(
|
|||||||
const room = dmChannelId ? connectionManager.getRoom(dmChannelId) : undefined;
|
const room = dmChannelId ? connectionManager.getRoom(dmChannelId) : undefined;
|
||||||
if (room && room.roomType === 'dm') {
|
if (room && room.roomType === 'dm') {
|
||||||
const meta = room.metadata as DmRoomMeta;
|
const meta = room.metadata as DmRoomMeta;
|
||||||
|
const hostCallerId = meta.callerId;
|
||||||
|
const localDmId = dmChannelId!;
|
||||||
connectionManager.clearVoiceWs(meta.callerId);
|
connectionManager.clearVoiceWs(meta.callerId);
|
||||||
connectionManager.destroyRoom(dmChannelId!);
|
connectionManager.destroyRoom(localDmId);
|
||||||
|
|
||||||
connectionManager.sendToDmMembers(dmChannelId!, {
|
connectionManager.sendToDmMembers(localDmId, {
|
||||||
type: 'dm_call_rejected',
|
type: 'dm_call_rejected',
|
||||||
dmChannelId: dmChannelId!,
|
dmChannelId: localDmId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizedSource = sourceInstance.startsWith('http') ? sourceInstance : `https://${sourceInstance}`;
|
const normalizedSource = sourceInstance.startsWith('http') ? sourceInstance : `https://${sourceInstance}`;
|
||||||
fanOutCallEvent(dmChannelId!, event.federatedId, 'dm_call_end', {
|
void fanOutCallEvent(localDmId, event.federatedId, 'dm_call_end', {
|
||||||
call: { endedBy: event.call.rejector },
|
call: { endedBy: event.call.rejector },
|
||||||
}, normalizedSource, db).catch(err =>
|
}, normalizedSource, db).then(failures => {
|
||||||
console.error('[federation] Fan-out dm_call_end (reject) failed:', err)
|
emitHostFanoutUndeliverable(hostCallerId, localDmId, event.federatedId!, 'reject', failures);
|
||||||
|
}).catch(err =>
|
||||||
|
console.error('[federation] Fan-out dm_call_end (reject) threw:', err),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const fedCall = connectionManager.getFederatedCall(event.federatedId);
|
const fedCall = connectionManager.getFederatedCall(event.federatedId);
|
||||||
@@ -4572,23 +4582,27 @@ function processDmCallEndEvent(
|
|||||||
const room = dmChannelId ? connectionManager.getRoom(dmChannelId) : undefined;
|
const room = dmChannelId ? connectionManager.getRoom(dmChannelId) : undefined;
|
||||||
if (room && room.roomType === 'dm') {
|
if (room && room.roomType === 'dm') {
|
||||||
const meta = room.metadata as DmRoomMeta;
|
const meta = room.metadata as DmRoomMeta;
|
||||||
|
const hostCallerId = meta.callerId;
|
||||||
|
const localDmId = dmChannelId!;
|
||||||
connectionManager.clearVoiceWs(meta.callerId);
|
connectionManager.clearVoiceWs(meta.callerId);
|
||||||
for (const pid of room.participants) {
|
for (const pid of room.participants) {
|
||||||
connectionManager.clearVoiceUserStatus(pid);
|
connectionManager.clearVoiceUserStatus(pid);
|
||||||
connectionManager.clearVoiceWs(pid);
|
connectionManager.clearVoiceWs(pid);
|
||||||
}
|
}
|
||||||
connectionManager.destroyRoom(dmChannelId!);
|
connectionManager.destroyRoom(localDmId);
|
||||||
|
|
||||||
connectionManager.sendToDmMembers(dmChannelId!, {
|
connectionManager.sendToDmMembers(localDmId, {
|
||||||
type: 'dm_call_ended',
|
type: 'dm_call_ended',
|
||||||
dmChannelId: dmChannelId!,
|
dmChannelId: localDmId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizedSource = sourceInstance.startsWith('http') ? sourceInstance : `https://${sourceInstance}`;
|
const normalizedSource = sourceInstance.startsWith('http') ? sourceInstance : `https://${sourceInstance}`;
|
||||||
fanOutCallEvent(dmChannelId!, event.federatedId, 'dm_call_end', {
|
void fanOutCallEvent(localDmId, event.federatedId, 'dm_call_end', {
|
||||||
call: { endedBy: event.call.endedBy },
|
call: { endedBy: event.call.endedBy },
|
||||||
}, normalizedSource, db).catch(err =>
|
}, normalizedSource, db).then(failures => {
|
||||||
console.error('[federation] Fan-out dm_call_end failed:', err)
|
emitHostFanoutUndeliverable(hostCallerId, localDmId, event.federatedId!, 'end', failures);
|
||||||
|
}).catch(err =>
|
||||||
|
console.error('[federation] Fan-out dm_call_end threw:', err),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const fedCall = connectionManager.getFederatedCall(event.federatedId);
|
const fedCall = connectionManager.getFederatedCall(event.federatedId);
|
||||||
@@ -5144,7 +5158,7 @@ async function fanOutCallEvent(
|
|||||||
extraFields: Partial<FederationRelayEvent>,
|
extraFields: Partial<FederationRelayEvent>,
|
||||||
excludeOrigin: string | undefined,
|
excludeOrigin: string | undefined,
|
||||||
db: ReturnType<typeof getDb>,
|
db: ReturnType<typeof getDb>,
|
||||||
): Promise<void> {
|
): Promise<CallFanoutFailure[]> {
|
||||||
const members = db.select({ homeInstance: schema.users.homeInstance })
|
const members = db.select({ homeInstance: schema.users.homeInstance })
|
||||||
.from(schema.dmMembers)
|
.from(schema.dmMembers)
|
||||||
.innerJoin(schema.users, eq(schema.dmMembers.userId, schema.users.id))
|
.innerJoin(schema.users, eq(schema.dmMembers.userId, schema.users.id))
|
||||||
@@ -5161,7 +5175,7 @@ async function fanOutCallEvent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (targets.size === 0) return;
|
if (targets.size === 0) return [];
|
||||||
|
|
||||||
const relayEvent: FederationRelayEvent = {
|
const relayEvent: FederationRelayEvent = {
|
||||||
eventType,
|
eventType,
|
||||||
@@ -5172,11 +5186,51 @@ async function fanOutCallEvent(
|
|||||||
...extraFields,
|
...extraFields,
|
||||||
} as FederationRelayEvent;
|
} as FederationRelayEvent;
|
||||||
|
|
||||||
await Promise.all(
|
const labelByOrigin = new Map<string, string | null>();
|
||||||
Array.from(targets).map(origin =>
|
for (const r of db.select({ origin: schema.federationPeers.origin, instanceName: schema.federationPeers.instanceName })
|
||||||
sendCallRelay(origin, [relayEvent]).catch(err =>
|
.from(schema.federationPeers)
|
||||||
console.error(`[federation] Fan-out ${eventType} to ${origin} failed:`, err)
|
.all()) {
|
||||||
)
|
labelByOrigin.set(r.origin, r.instanceName ?? null);
|
||||||
)
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
Array.from(targets).map(async origin => ({ origin, result: await sendCallRelay(origin, [relayEvent]) })),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const failures: CallFanoutFailure[] = [];
|
||||||
|
for (const { origin, result } of results) {
|
||||||
|
if (!result.ok) {
|
||||||
|
console.error(`[federation] Fan-out ${eventType} to ${origin} failed (${result.reason}): ${result.error}`);
|
||||||
|
failures.push({
|
||||||
|
origin,
|
||||||
|
peerLabel: labelByOrigin.get(origin) ?? undefined,
|
||||||
|
reason: mapCallReasonToEventReason(result.reason),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Emit a non-terminal dm_call_undeliverable for a host-side fan-out failure. */
|
||||||
|
function emitHostFanoutUndeliverable(
|
||||||
|
userId: string,
|
||||||
|
dmChannelId: string,
|
||||||
|
federatedId: string,
|
||||||
|
phase: 'accept' | 'reject' | 'end',
|
||||||
|
fanoutFailures: CallFanoutFailure[],
|
||||||
|
): void {
|
||||||
|
if (fanoutFailures.length === 0) return;
|
||||||
|
const failures: DmCallUndeliverableFailure[] = fanoutFailures.map(f => ({
|
||||||
|
reason: f.reason,
|
||||||
|
peerOrigin: f.origin,
|
||||||
|
peerLabel: f.peerLabel,
|
||||||
|
}));
|
||||||
|
connectionManager.sendToUser(userId, {
|
||||||
|
type: 'dm_call_undeliverable',
|
||||||
|
dmChannelId,
|
||||||
|
federatedCallId: federatedId,
|
||||||
|
terminal: false,
|
||||||
|
phase,
|
||||||
|
failures,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1972,7 +1972,7 @@ async function sendFederatedCallStart(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Map a sendCallRelay reason to the event-surface reason. */
|
/** Map a sendCallRelay reason to the event-surface reason. */
|
||||||
function mapCallReasonToEventReason(reason: CallRelayFailureReason): DmCallUndeliverableReason {
|
export function mapCallReasonToEventReason(reason: CallRelayFailureReason): DmCallUndeliverableReason {
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
case 'peer_rejected': return 'peer_rejected';
|
case 'peer_rejected': return 'peer_rejected';
|
||||||
case 'peer_awaiting_approval': return 'peer_awaiting_approval';
|
case 'peer_awaiting_approval': return 'peer_awaiting_approval';
|
||||||
|
|||||||
Reference in New Issue
Block a user