@telnyx/react-voice-commons-sdk 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # CHANGELOG.md
2
2
 
3
+ ## [0.4.3] (2026-04-30)
4
+
5
+ ### Bug Fixing
6
+
7
+ - **VoIP push always rebuilds the `TelnyxRTC` instance.** `SessionManager.handlePushNotification` now unconditionally disposes any prior client and runs `_connect()` afresh, so the new socket's URL bakes in THIS push's `voice_sdk_id`. Reusing a prior connection would cause the gateway to route this push's INVITE to whichever client announced with the new `voice_sdk_id` (i.e. a different connection), leaving us listening on the wrong socket until CallKit timed out.
8
+ - **Dropped the `loginFromStoredConfig()` fallback inside `TelnyxVoipClient.handlePushNotification`.** When SessionManager's push-driven `_connect()` didn't immediately reach `CONNECTED`, the fallback would create a second TelnyxRTC instance with no `voice_sdk_id` awareness. The gateway then `punt`-ed one of the parallel sessions, dropping the active call. SessionManager already loads stored credentials internally for the cold-launch path; the redundant fallback only created races.
9
+
10
+ ### Dependencies
11
+
12
+ - Now requires `@telnyx/react-native-voice-sdk >= 0.4.5`, which removes the auto-reconnect on socket error/close that was triggering the multi-instance race. See the [voice-sdk 0.4.5 changelog](../package/CHANGELOG.md#045-2026-04-30) for details.
13
+
3
14
  ## [0.4.2] (2026-04-27)
4
15
 
5
16
  ### Bug Fixing
@@ -202,39 +202,22 @@ class SessionManager {
202
202
  );
203
203
  // Store the push notification payload for when the client is created
204
204
  this._pendingPushPayload = payload;
205
- // If we have a TelnyxRTC instance but its socket isn't fresh — either not
206
- // connected at all, or connected but with no traffic for longer than the
207
- // threshold dispose it so we go through the full _connect() path below
208
- // instead of calling processVoIPNotification on a stale client.
209
- //
210
- // This handles two cases iOS gives us with the same code path:
211
- // 1. Terminated-then-cold-launched: a client exists (constructed at
212
- // app boot) but its connection was never opened (idleMs=Infinity).
213
- // 2. Suspended-then-thawed: a client exists with `connected=true`,
214
- // but the kernel killed the TLS session during freeze without
215
- // notifying the JS WebSocket wrapper, so no error event fired.
216
- //
217
- // 30s threshold matches the server's keep-alive ping cadence (~20s),
218
- // so a live session always remains fresh.
219
- const STALE_THRESHOLD_MS = 30000;
205
+ // Each push always rebuilds the TelnyxRTC client. The voice_sdk_id is
206
+ // baked into the WebSocket URL at socket-open time and can't be mutated
207
+ // mid-flight; reusing a connection from a previous push means the
208
+ // gateway routes THIS push's INVITE to a different (correctly-stamped)
209
+ // client and we sit on the wrong socket forever.
220
210
  if (this._telnyxClient) {
221
- const client = this._telnyxClient;
222
- const isFresh =
223
- typeof client.isFresh === 'function'
224
- ? client.isFresh(STALE_THRESHOLD_MS)
225
- : !!client.connected;
226
- if (!isFresh) {
227
- try {
228
- await this._telnyxClient.disconnect();
229
- } catch (err) {
230
- console.warn('SessionManager: disconnect of stale client threw:', err);
231
- }
232
- this._telnyxClient = undefined;
233
- if (this.currentState !== connection_state_1.TelnyxConnectionState.DISCONNECTED) {
234
- this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
235
- }
211
+ try {
212
+ await this._telnyxClient.disconnect();
213
+ } catch (err) {
214
+ console.warn('SessionManager: disconnect of prior client threw:', err);
236
215
  }
237
216
  }
217
+ this._telnyxClient = undefined;
218
+ if (this.currentState !== connection_state_1.TelnyxConnectionState.DISCONNECTED) {
219
+ this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
220
+ }
238
221
  // If we don't have a config yet but we're processing a push notification,
239
222
  // attempt to load stored config first (for terminated app startup)
240
223
  if (!this._currentConfig && !this._telnyxClient) {
@@ -333,21 +333,14 @@ class TelnyxVoipClient {
333
333
  console.log('TelnyxVoipClient: Handling push notification:', payload);
334
334
  }
335
335
  try {
336
- // First, pass the push notification to the session manager for processing
337
- // This will set the isCallFromPush flag on the TelnyxRTC client
336
+ // SessionManager owns the entire push lifecycle: it disposes any prior
337
+ // client, loads stored credentials if needed, and rebuilds the
338
+ // TelnyxRTC bound to THIS push's voice_sdk_id. We deliberately do not
339
+ // fall back to a generic loginFromStoredConfig() here — that would
340
+ // create a parallel client with no voice_sdk_id awareness, which the
341
+ // gateway then has to `punt` once the SessionManager-driven session
342
+ // also registers, dropping the active call.
338
343
  await this._sessionManager.handlePushNotification(payload);
339
- // Connect if not already connected
340
- const currentState = this.currentConnectionState;
341
- if (currentState !== connection_state_1.TelnyxConnectionState.CONNECTED) {
342
- // Try to login from stored config - now the push flags should be set
343
- const loginSuccess = await this.loginFromStoredConfig();
344
- if (!loginSuccess) {
345
- console.warn(
346
- 'TelnyxVoipClient: Could not login from stored config for push notification'
347
- );
348
- return;
349
- }
350
- }
351
344
  } catch (error) {
352
345
  console.error('TelnyxVoipClient: Error handling push notification:', error);
353
346
  throw error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telnyx/react-voice-commons-sdk",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "A high-level, state-agnostic, drop-in module for the Telnyx React Native SDK that simplifies WebRTC voice calling integration",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/index.js",
@@ -86,7 +86,7 @@
86
86
  },
87
87
  "dependencies": {
88
88
  "@react-native-community/eslint-config": "^3.2.0",
89
- "@telnyx/react-native-voice-sdk": ">=0.4.4",
89
+ "@telnyx/react-native-voice-sdk": ">=0.4.5",
90
90
  "eventemitter3": "^5.0.1",
91
91
  "expo": "~53.0.22",
92
92
  "react-native-url-polyfill": "^3.0.0",
@@ -178,39 +178,22 @@ export class SessionManager {
178
178
  // Store the push notification payload for when the client is created
179
179
  (this as any)._pendingPushPayload = payload;
180
180
 
181
- // If we have a TelnyxRTC instance but its socket isn't fresh — either not
182
- // connected at all, or connected but with no traffic for longer than the
183
- // threshold dispose it so we go through the full _connect() path below
184
- // instead of calling processVoIPNotification on a stale client.
185
- //
186
- // This handles two cases iOS gives us with the same code path:
187
- // 1. Terminated-then-cold-launched: a client exists (constructed at
188
- // app boot) but its connection was never opened (idleMs=Infinity).
189
- // 2. Suspended-then-thawed: a client exists with `connected=true`,
190
- // but the kernel killed the TLS session during freeze without
191
- // notifying the JS WebSocket wrapper, so no error event fired.
192
- //
193
- // 30s threshold matches the server's keep-alive ping cadence (~20s),
194
- // so a live session always remains fresh.
195
- const STALE_THRESHOLD_MS = 30000;
181
+ // Each push always rebuilds the TelnyxRTC client. The voice_sdk_id is
182
+ // baked into the WebSocket URL at socket-open time and can't be mutated
183
+ // mid-flight; reusing a connection from a previous push means the
184
+ // gateway routes THIS push's INVITE to a different (correctly-stamped)
185
+ // client and we sit on the wrong socket forever.
196
186
  if (this._telnyxClient) {
197
- const client = this._telnyxClient as any;
198
- const isFresh =
199
- typeof client.isFresh === 'function'
200
- ? client.isFresh(STALE_THRESHOLD_MS)
201
- : !!client.connected;
202
- if (!isFresh) {
203
- try {
204
- await this._telnyxClient.disconnect();
205
- } catch (err) {
206
- console.warn('SessionManager: disconnect of stale client threw:', err);
207
- }
208
- this._telnyxClient = undefined;
209
- if (this.currentState !== TelnyxConnectionState.DISCONNECTED) {
210
- this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
211
- }
187
+ try {
188
+ await this._telnyxClient.disconnect();
189
+ } catch (err) {
190
+ console.warn('SessionManager: disconnect of prior client threw:', err);
212
191
  }
213
192
  }
193
+ this._telnyxClient = undefined;
194
+ if (this.currentState !== TelnyxConnectionState.DISCONNECTED) {
195
+ this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
196
+ }
214
197
 
215
198
  // If we don't have a config yet but we're processing a push notification,
216
199
  // attempt to load stored config first (for terminated app startup)
@@ -401,22 +401,14 @@ export class TelnyxVoipClient {
401
401
  }
402
402
 
403
403
  try {
404
- // First, pass the push notification to the session manager for processing
405
- // This will set the isCallFromPush flag on the TelnyxRTC client
404
+ // SessionManager owns the entire push lifecycle: it disposes any prior
405
+ // client, loads stored credentials if needed, and rebuilds the
406
+ // TelnyxRTC bound to THIS push's voice_sdk_id. We deliberately do not
407
+ // fall back to a generic loginFromStoredConfig() here — that would
408
+ // create a parallel client with no voice_sdk_id awareness, which the
409
+ // gateway then has to `punt` once the SessionManager-driven session
410
+ // also registers, dropping the active call.
406
411
  await this._sessionManager.handlePushNotification(payload);
407
-
408
- // Connect if not already connected
409
- const currentState = this.currentConnectionState;
410
- if (currentState !== TelnyxConnectionState.CONNECTED) {
411
- // Try to login from stored config - now the push flags should be set
412
- const loginSuccess = await this.loginFromStoredConfig();
413
- if (!loginSuccess) {
414
- console.warn(
415
- 'TelnyxVoipClient: Could not login from stored config for push notification'
416
- );
417
- return;
418
- }
419
- }
420
412
  } catch (error) {
421
413
  console.error('TelnyxVoipClient: Error handling push notification:', error);
422
414
  throw error;