import { SIPClient } from '@/lib/sip-client'; import type { SIPConfig, ConnectionStatus, CallState } from '@/types/sip'; // Singleton SIP client — survives React StrictMode remounts let sipClient: SIPClient | null = null; let connected = false; let outboundPending = false; let outboundActive = false; let activeAgentId: string | null = null; type StateUpdater = { setConnectionStatus: (status: ConnectionStatus) => void; setCallState: (state: CallState) => void; setCallerNumber: (number: string | null) => void; setCallUcid: (ucid: string | null) => void; }; let stateUpdater: StateUpdater | null = null; export function registerSipStateUpdater(updater: StateUpdater) { stateUpdater = updater; } export function setOutboundPending(pending: boolean) { outboundPending = pending; } export function isOutboundPending(): boolean { return outboundPending; } export function connectSip(config: SIPConfig): void { if (connected || sipClient?.isRegistered() || sipClient?.isConnected()) { return; } if (!config.wsServer || !config.uri) { console.warn('SIP config incomplete — wsServer and uri required'); return; } if (sipClient) { sipClient.disconnect(); } // Resolve agent identity for logging try { const agentCfg = JSON.parse(localStorage.getItem('helix_agent_config') ?? '{}'); activeAgentId = agentCfg.ozonetelAgentId ?? null; const ext = config.uri?.match(/sip:(\d+)@/)?.[1] ?? 'unknown'; console.log(`[SIP] Connecting agent=${activeAgentId} ext=${ext} ws=${config.wsServer}`); } catch { console.log(`[SIP] Connecting uri=${config.uri}`); } connected = true; stateUpdater?.setConnectionStatus('connecting'); sipClient = new SIPClient( config, (status) => stateUpdater?.setConnectionStatus(status), (state, number, ucid) => { // Auto-answer SIP when it's a bridge from our outbound call if (state === 'ringing-in' && outboundPending) { outboundPending = false; outboundActive = true; console.log('[SIP-MGR] Outbound bridge detected — auto-answering'); setTimeout(() => { sipClient?.answer(); setTimeout(() => stateUpdater?.setCallState('active'), 300); }, 500); if (ucid) stateUpdater?.setCallUcid(ucid); return; } console.log(`[SIP] ${activeAgentId} | state=${state} | caller=${number ?? 'none'} | ucid=${ucid ?? 'none'} | outbound=${outboundActive}`); stateUpdater?.setCallState(state); if (!outboundActive && number !== undefined) { stateUpdater?.setCallerNumber(number ?? null); } if (ucid) stateUpdater?.setCallUcid(ucid); if (state === 'ended' || state === 'failed') { outboundActive = false; outboundPending = false; } }, ); sipClient.connect(); } export function disconnectSip(force = false): void { // Guard: don't disconnect SIP during an active or pending call // unless explicitly forced (e.g., logout, page unload). // This prevents React re-render cycles from killing the // SIP WebSocket mid-dial. if (!force && (outboundPending || outboundActive)) { console.log('[SIP-MGR] Disconnect blocked — call in progress'); return; } console.log(`[SIP] Disconnecting agent=${activeAgentId}` + (force ? ' (forced)' : '')); sipClient?.disconnect(); sipClient = null; connected = false; outboundPending = false; outboundActive = false; activeAgentId = null; stateUpdater?.setConnectionStatus('disconnected'); stateUpdater?.setCallUcid(null); } export function getSipClient(): SIPClient | null { return sipClient; }