import type { IAppConfig } from '../config.ts'; import type { ICallAnsweredEvent, ICallEndedEvent, ICallRingingEvent, IDeviceRegisteredEvent, IIncomingCallEvent, ILegAddedEvent, ILegRemovedEvent, ILegStateChangedEvent, IOutboundCallEvent, IOutboundCallStartedEvent, IProviderRegisteredEvent, } from '../shared/proxy-events.ts'; import type { IActiveCall, ICallHistoryEntry, IDeviceStatus, IProviderStatus, IStatusSnapshot, TLegType, } from '../shared/status.ts'; const MAX_HISTORY = 100; const CODEC_NAMES: Record = { 0: 'PCMU', 8: 'PCMA', 9: 'G.722', 111: 'Opus', }; export class StatusStore { private appConfig: IAppConfig; private providerStatuses = new Map(); private deviceStatuses = new Map(); private activeCalls = new Map(); private callHistory: ICallHistoryEntry[] = []; constructor(appConfig: IAppConfig) { this.appConfig = appConfig; this.rebuildConfigState(); } updateConfig(appConfig: IAppConfig): void { this.appConfig = appConfig; this.rebuildConfigState(); } buildStatusSnapshot( instanceId: string, startTime: number, browserDeviceIds: string[], voicemailCounts: Record, ): IStatusSnapshot { const devices = [...this.deviceStatuses.values()]; for (const deviceId of browserDeviceIds) { devices.push({ id: deviceId, displayName: 'Browser', address: null, port: 0, aor: null, connected: true, isBrowser: true, }); } return { instanceId, uptime: Math.floor((Date.now() - startTime) / 1000), lanIp: this.appConfig.proxy.lanIp, providers: [...this.providerStatuses.values()], devices, calls: [...this.activeCalls.values()].map((call) => ({ ...call, duration: Math.floor((Date.now() - call.startedAt) / 1000), legs: [...call.legs.values()].map((leg) => ({ ...leg, pktSent: 0, pktReceived: 0, transcoding: false, })), })), callHistory: this.callHistory, contacts: this.appConfig.contacts || [], voicemailCounts, }; } noteDashboardCallStarted(callId: string, number: string, providerId?: string): void { this.activeCalls.set(callId, { id: callId, direction: 'outbound', callerNumber: null, calleeNumber: number, providerUsed: providerId || null, state: 'setting-up', startedAt: Date.now(), legs: new Map(), }); } noteProviderRegistered(data: IProviderRegisteredEvent): { wasRegistered: boolean } | null { const provider = this.providerStatuses.get(data.provider_id); if (!provider) { return null; } const wasRegistered = provider.registered; provider.registered = data.registered; provider.publicIp = data.public_ip; return { wasRegistered }; } noteDeviceRegistered(data: IDeviceRegisteredEvent): boolean { const device = this.deviceStatuses.get(data.device_id); if (!device) { return false; } device.address = data.address; device.port = data.port; device.aor = data.aor; device.connected = true; return true; } noteIncomingCall(data: IIncomingCallEvent): void { this.activeCalls.set(data.call_id, { id: data.call_id, direction: 'inbound', callerNumber: data.from_uri, calleeNumber: data.to_number, providerUsed: data.provider_id, state: 'ringing', startedAt: Date.now(), legs: new Map(), }); } noteOutboundDeviceCall(data: IOutboundCallEvent): void { this.activeCalls.set(data.call_id, { id: data.call_id, direction: 'outbound', callerNumber: data.from_device, calleeNumber: data.to_number, providerUsed: null, state: 'setting-up', startedAt: Date.now(), legs: new Map(), }); } noteOutboundCallStarted(data: IOutboundCallStartedEvent): void { this.activeCalls.set(data.call_id, { id: data.call_id, direction: 'outbound', callerNumber: null, calleeNumber: data.number, providerUsed: data.provider_id, state: 'setting-up', startedAt: Date.now(), legs: new Map(), }); } noteCallRinging(data: ICallRingingEvent): void { const call = this.activeCalls.get(data.call_id); if (call) { call.state = 'ringing'; } } noteCallAnswered(data: ICallAnsweredEvent): boolean { const call = this.activeCalls.get(data.call_id); if (!call) { return false; } call.state = 'connected'; if (data.provider_media_addr && data.provider_media_port) { for (const leg of call.legs.values()) { if (leg.type !== 'sip-provider') { continue; } leg.remoteMedia = `${data.provider_media_addr}:${data.provider_media_port}`; if (data.sip_pt !== undefined) { leg.codec = CODEC_NAMES[data.sip_pt] || `PT${data.sip_pt}`; } break; } } return true; } noteCallEnded(data: ICallEndedEvent): boolean { const call = this.activeCalls.get(data.call_id); if (!call) { return false; } this.callHistory.unshift({ id: call.id, direction: call.direction, callerNumber: call.callerNumber, calleeNumber: call.calleeNumber, providerUsed: call.providerUsed, startedAt: call.startedAt, duration: data.duration, legs: [...call.legs.values()].map((leg) => ({ id: leg.id, type: leg.type, metadata: leg.metadata || {}, })), }); if (this.callHistory.length > MAX_HISTORY) { this.callHistory.pop(); } this.activeCalls.delete(data.call_id); return true; } noteLegAdded(data: ILegAddedEvent): void { const call = this.activeCalls.get(data.call_id); if (!call) { return; } call.legs.set(data.leg_id, { id: data.leg_id, type: data.kind, state: data.state, codec: data.codec ?? null, rtpPort: data.rtpPort ?? null, remoteMedia: data.remoteMedia ?? null, metadata: data.metadata || {}, }); } noteLegRemoved(data: ILegRemovedEvent): void { this.activeCalls.get(data.call_id)?.legs.delete(data.leg_id); } noteLegStateChanged(data: ILegStateChangedEvent): void { const call = this.activeCalls.get(data.call_id); if (!call) { return; } const existingLeg = call.legs.get(data.leg_id); if (existingLeg) { existingLeg.state = data.state; if (data.metadata) { existingLeg.metadata = data.metadata; } return; } call.legs.set(data.leg_id, { id: data.leg_id, type: this.inferLegType(data.leg_id), state: data.state, codec: null, rtpPort: null, remoteMedia: null, metadata: data.metadata || {}, }); } private rebuildConfigState(): void { const nextProviderStatuses = new Map(); for (const provider of this.appConfig.providers) { const previous = this.providerStatuses.get(provider.id); nextProviderStatuses.set(provider.id, { id: provider.id, displayName: provider.displayName, registered: previous?.registered ?? false, publicIp: previous?.publicIp ?? null, }); } this.providerStatuses = nextProviderStatuses; const nextDeviceStatuses = new Map(); for (const device of this.appConfig.devices) { const previous = this.deviceStatuses.get(device.id); nextDeviceStatuses.set(device.id, { id: device.id, displayName: device.displayName, address: previous?.address ?? null, port: previous?.port ?? 0, aor: previous?.aor ?? null, connected: previous?.connected ?? false, isBrowser: false, }); } this.deviceStatuses = nextDeviceStatuses; } private inferLegType(legId: string): TLegType { if (legId.includes('-prov')) { return 'sip-provider'; } if (legId.includes('-dev')) { return 'sip-device'; } return 'webrtc'; } }