feat(runtime): refactor runtime state and proxy event handling for typed WebRTC linking and shared status models
This commit is contained in:
313
ts/runtime/status-store.ts
Normal file
313
ts/runtime/status-store.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
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<number, string> = {
|
||||
0: 'PCMU',
|
||||
8: 'PCMA',
|
||||
9: 'G.722',
|
||||
111: 'Opus',
|
||||
};
|
||||
|
||||
export class StatusStore {
|
||||
private appConfig: IAppConfig;
|
||||
private providerStatuses = new Map<string, IProviderStatus>();
|
||||
private deviceStatuses = new Map<string, IDeviceStatus>();
|
||||
private activeCalls = new Map<string, IActiveCall>();
|
||||
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<string, number>,
|
||||
): 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<string, IProviderStatus>();
|
||||
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<string, IDeviceStatus>();
|
||||
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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user