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.
@@ -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: 10000,
90
- maxContacts: 20000,
91
- messageCleanupIntervalMs: 5 * 60 * 1000, // 5 minutes
94
+ maxChats: 5000,
95
+ maxContacts: 10000,
96
+ maxPresences: 500,
97
+ messageCleanupIntervalMs: 2 * 60 * 1000, // 2 minutes
92
98
  maxHistoryCacheSize: 15000,
93
- bufferTimeoutMs: 30000,
94
- maxBufferCount: 10000
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
- const events = ['close', 'error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response'];
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
- this.socket.on('close', (code, reason) => {
47
- this.emit('close', code, reason);
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
- this.socketListeners.set('close-internal', (...args) => this.socket?.emit('close', ...args));
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
 
@@ -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: 5000,
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 with a 20s timeout.');
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
- // TODO: investigate
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
- }, 20000);
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: 10000,
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: 5000,
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: 5000,
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: 1000,
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(2000);
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(1000);
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: 5000,
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: 5000,
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(2000);
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) {
@@ -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: 5000,
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
- }, 100);
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, 100);
173
+ flushPendingTimeout = setTimeout(flush, 10);
173
174
  }
174
175
  }
175
176
  }
@@ -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 PHONE_REQUEST_DELAY = 3000;
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 = PHONE_REQUEST_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(() => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "violetics",
3
3
  "type": "module",
4
- "version": "7.0.16-alpha",
4
+ "version": "7.0.18-alpha",
5
5
  "description": "A WebSockets library for interacting with WhatsApp Web",
6
6
  "keywords": [
7
7
  "whatsapp",