violetics 7.0.11-alpha → 7.0.12-alpha

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.
@@ -47,13 +47,13 @@ export const DEFAULT_CONNECTION_CONFIG = {
47
47
  version: version,
48
48
  browser: Browsers.macOS('Chrome'),
49
49
  waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat',
50
- connectTimeoutMs: 60000,
51
- keepAliveIntervalMs: 30000,
50
+ connectTimeoutMs: 45000, // 45 seconds - faster than 60s
51
+ keepAliveIntervalMs: 20000, // 20 seconds - faster disconnect detection
52
52
  logger: logger.child({ class: 'baileys' }),
53
53
  emitOwnEvents: true,
54
- defaultQueryTimeoutMs: 90000,
54
+ defaultQueryTimeoutMs: 60000, // 60 seconds - reduced from 90s
55
55
  customUploadHosts: [],
56
- retryRequestDelayMs: 250,
56
+ retryRequestDelayMs: 200, // Faster retry (was 250ms)
57
57
  maxMsgRetryCount: 5,
58
58
  fireInitQueries: true,
59
59
  auth: undefined,
@@ -77,7 +77,21 @@ export const DEFAULT_CONNECTION_CONFIG = {
77
77
  countryCode: 'US',
78
78
  getMessage: async () => undefined,
79
79
  cachedGroupMetadata: async () => undefined,
80
- makeSignalRepository: makeLibSignalRepository
80
+ makeSignalRepository: makeLibSignalRepository,
81
+
82
+ // New connection stability options
83
+ maxReconnectAttempts: 5,
84
+ reconnectDelay: 2000,
85
+ maxReconnectDelay: 30000,
86
+
87
+ // Memory management options
88
+ maxMessagesPerChat: 2000,
89
+ maxChats: 10000,
90
+ maxContacts: 20000,
91
+ messageCleanupIntervalMs: 5 * 60 * 1000, // 5 minutes
92
+ maxHistoryCacheSize: 15000,
93
+ bufferTimeoutMs: 30000,
94
+ maxBufferCount: 10000
81
95
  };
82
96
  export const MEDIA_PATH_MAP = {
83
97
  image: '/mms/image',
@@ -5,6 +5,11 @@ export class WebSocketClient extends AbstractSocketClient {
5
5
  constructor() {
6
6
  super(...arguments);
7
7
  this.socket = null;
8
+ this.reconnectAttempts = 0;
9
+ this.maxReconnectAttempts = this.config.maxReconnectAttempts || 5;
10
+ this.reconnectDelay = this.config.reconnectDelay || 1000;
11
+ this.maxReconnectDelay = this.config.maxReconnectDelay || 30000;
12
+ this.reconnecting = false;
8
13
  }
9
14
  get isOpen() {
10
15
  return this.socket?.readyState === WebSocket.OPEN;
@@ -19,9 +24,10 @@ export class WebSocketClient extends AbstractSocketClient {
19
24
  return this.socket?.readyState === WebSocket.CONNECTING;
20
25
  }
21
26
  connect() {
22
- if (this.socket) {
27
+ if (this.socket && !this.isClosed) {
23
28
  return;
24
29
  }
30
+ this.reconnecting = false;
25
31
  this.socket = new WebSocket(this.url, {
26
32
  origin: DEFAULT_ORIGIN,
27
33
  headers: this.config.options?.headers,
@@ -34,21 +40,72 @@ export class WebSocketClient extends AbstractSocketClient {
34
40
  for (const event of events) {
35
41
  this.socket?.on(event, (...args) => this.emit(event, ...args));
36
42
  }
43
+
44
+ // Handle close to attempt reconnect
45
+ this.socket.on('close', (code, reason) => {
46
+ this.emit('close', code, reason);
47
+ this.attemptReconnect(code, reason);
48
+ });
49
+ }
50
+
51
+ async attemptReconnect(code, reason) {
52
+ // Don't reconnect if intentionally closed or max attempts reached
53
+ if (this.reconnecting || code === 1000 || this.reconnectAttempts >= this.maxReconnectAttempts) {
54
+ return;
55
+ }
56
+
57
+ this.reconnecting = true;
58
+ const delay = Math.min(
59
+ this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
60
+ this.maxReconnectDelay
61
+ );
62
+
63
+ this.emit('reconnecting', {
64
+ attempt: this.reconnectAttempts + 1,
65
+ maxAttempts: this.maxReconnectAttempts,
66
+ delay,
67
+ code,
68
+ reason: reason?.toString() || 'Connection closed'
69
+ });
70
+
71
+ await new Promise(resolve => setTimeout(resolve, delay));
72
+
73
+ this.reconnectAttempts++;
74
+ this.reconnecting = false;
75
+
76
+ // Reconnect
77
+ this.connect();
37
78
  }
79
+
38
80
  async close() {
81
+ this.reconnecting = false;
82
+ this.reconnectAttempts = 0;
83
+
39
84
  if (!this.socket) {
40
85
  return;
41
86
  }
42
87
  const closePromise = new Promise(resolve => {
43
88
  this.socket?.once('close', resolve);
44
89
  });
45
- this.socket.close();
90
+ this.socket.close(1000, 'Intentional close');
46
91
  await closePromise;
47
92
  this.socket = null;
48
93
  }
94
+
49
95
  send(str, cb) {
50
96
  this.socket?.send(str, cb);
51
97
  return Boolean(this.socket);
52
98
  }
99
+
100
+ get connectionStats() {
101
+ return {
102
+ isOpen: this.isOpen,
103
+ isClosed: this.isClosed,
104
+ isConnecting: this.isConnecting,
105
+ reconnectAttempts: this.reconnectAttempts,
106
+ maxReconnectAttempts: this.maxReconnectAttempts,
107
+ isReconnecting: this.reconnecting
108
+ };
109
+ }
53
110
  }
54
111
  //# sourceMappingURL=websocket.js.map
@@ -20,20 +20,30 @@ export const makeMessagesRecvSocket = (config) => {
20
20
  const msgRetryCache = config.msgRetryCounterCache ||
21
21
  new NodeCache({
22
22
  stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
23
- useClones: false
23
+ useClones: false,
24
+ checkperiod: 300, // Check every 5 min
25
+ maxKeys: 10000 // Limit to prevent memory bloat
24
26
  });
25
27
  const callOfferCache = config.callOfferCache ||
26
28
  new NodeCache({
27
29
  stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
28
- useClones: false
30
+ useClones: false,
31
+ checkperiod: 60,
32
+ maxKeys: 5000
29
33
  });
30
34
  const placeholderResendCache = config.placeholderResendCache ||
31
35
  new NodeCache({
32
36
  stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
33
- useClones: false
37
+ useClones: false,
38
+ checkperiod: 300,
39
+ maxKeys: 5000
34
40
  });
35
41
  // Debounce identity-change session refreshes per JID to avoid bursts
36
- const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false });
42
+ const identityAssertDebounce = new NodeCache({
43
+ stdTTL: 10,
44
+ useClones: false,
45
+ maxKeys: 1000
46
+ });
37
47
  let sendActiveReceipts = false;
38
48
  const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
39
49
  if (!authState.creds.me?.id) {
@@ -1662,11 +1672,18 @@ export const makeMessagesRecvSocket = (config) => {
1662
1672
  await upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
1663
1673
  }
1664
1674
  });
1665
- ev.on('connection.update', ({ isOnline }) => {
1675
+ ev.on('connection.update', ({ isOnline, connection }) => {
1666
1676
  if (typeof isOnline !== 'undefined') {
1667
1677
  sendActiveReceipts = isOnline;
1668
1678
  logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`);
1669
1679
  }
1680
+ // Clean up caches on disconnect
1681
+ if (connection === 'close' || connection === 'loggedOut') {
1682
+ msgRetryCache.flush();
1683
+ placeholderResendCache.flush();
1684
+ identityAssertDebounce.flush();
1685
+ logger.debug('flushed message caches on disconnect');
1686
+ }
1670
1687
  });
1671
1688
  return {
1672
1689
  ...sock,
@@ -266,7 +266,11 @@ export const makeSocket = (config) => {
266
266
  }
267
267
  return [];
268
268
  };
269
- const ev = makeEventBuffer(logger);
269
+ const ev = makeEventBuffer(logger, {
270
+ maxHistoryCacheSize: config.maxHistoryCacheSize,
271
+ bufferTimeoutMs: config.bufferTimeoutMs,
272
+ maxBufferCount: config.maxBufferCount
273
+ });
270
274
  const { creds } = authState;
271
275
  // add transaction capability
272
276
  const keys = addTransactionCapability(authState.keys, logger, transactionOpts);
@@ -486,25 +490,43 @@ export const makeSocket = (config) => {
486
490
  return;
487
491
  }
488
492
  closed = true;
489
- logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed');
493
+
494
+ const disconnectReason = error?.output?.statusCode;
495
+ const isLogout = disconnectReason === DisconnectReason.loggedOut;
496
+
497
+ logger.info({
498
+ trace: error?.stack,
499
+ reason: disconnectReason,
500
+ isLogout
501
+ }, error ? 'connection errored' : 'connection closed');
502
+
490
503
  clearInterval(keepAliveReq);
491
504
  clearTimeout(qrTimer);
505
+
506
+ // Clean up event listeners but preserve reconnect listener
492
507
  ws.removeAllListeners('close');
493
508
  ws.removeAllListeners('open');
494
509
  ws.removeAllListeners('message');
495
- if (!ws.isClosed && !ws.isClosing) {
510
+ ws.removeAllListeners('reconnecting');
511
+
512
+ // Only close websocket if not reconnecting
513
+ if (!ws.reconnecting && !ws.isClosed && !ws.isClosing) {
496
514
  try {
497
515
  await ws.close();
498
516
  }
499
517
  catch { }
500
518
  }
519
+
520
+ // Emit final connection update
501
521
  ev.emit('connection.update', {
502
- connection: 'close',
522
+ connection: isLogout ? 'loggedOut' : 'close',
503
523
  lastDisconnect: {
504
524
  error,
505
525
  date: new Date()
506
- }
526
+ },
527
+ isOnline: false
507
528
  });
529
+
508
530
  ev.removeAllListeners('connection.update');
509
531
  };
510
532
  const waitForSocketOpen = async () => {
@@ -538,6 +560,11 @@ export const makeSocket = (config) => {
538
560
  it could be that the network is down
539
561
  */
540
562
  if (diff > keepAliveIntervalMs + 60000) {
563
+ // Don't trigger disconnect if we're in reconnecting state
564
+ if (ws.reconnecting) {
565
+ logger.debug('skipping keepalive disconnect - reconnecting');
566
+ return;
567
+ }
541
568
  void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }));
542
569
  }
543
570
  else if (ws.isOpen) {
@@ -571,27 +598,63 @@ export const makeSocket = (config) => {
571
598
  });
572
599
  /** logout & invalidate connection */
573
600
  const logout = async (msg) => {
601
+ logger.info({ msg }, 'initiating logout');
602
+
603
+ // Prevent reconnect on logout
604
+ if (ws) {
605
+ ws.maxReconnectAttempts = 0;
606
+ ws.reconnecting = false;
607
+ }
608
+
574
609
  const jid = authState.creds.me?.id;
575
- if (jid) {
576
- await sendNode({
577
- tag: 'iq',
578
- attrs: {
579
- to: S_WHATSAPP_NET,
580
- type: 'set',
581
- id: generateMessageTag(),
582
- xmlns: 'md'
583
- },
584
- content: [
585
- {
586
- tag: 'remove-companion-device',
587
- attrs: {
588
- jid,
589
- reason: 'user_initiated'
610
+
611
+ // Try to send logout IQ to WA server
612
+ try {
613
+ if (jid) {
614
+ const logoutNode = {
615
+ tag: 'iq',
616
+ attrs: {
617
+ to: S_WHATSAPP_NET,
618
+ type: 'set',
619
+ id: generateMessageTag(),
620
+ xmlns: 'md'
621
+ },
622
+ content: [
623
+ {
624
+ tag: 'remove-companion-device',
625
+ attrs: {
626
+ jid,
627
+ reason: 'user_initiated'
628
+ }
590
629
  }
591
- }
592
- ]
593
- });
630
+ ]
631
+ };
632
+ await sendNode(logoutNode);
633
+ logger.info({ jid }, 'sent logout IQ to server');
634
+ }
635
+ } catch (e) {
636
+ logger.warn({ err: e }, 'failed to send logout IQ, continuing with local logout');
637
+ }
638
+
639
+ // Emit proper logout event BEFORE ending connection
640
+ ev.emit('connection.update', {
641
+ connection: 'loggedOut',
642
+ lastDisconnect: {
643
+ error: new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }),
644
+ date: new Date()
645
+ },
646
+ isOnline: false,
647
+ qr: undefined
648
+ });
649
+
650
+ // Clear sensitive credentials
651
+ if (authState.creds) {
652
+ authState.creds.me = undefined;
653
+ authState.creds.lid = undefined;
654
+ authState.creds.deviceName = undefined;
594
655
  }
656
+
657
+ // End the connection
595
658
  void end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }));
596
659
  };
597
660
  const requestPairingCode = async (phoneNumber, customPairingCode) => {
@@ -681,6 +744,8 @@ export const makeSocket = (config) => {
681
744
  };
682
745
  ws.on('message', onMessageReceived);
683
746
  ws.on('open', async () => {
747
+ // Reset reconnect attempts on successful connection
748
+ ws.reconnectAttempts = 0;
684
749
  try {
685
750
  await validateConnection();
686
751
  }
@@ -690,7 +755,26 @@ export const makeSocket = (config) => {
690
755
  }
691
756
  });
692
757
  ws.on('error', mapWebSocketError(end));
693
- ws.on('close', () => void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })));
758
+ ws.on('close', (code, reason) => {
759
+ // Check if reconnecting - if so, don't end the connection
760
+ if (ws.reconnecting) {
761
+ logger.info({ code, reason: reason?.toString() }, 'connection closed, waiting for reconnect');
762
+ return;
763
+ }
764
+ void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed, data: { code, reason: reason?.toString() } }));
765
+ });
766
+ ws.on('reconnecting', (info) => {
767
+ ev.emit('connection.update', {
768
+ connection: 'reconnecting',
769
+ lastDisconnect: {
770
+ error: new Boom('Reconnecting', { statusCode: DisconnectReason.connectionClosed }),
771
+ date: new Date()
772
+ },
773
+ reconnectAttempt: info.attempt,
774
+ maxReconnectAttempts: info.maxAttempts,
775
+ reconnectDelay: info.delay
776
+ });
777
+ });
694
778
  // the server terminated the connection
695
779
  ws.on('CB:xmlstreamend', () => void end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })));
696
780
  // QR gen
@@ -37,6 +37,75 @@ export const makeInMemoryStore = (config) => {
37
37
  const state = { connection: 'close' };
38
38
  const labels = new ObjectRepository();
39
39
  const labelAssociations = new KeyedDB(labelAssociationKey, labelAssociationKey.key);
40
+
41
+ // Memory management - configurable limits
42
+ const MAX_MESSAGES_PER_CHAT = config.maxMessagesPerChat || 2000;
43
+ const MAX_CHATS = config.maxChats || 10000;
44
+ const MAX_CONTACTS = config.maxContacts || 20000;
45
+ const MESSAGE_CLEANUP_INTERVAL = config.messageCleanupIntervalMs || 5 * 60 * 1000; // 5 min
46
+ let cleanupTimer = null;
47
+
48
+ // Check and enforce memory limits
49
+ const enforceMemoryLimits = () => {
50
+ // Cleanup old messages if exceeds limit
51
+ for (const jid in messages) {
52
+ const list = messages[jid];
53
+ if (list && list.array && list.array.length > MAX_MESSAGES_PER_CHAT) {
54
+ const excess = list.array.length - MAX_MESSAGES_PER_CHAT;
55
+ list.array.splice(0, excess);
56
+ logger.debug({ jid, removed: excess, remaining: list.array.length }, 'cleaned excess messages from chat');
57
+ }
58
+ }
59
+
60
+ // Limit total chats
61
+ if (chats.length > MAX_CHATS) {
62
+ const toRemove = chats.length - MAX_CHATS;
63
+ const sortedChats = [...chats].sort((a, b) => {
64
+ const aTime = a.conversationTimestamp || 0;
65
+ const bTime = b.conversationTimestamp || 0;
66
+ return aTime - bTime;
67
+ });
68
+ for (let i = 0; i < toRemove; i++) {
69
+ const chatId = sortedChats[i]?.id;
70
+ if (chatId) {
71
+ chats.deleteById(chatId);
72
+ delete messages[chatId];
73
+ }
74
+ }
75
+ logger.debug({ removed: toRemove }, 'cleaned old chats due to memory limit');
76
+ }
77
+
78
+ // Limit contacts
79
+ const contactIds = Object.keys(contacts);
80
+ if (contactIds.length > MAX_CONTACTS) {
81
+ const toRemove = contactIds.length - MAX_CONTACTS;
82
+ const sortedContacts = contactIds
83
+ .map(id => ({ id, contact: contacts[id] }))
84
+ .sort((a, b) => {
85
+ const aTime = a.contact?.lastSeen || 0;
86
+ const bTime = b.contact?.lastSeen || 0;
87
+ return aTime - bTime;
88
+ });
89
+ for (let i = 0; i < toRemove; i++) {
90
+ delete contacts[sortedContacts[i].id];
91
+ }
92
+ logger.debug({ removed: toRemove }, 'cleaned old contacts due to memory limit');
93
+ }
94
+ };
95
+
96
+ // Start periodic cleanup
97
+ const startMemoryCleanup = () => {
98
+ if (cleanupTimer) return;
99
+ cleanupTimer = setInterval(enforceMemoryLimits, MESSAGE_CLEANUP_INTERVAL);
100
+ cleanupTimer.unref(); // Don't block process exit
101
+ };
102
+
103
+ const stopMemoryCleanup = () => {
104
+ if (cleanupTimer) {
105
+ clearInterval(cleanupTimer);
106
+ cleanupTimer = null;
107
+ }
108
+ };
40
109
  const assertMessageList = (jid) => {
41
110
  if (!messages[jid]) {
42
111
  messages[jid] = makeMessagesDictionary();
@@ -65,6 +134,13 @@ export const makeInMemoryStore = (config) => {
65
134
  const bind = (ev) => {
66
135
  ev.on('connection.update', update => {
67
136
  Object.assign(state, update);
137
+
138
+ // Start cleanup when connected, stop when disconnected
139
+ if (update.connection === 'open') {
140
+ startMemoryCleanup();
141
+ } else if (update.connection === 'close' || update.connection === 'loggedOut') {
142
+ stopMemoryCleanup();
143
+ }
68
144
  });
69
145
  ev.on('messaging-history.set', ({ chats: newChats, contacts: newContacts, messages: newMessages, isLatest, syncType }) => {
70
146
  if (syncType === proto.HistorySync.HistorySyncType.ON_DEMAND) {
@@ -395,6 +471,44 @@ export const makeInMemoryStore = (config) => {
395
471
  const json = JSON.parse(jsonStr);
396
472
  fromJSON(json);
397
473
  }
474
+ },
475
+ /** Clear all stored data - useful for logout */
476
+ clear: () => {
477
+ chats.clear();
478
+ for (const key of Object.keys(messages)) {
479
+ delete messages[key];
480
+ }
481
+ for (const key of Object.keys(contacts)) {
482
+ delete contacts[key];
483
+ }
484
+ for (const key of Object.keys(groupMetadata)) {
485
+ delete groupMetadata[key];
486
+ }
487
+ for (const key of Object.keys(presences)) {
488
+ delete presences[key];
489
+ }
490
+ labels.clear();
491
+ labelAssociations.clear();
492
+ stopMemoryCleanup();
493
+ logger.info('Cleared all store data');
494
+ },
495
+ /** Get memory usage stats */
496
+ getMemoryStats: () => {
497
+ let totalMessages = 0;
498
+ for (const jid in messages) {
499
+ totalMessages += messages[jid]?.array?.length || 0;
500
+ }
501
+ return {
502
+ chats: chats.length,
503
+ messages: totalMessages,
504
+ contacts: Object.keys(contacts).length,
505
+ groups: Object.keys(groupMetadata).length,
506
+ labels: labels.count(),
507
+ labelAssociations: labelAssociations.length,
508
+ maxMessagesPerChat: MAX_MESSAGES_PER_CHAT,
509
+ maxChats: MAX_CHATS,
510
+ maxContacts: MAX_CONTACTS
511
+ };
398
512
  }
399
513
  };
400
514
  };
@@ -22,16 +22,19 @@ const BUFFERABLE_EVENT_SET = new Set(BUFFERABLE_EVENT);
22
22
  * The event buffer logically consolidates different events into a single event
23
23
  * making the data processing more efficient.
24
24
  */
25
- export const makeEventBuffer = (logger) => {
25
+ export const makeEventBuffer = (logger, config = {}) => {
26
26
  const ev = new EventEmitter();
27
27
  const historyCache = new Set();
28
28
  let data = makeBufferData();
29
29
  let isBuffering = false;
30
30
  let bufferTimeout = null;
31
- let flushPendingTimeout = null; // Add a specific timer for the debounced flush to prevent leak
31
+ let flushPendingTimeout = null;
32
32
  let bufferCount = 0;
33
- const MAX_HISTORY_CACHE_SIZE = 10000; // Limit the history cache size to prevent memory bloat
34
- const BUFFER_TIMEOUT_MS = 30000; // 30 seconds
33
+
34
+ // Make limits configurable
35
+ const MAX_HISTORY_CACHE_SIZE = config.maxHistoryCacheSize || 15000;
36
+ const BUFFER_TIMEOUT_MS = config.bufferTimeoutMs || 30000;
37
+ const MAX_BUFFER_COUNT = config.maxBufferCount || 10000;
35
38
  // take the generic event and fire it as a baileys event
36
39
  ev.on('event', (map) => {
37
40
  for (const event in map) {
@@ -55,6 +58,12 @@ export const makeEventBuffer = (logger) => {
55
58
  }
56
59
  // Always increment count when requested
57
60
  bufferCount++;
61
+
62
+ // Auto-flush if buffer count exceeds limit
63
+ if (bufferCount > MAX_BUFFER_COUNT) {
64
+ logger.debug({ bufferCount }, 'buffer count exceeded limit, auto-flushing');
65
+ flush();
66
+ }
58
67
  }
59
68
  function flush() {
60
69
  if (!isBuffering) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "violetics",
3
3
  "type": "module",
4
- "version": "7.0.11-alpha",
4
+ "version": "7.0.12-alpha",
5
5
  "description": "A WebSockets library for interacting with WhatsApp Web",
6
6
  "keywords": [
7
7
  "whatsapp",