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.
- package/lib/Defaults/index.js +19 -5
- package/lib/Socket/Client/websocket.js +59 -2
- package/lib/Socket/messages-recv.js +22 -5
- package/lib/Socket/socket.js +108 -24
- package/lib/Store/make-in-memory-store.js +114 -0
- package/lib/Utils/event-buffer.js +13 -4
- package/package.json +1 -1
package/lib/Defaults/index.js
CHANGED
|
@@ -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:
|
|
51
|
-
keepAliveIntervalMs:
|
|
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:
|
|
54
|
+
defaultQueryTimeoutMs: 60000, // 60 seconds - reduced from 90s
|
|
55
55
|
customUploadHosts: [],
|
|
56
|
-
retryRequestDelayMs:
|
|
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({
|
|
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,
|
package/lib/Socket/socket.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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', () =>
|
|
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;
|
|
31
|
+
let flushPendingTimeout = null;
|
|
32
32
|
let bufferCount = 0;
|
|
33
|
-
|
|
34
|
-
|
|
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) {
|