@crysnovax/baileys 2.5.0 → 2.5.2

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.
@@ -1,24 +1,31 @@
1
1
  import NodeCache from '@cacheable/node-cache';
2
2
  import { Boom } from '@hapi/boom';
3
3
  import { proto } from '../../WAProto/index.js';
4
- import { DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js';
4
+ import { DEFAULT_CACHE_TTLS, HISTORY_SYNC_PAUSED_TIMEOUT_MS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js';
5
5
  import { ALL_WA_PATCH_NAMES } from '../Types/index.js';
6
6
  import { SyncState } from '../Types/State.js';
7
- import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, extractSyncdPatches, generateProfilePicture, getHistoryMsg, newLTHashState, processSyncAction } from '../Utils/index.js';
7
+ import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, ensureLTHashStateVersion, extractSyncdPatches, generateProfilePicture, getHistoryMsg, isAppStateSyncIrrecoverable, isMissingKeyError, MAX_SYNC_ATTEMPTS, newLTHashState, processSyncAction } from '../Utils/index.js';
8
8
  import { makeMutex } from '../Utils/make-mutex.js';
9
9
  import processMessage from '../Utils/process-message.js';
10
10
  import { buildTcTokenFromJid } from '../Utils/tc-token-utils.js';
11
- import { getBinaryNodeChild, getBinaryNodeChildren, isLidUser, isPnUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
11
+ import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
12
12
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
13
13
  import { makeSocket } from './socket.js';
14
- const MAX_SYNC_ATTEMPTS = 2;
15
- // Lia@Note 08-02-26 --- I know it's not efficient for RSS ಥ⁠‿⁠ಥ
16
- const USER_ID_CACHE = new Map();
17
14
  export const makeChatsSocket = (config) => {
18
15
  const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config;
19
16
  const sock = makeSocket(config);
20
17
  const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession, registerSocketEndHandler } = sock;
18
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
21
19
  let privacySettings;
20
+ /** Server-assigned AB props for protocol behavior. */
21
+ const serverProps = {
22
+ /** AB prop 10518: gate tctoken on 1:1 messages. Default true (safe: avoids 463). */
23
+ privacyTokenOn1to1: true,
24
+ /** AB prop 9666: gate tctoken on profile picture IQs. WA Web default: true. */
25
+ profilePicPrivacyToken: true,
26
+ /** AB prop 14303: issue tctokens to LID instead of PN. WA Web default: false. */
27
+ lidTrustedTokenIssueToLid: false
28
+ };
22
29
  let syncState = SyncState.Connecting;
23
30
  /** this mutex ensures that messages are processed in order */
24
31
  const messageMutex = makeMutex();
@@ -30,14 +37,20 @@ export const makeChatsSocket = (config) => {
30
37
  const notificationMutex = makeMutex();
31
38
  // Timeout for AwaitingInitialSync state
32
39
  let awaitingSyncTimeout;
40
+ // In-memory history sync completion tracking (resets on reconnection)
41
+ const historySyncStatus = {
42
+ initialBootstrapComplete: false,
43
+ recentSyncComplete: false
44
+ };
45
+ let historySyncPausedTimeout;
46
+ // Collections blocked on missing app state sync keys (mirrors WA Web's "Blocked" state).
47
+ // When a key arrives via APP_STATE_SYNC_KEY_SHARE, these are re-synced.
48
+ const blockedCollections = new Set();
33
49
  const placeholderResendCache = config.placeholderResendCache ||
34
50
  new NodeCache({
35
51
  stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
36
52
  useClones: false
37
53
  });
38
- // if (!config.placeholderResendCache) {
39
- // config.placeholderResendCache = placeholderResendCache;
40
- // }
41
54
  /** helper function to fetch the given app state sync key */
42
55
  const getAppStateSyncKey = async (keyId) => {
43
56
  const { [keyId]: key } = await authState.keys.get('app-state-sync-key', [keyId]);
@@ -174,37 +187,24 @@ export const makeChatsSocket = (config) => {
174
187
  return result.list;
175
188
  }
176
189
  };
177
- // Lia@Note 06-02-26 --- Quick helper only. This function exists solely for faster lookup with caching.
190
+ // crysnovax@Note 06-02-26 --- Quick helper only. This function exists solely for faster lookup with caching.
178
191
  const findUserId = async (pnLid) => {
179
- const cachedId = USER_ID_CACHE.get(pnLid);
180
- if (cachedId) {
181
- return cachedId;
182
- }
183
- const userId = {};
184
- if (isPnUser(pnLid)) {
185
- userId.phoneNumber = pnLid;
186
- userId.lid = (await signalRepository.lidMapping.getLIDsForPNs([pnLid]))?.[0]?.lid;
187
- if (!userId.lid) {
188
- userId.lid = 'id-not-found';
189
- return userId; // Lia@Note 06-02-26 --- Early return to skip caching for non-existent ID
190
- }
192
+ const normalizedJid = jidNormalizedUser(pnLid);
193
+ const userId = {
194
+ lid: undefined,
195
+ phoneNumber: undefined
196
+ };
197
+ if (isPnUser(normalizedJid) || isHostedPnUser(normalizedJid)) {
198
+ userId.phoneNumber = normalizedJid;
199
+ userId.lid = jidNormalizedUser((await signalRepository.lidMapping.getLIDsForPNs([normalizedJid]))?.[0]?.lid);
191
200
  }
192
- else if (isLidUser(pnLid)) {
193
- userId.lid = pnLid;
194
- userId.phoneNumber = (await signalRepository.lidMapping.getPNsForLIDs([pnLid]))?.[0]?.pn;
195
- if (!userId.phoneNumber) {
196
- userId.phoneNumber = 'id-not-found';
197
- return userId; // Lia@Note 06-02-26 --- Early return to skip caching for non-existent ID
198
- }
201
+ else if (isLidUser(normalizedJid) || isHostedLidUser(normalizedJid)) {
202
+ userId.lid = normalizedJid;
203
+ userId.phoneNumber = jidNormalizedUser((await signalRepository.lidMapping.getPNsForLIDs([normalizedJid]))?.[0]?.pn);
199
204
  }
200
205
  else {
201
206
  throw new Boom('Invalid id input to find user ids', { statusCode: 400 });
202
207
  }
203
- userId.phoneNumber = jidNormalizedUser(userId.phoneNumber);
204
- userId.lid = jidNormalizedUser(userId.lid);
205
- // Lia@Note 06-02-26 --- I know... it's dirty (⁠╯⁠︵⁠╰⁠,⁠)
206
- USER_ID_CACHE.set(userId.phoneNumber, userId);
207
- USER_ID_CACHE.set(userId.lid, userId);
208
208
  return userId;
209
209
  };
210
210
  /** update the profile picture for yourself or a group */
@@ -293,6 +293,42 @@ export const makeChatsSocket = (config) => {
293
293
  return getBinaryNodeChildren(listNode, 'item').map(n => n.attrs.jid);
294
294
  };
295
295
  const updateBlockStatus = async (jid, action) => {
296
+ const normalizedJid = jidNormalizedUser(jid);
297
+ let lid;
298
+ let pn_jid;
299
+ if (isLidUser(normalizedJid) || isHostedLidUser(normalizedJid)) {
300
+ lid = normalizedJid;
301
+ if (action === 'block') {
302
+ const pn = (await findUserId(normalizedJid)).phoneNumber;
303
+ if (!pn) {
304
+ throw new Boom(`Unable to resolve PN JID for LID: ${jid}`, { statusCode: 400 });
305
+ }
306
+ pn_jid = jidNormalizedUser(pn);
307
+ }
308
+ }
309
+ else if (isPnUser(normalizedJid) || isHostedPnUser(normalizedJid)) {
310
+ const mapped = (await findUserId(normalizedJid)).lid;
311
+ if (!mapped) {
312
+ throw new Boom(`Unable to resolve LID for PN JID: ${jid}`, { statusCode: 400 });
313
+ }
314
+ lid = mapped;
315
+ if (action === 'block') {
316
+ pn_jid = jidNormalizedUser(normalizedJid);
317
+ }
318
+ }
319
+ else {
320
+ throw new Boom(`Invalid jid: ${jid}`, { statusCode: 400 });
321
+ }
322
+ const itemAttrs = {
323
+ action,
324
+ jid: lid
325
+ };
326
+ if (action === 'block') {
327
+ if (!pn_jid) {
328
+ throw new Boom(`pn_jid required for block: ${jid}`, { statusCode: 400 });
329
+ }
330
+ itemAttrs.pn_jid = pn_jid;
331
+ }
296
332
  await query({
297
333
  tag: 'iq',
298
334
  attrs: {
@@ -303,10 +339,7 @@ export const makeChatsSocket = (config) => {
303
339
  content: [
304
340
  {
305
341
  tag: 'item',
306
- attrs: {
307
- action,
308
- jid
309
- }
342
+ attrs: itemAttrs
310
343
  }
311
344
  ]
312
345
  });
@@ -405,6 +438,9 @@ export const makeChatsSocket = (config) => {
405
438
  const collectionsToHandle = new Set(collections);
406
439
  // in case something goes wrong -- ensure we don't enter a loop that cannot be exited from
407
440
  const attemptsMap = {};
441
+ // collections that failed and need a full snapshot on retry
442
+ // mirrors WA Web's ErrorFatal -> force snapshot behavior
443
+ const forceSnapshotCollections = new Set();
408
444
  // keep executing till all collections are done
409
445
  // sometimes a single patch request will not return all the patches (God knows why)
410
446
  // so we fetch till they're all done (this is determined by the "has_more_patches" flag)
@@ -415,6 +451,7 @@ export const makeChatsSocket = (config) => {
415
451
  const result = await authState.keys.get('app-state-sync-version', [name]);
416
452
  let state = result[name];
417
453
  if (state) {
454
+ state = ensureLTHashStateVersion(state);
418
455
  if (typeof initialVersionMap[name] === 'undefined') {
419
456
  initialVersionMap[name] = state.version;
420
457
  }
@@ -423,14 +460,18 @@ export const makeChatsSocket = (config) => {
423
460
  state = newLTHashState();
424
461
  }
425
462
  states[name] = state;
426
- logger.info(`resyncing ${name} from v${state.version}`);
463
+ const shouldForceSnapshot = forceSnapshotCollections.has(name);
464
+ if (shouldForceSnapshot) {
465
+ forceSnapshotCollections.delete(name);
466
+ }
467
+ logger.info(`resyncing ${name} from v${state.version}${shouldForceSnapshot ? ' (forcing snapshot)' : ''}`);
427
468
  nodes.push({
428
469
  tag: 'collection',
429
470
  attrs: {
430
471
  name,
431
472
  version: state.version.toString(),
432
- // return snapshot if being synced from scratch
433
- return_snapshot: (!state.version).toString()
473
+ // return snapshot if syncing from scratch or forcing after a failed attempt
474
+ return_snapshot: (shouldForceSnapshot || !state.version).toString()
434
475
  }
435
476
  });
436
477
  }
@@ -456,7 +497,7 @@ export const makeChatsSocket = (config) => {
456
497
  const { patches, hasMorePatches, snapshot } = decoded[name];
457
498
  try {
458
499
  if (snapshot) {
459
- const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getCachedAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot);
500
+ const { state: newState, mutationMap } = await decodeSyncdSnapshot(name, snapshot, getCachedAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot, logger);
460
501
  states[name] = newState;
461
502
  Object.assign(globalMutationMap, mutationMap);
462
503
  logger.info(`restored state of ${name} from snapshot to v${newState.version} with mutations`);
@@ -479,19 +520,37 @@ export const makeChatsSocket = (config) => {
479
520
  }
480
521
  }
481
522
  catch (error) {
482
- // if retry attempts overshoot
483
- // or key not found
484
- const isIrrecoverableError = attemptsMap[name] >= MAX_SYNC_ATTEMPTS ||
485
- error.output?.statusCode === 404 ||
486
- error.name === 'TypeError';
487
- logger.info({ name, error: error.stack }, `failed to sync state from version${isIrrecoverableError ? '' : ', removing and trying from scratch'}`);
488
- await authState.keys.set({ 'app-state-sync-version': { [name]: null } });
489
- // increment number of retries
490
523
  attemptsMap[name] = (attemptsMap[name] || 0) + 1;
491
- if (isIrrecoverableError) {
492
- // stop retrying
524
+ const logData = {
525
+ name,
526
+ attempt: attemptsMap[name],
527
+ version: states[name].version,
528
+ statusCode: error.output?.statusCode,
529
+ errorType: error.name,
530
+ error: error.stack
531
+ };
532
+ if (isMissingKeyError(error) && attemptsMap[name] >= MAX_SYNC_ATTEMPTS) {
533
+ // WA Web treats missing keys as "Blocked" — park the collection
534
+ // until the key arrives via APP_STATE_SYNC_KEY_SHARE.
535
+ logger.warn(logData, `${name} blocked on missing key from v${states[name].version}, parking after ${attemptsMap[name]} attempts`);
536
+ blockedCollections.add(name);
493
537
  collectionsToHandle.delete(name);
494
538
  }
539
+ else if (isMissingKeyError(error)) {
540
+ // Retry with a snapshot which may use a different key.
541
+ logger.info(logData, `${name} blocked on missing key from v${states[name].version}, retrying with snapshot`);
542
+ forceSnapshotCollections.add(name);
543
+ }
544
+ else if (isAppStateSyncIrrecoverable(error, attemptsMap[name])) {
545
+ logger.warn(logData, `failed to sync ${name} from v${states[name].version}, giving up`);
546
+ collectionsToHandle.delete(name);
547
+ }
548
+ else {
549
+ logger.info(logData, `failed to sync ${name} from v${states[name].version}, forcing snapshot retry`);
550
+ // force a full snapshot on retry to recover from
551
+ // corrupted local state (e.g. LTHash MAC mismatch)
552
+ forceSnapshotCollections.add(name);
553
+ }
495
554
  }
496
555
  }
497
556
  }
@@ -506,25 +565,25 @@ export const makeChatsSocket = (config) => {
506
565
  * type = "preview" for a low res picture
507
566
  * type = "image for the high res picture"
508
567
  */
509
- const profilePictureUrl = async (jid, type = 'image', timeoutMs) => {
510
- // Lia@Changes 06-02-26 --- Refactor profilePictureUrl() to use tctoken and adjust error handling
511
- jid = jidNormalizedUser(jid);
512
- const baseContent = {
513
- tag: 'picture',
514
- attrs: {
515
- type,
516
- query: 'url'
517
- }
518
- };
519
- const tcTokenData = await authState.keys.get('tctoken', [jid]);
520
- const tcTokenBuffer = tcTokenData?.[jid]?.token
521
- if (tcTokenBuffer) {
522
- baseContent.content = [{
523
- tag: 'tctoken',
524
- attrs: {},
525
- content: tcTokenBuffer
526
- }];
568
+ const profilePictureUrl = async (jid, type = 'image', timeoutMs = 5000, shouldIncludeTcToken = false) => {
569
+ const baseContent = [{ tag: 'picture', attrs: { type, query: 'url' } }];
570
+ // WA Web only includes tctoken for user JIDs (not groups/newsletters)
571
+ // and never for own profile pic (Chat model for self has no tcToken).
572
+ // Including tctoken for own JID causes the server to never respond.
573
+ const normalizedJid = jidNormalizedUser(jid);
574
+ const isUserJid = isPnUser(normalizedJid) || isLidUser(normalizedJid);
575
+ const me = authState.creds.me;
576
+ const isSelf = me && (normalizedJid === jidNormalizedUser(me.id) || (me.lid && normalizedJid === jidNormalizedUser(me.lid)));
577
+ let content = baseContent;
578
+ if (shouldIncludeTcToken && serverProps.profilePicPrivacyToken && isUserJid && !isSelf) {
579
+ content = await buildTcTokenFromJid({
580
+ authState,
581
+ jid: normalizedJid,
582
+ baseContent,
583
+ getLIDForPN
584
+ });
527
585
  }
586
+ jid = jidNormalizedUser(jid);
528
587
  const result = await query({
529
588
  tag: 'iq',
530
589
  attrs: {
@@ -533,16 +592,9 @@ export const makeChatsSocket = (config) => {
533
592
  type: 'get',
534
593
  xmlns: 'w:profile:picture'
535
594
  },
536
- content: [baseContent]
595
+ content
537
596
  }, timeoutMs);
538
597
  const child = getBinaryNodeChild(result, 'picture');
539
- if (!child) {
540
- throw new Boom('Picture node missing', { statusCode: 404 });
541
- }
542
- const status = child.attrs?.status;
543
- if (status === '404' || status === '204') {
544
- throw new Boom('Profile picture not set', { statusCode: 404 });
545
- }
546
598
  return child?.attrs?.url;
547
599
  };
548
600
  const createCallLink = async (type, event, timeoutMs) => {
@@ -606,7 +658,12 @@ export const makeChatsSocket = (config) => {
606
658
  * @param tcToken token for subscription, use if present
607
659
  */
608
660
  const presenceSubscribe = async (toJid) => {
609
- const tcTokenContent = await buildTcTokenFromJid({ authState, jid: toJid });
661
+ // Only include tctoken for user JIDs groups/newsletters don't use tctokens
662
+ const normalizedToJid = jidNormalizedUser(toJid);
663
+ const isUserJid = isPnUser(normalizedToJid) || isLidUser(normalizedToJid);
664
+ const tcTokenContent = isUserJid
665
+ ? await buildTcTokenFromJid({ authState, jid: normalizedToJid, getLIDForPN })
666
+ : undefined;
610
667
  return sendNode({
611
668
  tag: 'presence',
612
669
  attrs: {
@@ -627,7 +684,8 @@ export const makeChatsSocket = (config) => {
627
684
  if (tag === 'presence') {
628
685
  presence = {
629
686
  lastKnownPresence: attrs.type === 'unavailable' ? 'unavailable' : 'available',
630
- lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined
687
+ lastSeen: attrs.last && attrs.last !== 'deny' ? +attrs.last : undefined,
688
+ groupOnlineCount: attrs.count ? +attrs.count : undefined
631
689
  };
632
690
  }
633
691
  else if (Array.isArray(content)) {
@@ -661,7 +719,7 @@ export const makeChatsSocket = (config) => {
661
719
  logger.debug({ patch: patchCreate }, 'applying app patch');
662
720
  await resyncAppState([name], false);
663
721
  const { [name]: currentSyncVersion } = await authState.keys.get('app-state-sync-version', [name]);
664
- initial = currentSyncVersion || newLTHashState();
722
+ initial = currentSyncVersion ? ensureLTHashStateVersion(currentSyncVersion) : newLTHashState();
665
723
  encodeResult = await encodeSyncdPatch(patchCreate, myAppStateKeyId, initial, getAppStateSyncKey);
666
724
  const { patch, state } = encodeResult;
667
725
  const node = {
@@ -707,22 +765,21 @@ export const makeChatsSocket = (config) => {
707
765
  }
708
766
  }
709
767
  };
710
- /** sending non-abt props may fix QR scan fail if server expects */
768
+ /** fetch AB props */
711
769
  const fetchProps = async () => {
712
- //TODO: implement both protocol 1 and protocol 2 prop fetching, specially for abKey for WM
713
770
  const resultNode = await query({
714
771
  tag: 'iq',
715
772
  attrs: {
716
773
  to: S_WHATSAPP_NET,
717
- xmlns: 'w',
774
+ xmlns: 'abt',
718
775
  type: 'get'
719
776
  },
720
777
  content: [
721
778
  {
722
779
  tag: 'props',
723
780
  attrs: {
724
- protocol: '2',
725
- hash: authState?.creds?.lastPropHash || ''
781
+ protocol: '1',
782
+ ...(authState?.creds?.lastPropHash ? { hash: authState.creds.lastPropHash } : {})
726
783
  }
727
784
  }
728
785
  ]
@@ -737,7 +794,20 @@ export const makeChatsSocket = (config) => {
737
794
  }
738
795
  props = reduceBinaryNodeToDictionary(propsNode, 'prop');
739
796
  }
740
- logger.debug('fetched props');
797
+ // Extract protocol-relevant AB props (only the ones we need)
798
+ const privacyTokenProp = props['10518'] ?? props['privacy_token_sending_on_all_1_on_1_messages'];
799
+ if (privacyTokenProp !== undefined) {
800
+ serverProps.privacyTokenOn1to1 = privacyTokenProp === 'true' || privacyTokenProp === '1';
801
+ }
802
+ const profilePicProp = props['9666'] ?? props['profile_scraping_privacy_token_in_photo_iq'];
803
+ if (profilePicProp !== undefined) {
804
+ serverProps.profilePicPrivacyToken = profilePicProp === 'true' || profilePicProp === '1';
805
+ }
806
+ const lidIssueProp = props['14303'] ?? props['lid_trusted_token_issue_to_lid'];
807
+ if (lidIssueProp !== undefined) {
808
+ serverProps.lidTrustedTokenIssueToLid = lidIssueProp === 'true' || lidIssueProp === '1';
809
+ }
810
+ logger.debug({ serverProps }, 'fetched props');
741
811
  return props;
742
812
  };
743
813
  /**
@@ -877,6 +947,47 @@ export const makeChatsSocket = (config) => {
877
947
  ? shouldSyncHistoryMessage(historyMsg) &&
878
948
  PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
879
949
  : false;
950
+ if (historyMsg && shouldProcessHistoryMsg) {
951
+ const syncType = historyMsg.syncType;
952
+ // INITIAL_BOOTSTRAP — fire immediately, no progress check (same as WA Web K function)
953
+ if (syncType === proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP &&
954
+ !historySyncStatus.initialBootstrapComplete) {
955
+ historySyncStatus.initialBootstrapComplete = true;
956
+ ev.emit('messaging-history.status', {
957
+ syncType,
958
+ status: 'complete',
959
+ explicit: true
960
+ });
961
+ }
962
+ // RECENT with progress === 100 — explicit completion
963
+ if (syncType === proto.HistorySync.HistorySyncType.RECENT &&
964
+ historyMsg.progress === 100 &&
965
+ !historySyncStatus.recentSyncComplete) {
966
+ historySyncStatus.recentSyncComplete = true;
967
+ clearTimeout(historySyncPausedTimeout);
968
+ historySyncPausedTimeout = undefined;
969
+ ev.emit('messaging-history.status', {
970
+ syncType,
971
+ status: 'complete',
972
+ explicit: true
973
+ });
974
+ }
975
+ // Reset 120s paused timeout on any RECENT chunk (like WA Web's handleChunkProgress)
976
+ if (syncType === proto.HistorySync.HistorySyncType.RECENT && !historySyncStatus.recentSyncComplete) {
977
+ clearTimeout(historySyncPausedTimeout);
978
+ historySyncPausedTimeout = setTimeout(() => {
979
+ if (!historySyncStatus.recentSyncComplete) {
980
+ historySyncStatus.recentSyncComplete = true;
981
+ ev.emit('messaging-history.status', {
982
+ syncType: proto.HistorySync.HistorySyncType.RECENT,
983
+ status: 'paused',
984
+ explicit: false
985
+ });
986
+ }
987
+ historySyncPausedTimeout = undefined;
988
+ }, HISTORY_SYNC_PAUSED_TIMEOUT_MS);
989
+ }
990
+ }
880
991
  // State machine: decide on sync and flush
881
992
  if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
882
993
  if (awaitingSyncTimeout) {
@@ -896,6 +1007,8 @@ export const makeChatsSocket = (config) => {
896
1007
  }
897
1008
  const doAppStateSync = async () => {
898
1009
  if (syncState === SyncState.Syncing) {
1010
+ // All collections will be synced, so clear any blocked ones
1011
+ blockedCollections.clear();
899
1012
  logger.info('Doing app state sync');
900
1013
  await resyncAppState(ALL_WA_PATCH_NAMES, true);
901
1014
  // Sync is complete, go online and flush everything
@@ -955,6 +1068,11 @@ export const makeChatsSocket = (config) => {
955
1068
  }
956
1069
  });
957
1070
  ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
1071
+ if (connection === 'close') {
1072
+ blockedCollections.clear();
1073
+ clearTimeout(historySyncPausedTimeout);
1074
+ historySyncPausedTimeout = undefined;
1075
+ }
958
1076
  if (connection === 'open') {
959
1077
  if (fireInitQueries) {
960
1078
  executeInitQueries().catch(error => onUnexpectedError(error, 'init queries'));
@@ -964,6 +1082,10 @@ export const makeChatsSocket = (config) => {
964
1082
  if (!receivedPendingNotifications || syncState !== SyncState.Connecting) {
965
1083
  return;
966
1084
  }
1085
+ historySyncStatus.initialBootstrapComplete = false;
1086
+ historySyncStatus.recentSyncComplete = false;
1087
+ clearTimeout(historySyncPausedTimeout);
1088
+ historySyncPausedTimeout = undefined;
967
1089
  syncState = SyncState.AwaitingInitialSync;
968
1090
  logger.info('Connection is now AwaitingInitialSync, buffering events');
969
1091
  ev.buffer();
@@ -976,19 +1098,49 @@ export const makeChatsSocket = (config) => {
976
1098
  setTimeout(() => ev.flush(), 0);
977
1099
  return;
978
1100
  }
979
- logger.info('History sync is enabled, awaiting notification with a 20s timeout.');
1101
+ // On reconnection (accountSyncCounter > 0), the server does not push
1102
+ // history sync notifications — the device already has its data.
1103
+ // Skip the 20s wait and go online immediately.
1104
+ if (authState.creds.accountSyncCounter > 0) {
1105
+ logger.info('Reconnection with existing sync data, skipping history sync wait. Transitioning to Online.');
1106
+ syncState = SyncState.Online;
1107
+ setTimeout(() => ev.flush(), 0);
1108
+ return;
1109
+ }
1110
+ logger.info('First connection, awaiting history sync notification with a 20s timeout.');
980
1111
  if (awaitingSyncTimeout) {
981
1112
  clearTimeout(awaitingSyncTimeout);
982
1113
  }
983
1114
  awaitingSyncTimeout = setTimeout(() => {
984
1115
  if (syncState === SyncState.AwaitingInitialSync) {
985
- // TODO: investigate
986
1116
  logger.warn('Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer');
987
1117
  syncState = SyncState.Online;
988
1118
  ev.flush();
1119
+ // Increment so subsequent reconnections skip the 20s wait.
1120
+ // Late-arriving history is still processed via processMessage
1121
+ // regardless of the state machine phase.
1122
+ const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1;
1123
+ ev.emit('creds.update', { accountSyncCounter });
989
1124
  }
990
1125
  }, 20000);
991
1126
  });
1127
+ // When an app state sync key arrives (myAppStateKeyId is set) and there are
1128
+ // collections blocked on a missing key, trigger a re-sync for just those collections.
1129
+ // This mirrors WA Web's Blocked → retry-on-key-arrival behavior.
1130
+ ev.on('creds.update', ({ myAppStateKeyId }) => {
1131
+ if (!myAppStateKeyId || blockedCollections.size === 0) {
1132
+ return;
1133
+ }
1134
+ // If we're in the middle of a full sync, doAppStateSync handles all collections
1135
+ if (syncState === SyncState.Syncing) {
1136
+ blockedCollections.clear();
1137
+ return;
1138
+ }
1139
+ const collections = [...blockedCollections];
1140
+ blockedCollections.clear();
1141
+ logger.info({ collections }, 'app state sync key arrived, re-syncing blocked collections');
1142
+ resyncAppState(collections, false).catch(error => onUnexpectedError(error, 'blocked collections resync'));
1143
+ });
992
1144
  ev.on('lid-mapping.update', async ({ lid, pn }) => {
993
1145
  try {
994
1146
  await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }]);
@@ -1010,6 +1162,8 @@ export const makeChatsSocket = (config) => {
1010
1162
  });
1011
1163
  return {
1012
1164
  ...sock,
1165
+ findUserId,
1166
+ serverProps,
1013
1167
  createCallLink,
1014
1168
  getBotListV2,
1015
1169
  messageMutex,
@@ -1025,7 +1179,6 @@ export const makeChatsSocket = (config) => {
1025
1179
  fetchBlocklist,
1026
1180
  fetchStatus,
1027
1181
  fetchDisappearingDuration,
1028
- findUserId,
1029
1182
  updateProfilePicture,
1030
1183
  removeProfilePicture,
1031
1184
  updateProfileStatus,
@@ -1047,6 +1200,7 @@ export const makeChatsSocket = (config) => {
1047
1200
  cleanDirtyBits,
1048
1201
  addOrEditContact,
1049
1202
  removeContact,
1203
+ placeholderResendCache,
1050
1204
  addLabel,
1051
1205
  addChatLabel,
1052
1206
  removeChatLabel,
@@ -1056,4 +1210,5 @@ export const makeChatsSocket = (config) => {
1056
1210
  addOrEditQuickReply,
1057
1211
  removeQuickReply
1058
1212
  };
1059
- };
1213
+ };
1214
+ //# sourceMappingURL=chats.js.map