@telnyx/react-voice-commons-sdk 0.4.3 → 1.1.0
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 +23 -0
- package/README.md +28 -7
- package/android/src/main/java/com/telnyx/react_voice_commons/CallForegroundService.kt +16 -2
- package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxFirebaseMessagingService.kt +16 -2
- package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxNotificationHelper.kt +345 -24
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +74 -1
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +36 -1
- package/ios/CallKitBridge.m +20 -1
- package/ios/CallKitBridge.swift +935 -60
- package/ios/README.md +2 -2
- package/ios/VoicePnBridge.m +21 -1
- package/ios/VoicePnBridge.swift +62 -1
- package/lib/callkit/callkit-coordinator.d.ts +39 -3
- package/lib/callkit/callkit-coordinator.js +561 -148
- package/lib/callkit/callkit.d.ts +7 -0
- package/lib/callkit/callkit.js +83 -11
- package/lib/hooks/useAppStateHandler.js +6 -2
- package/lib/index.d.ts +1 -0
- package/lib/index.js +9 -0
- package/lib/internal/CallKitHandler.d.ts +3 -0
- package/lib/internal/CallKitHandler.js +52 -46
- package/lib/internal/calls/call-state-controller.d.ts +39 -1
- package/lib/internal/calls/call-state-controller.js +152 -64
- package/lib/internal/session/session-manager.d.ts +16 -1
- package/lib/internal/session/session-manager.js +229 -58
- package/lib/internal/voice-pn-bridge.d.ts +44 -0
- package/lib/internal/voice-pn-bridge.js +94 -0
- package/lib/models/call.js +35 -0
- package/lib/models/config.d.ts +18 -0
- package/lib/models/config.js +2 -0
- package/lib/telnyx-voice-app.js +85 -43
- package/lib/telnyx-voip-client.d.ts +48 -5
- package/lib/telnyx-voip-client.js +276 -22
- package/package.json +7 -5
- package/src/callkit/callkit-coordinator.ts +642 -160
- package/src/callkit/callkit.ts +110 -12
- package/src/hooks/useAppStateHandler.ts +7 -2
- package/src/index.ts +3 -0
- package/src/internal/CallKitHandler.tsx +60 -55
- package/src/internal/calls/call-state-controller.ts +173 -74
- package/src/internal/session/session-manager.ts +277 -61
- package/src/internal/voice-pn-bridge.ts +111 -0
- package/src/models/call.ts +34 -0
- package/src/models/config.ts +18 -0
- package/src/telnyx-voice-app.tsx +96 -45
- package/src/telnyx-voip-client.ts +260 -24
- package/src/types/telnyx-sdk.d.ts +12 -1
|
@@ -6,7 +6,6 @@ const operators_1 = require('rxjs/operators');
|
|
|
6
6
|
const call_1 = require('../../models/call');
|
|
7
7
|
const call_state_1 = require('../../models/call-state');
|
|
8
8
|
const callkit_coordinator_1 = require('../../callkit/callkit-coordinator');
|
|
9
|
-
const voice_pn_bridge_1 = require('../voice-pn-bridge');
|
|
10
9
|
/**
|
|
11
10
|
* Central state machine for call management.
|
|
12
11
|
*
|
|
@@ -19,6 +18,38 @@ class CallStateController {
|
|
|
19
18
|
this._calls = new rxjs_1.BehaviorSubject([]);
|
|
20
19
|
this._callMap = new Map();
|
|
21
20
|
this._disposed = false;
|
|
21
|
+
// Explicitly-tracked active call ID for multi-call scenarios.
|
|
22
|
+
// When set, activeCall$/currentActiveCall prefer this call over the
|
|
23
|
+
// first-match heuristic. Cleared automatically when the call reaches
|
|
24
|
+
// a terminal state, or manually via clearActiveCall().
|
|
25
|
+
this._activeCallId = null;
|
|
26
|
+
this._handleTelnyxIncomingCall = (telnyxCall, msg) => {
|
|
27
|
+
console.log('CallStateController: Incoming call received:', telnyxCall.callId);
|
|
28
|
+
this._handleIncomingCall(telnyxCall, msg, false);
|
|
29
|
+
};
|
|
30
|
+
this._handleTelnyxReattachedCall = (telnyxCall, msg) => {
|
|
31
|
+
console.log('CallStateController: Reattached call received:', telnyxCall.callId);
|
|
32
|
+
this._handleIncomingCall(telnyxCall, msg, true);
|
|
33
|
+
};
|
|
34
|
+
this._handleTelnyxCallStateChanged = (telnyxCall, state) => {
|
|
35
|
+
console.log(
|
|
36
|
+
'CallStateController: Call state changed from TelnyxRTC:',
|
|
37
|
+
telnyxCall.callId,
|
|
38
|
+
state
|
|
39
|
+
);
|
|
40
|
+
// Find our wrapper call and update if needed
|
|
41
|
+
const call = this.findCallByTelnyxCall(telnyxCall);
|
|
42
|
+
if (call) {
|
|
43
|
+
console.log(
|
|
44
|
+
'CallStateController: Found wrapper call, state sync handled by Call subscription'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
this._handleTelnyxCallRemoved = (callId) => {
|
|
49
|
+
console.log('CallStateController: Call removed from TelnyxRTC:', callId);
|
|
50
|
+
// The call cleanup is already handled by our call state subscription.
|
|
51
|
+
// This event is informational for logging/debugging.
|
|
52
|
+
};
|
|
22
53
|
console.log('CallStateController: Constructor called - instance created');
|
|
23
54
|
// Don't set up client listeners here - client doesn't exist yet
|
|
24
55
|
// Will be called when client is available
|
|
@@ -35,18 +66,18 @@ class CallStateController {
|
|
|
35
66
|
get activeCall$() {
|
|
36
67
|
return this.calls$.pipe(
|
|
37
68
|
(0, operators_1.map)((calls) => {
|
|
38
|
-
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
call.currentState === call_state_1.TelnyxCallState.ACTIVE ||
|
|
45
|
-
call.currentState === call_state_1.TelnyxCallState.HELD
|
|
46
|
-
) || null
|
|
47
|
-
);
|
|
69
|
+
const call = this._selectActiveCall(calls);
|
|
70
|
+
return {
|
|
71
|
+
call,
|
|
72
|
+
callId: call?.callId ?? null,
|
|
73
|
+
state: call?.currentState ?? null,
|
|
74
|
+
};
|
|
48
75
|
}),
|
|
49
|
-
(0, operators_1.distinctUntilChanged)(
|
|
76
|
+
(0, operators_1.distinctUntilChanged)(
|
|
77
|
+
(previous, current) =>
|
|
78
|
+
previous.callId === current.callId && previous.state === current.state
|
|
79
|
+
),
|
|
80
|
+
(0, operators_1.map)(({ call }) => call)
|
|
50
81
|
);
|
|
51
82
|
}
|
|
52
83
|
/**
|
|
@@ -59,16 +90,7 @@ class CallStateController {
|
|
|
59
90
|
* Current active call (synchronous access)
|
|
60
91
|
*/
|
|
61
92
|
get currentActiveCall() {
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
calls.find(
|
|
65
|
-
(call) =>
|
|
66
|
-
call.currentState === call_state_1.TelnyxCallState.RINGING ||
|
|
67
|
-
call.currentState === call_state_1.TelnyxCallState.CONNECTING ||
|
|
68
|
-
call.currentState === call_state_1.TelnyxCallState.ACTIVE ||
|
|
69
|
-
call.currentState === call_state_1.TelnyxCallState.HELD
|
|
70
|
-
) || null
|
|
71
|
-
);
|
|
93
|
+
return this._selectActiveCall(this.currentCalls);
|
|
72
94
|
}
|
|
73
95
|
/**
|
|
74
96
|
* Access any active call tracked by the client.
|
|
@@ -94,6 +116,32 @@ class CallStateController {
|
|
|
94
116
|
console.warn('CallStateController: Could not find call to set connecting:', callId);
|
|
95
117
|
}
|
|
96
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Explicitly set the active call for multi-call scenarios.
|
|
121
|
+
* When set, activeCall$ and currentActiveCall prefer this call over
|
|
122
|
+
* the first-match heuristic. The ID is cleared automatically when the
|
|
123
|
+
* call reaches a terminal state.
|
|
124
|
+
* @param callId The ID of the call to mark as active
|
|
125
|
+
*/
|
|
126
|
+
setActiveCall(callId) {
|
|
127
|
+
const call = this._callMap.get(callId);
|
|
128
|
+
if (call) {
|
|
129
|
+
this._activeCallId = callId;
|
|
130
|
+
this._calls.next([...this.currentCalls]);
|
|
131
|
+
} else {
|
|
132
|
+
console.warn('CallStateController: Cannot set active call - call not found:', callId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Clear the explicitly-tracked active call ID, reverting to the
|
|
137
|
+
* first-match heuristic for active call selection.
|
|
138
|
+
*/
|
|
139
|
+
clearActiveCall() {
|
|
140
|
+
if (this._activeCallId !== null) {
|
|
141
|
+
this._activeCallId = null;
|
|
142
|
+
this._calls.next([...this.currentCalls]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
97
145
|
/**
|
|
98
146
|
* Find a call by its underlying Telnyx call ID
|
|
99
147
|
* @param telnyxCall The Telnyx call object to find
|
|
@@ -106,6 +154,31 @@ class CallStateController {
|
|
|
106
154
|
}
|
|
107
155
|
return null;
|
|
108
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Select the active call, preferring the explicitly-tracked call ID
|
|
159
|
+
* over the first-match heuristic.
|
|
160
|
+
*/
|
|
161
|
+
_selectActiveCall(calls) {
|
|
162
|
+
if (this._activeCallId) {
|
|
163
|
+
const tracked = calls.find((c) => c.callId === this._activeCallId);
|
|
164
|
+
if (tracked && this._isNonTerminal(tracked)) {
|
|
165
|
+
return tracked;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Fall back to first non-terminal call (backward compatible)
|
|
169
|
+
return calls.find((c) => this._isNonTerminal(c)) || null;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Check if a call is in a non-terminal (active or connecting) state.
|
|
173
|
+
*/
|
|
174
|
+
_isNonTerminal(call) {
|
|
175
|
+
return (
|
|
176
|
+
call.currentState === call_state_1.TelnyxCallState.RINGING ||
|
|
177
|
+
call.currentState === call_state_1.TelnyxCallState.CONNECTING ||
|
|
178
|
+
call.currentState === call_state_1.TelnyxCallState.ACTIVE ||
|
|
179
|
+
call.currentState === call_state_1.TelnyxCallState.HELD
|
|
180
|
+
);
|
|
181
|
+
}
|
|
109
182
|
/**
|
|
110
183
|
* Initialize client listeners when the Telnyx client becomes available
|
|
111
184
|
* This should be called by the session manager after client creation
|
|
@@ -133,7 +206,10 @@ class CallStateController {
|
|
|
133
206
|
destinationNumber: destination,
|
|
134
207
|
callerIdName: callerName,
|
|
135
208
|
callerIdNumber: callerNumber,
|
|
136
|
-
customHeaders,
|
|
209
|
+
customHeaders: this._normalizeCustomHeaders(customHeaders),
|
|
210
|
+
peerConnectionOptions: {
|
|
211
|
+
useTrickleIce: this._sessionManager.useTrickleIce,
|
|
212
|
+
},
|
|
137
213
|
};
|
|
138
214
|
const telnyxCall = await this._sessionManager.telnyxClient.newCall(callOptions);
|
|
139
215
|
// Create our wrapper Call object
|
|
@@ -154,6 +230,18 @@ class CallStateController {
|
|
|
154
230
|
throw error;
|
|
155
231
|
}
|
|
156
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Normalize public custom headers into the format expected by the underlying SDK.
|
|
235
|
+
*/
|
|
236
|
+
_normalizeCustomHeaders(customHeaders) {
|
|
237
|
+
if (!customHeaders) {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
if (Array.isArray(customHeaders)) {
|
|
241
|
+
return customHeaders;
|
|
242
|
+
}
|
|
243
|
+
return Object.entries(customHeaders).map(([name, value]) => ({ name, value }));
|
|
244
|
+
}
|
|
157
245
|
/**
|
|
158
246
|
* Set callbacks for waiting for invite logic (used for push notifications)
|
|
159
247
|
*/
|
|
@@ -182,6 +270,7 @@ class CallStateController {
|
|
|
182
270
|
}
|
|
183
271
|
}
|
|
184
272
|
this._callMap.clear();
|
|
273
|
+
this._activeCallId = null;
|
|
185
274
|
this._calls.next([]);
|
|
186
275
|
}
|
|
187
276
|
/**
|
|
@@ -192,9 +281,11 @@ class CallStateController {
|
|
|
192
281
|
return;
|
|
193
282
|
}
|
|
194
283
|
this._disposed = true;
|
|
284
|
+
this._removeClientListeners();
|
|
195
285
|
// Dispose of all calls
|
|
196
286
|
this.currentCalls.forEach((call) => call.dispose());
|
|
197
287
|
this._callMap.clear();
|
|
288
|
+
this._activeCallId = null;
|
|
198
289
|
// CallKit cleanup is now handled by CallKitCoordinator automatically
|
|
199
290
|
this._calls.complete();
|
|
200
291
|
}
|
|
@@ -203,30 +294,22 @@ class CallStateController {
|
|
|
203
294
|
*/
|
|
204
295
|
_setupClientListeners() {
|
|
205
296
|
console.log('CallStateController: Setting up client listeners...');
|
|
206
|
-
|
|
297
|
+
const telnyxClient = this._sessionManager.telnyxClient;
|
|
298
|
+
if (!telnyxClient) {
|
|
207
299
|
console.log('CallStateController: No telnyxClient available yet, skipping listener setup');
|
|
208
300
|
return;
|
|
209
301
|
}
|
|
302
|
+
this._removeClientListeners();
|
|
303
|
+
this._listenerClient = telnyxClient;
|
|
210
304
|
console.log('CallStateController: TelnyxClient found, setting up incoming call listener');
|
|
211
|
-
console.log(
|
|
212
|
-
'CallStateController: Client instance:',
|
|
213
|
-
this._sessionManager.telnyxClient.constructor.name
|
|
214
|
-
);
|
|
305
|
+
console.log('CallStateController: Client instance:', telnyxClient.constructor.name);
|
|
215
306
|
// Listen for incoming calls
|
|
216
|
-
|
|
217
|
-
console.log('CallStateController: Incoming call received:', telnyxCall.callId);
|
|
218
|
-
this._handleIncomingCall(telnyxCall, msg, false);
|
|
219
|
-
});
|
|
307
|
+
telnyxClient.on('telnyx.call.incoming', this._handleTelnyxIncomingCall);
|
|
220
308
|
// Listen for reattached calls (after network reconnection)
|
|
221
|
-
|
|
222
|
-
console.log('CallStateController: Reattached call received:', telnyxCall.callId);
|
|
223
|
-
this._handleIncomingCall(telnyxCall, msg, true);
|
|
224
|
-
});
|
|
309
|
+
telnyxClient.on('telnyx.call.reattached', this._handleTelnyxReattachedCall);
|
|
225
310
|
// Verify listeners are set up
|
|
226
|
-
const incomingListeners =
|
|
227
|
-
|
|
228
|
-
const reattachedListeners =
|
|
229
|
-
this._sessionManager.telnyxClient.listenerCount('telnyx.call.reattached');
|
|
311
|
+
const incomingListeners = telnyxClient.listenerCount('telnyx.call.incoming');
|
|
312
|
+
const reattachedListeners = telnyxClient.listenerCount('telnyx.call.reattached');
|
|
230
313
|
console.log(
|
|
231
314
|
'CallStateController: Listeners registered - incoming:',
|
|
232
315
|
incomingListeners,
|
|
@@ -234,28 +317,21 @@ class CallStateController {
|
|
|
234
317
|
reattachedListeners
|
|
235
318
|
);
|
|
236
319
|
// Listen for call state changes from the TelnyxRTC client (multi-call support)
|
|
237
|
-
|
|
238
|
-
console.log(
|
|
239
|
-
'CallStateController: Call state changed from TelnyxRTC:',
|
|
240
|
-
telnyxCall.callId,
|
|
241
|
-
state
|
|
242
|
-
);
|
|
243
|
-
// Find our wrapper call and update if needed
|
|
244
|
-
const call = this.findCallByTelnyxCall(telnyxCall);
|
|
245
|
-
if (call) {
|
|
246
|
-
console.log(
|
|
247
|
-
'CallStateController: Found wrapper call, state sync handled by Call subscription'
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
});
|
|
320
|
+
telnyxClient.on('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
|
|
251
321
|
// Listen for call removal events from TelnyxRTC (multi-call support)
|
|
252
|
-
|
|
253
|
-
console.log('CallStateController: Call removed from TelnyxRTC:', callId);
|
|
254
|
-
// The call cleanup is already handled by our call state subscription
|
|
255
|
-
// This event is informational for logging/debugging
|
|
256
|
-
});
|
|
322
|
+
telnyxClient.on('telnyx.call.removed', this._handleTelnyxCallRemoved);
|
|
257
323
|
console.log('CallStateController: Client listeners set up successfully');
|
|
258
324
|
}
|
|
325
|
+
_removeClientListeners() {
|
|
326
|
+
if (!this._listenerClient) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
this._listenerClient.off('telnyx.call.incoming', this._handleTelnyxIncomingCall);
|
|
330
|
+
this._listenerClient.off('telnyx.call.reattached', this._handleTelnyxReattachedCall);
|
|
331
|
+
this._listenerClient.off('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
|
|
332
|
+
this._listenerClient.off('telnyx.call.removed', this._handleTelnyxCallRemoved);
|
|
333
|
+
this._listenerClient = undefined;
|
|
334
|
+
}
|
|
259
335
|
/**
|
|
260
336
|
* Handle incoming call or reattached call
|
|
261
337
|
*/
|
|
@@ -330,6 +406,10 @@ class CallStateController {
|
|
|
330
406
|
*/
|
|
331
407
|
_addCall(call) {
|
|
332
408
|
this._callMap.set(call.callId, call);
|
|
409
|
+
// Auto-track as active if no active call is currently set
|
|
410
|
+
if (this._activeCallId === null) {
|
|
411
|
+
this._activeCallId = call.callId;
|
|
412
|
+
}
|
|
333
413
|
const currentCalls = this.currentCalls;
|
|
334
414
|
currentCalls.push(call);
|
|
335
415
|
this._calls.next([...currentCalls]);
|
|
@@ -342,7 +422,11 @@ class CallStateController {
|
|
|
342
422
|
callkit_coordinator_1.callKitCoordinator.getCallKitUUID(telnyxCall);
|
|
343
423
|
if (existingCallKitUUID) {
|
|
344
424
|
console.log(
|
|
345
|
-
'CallStateController:
|
|
425
|
+
'CallStateController: Linking push-created CallKit call to its signaling call:',
|
|
426
|
+
existingCallKitUUID
|
|
427
|
+
);
|
|
428
|
+
callkit_coordinator_1.callKitCoordinator.linkExistingCallKitCall(
|
|
429
|
+
telnyxCall,
|
|
346
430
|
existingCallKitUUID
|
|
347
431
|
);
|
|
348
432
|
} else if (call.isIncoming) {
|
|
@@ -367,16 +451,20 @@ class CallStateController {
|
|
|
367
451
|
call.callState$.subscribe((state) => {
|
|
368
452
|
// CallKitCoordinator automatically updates CallKit via setupWebRTCCallListeners
|
|
369
453
|
console.log('CallStateController: Call state changed to:', state);
|
|
454
|
+
// Re-emit the call list so calls$/activeCall$ subscribers see state changes.
|
|
455
|
+
// distinctUntilChanged uses reference equality; without a new array
|
|
456
|
+
// reference, non-terminal transitions leave activeCall$ stale.
|
|
457
|
+
this._calls.next([...this.currentCalls]);
|
|
370
458
|
// Clean up when call ends - delay to next tick so external subscribers
|
|
371
459
|
// receive the ENDED/FAILED state before the call is disposed
|
|
372
460
|
if (
|
|
373
461
|
state === call_state_1.TelnyxCallState.ENDED ||
|
|
374
462
|
state === call_state_1.TelnyxCallState.FAILED
|
|
375
463
|
) {
|
|
376
|
-
// Clear
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
464
|
+
// Clear active call ID if the ending call was the tracked active call
|
|
465
|
+
if (this._activeCallId === call.callId) {
|
|
466
|
+
this._activeCallId = null;
|
|
467
|
+
}
|
|
380
468
|
setTimeout(() => this._removeCall(call.callId), 0);
|
|
381
469
|
}
|
|
382
470
|
});
|
|
@@ -14,6 +14,10 @@ export declare class SessionManager {
|
|
|
14
14
|
private _currentConfig?;
|
|
15
15
|
private _sessionId;
|
|
16
16
|
private _disposed;
|
|
17
|
+
private _disposing;
|
|
18
|
+
private _connectionGeneration;
|
|
19
|
+
private _disposePromise?;
|
|
20
|
+
private _connectPromise?;
|
|
17
21
|
private _onClientReady?;
|
|
18
22
|
private _onDisconnect?;
|
|
19
23
|
constructor();
|
|
@@ -42,6 +46,7 @@ export declare class SessionManager {
|
|
|
42
46
|
* Get the underlying Telnyx client instance
|
|
43
47
|
*/
|
|
44
48
|
get telnyxClient(): TelnyxSDK.TelnyxRTC | undefined;
|
|
49
|
+
get useTrickleIce(): boolean;
|
|
45
50
|
/**
|
|
46
51
|
* Connect using credential authentication
|
|
47
52
|
*/
|
|
@@ -75,14 +80,17 @@ export declare class SessionManager {
|
|
|
75
80
|
* Handle push notification (async version)
|
|
76
81
|
*/
|
|
77
82
|
handlePushNotification(payload: Record<string, any>): Promise<void>;
|
|
83
|
+
private _hasActiveOrHeldCall;
|
|
78
84
|
/**
|
|
79
85
|
* Dispose of the session manager and clean up resources
|
|
80
86
|
*/
|
|
81
|
-
dispose(): void
|
|
87
|
+
dispose(): Promise<void>;
|
|
82
88
|
/**
|
|
83
89
|
* Internal method to establish connection with or without push notification handling
|
|
84
90
|
*/
|
|
85
91
|
private _connect;
|
|
92
|
+
private _dispose;
|
|
93
|
+
private _runConnect;
|
|
86
94
|
/**
|
|
87
95
|
* Set up event listeners for the Telnyx client
|
|
88
96
|
*/
|
|
@@ -95,4 +103,11 @@ export declare class SessionManager {
|
|
|
95
103
|
* Generate a unique session ID
|
|
96
104
|
*/
|
|
97
105
|
private _generateSessionId;
|
|
106
|
+
private _assertCanStartConnection;
|
|
107
|
+
private _isTeardownActive;
|
|
108
|
+
private _isConnectCanceled;
|
|
109
|
+
private _throwIfConnectCanceled;
|
|
110
|
+
private _createConnectCanceledError;
|
|
111
|
+
private _disconnectClient;
|
|
112
|
+
private _disconnectAndForgetClient;
|
|
98
113
|
}
|