Skip to content

Commit ae93281

Browse files
feat: add recoveredCallId to call object for recovery correlation (#553)
* feat: add recoveredCallId to call object for recovery correlation (ENGDESK-50308) When a call is recovered after a network reconnection (reattach), the SDK creates a new call object. Customers tracking calls by ID see the old call destroyed and a new call created, causing duplicate UI elements (e.g. dialers). This adds a 'recoveredCallId' field to the call object, set automatically during the reattach/recovery flow. Customers can use it to correlate the new call with the ended call and clean up stale UI. Changes: - Add recoveredCallId to IWebRTCCall and IVertoCallOptions interfaces - Add recoveredCallId public property to BaseCall - Set recoveredCallId in VertoHandler Attach recovery flow - Add TelnyxRTC docs with usage example - Add 3 VertoHandler tests covering recovery, fresh attach, and ID change * feat: add recoveredCallId to call object for recovery correlation (ENGDESK-50308) When a call is recovered after a network reconnection (reattach), the SDK creates a new call object. Customers tracking calls by ID see the old call destroyed and a new call created, causing duplicate UI elements (e.g. dialers). This adds a 'recoveredCallId' field to the call object, set automatically during the reattach/recovery flow. Customers can use it to correlate the new call with the ended call and clean up stale UI. The isRecovering flag is now derived from recoveredCallId — if a recoveredCallId is present, the call is recovering. Changes: - Add recoveredCallId to IWebRTCCall and IVertoCallOptions interfaces - Add recoveredCallId public property to BaseCall, derive _isRecovering from it - Remove isRecovering constructor param from BaseCall - Set recoveredCallId via _buildCall in VertoHandler Attach recovery flow - Add TelnyxRTC docs with usage example - Add 3 VertoHandler tests covering recovery, fresh attach, and ID change * docs: update ts docs * chore: update changelog for webrtc@2.25.26-beta.2 * chore: release webrtc 2.25.26-beta.2 * fix: remove duplicate CHANGELOG entry --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent eb6c8bd commit ae93281

File tree

9 files changed

+207
-16
lines changed

9 files changed

+207
-16
lines changed

packages/js/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## [2.25.26-beta.2](https://github.com/team-telnyx/webrtc/compare/webrtc/v2.25.25...webrtc/v2.25.26-beta.2) (2026-03-13)
2+
3+
- docs: update ts docs
4+
- feat: add recoveredCallId to call object for recovery correlation (ENGDESK-50308)
15
## [2.25.25](https://github.com/team-telnyx/webrtc/compare/webrtc/v2.25.24...webrtc/v2.25.25) (2026-03-11)
26

37
- docs: update ts docs

packages/js/docs/ts/classes/Call.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ call.muteAudio();
5555
- [direction](#direction)
5656
- [id](#id)
5757
- [prevState](#prevstate)
58+
- [recoveredCallId](#recoveredcallid)
5859
- [state](#state)
5960

6061
### Accessors
@@ -126,6 +127,38 @@ BaseCall.prevState
126127

127128
---
128129

130+
### recoveredCallId
131+
132+
**recoveredCallId**: `string` = `''`
133+
134+
The call ID of the previous call that this call is recovering from.
135+
Present only when the call was created as part of a reattachment/recovery
136+
flow (e.g. after a network reconnection).
137+
138+
Use this to match the new call object to the ended/destroyed call
139+
and prevent duplicate UI elements such as dialers.
140+
141+
**`Example`**
142+
143+
```js
144+
client.on('telnyx.notification', (notification) => {
145+
if (notification.type === 'callUpdate') {
146+
const call = notification.call;
147+
if (call.recoveredCallId) {
148+
// This call replaced a previous call after recovery
149+
// Remove the old dialer for call.recoveredCallId
150+
removeDialer(call.recoveredCallId);
151+
}
152+
}
153+
});
154+
```
155+
156+
#### Inherited from
157+
158+
BaseCall.recoveredCallId
159+
160+
---
161+
129162
### state
130163

131164
**state**: `string`

packages/js/docs/ts/classes/TelnyxRTC.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,25 @@ client.newCall({
993993
});
994994
```
995995

996+
### Call Recovery and `recoveredCallId`
997+
998+
When a call is recovered after a network reconnection (reattach), the SDK
999+
creates a new call object and sets `recoveredCallId` to the ID of the ended call.
1000+
Use this to correlate the new call with the old one and avoid duplicate UI elements:
1001+
1002+
```js
1003+
client.on('telnyx.notification', (notification) => {
1004+
if (notification.type === 'callUpdate') {
1005+
const call = notification.call;
1006+
if (call.recoveredCallId) {
1007+
// This call replaced a previous call after recovery.
1008+
// Remove the old dialer/UI for call.recoveredCallId
1009+
removeDialer(call.recoveredCallId);
1010+
}
1011+
}
1012+
});
1013+
```
1014+
9961015
### Voice Isolation
9971016

9981017
Voice isolation options can be set by passing an `audio` object to the `newCall` method. This property controls the settings of a MediaStreamTrack object. For reference on available audio constraints, see [MediaTrackConstraints](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints).

packages/js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@telnyx/webrtc",
3-
"version": "2.25.25",
3+
"version": "2.25.26-beta.2",
44
"description": "Telnyx WebRTC Client",
55
"keywords": [
66
"telnyx",

packages/js/src/Modules/Verto/tests/webrtc/VertoHandler.test.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,80 @@ describe('VertoHandler', () => {
141141
});
142142
});
143143

144-
// describe('verto.attach', () => {
144+
describe('telnyx_rtc.attach', () => {
145+
it('should set recoveredCallId on the new call when recovering from an existing call', async (done) => {
146+
await instance.connect();
147+
const callId = 'e2fda6dc-fc9d-4d77-8096-53bb502443b6';
148+
_setupCall({ id: callId });
149+
call.setState(State.Active);
150+
151+
// Mock answer to prevent actual WebRTC peer creation
152+
const originalAnswer = Call.prototype.answer;
153+
Call.prototype.answer = jest.fn();
154+
155+
const msg = JSON.parse(
156+
`{"jsonrpc":"2.0","id":4405,"method":"telnyx_rtc.attach","params":{"callID":"${callId}","sdp":"SDP","caller_id_name":"Extension 1004","caller_id_number":"1004","callee_id_name":"Outbound Call","callee_id_number":"1003"}}`
157+
);
158+
handler.handleMessage(msg);
159+
160+
const newCall = instance.calls[callId];
161+
expect(newCall).toBeDefined();
162+
expect(newCall.recoveredCallId).toEqual(callId);
163+
164+
Call.prototype.answer = originalAnswer;
165+
done();
166+
});
167+
168+
it('should NOT set recoveredCallId when no existing call (fresh attach)', async (done) => {
169+
await instance.connect();
170+
const callId = 'fresh-call-id-1234';
171+
172+
// Mock answer to prevent actual WebRTC peer creation
173+
const originalAnswer = Call.prototype.answer;
174+
Call.prototype.answer = jest.fn();
175+
176+
const msg = JSON.parse(
177+
`{"jsonrpc":"2.0","id":4406,"method":"telnyx_rtc.attach","params":{"callID":"${callId}","sdp":"SDP","caller_id_name":"Extension 1004","caller_id_number":"1004","callee_id_name":"Outbound Call","callee_id_number":"1003"}}`
178+
);
179+
handler.handleMessage(msg);
180+
181+
const newCall = instance.calls[callId];
182+
expect(newCall).toBeDefined();
183+
expect(newCall.recoveredCallId).toBeFalsy();
184+
185+
Call.prototype.answer = originalAnswer;
186+
done();
187+
});
145188

146-
// })
189+
it('should set recoveredCallId when call ID changes during recovery', async (done) => {
190+
await instance.connect();
191+
const oldCallId = 'old-call-id-1234';
192+
const newCallId = 'new-call-id-5678';
193+
_setupCall({ id: oldCallId });
194+
call.setState(State.Active);
147195

148-
// describe('verto.event', () => {
196+
// Mock answer to prevent actual WebRTC peer creation
197+
const originalAnswer = Call.prototype.answer;
198+
Call.prototype.answer = jest.fn();
149199

150-
// })
200+
// Server sends attach with a DIFFERENT callID — old call won't be found
201+
// so it goes through the !existingCall branch (no recoveredCallId)
202+
const msg = JSON.parse(
203+
`{"jsonrpc":"2.0","id":4407,"method":"telnyx_rtc.attach","params":{"callID":"${newCallId}","sdp":"SDP","caller_id_name":"Extension 1004","caller_id_number":"1004","callee_id_name":"Outbound Call","callee_id_number":"1003"}}`
204+
);
205+
handler.handleMessage(msg);
206+
207+
const newCall = instance.calls[newCallId];
208+
expect(newCall).toBeDefined();
209+
// When callID differs, existingCall is null → no recoveredCallId set
210+
expect(newCall.recoveredCallId).toBeFalsy();
211+
// Old call should still exist
212+
expect(instance.calls[oldCallId]).toBeDefined();
213+
214+
Call.prototype.answer = originalAnswer;
215+
done();
216+
});
217+
});
151218

152219
describe('telnyx_rtc.info', () => {
153220
it('should dispatch a notification', () => {

packages/js/src/Modules/Verto/webrtc/BaseCall.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,30 @@ export default abstract class BaseCall implements IWebRTCCall {
7575
*/
7676
public id: string = '';
7777

78+
/**
79+
* The call ID of the previous call that this call is recovering from.
80+
* Present only when the call was created as part of a reattachment/recovery
81+
* flow (e.g. after a network reconnection).
82+
*
83+
* Use this to match the new call object to the ended/destroyed call
84+
* and prevent duplicate UI elements such as dialers.
85+
*
86+
* @example
87+
* ```js
88+
* client.on('telnyx.notification', (notification) => {
89+
* if (notification.type === 'callUpdate') {
90+
* const call = notification.call;
91+
* if (call.recoveredCallId) {
92+
* // This call replaced a previous call after recovery
93+
* // Remove the old dialer for call.recoveredCallId
94+
* removeDialer(call.recoveredCallId);
95+
* }
96+
* }
97+
* });
98+
* ```
99+
*/
100+
public recoveredCallId: string = '';
101+
78102
/**
79103
* The `state` of the call.
80104
*
@@ -175,10 +199,11 @@ export default abstract class BaseCall implements IWebRTCCall {
175199

176200
private _creatingPeer: boolean = false;
177201

202+
private _isRecovering: boolean = false;
203+
178204
constructor(
179205
protected session: BrowserSession,
180206
opts?: IVertoCallOptions,
181-
private _isRecovering: boolean = false
182207
) {
183208
const {
184209
iceServers,
@@ -1743,7 +1768,7 @@ export default abstract class BaseCall implements IWebRTCCall {
17431768
}
17441769

17451770
private _init() {
1746-
const { id, userVariables, remoteCallerNumber, onNotification } =
1771+
const { id, userVariables, remoteCallerNumber, onNotification, recoveredCallId } =
17471772
this.options;
17481773
if (id) {
17491774
this.options.id = id.toString();
@@ -1752,6 +1777,11 @@ export default abstract class BaseCall implements IWebRTCCall {
17521777
}
17531778
this.id = this.options.id;
17541779

1780+
if (recoveredCallId) {
1781+
this.recoveredCallId = recoveredCallId;
1782+
this._isRecovering = true;
1783+
}
1784+
17551785
if (!userVariables || objEmpty(userVariables)) {
17561786
this.options.userVariables = this.session.options.userVariables || {};
17571787
}

packages/js/src/Modules/Verto/webrtc/VertoHandler.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class VertoHandler {
6363
return this._handlePvtEvent(params.pvtData);
6464
}
6565

66-
const _buildCall = (isRecovering: boolean = false) => {
66+
const _buildCall = (recoveredCallId?: string) => {
6767
const callOptions: IVertoCallOptions = {
6868
audio: true,
6969
// So far, if SIP configuration supports video, then we will always get video section in SDP.
@@ -120,7 +120,11 @@ class VertoHandler {
120120
callOptions.customHeaders = params.dialogParams.custom_headers;
121121
}
122122

123-
const call = new Call(session, callOptions, isRecovering);
123+
if (recoveredCallId) {
124+
callOptions.recoveredCallId = recoveredCallId;
125+
}
126+
127+
const call = new Call(session, callOptions);
124128
call.nodeId = this.nodeId;
125129
return call;
126130
};
@@ -207,20 +211,17 @@ class VertoHandler {
207211
return;
208212
}
209213

210-
/**
211-
* We call our recovery flow with recovering call state during the call lifecycle.
212-
*/
213-
const isRecovering = !!existingCall;
214+
const recoveredCallId = existingCall.id;
214215

215216
logger.info(
216217
`[${new Date().toISOString()}][${callID}] closing existing call on ATTACH.`
217218
);
218-
existingCall.hangup({ isRecovering }, false);
219+
existingCall.hangup({ isRecovering: true }, false);
219220

220221
logger.info(
221-
`[${new Date().toISOString()}][${callID}] Attach: Creating new call for recovery`
222+
`[${new Date().toISOString()}][${callID}] Attach: Creating new call for recovery (recoveredCallId: ${recoveredCallId})`
222223
);
223-
const call = _buildCall(isRecovering);
224+
const call = _buildCall(recoveredCallId);
224225
call.answer();
225226
this._ack(id, method);
226227
break;

packages/js/src/Modules/Verto/webrtc/interfaces.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ export interface IVertoCallOptions {
8484
// Depricated: use only IVertoOptions.keepConnectionAliveOnSocketClose
8585
keepConnectionAliveOnSocketClose?: boolean;
8686
mutedMicOnStart?: boolean;
87+
/**
88+
* The call ID of the previous call that this call is recovering from.
89+
* Set automatically during reattachment/recovery when a new call object
90+
* replaces an existing one (e.g. after network reconnection).
91+
*
92+
* Customers can use this to correlate the new call object with the
93+
* ended/destroyed call and avoid duplicate UI elements (e.g. dialers).
94+
*/
95+
recoveredCallId?: string;
8796
}
8897

8998
export interface IStatsBinding {
@@ -117,6 +126,15 @@ export interface AnswerParams {
117126

118127
export interface IWebRTCCall {
119128
id: string;
129+
/**
130+
* The call ID of the previous call that this call is recovering from.
131+
* Present only when the call was created as part of a reattachment/recovery
132+
* flow (e.g. after a network reconnection).
133+
*
134+
* Use this to match the new call object to the ended/destroyed call
135+
* and prevent duplicate UI elements such as dialers.
136+
*/
137+
recoveredCallId?: string;
120138
state: string;
121139
prevState: string;
122140
direction: string;

packages/js/src/TelnyxRTC.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,25 @@ export class TelnyxRTC extends TelnyxRTCClient {
208208
* });
209209
* ```
210210
*
211+
* ### Call Recovery and `recoveredCallId`
212+
*
213+
* When a call is recovered after a network reconnection (reattach), the SDK
214+
* creates a new call object and sets `recoveredCallId` to the ID of the ended call.
215+
* Use this to correlate the new call with the old one and avoid duplicate UI elements:
216+
*
217+
* ```js
218+
* client.on('telnyx.notification', (notification) => {
219+
* if (notification.type === 'callUpdate') {
220+
* const call = notification.call;
221+
* if (call.recoveredCallId) {
222+
* // This call replaced a previous call after recovery.
223+
* // Remove the old dialer/UI for call.recoveredCallId
224+
* removeDialer(call.recoveredCallId);
225+
* }
226+
* }
227+
* });
228+
* ```
229+
*
211230
* ### Voice Isolation
212231
*
213232
* Voice isolation options can be set by passing an `audio` object to the `newCall` method. This property controls the settings of a MediaStreamTrack object. For reference on available audio constraints, see [MediaTrackConstraints](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints).

0 commit comments

Comments
 (0)