quickblox 2.23.0 → 2.23.1-beta.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/.claude/settings.local.json +13 -0
- package/package.json +1 -1
- package/quickblox.js +23664 -23388
- package/quickblox.min.js +1 -1
- package/src/modules/chat/qbChat.js +296 -25
- package/src/qbConfig.js +2 -2
- package/src/qbStrophe.js +5 -0
|
@@ -101,6 +101,19 @@ function ChatProxy(service) {
|
|
|
101
101
|
this._checkExpiredSessionTimer = undefined;
|
|
102
102
|
this._sessionHasExpired = false;
|
|
103
103
|
this._pings = {};
|
|
104
|
+
|
|
105
|
+
// [QC-1550] XMPP connection is considered "verified" only after the first
|
|
106
|
+
// successful pong response. Strophe emits Status.CONNECTED at the transport
|
|
107
|
+
// layer (TCP/WebSocket handshake completed) before XMPP-level traffic is
|
|
108
|
+
// actually flowing — this creates a race window where the application thinks
|
|
109
|
+
// chat is alive but pings can still time out. Gates onReconnectListener and
|
|
110
|
+
// onDisconnectedListener to avoid firing them based on an unverified state.
|
|
111
|
+
this._isConnectionVerified = false;
|
|
112
|
+
// [QC-1550] On reconnect, onReconnectListener is deferred until the first
|
|
113
|
+
// successful pong. This flag marks that a reconnect is awaiting verification:
|
|
114
|
+
// the ping success callback reads it to decide whether to fire the deferred
|
|
115
|
+
// listener. Reset on listener fire, on ping failure, and on logout.
|
|
116
|
+
this._isReconnectListenerPending = false;
|
|
104
117
|
//
|
|
105
118
|
this.helpers = new Helpers();
|
|
106
119
|
//
|
|
@@ -299,7 +312,13 @@ function ChatProxy(service) {
|
|
|
299
312
|
var from = chatUtils.getAttr(stanza, 'from'),
|
|
300
313
|
to = chatUtils.getAttr(stanza, 'to'),
|
|
301
314
|
type = chatUtils.getAttr(stanza, 'type'),
|
|
302
|
-
messageId = chatUtils.getAttr(stanza, 'id')
|
|
315
|
+
messageId = chatUtils.getAttr(stanza, 'id');
|
|
316
|
+
|
|
317
|
+
// [QC-1454 DIAGNOSTIC] Log every _onMessage invocation
|
|
318
|
+
Utils.QBLog('[QBChat]', '[MSG_HANDLER] _onMessage invoked: type=' + type +
|
|
319
|
+
' from=' + from + ' id=' + messageId);
|
|
320
|
+
|
|
321
|
+
var
|
|
303
322
|
markable = chatUtils.getElement(stanza, 'markable'),
|
|
304
323
|
delivered = chatUtils.getElement(stanza, 'received'),
|
|
305
324
|
read = chatUtils.getElement(stanza, 'displayed'),
|
|
@@ -391,6 +410,9 @@ function ChatProxy(service) {
|
|
|
391
410
|
}
|
|
392
411
|
|
|
393
412
|
if (typeof self.onMessageListener === 'function' && (type === 'chat' || type === 'groupchat')) {
|
|
413
|
+
// [QC-1454 DIAGNOSTIC] Log before calling app's message listener
|
|
414
|
+
Utils.QBLog('[QBChat]', '[MSG_HANDLER] Calling onMessageListener: type=' + type +
|
|
415
|
+
' userId=' + userId + ' msgId=' + messageId);
|
|
394
416
|
Utils.safeCallbackCall(self.onMessageListener, userId, message);
|
|
395
417
|
}
|
|
396
418
|
|
|
@@ -416,6 +438,15 @@ function ChatProxy(service) {
|
|
|
416
438
|
}
|
|
417
439
|
}
|
|
418
440
|
|
|
441
|
+
// [QC-1454 DIAGNOSTIC] Log all MUC-related presences
|
|
442
|
+
if (xXMLNS && xXMLNS.indexOf('muc') !== -1) {
|
|
443
|
+
Utils.QBLog('[QBChat]', '[MUC_PRESENCE] from=' + from +
|
|
444
|
+
' type=' + (type || 'available') +
|
|
445
|
+
' statusCode=' + (statusCode || 'none') +
|
|
446
|
+
' id=' + id +
|
|
447
|
+
' xmlns=' + xXMLNS);
|
|
448
|
+
}
|
|
449
|
+
|
|
419
450
|
// MUC presences go here
|
|
420
451
|
if (xXMLNS && xXMLNS == 'http://jabber.org/protocol/muc#user') {
|
|
421
452
|
dialogId = self.helpers.getDialogIdFromNode(from);
|
|
@@ -866,6 +897,10 @@ ChatProxy.prototype = {
|
|
|
866
897
|
self.connection.XAddTrackedHandler(self._onSystemMessageListener, null, 'message', 'headline');
|
|
867
898
|
self.connection.XAddTrackedHandler(self._onMessageErrorListener, null, 'message', 'error');
|
|
868
899
|
|
|
900
|
+
// [QC-1454 DIAGNOSTIC] Log handler count after re-registration
|
|
901
|
+
Utils.QBLog('[QBChat]', '[HANDLERS] Registered handler count after reconnect: ' +
|
|
902
|
+
self.connection.XHandlerReferences.length);
|
|
903
|
+
|
|
869
904
|
var noTimerId = typeof self._checkConnectionPingTimer === 'undefined';
|
|
870
905
|
noTimerId = config.pingLocalhostTimeInterval === 0 ? false : noTimerId;
|
|
871
906
|
|
|
@@ -882,12 +917,35 @@ ChatProxy.prototype = {
|
|
|
882
917
|
' error: ', error);
|
|
883
918
|
self._chatPingFailedCounter += 1;
|
|
884
919
|
if (self._chatPingFailedCounter >= config.chatPingMissLimit) {
|
|
885
|
-
|
|
920
|
+
// [QC-1550] onDisconnectedListener should only fire when the
|
|
921
|
+
// previous connection was actually verified. If the ping miss
|
|
922
|
+
// limit is reached while we were still waiting for the first
|
|
923
|
+
// pong after reconnect (verification never succeeded), the
|
|
924
|
+
// application was never told the chat was "alive" — so we
|
|
925
|
+
// don't fire a misleading "disconnected" event for a state
|
|
926
|
+
// the consumer never observed.
|
|
927
|
+
if (self.isConnected && self._isConnectionVerified &&
|
|
928
|
+
typeof self.onDisconnectedListener === 'function') {
|
|
886
929
|
Utils.safeCallbackCall(self.onDisconnectedListener);
|
|
887
930
|
}
|
|
931
|
+
// [QC-1550] Reset verification state and clear any pending
|
|
932
|
+
// reconnect listener — the connection has failed; the next
|
|
933
|
+
// CONNECTED + pong cycle will rebuild verification from
|
|
934
|
+
// scratch.
|
|
935
|
+
self._isConnectionVerified = false;
|
|
936
|
+
self._isReconnectListenerPending = false;
|
|
888
937
|
self.isConnected = false;
|
|
889
938
|
self._isConnecting = false;
|
|
890
939
|
self._chatPingFailedCounter = 0;
|
|
940
|
+
// [QC-1550] Stop this ping interval — it belongs to the dead
|
|
941
|
+
// connection. Without this, the timer keeps firing pings on
|
|
942
|
+
// the old XMPP session and racing with the new connection's
|
|
943
|
+
// ping cycle, which produced spurious pong success/failure
|
|
944
|
+
// interleaving in the field logs.
|
|
945
|
+
if (self._checkConnectionPingTimer !== undefined) {
|
|
946
|
+
clearInterval(self._checkConnectionPingTimer);
|
|
947
|
+
self._checkConnectionPingTimer = undefined;
|
|
948
|
+
}
|
|
891
949
|
self._establishConnection(params,'CONNECTED have SDK ping failed');
|
|
892
950
|
}
|
|
893
951
|
} else {
|
|
@@ -896,6 +954,32 @@ ChatProxy.prototype = {
|
|
|
896
954
|
'ok, at ', Utils.getCurrentTime(),
|
|
897
955
|
'_chatPingFailedCounter: ', self._chatPingFailedCounter);
|
|
898
956
|
self._chatPingFailedCounter = 0;
|
|
957
|
+
|
|
958
|
+
// [QC-1550] First pong after reconnect verifies XMPP is live.
|
|
959
|
+
// Fire the deferred onReconnectListener now (was postponed in
|
|
960
|
+
// _postConnectActions). Skip if logout was triggered between
|
|
961
|
+
// CONNECTED and pong — listener fire would leak past logout.
|
|
962
|
+
if (self._isReconnectListenerPending && !self._isLogout) {
|
|
963
|
+
self._isConnectionVerified = true;
|
|
964
|
+
self._isReconnectListenerPending = false;
|
|
965
|
+
Utils.QBLog('[QBChat]',
|
|
966
|
+
'[QC-1550] First pong success, firing deferred onReconnectListener at ',
|
|
967
|
+
Utils.getCurrentTime());
|
|
968
|
+
if (typeof self.onLogListener === 'function') {
|
|
969
|
+
Utils.safeCallbackCall(self.onLogListener,
|
|
970
|
+
'[QBChat] [QC-1550] First pong success, firing deferred onReconnectListener at ' +
|
|
971
|
+
chatUtils.getLocalTime());
|
|
972
|
+
}
|
|
973
|
+
if (typeof self.onReconnectListener === 'function') {
|
|
974
|
+
Utils.safeCallbackCall(self.onReconnectListener);
|
|
975
|
+
}
|
|
976
|
+
} else if (!self._isConnectionVerified && !self._isLogout) {
|
|
977
|
+
// Edge case: pong succeeded but pending flag was cleared
|
|
978
|
+
// (e.g. by ping failure path racing with this success).
|
|
979
|
+
// Mark verified anyway so onDisconnectedListener gating
|
|
980
|
+
// in SDK-4 works correctly going forward.
|
|
981
|
+
self._isConnectionVerified = true;
|
|
982
|
+
}
|
|
899
983
|
}
|
|
900
984
|
});
|
|
901
985
|
} catch (err) {
|
|
@@ -950,11 +1034,30 @@ ChatProxy.prototype = {
|
|
|
950
1034
|
'[QBChat]' + '[SDK v'+config.version+']' +' Status.DISCONNECTED at ' +
|
|
951
1035
|
chatUtils.getLocalTime()+ ' DISCONNECTED CONDITION: ' + condition);
|
|
952
1036
|
}
|
|
953
|
-
// fire 'onDisconnectedListener' only once
|
|
954
|
-
|
|
1037
|
+
// [QC-1550] fire 'onDisconnectedListener' only once AND only when the
|
|
1038
|
+
// previous connection was actually verified (first pong succeeded).
|
|
1039
|
+
// See onDisconnectedListener gating in SDK-4 for full reasoning — this
|
|
1040
|
+
// is the same rule applied to transport-level disconnects.
|
|
1041
|
+
if (self.isConnected && self._isConnectionVerified &&
|
|
1042
|
+
typeof self.onDisconnectedListener === 'function') {
|
|
955
1043
|
Utils.safeCallbackCall(self.onDisconnectedListener);
|
|
956
1044
|
}
|
|
957
1045
|
|
|
1046
|
+
// [QC-1550] Reset verification state. The next CONNECTED + pong cycle
|
|
1047
|
+
// will rebuild it. Pending reconnect listener (if any) is cancelled —
|
|
1048
|
+
// the new cycle will set a fresh one in _postConnectActions.
|
|
1049
|
+
self._isConnectionVerified = false;
|
|
1050
|
+
self._isReconnectListenerPending = false;
|
|
1051
|
+
|
|
1052
|
+
// [QC-1550] Stop ping timer of the dead connection. Without this, the
|
|
1053
|
+
// interval keeps invoking self.pingchat() on a stale Strophe connection
|
|
1054
|
+
// until the new CONNECTED arrives, producing spurious failure logs and
|
|
1055
|
+
// racing with the new connection's freshly-started ping cycle.
|
|
1056
|
+
if (self._checkConnectionPingTimer !== undefined) {
|
|
1057
|
+
clearInterval(self._checkConnectionPingTimer);
|
|
1058
|
+
self._checkConnectionPingTimer = undefined;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
958
1061
|
self.isConnected = false;
|
|
959
1062
|
self._isConnecting = false;
|
|
960
1063
|
self.connection.reset();
|
|
@@ -1112,8 +1215,21 @@ ChatProxy.prototype = {
|
|
|
1112
1215
|
* - save user's JID;
|
|
1113
1216
|
* - enable carbons;
|
|
1114
1217
|
* - get and storage the user's roster (if the initial connect);
|
|
1115
|
-
* - recover the joined rooms and
|
|
1218
|
+
* - recover the joined rooms and defer 'onReconnectListener' until the first
|
|
1219
|
+
* pong confirms XMPP-level traffic (if the reconnect);
|
|
1116
1220
|
* - send initial presence to the chat server.
|
|
1221
|
+
*
|
|
1222
|
+
* [QC-1550] On reconnect, Strophe emits Status.CONNECTED as soon as the
|
|
1223
|
+
* transport handshake completes (WebSocket/BOSH), but the XMPP layer may not
|
|
1224
|
+
* yet be processing traffic — pings can still time out for several seconds.
|
|
1225
|
+
* Firing onReconnectListener at this moment leads consumers (e.g. UI overlays)
|
|
1226
|
+
* to believe chat is fully restored, then a subsequent ping miss triggers a
|
|
1227
|
+
* false "Lost connection" state. The fix defers the listener until the first
|
|
1228
|
+
* pong succeeds (see ping success branch in Strophe.Status.CONNECTED handler).
|
|
1229
|
+
*
|
|
1230
|
+
* Fallback: when ping is disabled (config.pingLocalhostTimeInterval === 0)
|
|
1231
|
+
* there is no pong to wait for, so we fire onReconnectListener immediately
|
|
1232
|
+
* to preserve backward compatibility for consumers who opted out of pings.
|
|
1117
1233
|
*/
|
|
1118
1234
|
_postConnectActions: function (callback, isInitialConnect) {
|
|
1119
1235
|
Utils.QBLog('[QBChat]', 'Status.CONNECTED at ' + chatUtils.getLocalTime());
|
|
@@ -1139,6 +1255,13 @@ ChatProxy.prototype = {
|
|
|
1139
1255
|
self._enableCarbons();
|
|
1140
1256
|
|
|
1141
1257
|
if (isInitialConnect) {
|
|
1258
|
+
// [QC-1550] Initial connect: no reconnect overlay race possible
|
|
1259
|
+
// (user just logged in), so we mark the connection as verified
|
|
1260
|
+
// immediately. This also enables onDisconnectedListener gating in
|
|
1261
|
+
// the DISCONNECTED handler from this point on.
|
|
1262
|
+
self._isConnectionVerified = true;
|
|
1263
|
+
self._isReconnectListenerPending = false;
|
|
1264
|
+
|
|
1142
1265
|
self.roster.get(function (contacts) {
|
|
1143
1266
|
xmppClient.send(presence);
|
|
1144
1267
|
|
|
@@ -1150,59 +1273,183 @@ ChatProxy.prototype = {
|
|
|
1150
1273
|
|
|
1151
1274
|
xmppClient.send(presence);
|
|
1152
1275
|
|
|
1153
|
-
Utils.QBLog('[QBChat]', 'Re-joining ' + rooms.length +
|
|
1276
|
+
Utils.QBLog('[QBChat]', 'Re-joining ' + rooms.length + ' rooms...');
|
|
1154
1277
|
|
|
1278
|
+
// [QC-1454 DIAGNOSTIC] Log each MUC re-join with callback to track server confirmation
|
|
1279
|
+
/* jshint -W083 */
|
|
1155
1280
|
for (var i = 0, len = rooms.length; i < len; i++) {
|
|
1156
|
-
|
|
1281
|
+
(function(roomJid, index) {
|
|
1282
|
+
Utils.QBLog('[QBChat]', '[MUC_REJOIN] Sending join for room ' +
|
|
1283
|
+
(index + 1) + '/' + len + ': ' + roomJid);
|
|
1284
|
+
self.muc.join(roomJid, function(errorOrStanza, result) {
|
|
1285
|
+
if (result && result.dialogId) {
|
|
1286
|
+
Utils.QBLog('[QBChat]', '[MUC_REJOIN] Room join CONFIRMED by server: ' +
|
|
1287
|
+
'dialogId=' + result.dialogId + ' room=' + roomJid);
|
|
1288
|
+
} else {
|
|
1289
|
+
Utils.QBLog('[QBChat]', '[MUC_REJOIN] Room join RESPONSE (possible error): ' +
|
|
1290
|
+
'room=' + roomJid + ' error=' + JSON.stringify(errorOrStanza));
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
})(rooms[i], i);
|
|
1157
1294
|
}
|
|
1158
1295
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1296
|
+
Utils.QBLog('[QBChat]', '[MUC_REJOIN] All ' + rooms.length +
|
|
1297
|
+
' join presences sent. Awaiting server confirmations...');
|
|
1298
|
+
|
|
1299
|
+
// [QC-1550] Defer onReconnectListener until first successful pong.
|
|
1300
|
+
// The ping success callback (in Strophe.Status.CONNECTED handler)
|
|
1301
|
+
// reads _isReconnectListenerPending and fires the listener once XMPP
|
|
1302
|
+
// is verified. Connection remains unverified until that point.
|
|
1303
|
+
self._isConnectionVerified = false;
|
|
1304
|
+
|
|
1305
|
+
if (config.pingLocalhostTimeInterval === 0) {
|
|
1306
|
+
// Ping disabled — no pong to wait for; preserve legacy behavior.
|
|
1307
|
+
Utils.QBLog('[QBChat]',
|
|
1308
|
+
'[QC-1550] pingLocalhostTimeInterval=0, firing onReconnectListener immediately (ping disabled)');
|
|
1309
|
+
self._isConnectionVerified = true;
|
|
1310
|
+
self._isReconnectListenerPending = false;
|
|
1311
|
+
if (typeof self.onReconnectListener === 'function') {
|
|
1312
|
+
Utils.safeCallbackCall(self.onReconnectListener);
|
|
1313
|
+
}
|
|
1314
|
+
} else {
|
|
1315
|
+
self._isReconnectListenerPending = true;
|
|
1316
|
+
Utils.QBLog('[QBChat]',
|
|
1317
|
+
'[QC-1550] onReconnectListener deferred until first pong success');
|
|
1318
|
+
if (typeof self.onLogListener === 'function') {
|
|
1319
|
+
Utils.safeCallbackCall(self.onLogListener,
|
|
1320
|
+
'[QBChat] [QC-1550] onReconnectListener deferred at ' +
|
|
1321
|
+
chatUtils.getLocalTime() + ' — awaiting first pong');
|
|
1322
|
+
}
|
|
1161
1323
|
}
|
|
1162
1324
|
}
|
|
1163
1325
|
},
|
|
1164
1326
|
|
|
1327
|
+
/**
|
|
1328
|
+
* Reconnect retry loop.
|
|
1329
|
+
*
|
|
1330
|
+
* [QC-1456] Safari / macOS WebSocket reconnect issue:
|
|
1331
|
+
*
|
|
1332
|
+
* When the device switches networks (e.g. LTE → WiFi), Safari's networking
|
|
1333
|
+
* stack does not update instantly. The OS-level DNS cache, routing table, and
|
|
1334
|
+
* socket layer still reference the old interface for several seconds. During
|
|
1335
|
+
* this window, `new WebSocket(url)` fails immediately with:
|
|
1336
|
+
*
|
|
1337
|
+
* "WebSocket connection failed: The Internet connection appears to be offline."
|
|
1338
|
+
*
|
|
1339
|
+
* This triggers CONNFAIL → DISCONNECTED in rapid succession. The SDK retries
|
|
1340
|
+
* every `chatReconnectionTimeInterval` seconds, but if the retry timer is
|
|
1341
|
+
* killed while a connect attempt is still in-flight (`_isConnecting === true`),
|
|
1342
|
+
* the next timer is only recreated after the full Strophe timeout cycle
|
|
1343
|
+
* (CONNFAIL → DISCONNECTED → _establishConnection), which can double the
|
|
1344
|
+
* effective retry interval from ~3s to ~6s per attempt. Over 10 attempts this
|
|
1345
|
+
* accumulates to 60+ seconds — matching the 1+ minute delays reported by QA.
|
|
1346
|
+
*
|
|
1347
|
+
* Fix: the retry timer is now stopped ONLY when actually connected or when the
|
|
1348
|
+
* session has expired. When `_isConnecting === true`, the timer tick is skipped
|
|
1349
|
+
* but the timer itself keeps running, ensuring consistent retry cadence.
|
|
1350
|
+
*
|
|
1351
|
+
* References:
|
|
1352
|
+
* - WebKit bug 228296: WebSocket does not recover after network change on iOS/macOS
|
|
1353
|
+
* https://bugs.webkit.org/show_bug.cgi?id=228296
|
|
1354
|
+
* - Apple Developer Forums: WebSocket disconnects on network switch (WiFi ↔ Cellular)
|
|
1355
|
+
* https://developer.apple.com/forums/thread/97379
|
|
1356
|
+
* - WebKit source (NetworkProcess): WebSocket connections are bound to the network
|
|
1357
|
+
* interface at creation time and are not migrated when the active interface changes
|
|
1358
|
+
* https://github.com/nicklama/mern-chat-app/issues/1 (community reproduction)
|
|
1359
|
+
* - Apple Technical Note TN3151: Choosing the right networking API — recommends
|
|
1360
|
+
* NWConnection / URLSession for connection migration; WebSocket API does not
|
|
1361
|
+
* participate in iOS Multipath TCP or connection migration
|
|
1362
|
+
* https://developer.apple.com/documentation/technotes/tn3151-choosing-the-right-networking-api
|
|
1363
|
+
*/
|
|
1165
1364
|
_establishConnection: function (params, description) {
|
|
1166
1365
|
var self = this;
|
|
1167
|
-
Utils.QBLog('[QBChat]', '_establishConnection called
|
|
1366
|
+
Utils.QBLog('[QBChat]', '[RECONNECT] _establishConnection called at ' +
|
|
1367
|
+
chatUtils.getLocalTime() + ' in ' + description +
|
|
1368
|
+
' _isLogout=' + self._isLogout +
|
|
1369
|
+
' timerExists=' + Boolean(self._checkConnectionTimer) +
|
|
1370
|
+
' _isConnecting=' + self._isConnecting +
|
|
1371
|
+
' isConnected=' + self.isConnected +
|
|
1372
|
+
' verified=' + self._isConnectionVerified +
|
|
1373
|
+
' pending=' + self._isReconnectListenerPending);
|
|
1168
1374
|
if (typeof self.onLogListener === 'function') {
|
|
1169
1375
|
Utils.safeCallbackCall(self.onLogListener,
|
|
1170
|
-
'[QBChat]
|
|
1376
|
+
'[QBChat] [RECONNECT] _establishConnection called at ' +
|
|
1377
|
+
chatUtils.getLocalTime() + ' in ' + description +
|
|
1378
|
+
' _isLogout=' + self._isLogout +
|
|
1379
|
+
' timerExists=' + Boolean(self._checkConnectionTimer) +
|
|
1380
|
+
' _isConnecting=' + self._isConnecting +
|
|
1381
|
+
' isConnected=' + self.isConnected +
|
|
1382
|
+
' verified=' + self._isConnectionVerified +
|
|
1383
|
+
' pending=' + self._isReconnectListenerPending);
|
|
1171
1384
|
}
|
|
1172
1385
|
if (self._isLogout || self._checkConnectionTimer) {
|
|
1173
|
-
Utils.QBLog('[QBChat]', '_establishConnection
|
|
1386
|
+
Utils.QBLog('[QBChat]', '[RECONNECT] _establishConnection SKIPPED at ' +
|
|
1387
|
+
chatUtils.getLocalTime() + ' — _isLogout=' + self._isLogout +
|
|
1388
|
+
' timerExists=' + Boolean(self._checkConnectionTimer));
|
|
1174
1389
|
if (typeof self.onLogListener === 'function') {
|
|
1175
1390
|
Utils.safeCallbackCall(self.onLogListener,
|
|
1176
|
-
'[QBChat]
|
|
1177
|
-
|
|
1391
|
+
'[QBChat] [RECONNECT] _establishConnection SKIPPED at ' +
|
|
1392
|
+
chatUtils.getLocalTime() + ' — _isLogout=' + self._isLogout +
|
|
1393
|
+
' timerExists=' + Boolean(self._checkConnectionTimer));
|
|
1178
1394
|
}
|
|
1179
1395
|
return;
|
|
1180
1396
|
}
|
|
1181
1397
|
|
|
1182
1398
|
var _connect = function () {
|
|
1183
|
-
Utils.QBLog('[QBChat]', '
|
|
1399
|
+
Utils.QBLog('[QBChat]', '[RECONNECT] _connect() at ' + chatUtils.getLocalTime() +
|
|
1400
|
+
' isConnected=' + self.isConnected +
|
|
1401
|
+
' _isConnecting=' + self._isConnecting +
|
|
1402
|
+
' _sessionHasExpired=' + self._sessionHasExpired +
|
|
1403
|
+
' verified=' + self._isConnectionVerified +
|
|
1404
|
+
' pending=' + self._isReconnectListenerPending);
|
|
1184
1405
|
if (typeof self.onLogListener === 'function') {
|
|
1185
1406
|
Utils.safeCallbackCall(self.onLogListener,
|
|
1186
|
-
'[QBChat]
|
|
1407
|
+
'[QBChat] [RECONNECT] _connect() at ' + chatUtils.getLocalTime() +
|
|
1408
|
+
' isConnected=' + self.isConnected +
|
|
1409
|
+
' _isConnecting=' + self._isConnecting +
|
|
1410
|
+
' _sessionHasExpired=' + self._sessionHasExpired +
|
|
1411
|
+
' verified=' + self._isConnectionVerified +
|
|
1412
|
+
' pending=' + self._isReconnectListenerPending);
|
|
1187
1413
|
}
|
|
1188
1414
|
if (!self.isConnected && !self._isConnecting && !self._sessionHasExpired) {
|
|
1189
|
-
Utils.QBLog('[QBChat]', '
|
|
1415
|
+
Utils.QBLog('[QBChat]', '[RECONNECT] executing connect() at ' + chatUtils.getLocalTime());
|
|
1190
1416
|
if (typeof self.onLogListener === 'function') {
|
|
1191
1417
|
Utils.safeCallbackCall(self.onLogListener,
|
|
1192
|
-
'[QBChat]
|
|
1418
|
+
'[QBChat] [RECONNECT] executing connect() at ' + chatUtils.getLocalTime() +
|
|
1419
|
+
' in ' + description);
|
|
1193
1420
|
}
|
|
1194
1421
|
self.connect(params);
|
|
1422
|
+
} else if (self.isConnected || self._sessionHasExpired) {
|
|
1423
|
+
// Stop retry timer only when actually connected or session expired (no point retrying).
|
|
1424
|
+
// Do NOT stop timer when _isConnecting — the in-flight attempt may fail,
|
|
1425
|
+
// and we need the timer to keep ticking for the next retry.
|
|
1426
|
+
// [QC-1550] Note: timer is stopped on isConnected, BUT verification
|
|
1427
|
+
// (_isConnectionVerified) may still be pending — the first pong has
|
|
1428
|
+
// not yet arrived. The retry loop terminates here; from this point
|
|
1429
|
+
// it is the ping interval that drives verification.
|
|
1430
|
+
Utils.QBLog('[QBChat]', '[RECONNECT] timer stopped at ' + chatUtils.getLocalTime() +
|
|
1431
|
+
' — isConnected=' + self.isConnected +
|
|
1432
|
+
' _sessionHasExpired=' + self._sessionHasExpired +
|
|
1433
|
+
' verified=' + self._isConnectionVerified +
|
|
1434
|
+
' pending=' + self._isReconnectListenerPending);
|
|
1435
|
+
clearInterval(self._checkConnectionTimer);
|
|
1436
|
+
self._checkConnectionTimer = undefined;
|
|
1195
1437
|
if (typeof self.onLogListener === 'function') {
|
|
1196
1438
|
Utils.safeCallbackCall(self.onLogListener,
|
|
1197
|
-
'[QBChat]
|
|
1439
|
+
'[QBChat] [RECONNECT] timer stopped at ' + chatUtils.getLocalTime() +
|
|
1440
|
+
' — isConnected=' + self.isConnected +
|
|
1441
|
+
' _sessionHasExpired=' + self._sessionHasExpired +
|
|
1442
|
+
' verified=' + self._isConnectionVerified +
|
|
1443
|
+
' pending=' + self._isReconnectListenerPending);
|
|
1198
1444
|
}
|
|
1199
1445
|
} else {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1446
|
+
// _isConnecting === true — another attempt is in flight, skip this tick
|
|
1447
|
+
Utils.QBLog('[QBChat]', '[RECONNECT] _connect() SKIPPED at ' + chatUtils.getLocalTime() +
|
|
1448
|
+
' — _isConnecting=true, waiting for next tick');
|
|
1203
1449
|
if (typeof self.onLogListener === 'function') {
|
|
1204
1450
|
Utils.safeCallbackCall(self.onLogListener,
|
|
1205
|
-
'[QBChat]
|
|
1451
|
+
'[QBChat] [RECONNECT] _connect() SKIPPED at ' + chatUtils.getLocalTime() +
|
|
1452
|
+
' — _isConnecting=true, waiting for next tick');
|
|
1206
1453
|
}
|
|
1207
1454
|
}
|
|
1208
1455
|
};
|
|
@@ -1210,10 +1457,12 @@ ChatProxy.prototype = {
|
|
|
1210
1457
|
_connect();
|
|
1211
1458
|
|
|
1212
1459
|
self._checkConnectionTimer = setInterval(function () {
|
|
1213
|
-
Utils.QBLog('[QBChat]', '
|
|
1460
|
+
Utils.QBLog('[QBChat]', '[RECONNECT] timer tick at ' + chatUtils.getLocalTime() +
|
|
1461
|
+
' interval=' + config.chatReconnectionTimeInterval + 's');
|
|
1214
1462
|
if (typeof self.onLogListener === 'function') {
|
|
1215
1463
|
Utils.safeCallbackCall(self.onLogListener,
|
|
1216
|
-
'[QBChat]
|
|
1464
|
+
'[QBChat] [RECONNECT] timer tick at ' + chatUtils.getLocalTime() +
|
|
1465
|
+
' interval=' + config.chatReconnectionTimeInterval + 's');
|
|
1217
1466
|
}
|
|
1218
1467
|
_connect();
|
|
1219
1468
|
}, config.chatReconnectionTimeInterval * 1000);
|
|
@@ -1228,6 +1477,21 @@ ChatProxy.prototype = {
|
|
|
1228
1477
|
}
|
|
1229
1478
|
clearInterval(this._checkConnectionTimer);
|
|
1230
1479
|
this._checkConnectionTimer = undefined;
|
|
1480
|
+
// [QC-1550] Stop ping timer of the previous connection. Without this,
|
|
1481
|
+
// the interval continues to invoke pingchat() on a stale Strophe
|
|
1482
|
+
// connection across the reconnect cycle, racing with the new
|
|
1483
|
+
// connection's freshly-started ping cycle once Status.CONNECTED arrives.
|
|
1484
|
+
if (this._checkConnectionPingTimer !== undefined) {
|
|
1485
|
+
clearInterval(this._checkConnectionPingTimer);
|
|
1486
|
+
this._checkConnectionPingTimer = undefined;
|
|
1487
|
+
}
|
|
1488
|
+
// [QC-1550] Reset XMPP verification state — the new CONNECTED + pong
|
|
1489
|
+
// cycle from _postConnectActions will rebuild it. Without this reset, a
|
|
1490
|
+
// stale _isConnectionVerified=true from the previous session would let
|
|
1491
|
+
// onDisconnectedListener fire on the next ping miss before the new
|
|
1492
|
+
// connection had a chance to verify itself.
|
|
1493
|
+
this._isConnectionVerified = false;
|
|
1494
|
+
this._isReconnectListenerPending = false;
|
|
1231
1495
|
this.muc.joinedRooms = {};
|
|
1232
1496
|
this.helpers.setUserCurrentJid('');
|
|
1233
1497
|
|
|
@@ -1621,6 +1885,13 @@ ChatProxy.prototype = {
|
|
|
1621
1885
|
this._checkConnectionTimer = undefined;
|
|
1622
1886
|
this._checkExpiredSessionTimer = undefined;
|
|
1623
1887
|
this.muc.joinedRooms = {};
|
|
1888
|
+
// [QC-1550] Reset XMPP verification state on explicit disconnect so the
|
|
1889
|
+
// next connect() starts from a clean slate. _isLogout guard below also
|
|
1890
|
+
// prevents firing of deferred listeners if a pong arrives in flight,
|
|
1891
|
+
// but resetting here keeps state consistent for consumers that call
|
|
1892
|
+
// disconnect() + connect() in sequence.
|
|
1893
|
+
this._isConnectionVerified = false;
|
|
1894
|
+
this._isReconnectListenerPending = false;
|
|
1624
1895
|
this._isLogout = true;
|
|
1625
1896
|
this.helpers.setUserCurrentJid('');
|
|
1626
1897
|
|
package/src/qbConfig.js
CHANGED
package/src/qbStrophe.js
CHANGED
|
@@ -74,6 +74,11 @@ function Connection(onLogListenerCallback) {
|
|
|
74
74
|
Utils.QBLog('[QBChat]', 'RECV:', data);
|
|
75
75
|
safeCallbackCall('RECV:', data);
|
|
76
76
|
|
|
77
|
+
// [QC-1454 DIAGNOSTIC] Flag groupchat messages at transport level
|
|
78
|
+
if (typeof data === 'string' && data.indexOf('groupchat') !== -1) {
|
|
79
|
+
Utils.QBLog('[QBChat]', '[TRANSPORT] Groupchat stanza received at WebSocket level');
|
|
80
|
+
}
|
|
81
|
+
|
|
77
82
|
try {
|
|
78
83
|
let parser = new DOMParser();
|
|
79
84
|
let xmlDoc = parser.parseFromString(data, 'text/xml');
|