feat(runtime): refactor runtime state and proxy event handling for typed WebRTC linking and shared status models

This commit is contained in:
2026-04-14 10:45:59 +00:00
parent 5a280c5c41
commit 51f7560730
15 changed files with 1105 additions and 813 deletions

View File

@@ -4,13 +4,36 @@
* The proxy-engine handles ALL SIP protocol mechanics. TypeScript only:
* - Sends configuration
* - Receives high-level events (incoming_call, call_ended, etc.)
* - Sends high-level commands (hangup, make_call, play_audio)
* - Sends high-level commands (hangup, make_call, add_leg, webrtc_offer)
*
* No raw SIP ever touches TypeScript.
*/
import path from 'node:path';
import { RustBridge } from '@push.rocks/smartrust';
import type { TProxyEventMap } from './shared/proxy-events.ts';
export type {
ICallAnsweredEvent,
ICallEndedEvent,
ICallRingingEvent,
IDeviceRegisteredEvent,
IIncomingCallEvent,
ILegAddedEvent,
ILegRemovedEvent,
ILegStateChangedEvent,
IOutboundCallEvent,
IOutboundCallStartedEvent,
IProviderRegisteredEvent,
IRecordingDoneEvent,
ISipUnhandledEvent,
IVoicemailErrorEvent,
IVoicemailStartedEvent,
IWebRtcAudioRxEvent,
IWebRtcIceCandidateEvent,
IWebRtcStateEvent,
IWebRtcTrackEvent,
TProxyEventMap,
} from './shared/proxy-events.ts';
// ---------------------------------------------------------------------------
// Command type map for smartrust
@@ -29,18 +52,6 @@ type TProxyCommands = {
params: { number: string; device_id?: string; provider_id?: string };
result: { call_id: string };
};
play_audio: {
params: { call_id: string; leg_id?: string; file_path: string; codec?: number };
result: Record<string, never>;
};
start_recording: {
params: { call_id: string; file_path: string; max_duration_ms?: number };
result: Record<string, never>;
};
stop_recording: {
params: { call_id: string };
result: { file_path: string; duration_ms: number };
};
add_leg: {
params: { call_id: string; number: string; provider_id?: string };
result: { leg_id: string };
@@ -121,50 +132,6 @@ type TProxyCommands = {
};
};
// ---------------------------------------------------------------------------
// Event types from Rust
// ---------------------------------------------------------------------------
export interface IIncomingCallEvent {
call_id: string;
from_uri: string;
to_number: string;
provider_id: string;
/** Whether registered browsers should see a `webrtc-incoming` toast for
* this call. Set by the Rust engine from the matched inbound route's
* `ringBrowsers` flag (defaults to `true` when no route matches, so
* deployments without explicit routes preserve pre-routing behavior). */
ring_browsers?: boolean;
}
export interface IOutboundCallEvent {
call_id: string;
from_device: string | null;
to_number: string;
}
export interface ICallEndedEvent {
call_id: string;
reason: string;
duration: number;
from_side?: string;
}
export interface IProviderRegisteredEvent {
provider_id: string;
registered: boolean;
public_ip: string | null;
}
export interface IDeviceRegisteredEvent {
device_id: string;
display_name: string;
address: string;
port: number;
aor: string;
expires: number;
}
// ---------------------------------------------------------------------------
// Bridge singleton
// ---------------------------------------------------------------------------
@@ -173,6 +140,16 @@ let bridge: RustBridge<TProxyCommands> | null = null;
let initialized = false;
let logFn: ((msg: string) => void) | undefined;
type TWebRtcIceCandidate = {
candidate?: string;
sdpMid?: string;
sdpMLineIndex?: number;
} | string;
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function buildLocalPaths(): string[] {
const root = process.cwd();
// Map Node's process.arch to tsrust's friendly target name.
@@ -231,8 +208,8 @@ export async function initProxyEngine(log?: (msg: string) => void): Promise<bool
initialized = true;
log?.('[proxy-engine] spawned and ready');
return true;
} catch (e: any) {
log?.(`[proxy-engine] init error: ${e.message}`);
} catch (error: unknown) {
log?.(`[proxy-engine] init error: ${errorMessage(error)}`);
bridge = null;
return false;
}
@@ -242,14 +219,14 @@ export async function initProxyEngine(log?: (msg: string) => void): Promise<bool
* Send the full app config to the proxy engine.
* This binds the SIP socket, starts provider registrations, etc.
*/
export async function configureProxyEngine(config: Record<string, unknown>): Promise<boolean> {
export async function configureProxyEngine(config: TProxyCommands['configure']['params']): Promise<boolean> {
if (!bridge || !initialized) return false;
try {
const result = await bridge.sendCommand('configure', config as any);
logFn?.(`[proxy-engine] configured, SIP bound on ${(result as any)?.bound || '?'}`);
const result = await sendProxyCommand('configure', config);
logFn?.(`[proxy-engine] configured, SIP bound on ${result.bound || '?'}`);
return true;
} catch (e: any) {
logFn?.(`[proxy-engine] configure error: ${e.message}`);
} catch (error: unknown) {
logFn?.(`[proxy-engine] configure error: ${errorMessage(error)}`);
return false;
}
}
@@ -260,14 +237,14 @@ export async function configureProxyEngine(config: Record<string, unknown>): Pro
export async function makeCall(number: string, deviceId?: string, providerId?: string): Promise<string | null> {
if (!bridge || !initialized) return null;
try {
const result = await bridge.sendCommand('make_call', {
const result = await sendProxyCommand('make_call', {
number,
device_id: deviceId,
provider_id: providerId,
} as any);
return (result as any)?.call_id || null;
} catch (e: any) {
logFn?.(`[proxy-engine] make_call error: ${e?.message || e}`);
});
return result.call_id || null;
} catch (error: unknown) {
logFn?.(`[proxy-engine] make_call error: ${errorMessage(error)}`);
return null;
}
}
@@ -278,7 +255,7 @@ export async function makeCall(number: string, deviceId?: string, providerId?: s
export async function hangupCall(callId: string): Promise<boolean> {
if (!bridge || !initialized) return false;
try {
await bridge.sendCommand('hangup', { call_id: callId } as any);
await sendProxyCommand('hangup', { call_id: callId });
return true;
} catch {
return false;
@@ -291,10 +268,9 @@ export async function hangupCall(callId: string): Promise<boolean> {
export async function webrtcOffer(sessionId: string, sdp: string): Promise<{ sdp: string } | null> {
if (!bridge || !initialized) return null;
try {
const result = await bridge.sendCommand('webrtc_offer', { session_id: sessionId, sdp } as any);
return result as any;
} catch (e: any) {
logFn?.(`[proxy-engine] webrtc_offer error: ${e?.message || e}`);
return await sendProxyCommand('webrtc_offer', { session_id: sessionId, sdp });
} catch (error: unknown) {
logFn?.(`[proxy-engine] webrtc_offer error: ${errorMessage(error)}`);
return null;
}
}
@@ -302,15 +278,15 @@ export async function webrtcOffer(sessionId: string, sdp: string): Promise<{ sdp
/**
* Forward an ICE candidate to the proxy engine.
*/
export async function webrtcIce(sessionId: string, candidate: any): Promise<void> {
export async function webrtcIce(sessionId: string, candidate: TWebRtcIceCandidate): Promise<void> {
if (!bridge || !initialized) return;
try {
await bridge.sendCommand('webrtc_ice', {
await sendProxyCommand('webrtc_ice', {
session_id: sessionId,
candidate: candidate?.candidate || candidate,
sdp_mid: candidate?.sdpMid,
sdp_mline_index: candidate?.sdpMLineIndex,
} as any);
candidate: typeof candidate === 'string' ? candidate : candidate.candidate || '',
sdp_mid: typeof candidate === 'string' ? undefined : candidate.sdpMid,
sdp_mline_index: typeof candidate === 'string' ? undefined : candidate.sdpMLineIndex,
});
} catch { /* ignore */ }
}
@@ -321,16 +297,16 @@ export async function webrtcIce(sessionId: string, candidate: any): Promise<void
export async function webrtcLink(sessionId: string, callId: string, providerMediaAddr: string, providerMediaPort: number, sipPt: number = 9): Promise<boolean> {
if (!bridge || !initialized) return false;
try {
await bridge.sendCommand('webrtc_link', {
await sendProxyCommand('webrtc_link', {
session_id: sessionId,
call_id: callId,
provider_media_addr: providerMediaAddr,
provider_media_port: providerMediaPort,
sip_pt: sipPt,
} as any);
});
return true;
} catch (e: any) {
logFn?.(`[proxy-engine] webrtc_link error: ${e?.message || e}`);
} catch (error: unknown) {
logFn?.(`[proxy-engine] webrtc_link error: ${errorMessage(error)}`);
return false;
}
}
@@ -341,14 +317,14 @@ export async function webrtcLink(sessionId: string, callId: string, providerMedi
export async function addLeg(callId: string, number: string, providerId?: string): Promise<string | null> {
if (!bridge || !initialized) return null;
try {
const result = await bridge.sendCommand('add_leg', {
const result = await sendProxyCommand('add_leg', {
call_id: callId,
number,
provider_id: providerId,
} as any);
return (result as any)?.leg_id || null;
} catch (e: any) {
logFn?.(`[proxy-engine] add_leg error: ${e?.message || e}`);
});
return result.leg_id || null;
} catch (error: unknown) {
logFn?.(`[proxy-engine] add_leg error: ${errorMessage(error)}`);
return null;
}
}
@@ -359,10 +335,10 @@ export async function addLeg(callId: string, number: string, providerId?: string
export async function removeLeg(callId: string, legId: string): Promise<boolean> {
if (!bridge || !initialized) return false;
try {
await bridge.sendCommand('remove_leg', { call_id: callId, leg_id: legId } as any);
await sendProxyCommand('remove_leg', { call_id: callId, leg_id: legId });
return true;
} catch (e: any) {
logFn?.(`[proxy-engine] remove_leg error: ${e?.message || e}`);
} catch (error: unknown) {
logFn?.(`[proxy-engine] remove_leg error: ${errorMessage(error)}`);
return false;
}
}
@@ -373,7 +349,7 @@ export async function removeLeg(callId: string, legId: string): Promise<boolean>
export async function webrtcClose(sessionId: string): Promise<void> {
if (!bridge || !initialized) return;
try {
await bridge.sendCommand('webrtc_close', { session_id: sessionId } as any);
await sendProxyCommand('webrtc_close', { session_id: sessionId });
} catch { /* ignore */ }
}
@@ -387,13 +363,13 @@ export async function webrtcClose(sessionId: string): Promise<void> {
export async function addDeviceLeg(callId: string, deviceId: string): Promise<string | null> {
if (!bridge || !initialized) return null;
try {
const result = await bridge.sendCommand('add_device_leg', {
const result = await sendProxyCommand('add_device_leg', {
call_id: callId,
device_id: deviceId,
} as any);
return (result as any)?.leg_id || null;
} catch (e: any) {
logFn?.(`[proxy-engine] add_device_leg error: ${e?.message || e}`);
});
return result.leg_id || null;
} catch (error: unknown) {
logFn?.(`[proxy-engine] add_device_leg error: ${errorMessage(error)}`);
return null;
}
}
@@ -408,14 +384,14 @@ export async function transferLeg(
): Promise<boolean> {
if (!bridge || !initialized) return false;
try {
await bridge.sendCommand('transfer_leg', {
await sendProxyCommand('transfer_leg', {
source_call_id: sourceCallId,
leg_id: legId,
target_call_id: targetCallId,
} as any);
});
return true;
} catch (e: any) {
logFn?.(`[proxy-engine] transfer_leg error: ${e?.message || e}`);
} catch (error: unknown) {
logFn?.(`[proxy-engine] transfer_leg error: ${errorMessage(error)}`);
return false;
}
}
@@ -431,15 +407,15 @@ export async function replaceLeg(
): Promise<string | null> {
if (!bridge || !initialized) return null;
try {
const result = await bridge.sendCommand('replace_leg', {
const result = await sendProxyCommand('replace_leg', {
call_id: callId,
old_leg_id: oldLegId,
number,
provider_id: providerId,
} as any);
return (result as any)?.new_leg_id || null;
} catch (e: any) {
logFn?.(`[proxy-engine] replace_leg error: ${e?.message || e}`);
});
return result.new_leg_id || null;
} catch (error: unknown) {
logFn?.(`[proxy-engine] replace_leg error: ${errorMessage(error)}`);
return null;
}
}
@@ -457,16 +433,15 @@ export async function startInteraction(
): Promise<{ result: 'digit' | 'timeout' | 'cancelled'; digit?: string } | null> {
if (!bridge || !initialized) return null;
try {
const result = await bridge.sendCommand('start_interaction', {
return await sendProxyCommand('start_interaction', {
call_id: callId,
leg_id: legId,
prompt_wav: promptWav,
expected_digits: expectedDigits,
timeout_ms: timeoutMs,
} as any);
return result as any;
} catch (e: any) {
logFn?.(`[proxy-engine] start_interaction error: ${e?.message || e}`);
});
} catch (error: unknown) {
logFn?.(`[proxy-engine] start_interaction error: ${errorMessage(error)}`);
return null;
}
}
@@ -482,14 +457,14 @@ export async function addToolLeg(
): Promise<string | null> {
if (!bridge || !initialized) return null;
try {
const result = await bridge.sendCommand('add_tool_leg', {
const result = await sendProxyCommand('add_tool_leg', {
call_id: callId,
tool_type: toolType,
config,
} as any);
return (result as any)?.tool_leg_id || null;
} catch (e: any) {
logFn?.(`[proxy-engine] add_tool_leg error: ${e?.message || e}`);
});
return result.tool_leg_id || null;
} catch (error: unknown) {
logFn?.(`[proxy-engine] add_tool_leg error: ${errorMessage(error)}`);
return null;
}
}
@@ -500,13 +475,13 @@ export async function addToolLeg(
export async function removeToolLeg(callId: string, toolLegId: string): Promise<boolean> {
if (!bridge || !initialized) return false;
try {
await bridge.sendCommand('remove_tool_leg', {
await sendProxyCommand('remove_tool_leg', {
call_id: callId,
tool_leg_id: toolLegId,
} as any);
});
return true;
} catch (e: any) {
logFn?.(`[proxy-engine] remove_tool_leg error: ${e?.message || e}`);
} catch (error: unknown) {
logFn?.(`[proxy-engine] remove_tool_leg error: ${errorMessage(error)}`);
return false;
}
}
@@ -522,15 +497,15 @@ export async function setLegMetadata(
): Promise<boolean> {
if (!bridge || !initialized) return false;
try {
await bridge.sendCommand('set_leg_metadata', {
await sendProxyCommand('set_leg_metadata', {
call_id: callId,
leg_id: legId,
key,
value,
} as any);
});
return true;
} catch (e: any) {
logFn?.(`[proxy-engine] set_leg_metadata error: ${e?.message || e}`);
} catch (error: unknown) {
logFn?.(`[proxy-engine] set_leg_metadata error: ${errorMessage(error)}`);
return false;
}
}
@@ -542,7 +517,7 @@ export async function setLegMetadata(
* dtmf_digit, recording_done, tool_recording_done, tool_transcription_done,
* leg_added, leg_removed, sip_unhandled
*/
export function onProxyEvent(event: string, handler: (data: any) => void): void {
export function onProxyEvent<K extends keyof TProxyEventMap>(event: K, handler: (data: TProxyEventMap[K]) => void): void {
if (!bridge) throw new Error('proxy engine not initialized');
bridge.on(`management:${event}`, handler);
}