refactor(voice-call): split manager into facade and context slices

This commit is contained in:
Peter Steinberger
2026-02-14 03:39:03 +01:00
parent edbd86074f
commit 89574f30cb
5 changed files with 120 additions and 457 deletions

View File

@@ -8,18 +8,32 @@ export type TranscriptWaiter = {
timeout: NodeJS.Timeout;
};
export type CallManagerContext = {
export type CallManagerRuntimeState = {
activeCalls: Map<CallId, CallRecord>;
providerCallIdMap: Map<string, CallId>;
processedEventIds: Set<string>;
/** Provider call IDs we already sent a reject hangup for; avoids duplicate hangup calls. */
rejectedProviderCallIds: Set<string>;
/** Optional runtime hook invoked after an event transitions a call into answered state. */
onCallAnswered?: (call: CallRecord) => void;
};
export type CallManagerRuntimeDeps = {
provider: VoiceCallProvider | null;
config: VoiceCallConfig;
storePath: string;
webhookUrl: string | null;
};
export type CallManagerTransientState = {
transcriptWaiters: Map<CallId, TranscriptWaiter>;
maxDurationTimers: Map<CallId, NodeJS.Timeout>;
};
export type CallManagerHooks = {
/** Optional runtime hook invoked after an event transitions a call into answered state. */
onCallAnswered?: (call: CallRecord) => void;
};
export type CallManagerContext = CallManagerRuntimeState &
CallManagerRuntimeDeps &
CallManagerTransientState &
CallManagerHooks;

View File

@@ -13,10 +13,21 @@ import {
startMaxDurationTimer,
} from "./timers.js";
function shouldAcceptInbound(
config: CallManagerContext["config"],
from: string | undefined,
): boolean {
type EventContext = Pick<
CallManagerContext,
| "activeCalls"
| "providerCallIdMap"
| "processedEventIds"
| "rejectedProviderCallIds"
| "provider"
| "config"
| "storePath"
| "transcriptWaiters"
| "maxDurationTimers"
| "onCallAnswered"
>;
function shouldAcceptInbound(config: EventContext["config"], from: string | undefined): boolean {
const { inboundPolicy: policy, allowFrom } = config;
switch (policy) {
@@ -49,7 +60,7 @@ function shouldAcceptInbound(
}
function createInboundCall(params: {
ctx: CallManagerContext;
ctx: EventContext;
providerCallId: string;
from: string;
to: string;
@@ -80,7 +91,7 @@ function createInboundCall(params: {
return callRecord;
}
export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): void {
export function processEvent(ctx: EventContext, event: NormalizedEvent): void {
if (ctx.processedEventIds.has(event.id)) {
return;
}

View File

@@ -19,8 +19,39 @@ import {
} from "./timers.js";
import { generateNotifyTwiml } from "./twiml.js";
type InitiateContext = Pick<
CallManagerContext,
"activeCalls" | "providerCallIdMap" | "provider" | "config" | "storePath" | "webhookUrl"
>;
type SpeakContext = Pick<
CallManagerContext,
"activeCalls" | "providerCallIdMap" | "provider" | "config" | "storePath"
>;
type ConversationContext = Pick<
CallManagerContext,
| "activeCalls"
| "providerCallIdMap"
| "provider"
| "config"
| "storePath"
| "transcriptWaiters"
| "maxDurationTimers"
>;
type EndCallContext = Pick<
CallManagerContext,
| "activeCalls"
| "providerCallIdMap"
| "provider"
| "storePath"
| "transcriptWaiters"
| "maxDurationTimers"
>;
export async function initiateCall(
ctx: CallManagerContext,
ctx: InitiateContext,
to: string,
sessionKey?: string,
options?: OutboundCallOptions | string,
@@ -113,7 +144,7 @@ export async function initiateCall(
}
export async function speak(
ctx: CallManagerContext,
ctx: SpeakContext,
callId: CallId,
text: string,
): Promise<{ success: boolean; error?: string }> {
@@ -149,7 +180,7 @@ export async function speak(
}
export async function speakInitialMessage(
ctx: CallManagerContext,
ctx: ConversationContext,
providerCallId: string,
): Promise<void> {
const call = getCallByProviderCallId({
@@ -197,7 +228,7 @@ export async function speakInitialMessage(
}
export async function continueCall(
ctx: CallManagerContext,
ctx: ConversationContext,
callId: CallId,
prompt: string,
): Promise<{ success: boolean; transcript?: string; error?: string }> {
@@ -234,7 +265,7 @@ export async function continueCall(
}
export async function endCall(
ctx: CallManagerContext,
ctx: EndCallContext,
callId: CallId,
): Promise<{ success: boolean; error?: string }> {
const call = ctx.activeCalls.get(callId);

View File

@@ -2,7 +2,20 @@ import type { CallManagerContext } from "./context.js";
import { TerminalStates, type CallId } from "../types.js";
import { persistCallRecord } from "./store.js";
export function clearMaxDurationTimer(ctx: CallManagerContext, callId: CallId): void {
type TimerContext = Pick<
CallManagerContext,
"activeCalls" | "maxDurationTimers" | "config" | "storePath" | "transcriptWaiters"
>;
type MaxDurationTimerContext = Pick<
TimerContext,
"activeCalls" | "maxDurationTimers" | "config" | "storePath"
>;
type TranscriptWaiterContext = Pick<TimerContext, "transcriptWaiters">;
export function clearMaxDurationTimer(
ctx: Pick<MaxDurationTimerContext, "maxDurationTimers">,
callId: CallId,
): void {
const timer = ctx.maxDurationTimers.get(callId);
if (timer) {
clearTimeout(timer);
@@ -11,7 +24,7 @@ export function clearMaxDurationTimer(ctx: CallManagerContext, callId: CallId):
}
export function startMaxDurationTimer(params: {
ctx: CallManagerContext;
ctx: MaxDurationTimerContext;
callId: CallId;
onTimeout: (callId: CallId) => Promise<void>;
}): void {
@@ -38,7 +51,7 @@ export function startMaxDurationTimer(params: {
params.ctx.maxDurationTimers.set(params.callId, timer);
}
export function clearTranscriptWaiter(ctx: CallManagerContext, callId: CallId): void {
export function clearTranscriptWaiter(ctx: TranscriptWaiterContext, callId: CallId): void {
const waiter = ctx.transcriptWaiters.get(callId);
if (!waiter) {
return;
@@ -48,7 +61,7 @@ export function clearTranscriptWaiter(ctx: CallManagerContext, callId: CallId):
}
export function rejectTranscriptWaiter(
ctx: CallManagerContext,
ctx: TranscriptWaiterContext,
callId: CallId,
reason: string,
): void {
@@ -61,7 +74,7 @@ export function rejectTranscriptWaiter(
}
export function resolveTranscriptWaiter(
ctx: CallManagerContext,
ctx: TranscriptWaiterContext,
callId: CallId,
transcript: string,
): void {
@@ -73,7 +86,7 @@ export function resolveTranscriptWaiter(
waiter.resolve(transcript);
}
export function waitForFinalTranscript(ctx: CallManagerContext, callId: CallId): Promise<string> {
export function waitForFinalTranscript(ctx: TimerContext, callId: CallId): Promise<string> {
// Only allow one in-flight waiter per call.
rejectTranscriptWaiter(ctx, callId, "Transcript waiter replaced");