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.
@@ -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
- if (self.isConnected && typeof self.onDisconnectedListener === 'function') {
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
- if (self.isConnected && typeof self.onDisconnectedListener === 'function') {
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 fire 'onReconnectListener' (if the reconnect);
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 + " rooms...");
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
- self.muc.join(rooms[i]);
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
- if (typeof self.onReconnectListener === 'function') {
1160
- Utils.safeCallbackCall(self.onReconnectListener);
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 in ' + description);
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]' + '_establishConnection called in ' + description);
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 return');
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]' + 'BREAK _establishConnection RETURN with self._isLogout: '+
1177
- self._isLogout?self._isLogout:'undefined'+' and self._checkConnectionTimer ' +self._checkConnectionTimer?self._checkConnectionTimer:'undefined');
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]', 'call _connect() in _establishConnection in '+description);
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]' + ' call _connect() in _establishConnection in '+description);
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]', ' start EXECUTE connect() in _establishConnection ');
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]' + ' start EXECUTE connect() in _establishConnection in '+description+' self.isConnected: '+self.isConnected+' self._isConnecting: '+self._isConnecting+' self._sessionHasExpired: '+self._sessionHasExpired);
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]' + 'call _connect() in _establishConnection in '+description+' is executed');
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
- Utils.QBLog('[QBChat]', 'stop timer in _establishConnection ');
1201
- clearInterval(self._checkConnectionTimer);
1202
- self._checkConnectionTimer = undefined;
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]' + 'stop timer in _establishConnection in '+description);
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]', 'self._checkConnectionTimer called with config.chatReconnectionTimeInterval = ' + config.chatReconnectionTimeInterval);
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]' + 'self._checkConnectionTimer called with config.chatReconnectionTimeInterval = ' + config.chatReconnectionTimeInterval);
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
@@ -12,8 +12,8 @@
12
12
  */
13
13
 
14
14
  var config = {
15
- version: '2.23.0',
16
- buildNumber: '1178',
15
+ version: '2.23.1-beta.3',
16
+ buildNumber: '1179',
17
17
  creds: {
18
18
  'appId': 0,
19
19
  'authKey': '',
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');