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.
- package/lib/Defaults/index.js +19 -5
- package/lib/Socket/Client/websocket.js +8 -1
- package/lib/Socket/chats.js +5 -4
- package/lib/Socket/messages-recv.js +30 -12
- package/lib/Socket/messages-send.js +9 -7
- package/lib/Socket/socket.js +153 -58
- package/lib/Store/make-in-memory-store.js +114 -0
- package/lib/Types/index.js +3 -0
- package/lib/Utils/auth-utils.js +5 -5
- package/lib/Utils/event-buffer.js +34 -9
- package/lib/Utils/generics.js +4 -1
- package/lib/Utils/identity-change-handler.js +1 -1
- package/lib/Utils/noise-handler.js +12 -0
- 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,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
|
-
|
|
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
|
});
|
package/lib/Socket/chats.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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;
|
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);
|
|
@@ -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
|
-
|
|
496
|
+
clearTimeout(keepAliveReq);
|
|
491
497
|
clearTimeout(qrTimer);
|
|
492
|
-
|
|
493
|
-
ws.removeAllListeners(
|
|
494
|
-
|
|
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
|
|
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
|
-
|
|
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 = () =>
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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', () =>
|
|
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
|
};
|
package/lib/Types/index.js
CHANGED
|
@@ -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
|
package/lib/Utils/auth-utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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;
|
|
31
|
+
let flushPendingTimeout = null;
|
|
32
32
|
let bufferCount = 0;
|
|
33
|
-
|
|
34
|
-
const
|
|
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);
|
|
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) =>
|
|
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 = () => {
|
package/lib/Utils/generics.js
CHANGED
|
@@ -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
|
|
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
|
};
|