mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
Adds TelephonyRegistrationService that: 1. On startup: queries platform for agent list, registers with the telephony dispatcher at TELEPHONY_DISPATCHER_URL 2. Every 30s: sends heartbeat to keep registration alive (90s TTL) 3. On shutdown: deregisters (best-effort, TTL cleans up anyway) 4. On heartbeat failure: auto re-registers Env vars: TELEPHONY_DISPATCHER_URL — where to register (outbound to dispatcher) TELEPHONY_CALLBACK_URL — where events come back (inbound to sidecar) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
115 lines
4.0 KiB
TypeScript
115 lines
4.0 KiB
TypeScript
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import axios from 'axios';
|
|
import { PlatformGraphqlService } from './platform/platform-graphql.service';
|
|
|
|
// On startup, registers this sidecar with the telephony dispatcher
|
|
// so Ozonetel events are routed to the correct sidecar by agentId.
|
|
//
|
|
// Flow:
|
|
// 1. Load agent list from platform (Agent entities in this workspace)
|
|
// 2. POST /api/supervisor/register to the dispatcher
|
|
// 3. Start heartbeat interval (every 30s)
|
|
// 4. On shutdown, DELETE /api/supervisor/register
|
|
|
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
|
|
@Injectable()
|
|
export class TelephonyRegistrationService implements OnModuleInit, OnModuleDestroy {
|
|
private readonly logger = new Logger(TelephonyRegistrationService.name);
|
|
private heartbeatTimer: NodeJS.Timeout | null = null;
|
|
|
|
constructor(
|
|
private config: ConfigService,
|
|
private platform: PlatformGraphqlService,
|
|
) {}
|
|
|
|
private get dispatcherUrl(): string {
|
|
return this.config.get<string>('TELEPHONY_DISPATCHER_URL') ?? '';
|
|
}
|
|
|
|
private get sidecarUrl(): string {
|
|
return this.config.get<string>('TELEPHONY_CALLBACK_URL') ?? '';
|
|
}
|
|
|
|
private get workspace(): string {
|
|
return process.env.PLATFORM_WORKSPACE_SUBDOMAIN ?? 'unknown';
|
|
}
|
|
|
|
async onModuleInit() {
|
|
if (!this.dispatcherUrl || !this.sidecarUrl) {
|
|
this.logger.warn('TELEPHONY_DISPATCHER_URL or TELEPHONY_CALLBACK_URL not set — skipping telephony registration');
|
|
return;
|
|
}
|
|
|
|
await this.register();
|
|
|
|
this.heartbeatTimer = setInterval(async () => {
|
|
try {
|
|
await axios.post(`${this.dispatcherUrl}/api/supervisor/heartbeat`, {
|
|
sidecarUrl: this.sidecarUrl,
|
|
}, { timeout: 5000 });
|
|
} catch (err: any) {
|
|
this.logger.warn(`Heartbeat failed: ${err.message} — attempting re-registration`);
|
|
await this.register();
|
|
}
|
|
}, HEARTBEAT_INTERVAL_MS);
|
|
}
|
|
|
|
async onModuleDestroy() {
|
|
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
|
|
if (this.dispatcherUrl && this.sidecarUrl) {
|
|
try {
|
|
await axios.delete(`${this.dispatcherUrl}/api/supervisor/register`, {
|
|
data: { sidecarUrl: this.sidecarUrl },
|
|
timeout: 5000,
|
|
});
|
|
this.logger.log('Deregistered from telephony dispatcher');
|
|
} catch {
|
|
// Best-effort — TTL will clean up anyway
|
|
}
|
|
}
|
|
}
|
|
|
|
private async register() {
|
|
try {
|
|
const agents = await this.loadAgentIds();
|
|
if (agents.length === 0) {
|
|
this.logger.warn('No agents found in workspace — skipping registration');
|
|
return;
|
|
}
|
|
|
|
await axios.post(`${this.dispatcherUrl}/api/supervisor/register`, {
|
|
sidecarUrl: this.sidecarUrl,
|
|
workspace: this.workspace,
|
|
agents,
|
|
}, { timeout: 5000 });
|
|
|
|
this.logger.log(`Registered with telephony dispatcher: ${agents.length} agents (${agents.join(', ')})`);
|
|
} catch (err: any) {
|
|
this.logger.error(`Registration failed: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
private async loadAgentIds(): Promise<string[]> {
|
|
try {
|
|
const apiKey = this.config.get<string>('PLATFORM_API_KEY');
|
|
if (!apiKey) return [];
|
|
|
|
const data = await this.platform.queryWithAuth<any>(
|
|
`{ agents(first: 50) { edges { node { ozonetelAgentId } } } }`,
|
|
undefined,
|
|
`Bearer ${apiKey}`,
|
|
);
|
|
|
|
return (data.agents?.edges ?? [])
|
|
.map((e: any) => e.node.ozonetelAgentId)
|
|
.filter((id: string) => id && id !== 'PENDING');
|
|
} catch (err: any) {
|
|
this.logger.warn(`Failed to load agents from platform: ${err.message}`);
|
|
return [];
|
|
}
|
|
}
|
|
}
|