@telnyx/react-voice-commons-sdk 0.3.0 → 0.3.1

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,12 @@
1
1
  # CHANGELOG.md
2
2
 
3
+ ## [0.3.1] (2026-04-16)
4
+
5
+ ### Bug Fixing
6
+
7
+ - Fixed stale `CONNECTED` state during background disconnect: `SessionManager.disconnect()` now emits `DISCONNECTED` before awaiting the underlying client teardown. Previously, observers (including the auto-reconnect logic in `TelnyxVoiceApp`) could read a stale `CONNECTED` value while the socket was being torn down, causing auto-reconnection to be skipped and subsequent `newCall()` attempts to fail with `Cannot make call when connection state is: DISCONNECTED` or `No connection exists. Please connect first.`
8
+ - Tracked calls are now cleared on disconnect. Previously, calls left in non-terminal states when the socket was torn down would accumulate across background/foreground cycles, since a dead socket never emits the `ENDED`/`FAILED` events that normally trigger per-call cleanup.
9
+
3
10
  ## [0.3.0] (2026-04-15)
4
11
 
5
12
  ### ⚠️ Breaking changes
@@ -71,6 +71,13 @@ export declare class CallStateController {
71
71
  isWaitingForInvite: () => boolean;
72
72
  onInviteAutoAccepted: () => void;
73
73
  }): void;
74
+ /**
75
+ * Clear all tracked calls. Called when the session disconnects so that
76
+ * calls left in non-terminal states (because the socket died before
77
+ * their ENDED/FAILED events could arrive) don't accumulate as ghosts
78
+ * across reconnect cycles.
79
+ */
80
+ clearAllCalls(): void;
74
81
  /**
75
82
  * Dispose of the controller and clean up resources
76
83
  */
@@ -161,6 +161,29 @@ class CallStateController {
161
161
  this._isWaitingForInvite = callbacks.isWaitingForInvite;
162
162
  this._onInviteAutoAccepted = callbacks.onInviteAutoAccepted;
163
163
  }
164
+ /**
165
+ * Clear all tracked calls. Called when the session disconnects so that
166
+ * calls left in non-terminal states (because the socket died before
167
+ * their ENDED/FAILED events could arrive) don't accumulate as ghosts
168
+ * across reconnect cycles.
169
+ */
170
+ clearAllCalls() {
171
+ if (this._callMap.size === 0) {
172
+ return;
173
+ }
174
+ console.log(
175
+ `CallStateController: Clearing ${this._callMap.size} tracked call(s) on disconnect`
176
+ );
177
+ for (const call of this._callMap.values()) {
178
+ try {
179
+ call.dispose();
180
+ } catch (error) {
181
+ console.warn('CallStateController: Error disposing call during clear:', error);
182
+ }
183
+ }
184
+ this._callMap.clear();
185
+ this._calls.next([]);
186
+ }
164
187
  /**
165
188
  * Dispose of the controller and clean up resources
166
189
  */
