violetics 7.0.17-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,10 +84,16 @@ 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
94
  maxChats: 5000,
90
95
  maxContacts: 10000,
96
+ maxPresences: 500,
91
97
  messageCleanupIntervalMs: 2 * 60 * 1000, // 2 minutes
92
98
  maxHistoryCacheSize: 15000,
93
99
  bufferTimeoutMs: 100,
@@ -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;
@@ -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 {
@@ -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(150);
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,7 +16,7 @@ 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 ||
@@ -31,7 +31,7 @@ export const makeMessagesSocket = (config) => {
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;
@@ -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) {
@@ -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.17-alpha",
4
+ "version": "7.0.18-alpha",
5
5
  "description": "A WebSockets library for interacting with WhatsApp Web",
6
6
  "keywords": [
7
7
  "whatsapp",