From 86ea6ec5f26b794df317ce07f274cfe48c6bfe39 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 31 Jul 2026 18:04:47 +0800 Subject: [PATCH] fix(voice-call): release Plivo provider call state --- extensions/voice-call/src/providers/plivo.ts | 59 ++++++++++++++++++-- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/extensions/voice-call/src/providers/plivo.ts b/extensions/voice-call/src/providers/plivo.ts index 4b5656539ce9..3340ed05e75a 100644 --- a/extensions/voice-call/src/providers/plivo.ts +++ b/extensions/voice-call/src/providers/plivo.ts @@ -71,6 +71,42 @@ export class PlivoProvider implements VoiceCallProvider { private pendingSpeakByCallId = new Map(); private pendingListenByCallId = new Map(); + /** + * Release all process-local metadata owned by one Plivo call. + * Terminal webhooks can be replayed, so this must stay idempotent. + */ + private releaseCallState(params: { + callId?: string; + providerCallId?: string; + callUuid?: string; + }): void { + if (params.callId) { + this.callIdToWebhookUrl.delete(params.callId); + this.pendingSpeakByCallId.delete(params.callId); + this.pendingListenByCallId.delete(params.callId); + } + + const callUuid = + params.callUuid || + (params.providerCallId + ? (this.requestUuidToCallUuid.get(params.providerCallId) ?? params.providerCallId) + : undefined); + if (params.providerCallId) { + this.requestUuidToCallUuid.delete(params.providerCallId); + this.callUuidToWebhookUrl.delete(params.providerCallId); + } + if (!callUuid) { + return; + } + + this.callUuidToWebhookUrl.delete(callUuid); + for (const [requestUuid, mappedCallUuid] of this.requestUuidToCallUuid) { + if (mappedCallUuid === callUuid) { + this.requestUuidToCallUuid.delete(requestUuid); + } + } + } + constructor(config: PlivoConfig, options: PlivoProviderOptions = {}) { if (!config.authId) { throw new Error("Plivo Auth ID is required"); @@ -286,18 +322,24 @@ export class PlivoProvider implements VoiceCallProvider { callStatus === "no-answer" || callStatus === "failed" ) { - return { + const event = { ...baseEvent, - type: "call.ended", + type: "call.ended" as const, reason: callStatus === "completed" - ? "completed" + ? ("completed" as const) : callStatus === "busy" - ? "busy" + ? ("busy" as const) : callStatus === "no-answer" - ? "no-answer" - : "failed", + ? ("no-answer" as const) + : ("failed" as const), }; + this.releaseCallState({ + callId: callIdOverride, + providerCallId: callUuid || requestUuid || undefined, + callUuid: callUuid || undefined, + }); + return event; } // Plivo will call our answer_url when the call is answered; if we don't have @@ -351,6 +393,11 @@ export class PlivoProvider implements VoiceCallProvider { async hangupCall(input: HangupCallInput): Promise { const callUuid = this.requestUuidToCallUuid.get(input.providerCallId); + this.releaseCallState({ + callId: input.callId, + providerCallId: input.providerCallId, + callUuid, + }); if (callUuid) { await this.apiRequest({ method: "DELETE",