@@ -15,6 +15,7 @@ export declare class SessionManager {
15
15
  private _sessionId;
16
16
  private _disposed;
17
17
  private _onClientReady?;
18
+ private _onDisconnect?;
18
19
  constructor();
19
20
  /**
20
21
  * Observable stream of connection state changes
@@ -24,6 +25,11 @@ export declare class SessionManager {
24
25
  * Set callback to be called when the Telnyx client is ready
25
26
  */
26
27
  setOnClientReady(callback: () => void): void;
28
+ /**
29
+ * Set callback to be called when the session disconnects, so dependent
30
+ * subsystems (e.g. the call state controller) can clear their state.
31
+ */
32
+ setOnDisconnect(callback: () => void): void;
27
33
  /**
28
34
  * Current connection state (synchronous access)
29
35
  */
@@ -45,7 +51,14 @@ export declare class SessionManager {
45
51
  */
46
52
  connectWithToken(config: TokenConfig): Promise<void>;
47
53
  /**
48
- * Disconnect from the Telnyx platform
54
+ * Disconnect from the Telnyx platform.
55
+ *
56
+ * The DISCONNECTED state is emitted BEFORE awaiting the underlying
57
+ * client teardown so that observers (including the auto-reconnect logic
58
+ * in TelnyxVoiceApp) cannot read a stale CONNECTED value during the
59
+ * short window while the socket is being torn down. Tracked calls are
60
+ * cleared here too, since a torn-down socket will never emit the
61
+ * ENDED/FAILED events that normally trigger per-call cleanup.
49
62
  */
50
63
  disconnect(): Promise<void>;
51
64
  /**
@@ -85,6 +85,13 @@ class SessionManager {
85
85
  setOnClientReady(callback) {
86
86
  this._onClientReady = callback;
87
87
  }
88
+ /**
89
+ * Set callback to be called when the session disconnects, so dependent
90
+ * subsystems (e.g. the call state controller) can clear their state.
91
+ */
92
+ setOnDisconnect(callback) {
93
+ this._onDisconnect = callback;
94
+ }
88
95
  /**
89
96
  * Current connection state (synchronous access)
90
97
  */
@@ -124,13 +131,28 @@ class SessionManager {
124
131
  await this._connect();
125
132
  }
126
133
  /**
127
- * Disconnect from the Telnyx platform
134
+ * Disconnect from the Telnyx platform.
135
+ *
136
+ * The DISCONNECTED state is emitted BEFORE awaiting the underlying
137
+ * client teardown so that observers (including the auto-reconnect logic
138
+ * in TelnyxVoiceApp) cannot read a stale CONNECTED value during the
139
+ * short window while the socket is being torn down. Tracked calls are
140
+ * cleared here too, since a torn-down socket will never emit the
141
+ * ENDED/FAILED events that normally trigger per-call cleanup.
128
142
  */
129
143
  async disconnect() {
130
144
  if (this._disposed) {
131
145
  return;
132
146
  }
133
147
  this._currentConfig = undefined;
148
+ this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
149
+ if (this._onDisconnect) {
150
+ try {
151
+ this._onDisconnect();
152
+ } catch (error) {
153
+ console.error('Error in onDisconnect callback:', error);
154
+ }
155
+ }
134
156
  if (this._telnyxClient) {
135
157
  try {
136
158
  await this._telnyxClient.disconnect();
@@ -138,7 +160,6 @@ class SessionManager {
138
160
  console.error('Error during disconnect:', error);
139
161
  }
140
162
  }
141
- this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
142
163
  }
143
164
  /**
144
165
  * Disable push notifications for the current session.
@@ -66,6 +66,11 @@ class TelnyxVoipClient {
66
66
  );
67
67
  this._callStateController.initializeClientListeners();
68
68
  });
69
+ // Clear any tracked calls when the session disconnects, so ghosts
70
+ // don't accumulate across background → foreground reconnect cycles.
71
+ this._sessionManager.setOnDisconnect(() => {
72
+ this._callStateController.clearAllCalls();
73
+ });
69
74
  if (this._options.debug) {
70
75
  console.log('TelnyxVoipClient initialized with options:', this._options);
71
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telnyx/react-voice-commons-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",
@@ -59,7 +59,7 @@
59
59
  "homepage": "https://github.com/team-telnyx/react-native-voice-commons",
60
60
  "repository": {
61
61
  "type": "git",
62
- "url": "git+https://github.com/team-telnyx/react-native-voice-commons.git"
62
+ "url": "https://github.com/team-telnyx/react-native-voice-commons.git"
63
63
  },
64
64
  "bugs": {
65
65
  "url": "https://github.com/team-telnyx/react-native-voice-commons/issues"
@@ -112,6 +112,7 @@
112
112
  "node": ">=16"
113
113
  },
114
114
  "publishConfig": {
115
- "access": "public"
115
+ "access": "public",
116
+ "provenance": true
116
117
  }
117
118
  }
@@ -190,6 +190,32 @@ export class CallStateController {
190
190
  this._onInviteAutoAccepted = callbacks.onInviteAutoAccepted;
191
191
  }
192
192
 
193
+ /**
194
+ * Clear all tracked calls. Called when the session disconnects so that
195
+ * calls left in non-terminal states (because the socket died before
196
+ * their ENDED/FAILED events could arrive) don't accumulate as ghosts
197
+ * across reconnect cycles.
198
+ */
199
+ clearAllCalls(): void {
200
+ if (this._callMap.size === 0) {
201
+ return;
202
+ }
203
+
204
+ console.log(
205
+ `CallStateController: Clearing ${this._callMap.size} tracked call(s) on disconnect`
206
+ );
207
+
208
+ for (const call of this._callMap.values()) {
209
+ try {
210
+ call.dispose();
211
+ } catch (error) {
212
+ console.warn('CallStateController: Error disposing call during clear:', error);
213
+ }
214
+ }
215
+ this._callMap.clear();
216
+ this._calls.next([]);
217
+ }
218
+
193
219
  /**
194
220
  * Dispose of the controller and clean up resources
195
221
  */
@@ -26,6 +26,7 @@ export class SessionManager {
26
26
  private _sessionId: string;
27
27
  private _disposed = false;
28
28
  private _onClientReady?: () => void;
29
+ private _onDisconnect?: () => void;
29
30
 
30
31
  constructor() {
31
32
  this._sessionId = this._generateSessionId();
@@ -45,6 +46,14 @@ export class SessionManager {
45
46
  this._onClientReady = callback;
46
47
  }
47
48
 
49
+ /**
50
+ * Set callback to be called when the session disconnects, so dependent
51
+ * subsystems (e.g. the call state controller) can clear their state.
52
+ */
53
+ setOnDisconnect(callback: () => void): void {
54
+ this._onDisconnect = callback;
55
+ }
56
+
48
57
  /**
49
58
  * Current connection state (synchronous access)
50
59
  */
@@ -91,7 +100,14 @@ export class SessionManager {
91
100
  }
92
101
 
93
102
  /**
94
- * Disconnect from the Telnyx platform
103
+ * Disconnect from the Telnyx platform.
104
+ *
105
+ * The DISCONNECTED state is emitted BEFORE awaiting the underlying
106
+ * client teardown so that observers (including the auto-reconnect logic
107
+ * in TelnyxVoiceApp) cannot read a stale CONNECTED value during the
108
+ * short window while the socket is being torn down. Tracked calls are
109
+ * cleared here too, since a torn-down socket will never emit the
110
+ * ENDED/FAILED events that normally trigger per-call cleanup.
95
111
  */
96
112
  async disconnect(): Promise<void> {
97
113
  if (this._disposed) {
@@ -99,6 +115,15 @@ export class SessionManager {
99
115
  }
100
116
 
101
117
  this._currentConfig = undefined;
118
+ this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
119
+
120
+ if (this._onDisconnect) {
121
+ try {
122
+ this._onDisconnect();
123
+ } catch (error) {
124
+ console.error('Error in onDisconnect callback:', error);
125
+ }
126
+ }
102
127
 
103
128
  if (this._telnyxClient) {
104
129
  try {
@@ -107,8 +132,6 @@ export class SessionManager {
107
132
  console.error('Error during disconnect:', error);
108
133
  }
109
134
  }
110
-
111
- this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
112
135
  }
113
136
 
114
137
  /**
@@ -82,6 +82,12 @@ export class TelnyxVoipClient {
82
82
  this._callStateController.initializeClientListeners();
83
83
  });
84
84
 
85
+ // Clear any tracked calls when the session disconnects, so ghosts
86
+ // don't accumulate across background → foreground reconnect cycles.
87
+ this._sessionManager.setOnDisconnect(() => {
88
+ this._callStateController.clearAllCalls();
89
+ });
90
+
85
91
  if (this._options.debug) {
86
92
  console.log('TelnyxVoipClient initialized with options:', this._options);
87
93
  }