Voice-call: avoid streaming on notify callbacks

This commit is contained in:
Ghost
2026-01-18 20:27:23 -08:00
parent 60b87826bb
commit 80dae2e5e8

View File

@@ -64,6 +64,8 @@ export class TwilioProvider implements VoiceCallProvider {
/** Storage for TwiML content (for notify mode with URL-based TwiML) */ /** Storage for TwiML content (for notify mode with URL-based TwiML) */
private readonly twimlStorage = new Map<string, string>(); private readonly twimlStorage = new Map<string, string>();
/** Track notify-mode calls to avoid streaming on follow-up callbacks */
private readonly notifyCalls = new Set<string>();
/** /**
* Delete stored TwiML for a given `callId`. * Delete stored TwiML for a given `callId`.
@@ -73,6 +75,7 @@ export class TwilioProvider implements VoiceCallProvider {
*/ */
private deleteStoredTwiml(callId: string): void { private deleteStoredTwiml(callId: string): void {
this.twimlStorage.delete(callId); this.twimlStorage.delete(callId);
this.notifyCalls.delete(callId);
} }
/** /**
@@ -137,7 +140,7 @@ export class TwilioProvider implements VoiceCallProvider {
*/ */
private async apiRequest<T = unknown>( private async apiRequest<T = unknown>(
endpoint: string, endpoint: string,
params: Record<string, string>, params: Record<string, string | string[]>,
options?: { allowNotFound?: boolean }, options?: { allowNotFound?: boolean },
): Promise<T> { ): Promise<T> {
return await twilioApiRequest<T>({ return await twilioApiRequest<T>({
@@ -286,6 +289,9 @@ export class TwilioProvider implements VoiceCallProvider {
if (!ctx) return TwilioProvider.EMPTY_TWIML; if (!ctx) return TwilioProvider.EMPTY_TWIML;
const params = new URLSearchParams(ctx.rawBody); const params = new URLSearchParams(ctx.rawBody);
const type =
typeof ctx.query?.type === "string" ? ctx.query.type.trim() : undefined;
const isStatusCallback = type === "status";
const callStatus = params.get("CallStatus"); const callStatus = params.get("CallStatus");
const direction = params.get("Direction"); const direction = params.get("Direction");
const callIdFromQuery = const callIdFromQuery =
@@ -297,13 +303,21 @@ export class TwilioProvider implements VoiceCallProvider {
// Handle initial TwiML request (when Twilio first initiates the call) // Handle initial TwiML request (when Twilio first initiates the call)
// Check if we have stored TwiML for this call (notify mode) // Check if we have stored TwiML for this call (notify mode)
if (callIdFromQuery) { if (callIdFromQuery && !isStatusCallback) {
const storedTwiml = this.twimlStorage.get(callIdFromQuery); const storedTwiml = this.twimlStorage.get(callIdFromQuery);
if (storedTwiml) { if (storedTwiml) {
// Clean up after serving (one-time use) // Clean up after serving (one-time use)
this.deleteStoredTwiml(callIdFromQuery); this.deleteStoredTwiml(callIdFromQuery);
return storedTwiml; return storedTwiml;
} }
if (this.notifyCalls.has(callIdFromQuery)) {
return TwilioProvider.EMPTY_TWIML;
}
}
// Status callbacks should not receive TwiML.
if (isStatusCallback) {
return TwilioProvider.EMPTY_TWIML;
} }
// Handle subsequent webhook requests (status callbacks, etc.) // Handle subsequent webhook requests (status callbacks, etc.)
@@ -385,6 +399,7 @@ export class TwilioProvider implements VoiceCallProvider {
// We now serve it from the webhook endpoint instead of sending inline // We now serve it from the webhook endpoint instead of sending inline
if (input.inlineTwiml) { if (input.inlineTwiml) {
this.twimlStorage.set(input.callId, input.inlineTwiml); this.twimlStorage.set(input.callId, input.inlineTwiml);
this.notifyCalls.add(input.callId);
} }
// Build request params - always use URL-based TwiML. // Build request params - always use URL-based TwiML.