violetics 7.0.11-alpha → 7.0.13-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,7 @@ export class WebSocketClient extends AbstractSocketClient {
5
5
  constructor() {
6
6
  super(...arguments);
7
7
  this.socket = null;
8
+ this.socketListeners = new Map();
8
9
  }
9
10
  get isOpen() {
10
11
  return this.socket?.readyState === WebSocket.OPEN;
@@ -32,13 +33,19 @@ export class WebSocketClient extends AbstractSocketClient {
32
33
  this.socket.setMaxListeners(0);
33
34
  const events = ['close', 'error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response'];
34
35
  for (const event of events) {
35
- this.socket?.on(event, (...args) => this.emit(event, ...args));
36
+ const handler = (...args) => this.emit(event, ...args);
37
+ this.socketListeners.set(event, handler);
38
+ this.socket?.on(event, handler);
36
39
  }
37
40
  }
38
41
  async close() {
39
42
  if (!this.socket) {
40
43
  return;
41
44
  }
45
+ for (const [event, handler] of this.socketListeners) {
46
+ this.socket.removeListener(event, handler);
47
+ }
48
+ this.socketListeners.clear();
42
49
  const closePromise = new Promise(resolve => {
43
50
  this.socket?.once('close', resolve);
44
51
  });
@@ -1,4 +1,4 @@
1
- import NodeCache from '@cacheable/node-cache';
1
+ import { LRUCache } from 'lru-cache';
2
2
  import { Boom } from '@hapi/boom';
3
3
  import { proto } from '../../WAProto/index.js';
4
4
  import { DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js';
@@ -29,9 +29,10 @@ export const makeChatsSocket = (config) => {
29
29
  // Timeout for AwaitingInitialSync state
30
30
  let awaitingSyncTimeout;
31
31
  const placeholderResendCache = config.placeholderResendCache ||
32
- new NodeCache({
33
- stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
34
- useClones: false
32
+ new LRUCache({
33
+ max: 5000,
34
+ ttl: DEFAULT_CACHE_TTLS.MSG_RETRY * 1000, // 1 hour
35
+ allowStale: false
35
36
  });
36
37
  if (!config.placeholderResendCache) {
37
38
  config.placeholderResendCache = placeholderResendCache;
@@ -1,4 +1,4 @@
1
- import NodeCache from '@cacheable/node-cache';
1
+ import { LRUCache } from 'lru-cache';
2
2
  import { Boom } from '@hapi/boom';
3
3
  import { randomBytes } from 'crypto';
4
4
  import Long from 'long';
@@ -18,22 +18,29 @@ export const makeMessagesRecvSocket = (config) => {
18
18
  /** this mutex ensures that each retryRequest will wait for the previous one to finish */
19
19
  const retryMutex = makeMutex();
20
20
  const msgRetryCache = config.msgRetryCounterCache ||
21
- new NodeCache({
22
- stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
23
- useClones: false
21
+ new LRUCache({
22
+ max: 10000,
23
+ ttl: DEFAULT_CACHE_TTLS.MSG_RETRY * 1000,
24
+ allowStale: false
24
25
  });
25
26
  const callOfferCache = config.callOfferCache ||
26
- new NodeCache({
27
- stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
28
- useClones: false
27
+ new LRUCache({
28
+ max: 5000,
29
+ ttl: DEFAULT_CACHE_TTLS.CALL_OFFER * 1000,
30
+ allowStale: false
29
31
  });
30
32
  const placeholderResendCache = config.placeholderResendCache ||
31
- new NodeCache({
32
- stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
33
- useClones: false
33
+ new LRUCache({
34
+ max: 5000,
35
+ ttl: DEFAULT_CACHE_TTLS.MSG_RETRY * 1000,
36
+ allowStale: false
34
37
  });
35
38
  // Debounce identity-change session refreshes per JID to avoid bursts
36
- const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false });
39
+ const identityAssertDebounce = new LRUCache({
40
+ max: 1000,
41
+ ttl: 10000,
42
+ allowStale: false
43
+ });
37
44
  let sendActiveReceipts = false;
38
45
  const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
39
46
  if (!authState.creds.me?.id) {
@@ -1662,11 +1669,22 @@ export const makeMessagesRecvSocket = (config) => {
1662
1669
  await upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
1663
1670
  }
1664
1671
  });
1665
- ev.on('connection.update', ({ isOnline }) => {
1672
+ ev.on('connection.update', ({ isOnline, connection }) => {
1666
1673
  if (typeof isOnline !== 'undefined') {
1667
1674
  sendActiveReceipts = isOnline;
1668
1675
  logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`);
1669
1676
  }
1677
+ // Clean up caches on disconnect - LRU cache will expire naturally, but we can reset
1678
+ if (connection === 'close' || connection === 'loggedOut') {
1679
+ try {
1680
+ // LRU cache doesn't have clear(), but TTL handles expiration
1681
+ // We can reset by creating new cache instances if needed
1682
+ // For now just log that disconnection happened
1683
+ logger.debug('connection closed, caches will expire via TTL');
1684
+ } catch (e) {
1685
+ logger.warn({ err: e }, 'error in cache cleanup');
1686
+ }
1687
+ }
1670
1688
  });
1671
1689
  return {
1672
1690
  ...sock,
@@ -1,4 +1,4 @@
1
- import NodeCache from '@cacheable/node-cache';
1
+ import { LRUCache } from 'lru-cache';
2
2
  import { Boom } from '@hapi/boom';
3
3
  import { randomBytes } from 'crypto';
4
4
  import { proto } from '../../WAProto/index.js';
@@ -20,13 +20,15 @@ export const makeMessagesSocket = (config) => {
20
20
  const sock = makeNewsletterSocket(config);
21
21
  const { ev, authState, messageMutex, signalRepository, upsertMessage, query, fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral } = sock;
22
22
  const userDevicesCache = config.userDevicesCache ||
23
- new NodeCache({
24
- stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, // 5 minutes
25
- useClones: false
23
+ new LRUCache({
24
+ max: 5000,
25
+ ttl: DEFAULT_CACHE_TTLS.USER_DEVICES * 1000, // 5 minutes
26
+ allowStale: false
26
27
  });
27
- const peerSessionsCache = new NodeCache({
28
- stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES,
29
- useClones: false
28
+ const peerSessionsCache = new LRUCache({
29
+ max: 5000,
30
+ ttl: DEFAULT_CACHE_TTLS.USER_DEVICES * 1000,
31
+ allowStale: false
30
32
  });
31
33
  // Initialize message retry manager if enabled
32
34
  const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null;
@@ -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);
@@ -276,6 +280,8 @@ export const makeSocket = (config) => {
276
280
  let keepAliveReq;
277
281
  let qrTimer;
278
282
  let closed = false;
283
+ let consecutivePingFailures = 0;
284
+ const MAX_PING_FAILURES = 3;
279
285
  /** log & process any unexpected errors */
280
286
  const onUnexpectedError = (err, msg) => {
281
287
  logger.error({ err }, `unexpected error in '${msg}'`);
@@ -487,14 +493,19 @@ export const makeSocket = (config) => {
487
493
  }
488
494
  closed = true;
489
495
  logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed');
490
- clearInterval(keepAliveReq);
496
+ clearTimeout(keepAliveReq);
491
497
  clearTimeout(qrTimer);
492
- ws.removeAllListeners('close');
493
- ws.removeAllListeners('open');
494
- ws.removeAllListeners('message');
498
+ consecutivePingFailures = 0;
499
+ ws.removeAllListeners();
500
+ if (ev.isBuffering()) {
501
+ ev.flush();
502
+ }
495
503
  if (!ws.isClosed && !ws.isClosing) {
496
504
  try {
497
- await ws.close();
505
+ await Promise.race([
506
+ ws.close(),
507
+ new Promise(resolve => setTimeout(resolve, 5000))
508
+ ]);
498
509
  }
499
510
  catch { }
500
511
  }
@@ -505,7 +516,8 @@ export const makeSocket = (config) => {
505
516
  date: new Date()
506
517
  }
507
518
  });
508
- ev.removeAllListeners('connection.update');
519
+ noise.destroy();
520
+ ev.removeAllListeners();
509
521
  };
510
522
  const waitForSocketOpen = async () => {
511
523
  if (ws.isOpen) {
@@ -528,37 +540,57 @@ export const makeSocket = (config) => {
528
540
  ws.off('error', onClose);
529
541
  });
530
542
  };
531
- const startKeepAliveRequest = () => (keepAliveReq = setInterval(() => {
532
- if (!lastDateRecv) {
533
- lastDateRecv = new Date();
534
- }
535
- const diff = Date.now() - lastDateRecv.getTime();
536
- /*
537
- check if it's been a suspicious amount of time since the server responded with our last seen
538
- it could be that the network is down
539
- */
540
- if (diff > keepAliveIntervalMs + 60000) {
541
- void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }));
542
- }
543
- else if (ws.isOpen) {
544
- // if its all good, send a keep alive request
545
- query({
546
- tag: 'iq',
547
- attrs: {
548
- id: generateMessageTag(),
549
- to: S_WHATSAPP_NET,
550
- type: 'get',
551
- xmlns: 'w:p'
552
- },
553
- content: [{ tag: 'ping', attrs: {} }]
554
- }).catch(err => {
555
- logger.error({ trace: err.stack }, 'error in sending keep alive');
556
- });
557
- }
558
- else {
559
- logger.warn('keep alive called when WS not open');
560
- }
561
- }, keepAliveIntervalMs));
543
+ const startKeepAliveRequest = () => {
544
+ const scheduleNextPing = () => {
545
+ keepAliveReq = setTimeout(async () => {
546
+ if (closed) {
547
+ return;
548
+ }
549
+ if (!lastDateRecv) {
550
+ lastDateRecv = new Date();
551
+ }
552
+ const diff = Date.now() - lastDateRecv.getTime();
553
+ if (diff > keepAliveIntervalMs * 2 + 5000) {
554
+ logger.warn({ diff, keepAliveIntervalMs }, 'connection silent for too long');
555
+ void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }));
556
+ return;
557
+ }
558
+ if (ws.isOpen) {
559
+ try {
560
+ await query({
561
+ tag: 'iq',
562
+ attrs: {
563
+ id: generateMessageTag(),
564
+ to: S_WHATSAPP_NET,
565
+ type: 'get',
566
+ xmlns: 'w:p'
567
+ },
568
+ content: [{ tag: 'ping', attrs: {} }]
569
+ });
570
+ consecutivePingFailures = 0;
571
+ }
572
+ catch (err) {
573
+ consecutivePingFailures++;
574
+ logger.error({ trace: err.stack, consecutivePingFailures, maxFailures: MAX_PING_FAILURES }, 'error in sending keep alive');
575
+ if (consecutivePingFailures >= MAX_PING_FAILURES) {
576
+ logger.warn('max ping failures reached, terminating connection');
577
+ void end(new Boom('Connection was lost (ping failures)', {
578
+ statusCode: DisconnectReason.connectionLost
579
+ }));
580
+ return;
581
+ }
582
+ }
583
+ }
584
+ else {
585
+ logger.warn('keep alive called when WS not open');
586
+ }
587
+ if (!closed) {
588
+ scheduleNextPing();
589
+ }
590
+ }, keepAliveIntervalMs);
591
+ };
592
+ scheduleNextPing();
593
+ };
562
594
  /** i have no idea why this exists. pls enlighten me */
563
595
  const sendPassiveIq = (tag) => query({
564
596
  tag: 'iq',
@@ -571,27 +603,63 @@ export const makeSocket = (config) => {
571
603
  });
572
604
  /** logout & invalidate connection */
573
605
  const logout = async (msg) => {
606
+ logger.info({ msg }, 'initiating logout');
607
+
608
+ // Prevent reconnect on logout
609
+ if (ws) {
610
+ ws.maxReconnectAttempts = 0;
611
+ ws.reconnecting = false;
612
+ }
613
+
574
614
  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'
615
+
616
+ // Try to send logout IQ to WA server
617
+ try {
618
+ if (jid) {
619
+ const logoutNode = {
620
+ tag: 'iq',
621
+ attrs: {
622
+ to: S_WHATSAPP_NET,
623
+ type: 'set',
624
+ id: generateMessageTag(),
625
+ xmlns: 'md'
626
+ },
627
+ content: [
628
+ {
629
+ tag: 'remove-companion-device',
630
+ attrs: {
631
+ jid,
632
+ reason: 'user_initiated'
633
+ }
590
634
  }
591
- }
592
- ]
593
- });
635
+ ]
636
+ };
637
+ await sendNode(logoutNode);
638
+ logger.info({ jid }, 'sent logout IQ to server');
639
+ }
640
+ } catch (e) {
641
+ logger.warn({ err: e }, 'failed to send logout IQ, continuing with local logout');
594
642
  }
643
+
644
+ // Emit proper logout event BEFORE ending connection
645
+ ev.emit('connection.update', {
646
+ connection: 'loggedOut',
647
+ lastDisconnect: {
648
+ error: new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }),
649
+ date: new Date()
650
+ },
651
+ isOnline: false,
652
+ qr: undefined
653
+ });
654
+
655
+ // Clear sensitive credentials
656
+ if (authState.creds) {
657
+ authState.creds.me = undefined;
658
+ authState.creds.lid = undefined;
659
+ authState.creds.deviceName = undefined;
660
+ }
661
+
662
+ // End the connection
595
663
  void end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }));
596
664
  };
597
665
  const requestPairingCode = async (phoneNumber, customPairingCode) => {
@@ -681,6 +749,8 @@ export const makeSocket = (config) => {
681
749
  };
682
750
  ws.on('message', onMessageReceived);
683
751
  ws.on('open', async () => {
752
+ // Reset reconnect attempts on successful connection
753
+ ws.reconnectAttempts = 0;
684
754
  try {
685
755
  await validateConnection();
686
756
  }
@@ -690,7 +760,26 @@ export const makeSocket = (config) => {
690
760
  }
691
761
  });
692
762
  ws.on('error', mapWebSocketError(end));
693
- ws.on('close', () => void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })));
763
+ ws.on('close', (code, reason) => {
764
+ // Check if reconnecting - if so, don't end the connection
765
+ if (ws.reconnecting) {
766
+ logger.info({ code, reason: reason?.toString() }, 'connection closed, waiting for reconnect');
767
+ return;
768
+ }
769
+ void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed, data: { code, reason: reason?.toString() } }));
770
+ });
771
+ ws.on('reconnecting', (info) => {
772
+ ev.emit('connection.update', {
773
+ connection: 'reconnecting',
774
+ lastDisconnect: {
775
+ error: new Boom('Reconnecting', { statusCode: DisconnectReason.connectionClosed }),
776
+ date: new Date()
777
+ },
778
+ reconnectAttempt: info.attempt,
779
+ maxReconnectAttempts: info.maxAttempts,
780
+ reconnectDelay: info.delay
781
+ });
782
+ });
694
783
  // the server terminated the connection
695
784
  ws.on('CB:xmlstreamend', () => void end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })));
696
785
  // QR gen
@@ -929,7 +1018,13 @@ export const makeSocket = (config) => {
929
1018
  waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
930
1019
  sendWAMBuffer,
931
1020
  executeUSyncQuery,
932
- onWhatsApp
1021
+ onWhatsApp,
1022
+ get connectionHealth() {
1023
+ return {
1024
+ lastMessageReceived: lastDateRecv,
1025
+ consecutivePingFailures
1026
+ };
1027
+ }
933
1028
  };
934
1029
  };
935
1030
  /**
@@ -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
  };
@@ -23,5 +23,8 @@ export var DisconnectReason;
23
23
  DisconnectReason[DisconnectReason["multideviceMismatch"] = 411] = "multideviceMismatch";
24
24
  DisconnectReason[DisconnectReason["forbidden"] = 403] = "forbidden";
25
25
  DisconnectReason[DisconnectReason["unavailableService"] = 503] = "unavailableService";
26
+ DisconnectReason[DisconnectReason["streamReplaced"] = 405] = "streamReplaced";
27
+ DisconnectReason[DisconnectReason["conflict"] = 409] = "conflict";
28
+ DisconnectReason[DisconnectReason["preconditionFailed"] = 412] = "preconditionFailed";
26
29
  })(DisconnectReason || (DisconnectReason = {}));
27
30
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- import NodeCache from '@cacheable/node-cache';
1
+ import { LRUCache } from 'lru-cache';
2
2
  import { AsyncLocalStorage } from 'async_hooks';
3
3
  import { Mutex } from 'async-mutex';
4
4
  import { randomBytes } from 'crypto';
@@ -15,10 +15,10 @@ import { PreKeyManager } from './pre-key-manager.js';
15
15
  */
16
16
  export function makeCacheableSignalKeyStore(store, logger, _cache) {
17
17
  const cache = _cache ||
18
- new NodeCache({
19
- stdTTL: DEFAULT_CACHE_TTLS.SIGNAL_STORE, // 5 minutes
20
- useClones: false,
21
- deleteOnExpire: true
18
+ new LRUCache({
19
+ max: 5000,
20
+ ttl: DEFAULT_CACHE_TTLS.SIGNAL_STORE * 1000, // 5 minutes
21
+ allowStale: false
22
22
  });
23
23
  // Mutex for protecting cache operations
24
24
  const cacheMutex = new Mutex();
@@ -22,16 +22,17 @@ 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
+ let activeBufferedTimeouts = new Set();
34
+ const MAX_HISTORY_CACHE_SIZE = config.maxHistoryCacheSize || 10000;
35
+ const BUFFER_TIMEOUT_MS = config.bufferTimeoutMs || 30000;
35
36
  // take the generic event and fire it as a baileys event
36
37
  ev.on('event', (map) => {
37
38
  for (const event in map) {
@@ -55,6 +56,12 @@ export const makeEventBuffer = (logger) => {
55
56
  }
56
57
  // Always increment count when requested
57
58
  bufferCount++;
59
+
60
+ // Auto-flush if buffer count exceeds limit
61
+ if (bufferCount > MAX_BUFFER_COUNT) {
62
+ logger.debug({ bufferCount }, 'buffer count exceeded limit, auto-flushing');
63
+ flush();
64
+ }
58
65
  }
59
66
  function flush() {
60
67
  if (!isBuffering) {
@@ -143,13 +150,14 @@ export const makeEventBuffer = (logger) => {
143
150
  buffer();
144
151
  try {
145
152
  const result = await work(...args);
146
- // If this is the only buffer, flush after a small delay
147
153
  if (bufferCount === 1) {
148
- setTimeout(() => {
154
+ const t = setTimeout(() => {
155
+ activeBufferedTimeouts.delete(t);
149
156
  if (isBuffering && bufferCount === 1) {
150
157
  flush();
151
158
  }
152
- }, 100); // Small delay to allow nested buffers
159
+ }, 100);
160
+ activeBufferedTimeouts.add(t);
153
161
  }
154
162
  return result;
155
163
  }
@@ -159,7 +167,6 @@ export const makeEventBuffer = (logger) => {
159
167
  finally {
160
168
  bufferCount = Math.max(0, bufferCount - 1);
161
169
  if (bufferCount === 0) {
162
- // Only schedule ONE timeout, not 10,000
163
170
  if (!flushPendingTimeout) {
164
171
  flushPendingTimeout = setTimeout(flush, 100);
165
172
  }
@@ -169,7 +176,25 @@ export const makeEventBuffer = (logger) => {
169
176
  },
170
177
  on: (...args) => ev.on(...args),
171
178
  off: (...args) => ev.off(...args),
172
- removeAllListeners: (...args) => ev.removeAllListeners(...args)
179
+ removeAllListeners: (...args) => {
180
+ if (bufferTimeout) {
181
+ clearTimeout(bufferTimeout);
182
+ bufferTimeout = null;
183
+ }
184
+ if (flushPendingTimeout) {
185
+ clearTimeout(flushPendingTimeout);
186
+ flushPendingTimeout = null;
187
+ }
188
+ for (const t of activeBufferedTimeouts) {
189
+ clearTimeout(t);
190
+ }
191
+ activeBufferedTimeouts.clear();
192
+ isBuffering = false;
193
+ bufferCount = 0;
194
+ historyCache.clear();
195
+ data = makeBufferData();
196
+ return ev.removeAllListeners(...args);
197
+ }
173
198
  };
174
199
  };
175
200
  const makeBufferData = () => {
@@ -281,7 +281,10 @@ export const getStatusFromReceiptType = (type) => {
281
281
  return status;
282
282
  };
283
283
  const CODE_MAP = {
284
- conflict: DisconnectReason.connectionReplaced
284
+ conflict: DisconnectReason.connectionReplaced,
285
+ 'stream-replaced': DisconnectReason.streamReplaced,
286
+ conflict: DisconnectReason.conflict,
287
+ 'precondition-failed': DisconnectReason.preconditionFailed
285
288
  };
286
289
  /**
287
290
  * Stream errors generally provide a reason, map that to a baileys DisconnectReason
@@ -1,4 +1,4 @@
1
- import NodeCache from '@cacheable/node-cache';
1
+ import { LRUCache } from 'lru-cache';
2
2
  import { areJidsSameUser, getBinaryNodeChild, jidDecode } from '../WABinary/index.js';
3
3
  import { isStringNullOrEmpty } from './generics.js';
4
4
  export async function handleIdentityChange(node, ctx) {
@@ -5,6 +5,7 @@ import { decodeBinaryNode } from '../WABinary/index.js';
5
5
  import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, sha256 } from './crypto.js';
6
6
  const IV_LENGTH = 12;
7
7
  const EMPTY_BUFFER = Buffer.alloc(0);
8
+ const MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB max buffer cap
8
9
  const generateIV = (counter) => {
9
10
  const iv = new ArrayBuffer(IV_LENGTH);
10
11
  new DataView(iv).setUint32(8, counter);
@@ -194,7 +195,18 @@ export const makeNoiseHandler = ({ keyPair: { private: privateKey, public: publi
194
195
  else {
195
196
  inBytes = Buffer.concat([inBytes, newData]);
196
197
  }
198
+ if (inBytes.length > MAX_BUFFER_SIZE) {
199
+ logger.error({ bufferSize: inBytes.length }, 'noise handler buffer exceeded max size, clearing');
200
+ inBytes = Buffer.alloc(0);
201
+ return;
202
+ }
197
203
  await processData(onFrame);
204
+ },
205
+ destroy: () => {
206
+ inBytes = Buffer.alloc(0);
207
+ transport = null;
208
+ pendingOnFrame = null;
209
+ isWaitingForTransport = false;
198
210
  }
199
211
  };
200
212
  };
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.13-alpha",
5
5
  "description": "A WebSockets library for interacting with WhatsApp Web",
6
6
  "keywords": [
7
7
  "whatsapp",