violetics 7.0.16-alpha → 7.0.18-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 +11 -5
- package/lib/Socket/Client/websocket.js +24 -11
- package/lib/Socket/chats.js +5 -6
- package/lib/Socket/messages-recv.js +11 -6
- package/lib/Socket/messages-send.js +5 -5
- package/lib/Store/make-in-memory-store.js +12 -0
- package/lib/Utils/auth-utils.js +1 -1
- package/lib/Utils/event-buffer.js +3 -2
- package/lib/Utils/generics.js +0 -1
- package/lib/Utils/message-retry-manager.js +4 -3
- package/package.json +1 -1
package/lib/Defaults/index.js
CHANGED
|
@@ -84,14 +84,20 @@ export const DEFAULT_CONNECTION_CONFIG = {
|
|
|
84
84
|
reconnectDelay: 2000,
|
|
85
85
|
maxReconnectDelay: 30000,
|
|
86
86
|
|
|
87
|
+
// Connection sync timeout (ms to wait for history sync before forcing flush)
|
|
88
|
+
awaitingSyncTimeoutMs: 5000,
|
|
89
|
+
// Message retry phone request delay
|
|
90
|
+
phoneRequestDelayMs: 2000,
|
|
91
|
+
|
|
87
92
|
// Memory management options
|
|
88
93
|
maxMessagesPerChat: 2000,
|
|
89
|
-
maxChats:
|
|
90
|
-
maxContacts:
|
|
91
|
-
|
|
94
|
+
maxChats: 5000,
|
|
95
|
+
maxContacts: 10000,
|
|
96
|
+
maxPresences: 500,
|
|
97
|
+
messageCleanupIntervalMs: 2 * 60 * 1000, // 2 minutes
|
|
92
98
|
maxHistoryCacheSize: 15000,
|
|
93
|
-
bufferTimeoutMs:
|
|
94
|
-
maxBufferCount:
|
|
99
|
+
bufferTimeoutMs: 100,
|
|
100
|
+
maxBufferCount: 500
|
|
95
101
|
};
|
|
96
102
|
export const MEDIA_PATH_MAP = {
|
|
97
103
|
image: '/mms/image',
|
|
@@ -11,6 +11,7 @@ export class WebSocketClient extends AbstractSocketClient {
|
|
|
11
11
|
this.reconnectDelay = this.config.reconnectDelay || 1000;
|
|
12
12
|
this.maxReconnectDelay = this.config.maxReconnectDelay || 30000;
|
|
13
13
|
this.reconnecting = false;
|
|
14
|
+
this._destroyed = false;
|
|
14
15
|
}
|
|
15
16
|
get isOpen() {
|
|
16
17
|
return this.socket?.readyState === WebSocket.OPEN;
|
|
@@ -28,6 +29,7 @@ export class WebSocketClient extends AbstractSocketClient {
|
|
|
28
29
|
if (this.socket) {
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
32
|
+
this._destroyed = false;
|
|
31
33
|
this.socket = new WebSocket(this.url, {
|
|
32
34
|
origin: DEFAULT_ORIGIN,
|
|
33
35
|
headers: this.config.options?.headers,
|
|
@@ -36,31 +38,35 @@ export class WebSocketClient extends AbstractSocketClient {
|
|
|
36
38
|
agent: this.config.agent
|
|
37
39
|
});
|
|
38
40
|
this.socket.setMaxListeners(0);
|
|
39
|
-
|
|
41
|
+
// Exclude 'close' from the generic forwarding loop — handled separately below
|
|
42
|
+
// so that reconnecting flag is set BEFORE the close event reaches listeners.
|
|
43
|
+
const events = ['error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response'];
|
|
40
44
|
for (const event of events) {
|
|
41
45
|
const handler = (...args) => this.emit(event, ...args);
|
|
42
46
|
this.socketListeners.set(event, handler);
|
|
43
47
|
this.socket?.on(event, handler);
|
|
44
48
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
// Close handler: set reconnecting flag synchronously BEFORE emitting so that
|
|
50
|
+
// socket.js close listeners can check ws.reconnecting and avoid calling end().
|
|
51
|
+
const closeHandler = (code, reason) => {
|
|
48
52
|
this.attemptReconnect(code, reason);
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
this.emit('close', code, reason);
|
|
54
|
+
};
|
|
55
|
+
this.socketListeners.set('close', closeHandler);
|
|
56
|
+
this.socket.on('close', closeHandler);
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
async attemptReconnect(code, reason) {
|
|
54
60
|
if (this.reconnecting || code === 1000 || this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
55
61
|
return;
|
|
56
62
|
}
|
|
57
|
-
|
|
63
|
+
|
|
58
64
|
this.reconnecting = true;
|
|
59
65
|
const delay = Math.min(
|
|
60
66
|
this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
|
|
61
67
|
this.maxReconnectDelay
|
|
62
68
|
);
|
|
63
|
-
|
|
69
|
+
|
|
64
70
|
this.emit('reconnecting', {
|
|
65
71
|
attempt: this.reconnectAttempts + 1,
|
|
66
72
|
maxAttempts: this.maxReconnectAttempts,
|
|
@@ -68,16 +74,23 @@ export class WebSocketClient extends AbstractSocketClient {
|
|
|
68
74
|
code,
|
|
69
75
|
reason: reason?.toString() || 'Connection closed'
|
|
70
76
|
});
|
|
71
|
-
|
|
77
|
+
|
|
72
78
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
73
|
-
|
|
79
|
+
|
|
80
|
+
// If close() was called during the delay, abort reconnect
|
|
81
|
+
if (this._destroyed) {
|
|
82
|
+
this.reconnecting = false;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
this.reconnectAttempts++;
|
|
75
87
|
this.reconnecting = false;
|
|
76
|
-
|
|
88
|
+
|
|
77
89
|
this.connect();
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
async close() {
|
|
93
|
+
this._destroyed = true;
|
|
81
94
|
this.reconnecting = false;
|
|
82
95
|
this.reconnectAttempts = 0;
|
|
83
96
|
|
package/lib/Socket/chats.js
CHANGED
|
@@ -13,7 +13,7 @@ import { USyncQuery, USyncUser } from '../WAUSync/index.js';
|
|
|
13
13
|
import { makeSocket } from './socket.js';
|
|
14
14
|
const MAX_SYNC_ATTEMPTS = 2;
|
|
15
15
|
export const makeChatsSocket = (config) => {
|
|
16
|
-
const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config;
|
|
16
|
+
const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage, awaitingSyncTimeoutMs = 5000 } = config;
|
|
17
17
|
const sock = makeSocket(config);
|
|
18
18
|
const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession } = sock;
|
|
19
19
|
let privacySettings;
|
|
@@ -30,7 +30,7 @@ export const makeChatsSocket = (config) => {
|
|
|
30
30
|
let awaitingSyncTimeout;
|
|
31
31
|
const placeholderResendCache = config.placeholderResendCache ||
|
|
32
32
|
new LRUCache({
|
|
33
|
-
max:
|
|
33
|
+
max: 250,
|
|
34
34
|
ttl: DEFAULT_CACHE_TTLS.MSG_RETRY * 1000, // 1 hour
|
|
35
35
|
allowStale: false
|
|
36
36
|
});
|
|
@@ -1006,18 +1006,17 @@ export const makeChatsSocket = (config) => {
|
|
|
1006
1006
|
setTimeout(() => ev.flush(), 0);
|
|
1007
1007
|
return;
|
|
1008
1008
|
}
|
|
1009
|
-
logger.info('History sync is enabled, awaiting notification
|
|
1009
|
+
logger.info({ awaitingSyncTimeoutMs }, 'History sync is enabled, awaiting notification.');
|
|
1010
1010
|
if (awaitingSyncTimeout) {
|
|
1011
1011
|
clearTimeout(awaitingSyncTimeout);
|
|
1012
1012
|
}
|
|
1013
1013
|
awaitingSyncTimeout = setTimeout(() => {
|
|
1014
1014
|
if (syncState === SyncState.AwaitingInitialSync) {
|
|
1015
|
-
|
|
1016
|
-
logger.warn('Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer');
|
|
1015
|
+
logger.warn({ awaitingSyncTimeoutMs }, 'Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer');
|
|
1017
1016
|
syncState = SyncState.Online;
|
|
1018
1017
|
ev.flush();
|
|
1019
1018
|
}
|
|
1020
|
-
},
|
|
1019
|
+
}, awaitingSyncTimeoutMs);
|
|
1021
1020
|
});
|
|
1022
1021
|
ev.on('lid-mapping.update', async ({ lid, pn }) => {
|
|
1023
1022
|
try {
|
|
@@ -19,25 +19,25 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
19
19
|
const retryMutex = makeMutex();
|
|
20
20
|
const msgRetryCache = config.msgRetryCounterCache ||
|
|
21
21
|
new LRUCache({
|
|
22
|
-
max:
|
|
22
|
+
max: 1000,
|
|
23
23
|
ttl: DEFAULT_CACHE_TTLS.MSG_RETRY * 1000,
|
|
24
24
|
allowStale: false
|
|
25
25
|
});
|
|
26
26
|
const callOfferCache = config.callOfferCache ||
|
|
27
27
|
new LRUCache({
|
|
28
|
-
max:
|
|
28
|
+
max: 500,
|
|
29
29
|
ttl: DEFAULT_CACHE_TTLS.CALL_OFFER * 1000,
|
|
30
30
|
allowStale: false
|
|
31
31
|
});
|
|
32
32
|
const placeholderResendCache = config.placeholderResendCache ||
|
|
33
33
|
new LRUCache({
|
|
34
|
-
max:
|
|
34
|
+
max: 250,
|
|
35
35
|
ttl: DEFAULT_CACHE_TTLS.MSG_RETRY * 1000,
|
|
36
36
|
allowStale: false
|
|
37
37
|
});
|
|
38
38
|
// Debounce identity-change session refreshes per JID to avoid bursts
|
|
39
39
|
const identityAssertDebounce = new LRUCache({
|
|
40
|
-
max:
|
|
40
|
+
max: 250,
|
|
41
41
|
ttl: 10000,
|
|
42
42
|
allowStale: false
|
|
43
43
|
});
|
|
@@ -71,7 +71,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
71
71
|
// metadata (LID details, timestamps, etc.) that the phone may omit
|
|
72
72
|
await placeholderResendCache.set(messageKey?.id, msgData || true);
|
|
73
73
|
}
|
|
74
|
-
await delay(
|
|
74
|
+
await delay(50);
|
|
75
75
|
if (!(await placeholderResendCache.get(messageKey?.id))) {
|
|
76
76
|
logger.debug({ messageKey }, 'message received while resend requested');
|
|
77
77
|
return 'RESOLVED';
|
|
@@ -1312,7 +1312,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1312
1312
|
logger.debug('Uploading pre-keys for error recovery');
|
|
1313
1313
|
await uploadPreKeys(5);
|
|
1314
1314
|
logger.debug('Waiting for server to process new pre-keys');
|
|
1315
|
-
await delay(
|
|
1315
|
+
await delay(500);
|
|
1316
1316
|
}
|
|
1317
1317
|
catch (uploadErr) {
|
|
1318
1318
|
logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
|
|
@@ -1582,7 +1582,12 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1582
1582
|
let isProcessing = false;
|
|
1583
1583
|
// Number of nodes to process before yielding to event loop
|
|
1584
1584
|
const BATCH_SIZE = 10;
|
|
1585
|
+
const MAX_OFFLINE_NODES = 500;
|
|
1585
1586
|
const enqueue = (type, node) => {
|
|
1587
|
+
if (nodes.length >= MAX_OFFLINE_NODES) {
|
|
1588
|
+
logger.warn({ queueSize: nodes.length }, 'offline node queue full, dropping oldest node');
|
|
1589
|
+
nodes.shift();
|
|
1590
|
+
}
|
|
1586
1591
|
nodes.push({ type, node });
|
|
1587
1592
|
if (isProcessing) {
|
|
1588
1593
|
return;
|
|
@@ -16,22 +16,22 @@ import { makeNewsletterSocket } from './newsletter.js';
|
|
|
16
16
|
const _isNewsletterJid = (jid) => typeof jid === 'string' && jid.endsWith('@newsletter');
|
|
17
17
|
|
|
18
18
|
export const makeMessagesSocket = (config) => {
|
|
19
|
-
const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount } = config;
|
|
19
|
+
const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount, phoneRequestDelayMs } = 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
23
|
new LRUCache({
|
|
24
|
-
max:
|
|
24
|
+
max: 500,
|
|
25
25
|
ttl: DEFAULT_CACHE_TTLS.USER_DEVICES * 1000, // 5 minutes
|
|
26
26
|
allowStale: false
|
|
27
27
|
});
|
|
28
28
|
const peerSessionsCache = new LRUCache({
|
|
29
|
-
max:
|
|
29
|
+
max: 500,
|
|
30
30
|
ttl: DEFAULT_CACHE_TTLS.USER_DEVICES * 1000,
|
|
31
31
|
allowStale: false
|
|
32
32
|
});
|
|
33
33
|
// Initialize message retry manager if enabled
|
|
34
|
-
const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null;
|
|
34
|
+
const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount, phoneRequestDelayMs) : null;
|
|
35
35
|
// Prevent race conditions in Signal session encryption by user
|
|
36
36
|
const encryptionMutex = makeKeyedMutex();
|
|
37
37
|
let mediaConn;
|
|
@@ -1389,7 +1389,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
1389
1389
|
: { is_group_status_mention: 'true' }
|
|
1390
1390
|
}]
|
|
1391
1391
|
});
|
|
1392
|
-
await delay(
|
|
1392
|
+
await delay(50);
|
|
1393
1393
|
} catch (error) {
|
|
1394
1394
|
logger.error(`Error sending status mention to ${id}: ${error}`);
|
|
1395
1395
|
}
|
|
@@ -42,6 +42,7 @@ export const makeInMemoryStore = (config) => {
|
|
|
42
42
|
const MAX_MESSAGES_PER_CHAT = config.maxMessagesPerChat || 2000;
|
|
43
43
|
const MAX_CHATS = config.maxChats || 10000;
|
|
44
44
|
const MAX_CONTACTS = config.maxContacts || 20000;
|
|
45
|
+
const MAX_PRESENCES = config.maxPresences || 500;
|
|
45
46
|
const MESSAGE_CLEANUP_INTERVAL = config.messageCleanupIntervalMs || 5 * 60 * 1000; // 5 min
|
|
46
47
|
let cleanupTimer = null;
|
|
47
48
|
|
|
@@ -70,11 +71,22 @@ export const makeInMemoryStore = (config) => {
|
|
|
70
71
|
if (chatId) {
|
|
71
72
|
chats.deleteById(chatId);
|
|
72
73
|
delete messages[chatId];
|
|
74
|
+
delete groupMetadata[chatId];
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
logger.debug({ removed: toRemove }, 'cleaned old chats due to memory limit');
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
// Limit presences
|
|
81
|
+
const presenceIds = Object.keys(presences);
|
|
82
|
+
if (presenceIds.length > MAX_PRESENCES) {
|
|
83
|
+
const toRemove = presenceIds.length - MAX_PRESENCES;
|
|
84
|
+
for (let i = 0; i < toRemove; i++) {
|
|
85
|
+
delete presences[presenceIds[i]];
|
|
86
|
+
}
|
|
87
|
+
logger.debug({ removed: toRemove }, 'cleaned old presences due to memory limit');
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
// Limit contacts
|
|
79
91
|
const contactIds = Object.keys(contacts);
|
|
80
92
|
if (contactIds.length > MAX_CONTACTS) {
|
package/lib/Utils/auth-utils.js
CHANGED
|
@@ -16,7 +16,7 @@ import { PreKeyManager } from './pre-key-manager.js';
|
|
|
16
16
|
export function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
17
17
|
const cache = _cache ||
|
|
18
18
|
new LRUCache({
|
|
19
|
-
max:
|
|
19
|
+
max: 500,
|
|
20
20
|
ttl: DEFAULT_CACHE_TTLS.SIGNAL_STORE * 1000, // 5 minutes
|
|
21
21
|
allowStale: false
|
|
22
22
|
});
|
|
@@ -24,6 +24,7 @@ const BUFFERABLE_EVENT_SET = new Set(BUFFERABLE_EVENT);
|
|
|
24
24
|
*/
|
|
25
25
|
export const makeEventBuffer = (logger, config = {}) => {
|
|
26
26
|
const ev = new EventEmitter();
|
|
27
|
+
ev.setMaxListeners(0); // Unlock listener limit for multi-bot environments
|
|
27
28
|
const historyCache = new Set();
|
|
28
29
|
let data = makeBufferData();
|
|
29
30
|
let isBuffering = false;
|
|
@@ -157,7 +158,7 @@ export const makeEventBuffer = (logger, config = {}) => {
|
|
|
157
158
|
if (isBuffering && bufferCount === 1) {
|
|
158
159
|
flush();
|
|
159
160
|
}
|
|
160
|
-
},
|
|
161
|
+
}, 10);
|
|
161
162
|
activeBufferedTimeouts.add(t);
|
|
162
163
|
}
|
|
163
164
|
return result;
|
|
@@ -169,7 +170,7 @@ export const makeEventBuffer = (logger, config = {}) => {
|
|
|
169
170
|
bufferCount = Math.max(0, bufferCount - 1);
|
|
170
171
|
if (bufferCount === 0) {
|
|
171
172
|
if (!flushPendingTimeout) {
|
|
172
|
-
flushPendingTimeout = setTimeout(flush,
|
|
173
|
+
flushPendingTimeout = setTimeout(flush, 10);
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
}
|
package/lib/Utils/generics.js
CHANGED
|
@@ -281,7 +281,6 @@ export const getStatusFromReceiptType = (type) => {
|
|
|
281
281
|
return status;
|
|
282
282
|
};
|
|
283
283
|
const CODE_MAP = {
|
|
284
|
-
conflict: DisconnectReason.connectionReplaced,
|
|
285
284
|
'stream-replaced': DisconnectReason.streamReplaced,
|
|
286
285
|
conflict: DisconnectReason.conflict,
|
|
287
286
|
'precondition-failed': DisconnectReason.preconditionFailed
|
|
@@ -4,7 +4,7 @@ const RECENT_MESSAGES_SIZE = 512;
|
|
|
4
4
|
const MESSAGE_KEY_SEPARATOR = '\u0000';
|
|
5
5
|
/** Timeout for session recreation - 1 hour */
|
|
6
6
|
const RECREATE_SESSION_TIMEOUT = 60 * 60 * 1000; // 1 hour in milliseconds
|
|
7
|
-
const
|
|
7
|
+
const DEFAULT_PHONE_REQUEST_DELAY = 2000;
|
|
8
8
|
// Retry reason codes matching WhatsApp Web's Signal error codes.
|
|
9
9
|
export var RetryReason;
|
|
10
10
|
(function (RetryReason) {
|
|
@@ -28,8 +28,9 @@ export var RetryReason;
|
|
|
28
28
|
/** Error codes that indicate a MAC failure and require immediate session recreation */
|
|
29
29
|
const MAC_ERROR_CODES = new Set([RetryReason.SignalErrorInvalidMessage, RetryReason.SignalErrorBadMac]);
|
|
30
30
|
export class MessageRetryManager {
|
|
31
|
-
constructor(logger, maxMsgRetryCount) {
|
|
31
|
+
constructor(logger, maxMsgRetryCount, phoneRequestDelayMs) {
|
|
32
32
|
this.logger = logger;
|
|
33
|
+
this.phoneRequestDelayMs = phoneRequestDelayMs ?? DEFAULT_PHONE_REQUEST_DELAY;
|
|
33
34
|
this.recentMessagesMap = new LRUCache({
|
|
34
35
|
max: RECENT_MESSAGES_SIZE,
|
|
35
36
|
ttl: 5 * 60 * 1000,
|
|
@@ -189,7 +190,7 @@ export class MessageRetryManager {
|
|
|
189
190
|
/**
|
|
190
191
|
* Schedule a phone request with delay
|
|
191
192
|
*/
|
|
192
|
-
schedulePhoneRequest(messageId, callback, delay =
|
|
193
|
+
schedulePhoneRequest(messageId, callback, delay = this.phoneRequestDelayMs) {
|
|
193
194
|
// Cancel any existing request for this message
|
|
194
195
|
this.cancelPendingPhoneRequest(messageId);
|
|
195
196
|
this.pendingPhoneRequests[messageId] = setTimeout(() => {
|