import { Observable, of } from 'rxjs'; import type { SignalingMessage } from '../../../shared-kernel'; import { DEFAULT_DISPLAY_NAME, SIGNALING_TYPE_IDENTIFY } from '../realtime.constants'; import { IdentifyCredentials } from '../realtime.types'; import { ConnectedSignalingManager, ServerSignalingCoordinator } from './server-signaling-coordinator'; import { WebRTCLogger } from '../logging/webrtc-logger'; interface SignalingTransportHandlerDependencies { signalingCoordinator: ServerSignalingCoordinator; logger: WebRTCLogger; getLocalPeerId(): string; } export class SignalingTransportHandler { private lastIdentifyCredentials: IdentifyCredentials | null = null; constructor( private readonly dependencies: SignalingTransportHandlerDependencies ) {} getIdentifyCredentials(): IdentifyCredentials | null { return this.lastIdentifyCredentials; } getIdentifyOderId(): string { return this.lastIdentifyCredentials?.oderId || this.dependencies.getLocalPeerId(); } getIdentifyDisplayName(): string { return this.lastIdentifyCredentials?.displayName || DEFAULT_DISPLAY_NAME; } getIdentifyDescription(): string | undefined { return this.lastIdentifyCredentials?.description; } getConnectedSignalingManagers(): ConnectedSignalingManager[] { return this.dependencies.signalingCoordinator.getConnectedSignalingManagers(); } getCurrentSignalingUrl(activeServerId: string | null): string | null { if (activeServerId) { const activeServerSignalUrl = this.dependencies.signalingCoordinator.getServerSignalUrl(activeServerId); if (activeServerSignalUrl) { return activeServerSignalUrl; } } return this.getConnectedSignalingManagers()[0]?.signalUrl ?? null; } connectToSignalingServer(serverUrl: string): Observable { const manager = this.dependencies.signalingCoordinator.ensureSignalingManager(serverUrl); if (manager.isSocketOpen()) { return of(true); } return manager.connect(serverUrl); } isSignalingConnectedTo(serverUrl: string): boolean { return this.dependencies.signalingCoordinator.isSignalingConnectedTo(serverUrl); } async ensureSignalingConnected(timeoutMs?: number): Promise { return await this.dependencies.signalingCoordinator.ensureAnySignalingConnected(timeoutMs); } sendSignalingMessage(message: Omit): void { const targetPeerId = message.to; if (targetPeerId) { const targetSignalUrl = this.dependencies.signalingCoordinator.getPeerSignalUrl(targetPeerId); if (targetSignalUrl) { const targetManager = this.dependencies.signalingCoordinator.ensureSignalingManager(targetSignalUrl); targetManager.sendSignalingMessage(message, this.dependencies.getLocalPeerId()); return; } } const connectedManagers = this.getConnectedSignalingManagers(); if (connectedManagers.length === 0) { this.dependencies.logger.error('[signaling] No active signaling connection for outbound message', new Error('No signaling manager available'), { type: message.type }); return; } for (const { manager } of connectedManagers) { manager.sendSignalingMessage(message, this.dependencies.getLocalPeerId()); } } sendRawMessage(message: Record): void { const targetPeerId = typeof message['targetUserId'] === 'string' ? message['targetUserId'] : null; const messageType = typeof message['type'] === 'string' ? message['type'] : 'unknown'; if (targetPeerId) { const targetSignalUrl = this.dependencies.signalingCoordinator.getPeerSignalUrl(targetPeerId); if (targetSignalUrl && this.sendRawMessageToSignalUrl(targetSignalUrl, message)) { return; } this.dependencies.logger.warn('[signaling] Missing peer signal route for outbound raw message', { targetPeerId, type: messageType }); } const serverId = typeof message['serverId'] === 'string' ? message['serverId'] : null; if (serverId) { const serverSignalUrl = this.dependencies.signalingCoordinator.getServerSignalUrl(serverId); if (serverSignalUrl && this.sendRawMessageToSignalUrl(serverSignalUrl, message)) { return; } this.dependencies.logger.warn('[signaling] Missing server signal route for outbound raw message', { serverId, type: messageType }); return; } const connectedManagers = this.getConnectedSignalingManagers(); if (connectedManagers.length === 0) { this.dependencies.logger.error('[signaling] No active signaling connection for outbound message', new Error('No signaling manager available'), { type: messageType }); return; } this.dependencies.logger.warn('[signaling] Broadcasting raw message to all signaling managers due to unresolved route', { connectedSignalUrls: connectedManagers.map(({ signalUrl }) => signalUrl), serverId, targetPeerId, type: messageType }); for (const { manager } of connectedManagers) { manager.sendRawMessage(message); } } sendRawMessageToSignalUrl(signalUrl: string, message: Record): boolean { const manager = this.dependencies.signalingCoordinator.getSignalingManager(signalUrl); if (!manager) { return false; } manager.sendRawMessage(message); return true; } identify( oderId: string, displayName: string, signalUrl?: string, profile?: Pick ): void { const normalizedDisplayName = displayName.trim() || DEFAULT_DISPLAY_NAME; const normalizedDescription = typeof profile?.description === 'string' ? (profile.description.trim() || undefined) : undefined; const normalizedProfileUpdatedAt = typeof profile?.profileUpdatedAt === 'number' && Number.isFinite(profile.profileUpdatedAt) && profile.profileUpdatedAt > 0 ? profile.profileUpdatedAt : undefined; this.lastIdentifyCredentials = { oderId, displayName: normalizedDisplayName, description: normalizedDescription, profileUpdatedAt: normalizedProfileUpdatedAt }; if (signalUrl) { this.sendRawMessageToSignalUrl(signalUrl, { type: SIGNALING_TYPE_IDENTIFY, oderId, displayName: normalizedDisplayName, description: normalizedDescription, profileUpdatedAt: normalizedProfileUpdatedAt, connectionScope: signalUrl }); return; } const connectedManagers = this.getConnectedSignalingManagers(); if (connectedManagers.length === 0) { return; } for (const { signalUrl: managerSignalUrl, manager } of connectedManagers) { manager.sendRawMessage({ type: SIGNALING_TYPE_IDENTIFY, oderId, displayName: normalizedDisplayName, description: normalizedDescription, profileUpdatedAt: normalizedProfileUpdatedAt, connectionScope: managerSignalUrl }); } } }