violetics 7.0.3-alpha → 7.0.5-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/WAProto/index.js +502 -58
- package/lib/Defaults/index.js +1 -1
- package/lib/Socket/Client/websocket.js +10 -1
- package/lib/Socket/chats.js +3 -2
- package/lib/Socket/messages-recv.js +108 -146
- package/lib/Socket/socket.js +23 -13
- package/lib/Utils/event-buffer.js +2 -2
- package/lib/Utils/index.js +2 -1
- package/lib/Utils/message-retry-manager.js +1 -1
- package/lib/Utils/noise-handler.js +6 -0
- package/lib/Utils/offline-node-processor.js +39 -0
- package/lib/Utils/stanza-ack.js +37 -0
- package/lib/WABinary/generic-utils.js +9 -9
- package/package.json +2 -2
package/lib/Defaults/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export class WebSocketClient extends AbstractSocketClient {
|
|
|
5
5
|
constructor() {
|
|
6
6
|
super(...arguments);
|
|
7
7
|
this.socket = null;
|
|
8
|
+
this._eventForwarders = [];
|
|
8
9
|
}
|
|
9
10
|
get isOpen() {
|
|
10
11
|
return this.socket?.readyState === WebSocket.OPEN;
|
|
@@ -31,14 +32,22 @@ export class WebSocketClient extends AbstractSocketClient {
|
|
|
31
32
|
});
|
|
32
33
|
this.socket.setMaxListeners(0);
|
|
33
34
|
const events = ['close', 'error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response'];
|
|
35
|
+
this._eventForwarders = [];
|
|
34
36
|
for (const event of events) {
|
|
35
|
-
|
|
37
|
+
const handler = (...args) => this.emit(event, ...args);
|
|
38
|
+
this.socket?.on(event, handler);
|
|
39
|
+
this._eventForwarders.push({ event, handler });
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
async close() {
|
|
39
43
|
if (!this.socket) {
|
|
40
44
|
return;
|
|
41
45
|
}
|
|
46
|
+
// Remove all forwarding listeners to prevent memory leaks
|
|
47
|
+
for (const { event, handler } of this._eventForwarders) {
|
|
48
|
+
this.socket?.off(event, handler);
|
|
49
|
+
}
|
|
50
|
+
this._eventForwarders = [];
|
|
42
51
|
const closePromise = new Promise(resolve => {
|
|
43
52
|
this.socket?.once('close', resolve);
|
|
44
53
|
});
|
package/lib/Socket/chats.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import NodeCache from '@cacheable/node-cache';
|
|
2
|
+
import { LRUCache } from 'lru-cache';
|
|
2
3
|
import { Boom } from '@hapi/boom';
|
|
3
4
|
import { proto } from '../../WAProto/index.js';
|
|
4
5
|
import { DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js';
|
|
@@ -12,12 +13,12 @@ import { getBinaryNodeChild, getBinaryNodeChildren, isLidUser, isPnUser, jidDeco
|
|
|
12
13
|
import { USyncQuery, USyncUser } from '../WAUSync/index.js';
|
|
13
14
|
import { makeSocket } from './socket.js';
|
|
14
15
|
const MAX_SYNC_ATTEMPTS = 2;
|
|
15
|
-
// Lia@Note 08-02-26 --- I know it's not efficient for RSS ಥ‿ಥ
|
|
16
|
-
const USER_ID_CACHE = new Map();
|
|
17
16
|
export const makeChatsSocket = (config) => {
|
|
18
17
|
const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage } = config;
|
|
19
18
|
const sock = makeSocket(config);
|
|
20
19
|
const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession } = sock;
|
|
20
|
+
// Scoped per-connection with LRU to prevent unbounded memory growth
|
|
21
|
+
const USER_ID_CACHE = new LRUCache({ max: 5000, ttl: 30 * 60 * 1000 });
|
|
21
22
|
let privacySettings;
|
|
22
23
|
let syncState = SyncState.Connecting;
|
|
23
24
|
/** this mutex ensures that messages are processed in order */
|
|
@@ -7,6 +7,8 @@ import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_
|
|
|
7
7
|
import { WAMessageStatus, WAMessageStubType } from '../Types/index.js';
|
|
8
8
|
import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
|
|
9
9
|
import { makeMutex } from '../Utils/make-mutex.js';
|
|
10
|
+
import { makeOfflineNodeProcessor } from '../Utils/offline-node-processor.js';
|
|
11
|
+
import { buildAckStanza } from '../Utils/stanza-ack.js';
|
|
10
12
|
import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
11
13
|
import { extractGroupMetadata } from './groups.js';
|
|
12
14
|
import { makeMessagesSocket } from './messages-send.js';
|
|
@@ -63,7 +65,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
63
65
|
// metadata (LID details, timestamps, etc.) that the phone may omit
|
|
64
66
|
await placeholderResendCache.set(messageKey?.id, msgData || true);
|
|
65
67
|
}
|
|
66
|
-
await delay(
|
|
68
|
+
await delay(500);
|
|
67
69
|
if (!(await placeholderResendCache.get(messageKey?.id))) {
|
|
68
70
|
logger.debug({ messageKey }, 'message received while resend requested');
|
|
69
71
|
return 'RESOLVED';
|
|
@@ -217,32 +219,9 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
217
219
|
break;
|
|
218
220
|
}
|
|
219
221
|
};
|
|
220
|
-
const sendMessageAck = async (
|
|
221
|
-
const stanza =
|
|
222
|
-
|
|
223
|
-
attrs: {
|
|
224
|
-
id: attrs.id,
|
|
225
|
-
to: attrs.from,
|
|
226
|
-
class: tag
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
|
-
if (!!errorCode) {
|
|
230
|
-
stanza.attrs.error = errorCode.toString();
|
|
231
|
-
}
|
|
232
|
-
if (!!attrs.participant) {
|
|
233
|
-
stanza.attrs.participant = attrs.participant;
|
|
234
|
-
}
|
|
235
|
-
if (!!attrs.recipient) {
|
|
236
|
-
stanza.attrs.recipient = attrs.recipient;
|
|
237
|
-
}
|
|
238
|
-
if (!!attrs.type &&
|
|
239
|
-
(tag !== 'message' || getBinaryNodeChild({ tag, attrs, content }, 'unavailable') || errorCode !== 0)) {
|
|
240
|
-
stanza.attrs.type = attrs.type;
|
|
241
|
-
}
|
|
242
|
-
if (tag === 'message' && getBinaryNodeChild({ tag, attrs, content }, 'unavailable')) {
|
|
243
|
-
stanza.attrs.from = authState.creds.me.id;
|
|
244
|
-
}
|
|
245
|
-
logger.debug({ recv: { tag, attrs }, sent: stanza.attrs }, 'sent ack');
|
|
222
|
+
const sendMessageAck = async (node, errorCode) => {
|
|
223
|
+
const stanza = buildAckStanza(node, errorCode, authState.creds.me.id);
|
|
224
|
+
logger.debug({ recv: { tag: node.tag, attrs: node.attrs }, sent: stanza.attrs }, 'sent ack');
|
|
246
225
|
await sendNode(stanza);
|
|
247
226
|
};
|
|
248
227
|
const rejectCall = async (callId, callFrom) => {
|
|
@@ -995,7 +974,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
995
974
|
]);
|
|
996
975
|
}
|
|
997
976
|
finally {
|
|
998
|
-
await sendMessageAck(node);
|
|
977
|
+
await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack receipt'));
|
|
999
978
|
}
|
|
1000
979
|
};
|
|
1001
980
|
const handleNotification = async (node) => {
|
|
@@ -1030,7 +1009,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1030
1009
|
]);
|
|
1031
1010
|
}
|
|
1032
1011
|
finally {
|
|
1033
|
-
await sendMessageAck(node);
|
|
1012
|
+
await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack notification'));
|
|
1034
1013
|
}
|
|
1035
1014
|
};
|
|
1036
1015
|
const handleMessage = async (node) => {
|
|
@@ -1046,36 +1025,34 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1046
1025
|
await sendMessageAck(node, NACK_REASONS.MissingMessageSecret);
|
|
1047
1026
|
return;
|
|
1048
1027
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
await signalRepository.
|
|
1028
|
+
let acked = false;
|
|
1029
|
+
try {
|
|
1030
|
+
const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
|
|
1031
|
+
const alt = msg.key.participantAlt || msg.key.remoteJidAlt;
|
|
1032
|
+
// store new mappings we didn't have before
|
|
1033
|
+
if (!!alt) {
|
|
1034
|
+
const altServer = jidDecode(alt)?.server;
|
|
1035
|
+
const primaryJid = msg.key.participant || msg.key.remoteJid;
|
|
1036
|
+
if (altServer === 'lid') {
|
|
1037
|
+
if (!(await signalRepository.lidMapping.getPNForLID(alt))) {
|
|
1038
|
+
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]);
|
|
1039
|
+
await signalRepository.migrateSession(primaryJid, alt);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]);
|
|
1044
|
+
await signalRepository.migrateSession(alt, primaryJid);
|
|
1059
1045
|
}
|
|
1060
1046
|
}
|
|
1061
|
-
else {
|
|
1062
|
-
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]);
|
|
1063
|
-
await signalRepository.migrateSession(alt, primaryJid);
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) {
|
|
1067
|
-
messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
|
|
1068
|
-
logger.debug({
|
|
1069
|
-
jid: msg.key.remoteJid,
|
|
1070
|
-
id: msg.key.id
|
|
1071
|
-
}, 'Added message to recent cache for retry receipts');
|
|
1072
|
-
}
|
|
1073
|
-
try {
|
|
1074
1047
|
await messageMutex.mutex(async () => {
|
|
1075
1048
|
await decrypt();
|
|
1049
|
+
if (msg.key?.remoteJid && msg.key?.id && msg.message && messageRetryManager) {
|
|
1050
|
+
messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
|
|
1051
|
+
}
|
|
1076
1052
|
// message failed to decrypt
|
|
1077
1053
|
if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
|
|
1078
1054
|
if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
|
|
1055
|
+
acked = true;
|
|
1079
1056
|
return sendMessageAck(node, NACK_REASONS.ParsingError);
|
|
1080
1057
|
}
|
|
1081
1058
|
if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
|
|
@@ -1087,11 +1064,13 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1087
1064
|
unavailableType === 'hosted_unavailable_fanout' ||
|
|
1088
1065
|
unavailableType === 'view_once_unavailable_fanout') {
|
|
1089
1066
|
logger.debug({ msgId: msg.key.id, unavailableType }, 'skipping placeholder resend for excluded unavailable type');
|
|
1067
|
+
acked = true;
|
|
1090
1068
|
return sendMessageAck(node);
|
|
1091
1069
|
}
|
|
1092
1070
|
const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
|
|
1093
1071
|
if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) {
|
|
1094
1072
|
logger.debug({ msgId: msg.key.id, messageAge }, 'skipping placeholder resend for old message');
|
|
1073
|
+
acked = true;
|
|
1095
1074
|
return sendMessageAck(node);
|
|
1096
1075
|
}
|
|
1097
1076
|
// Request the real content from the phone via placeholder resend PDO.
|
|
@@ -1115,19 +1094,20 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1115
1094
|
};
|
|
1116
1095
|
requestPlaceholderResend(cleanKey, msgData)
|
|
1117
1096
|
.then(requestId => {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1097
|
+
if (requestId && requestId !== 'RESOLVED') {
|
|
1098
|
+
logger.debug({ msgId: msg.key.id, requestId }, 'requested placeholder resend for unavailable message');
|
|
1099
|
+
ev.emit('messages.update', [
|
|
1100
|
+
{
|
|
1101
|
+
key: msg.key,
|
|
1102
|
+
update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] }
|
|
1103
|
+
}
|
|
1104
|
+
]);
|
|
1105
|
+
}
|
|
1106
|
+
})
|
|
1128
1107
|
.catch(err => {
|
|
1129
|
-
|
|
1130
|
-
|
|
1108
|
+
logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend for unavailable message');
|
|
1109
|
+
});
|
|
1110
|
+
acked = true;
|
|
1131
1111
|
await sendMessageAck(node);
|
|
1132
1112
|
// Don't return — fall through to upsertMessage so the stub is emitted
|
|
1133
1113
|
}
|
|
@@ -1137,6 +1117,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1137
1117
|
const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
|
|
1138
1118
|
if (messageAge > STATUS_EXPIRY_SECONDS) {
|
|
1139
1119
|
logger.debug({ msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, 'skipping retry for expired status message');
|
|
1120
|
+
acked = true;
|
|
1140
1121
|
return sendMessageAck(node);
|
|
1141
1122
|
}
|
|
1142
1123
|
}
|
|
@@ -1157,7 +1138,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1157
1138
|
logger.debug('Uploading pre-keys for error recovery');
|
|
1158
1139
|
await uploadPreKeys(5);
|
|
1159
1140
|
logger.debug('Waiting for server to process new pre-keys');
|
|
1160
|
-
await delay(
|
|
1141
|
+
await delay(200);
|
|
1161
1142
|
}
|
|
1162
1143
|
catch (uploadErr) {
|
|
1163
1144
|
logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
|
|
@@ -1180,6 +1161,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1180
1161
|
logger.error({ retryErr }, 'Failed to send retry after error handling');
|
|
1181
1162
|
}
|
|
1182
1163
|
}
|
|
1164
|
+
acked = true;
|
|
1183
1165
|
await sendMessageAck(node, NACK_REASONS.UnhandledError);
|
|
1184
1166
|
});
|
|
1185
1167
|
}
|
|
@@ -1208,6 +1190,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1208
1190
|
else if (!sendActiveReceipts) {
|
|
1209
1191
|
type = 'inactive';
|
|
1210
1192
|
}
|
|
1193
|
+
acked = true;
|
|
1211
1194
|
await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type);
|
|
1212
1195
|
// send ack for history message
|
|
1213
1196
|
const isAnyHistoryMsg = getHistoryMsg(msg.message);
|
|
@@ -1217,6 +1200,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1217
1200
|
}
|
|
1218
1201
|
}
|
|
1219
1202
|
else {
|
|
1203
|
+
acked = true;
|
|
1220
1204
|
await sendMessageAck(node);
|
|
1221
1205
|
logger.debug({ key: msg.key }, 'processed newsletter message without receipts');
|
|
1222
1206
|
}
|
|
@@ -1227,45 +1211,55 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1227
1211
|
}
|
|
1228
1212
|
catch (error) {
|
|
1229
1213
|
logger.error({ error, node: binaryNodeToString(node) }, 'error in handling message');
|
|
1214
|
+
if (!acked) {
|
|
1215
|
+
await sendMessageAck(node, NACK_REASONS.UnhandledError).catch(ackErr => logger.error({ ackErr }, 'failed to ack message after error'));
|
|
1216
|
+
}
|
|
1230
1217
|
}
|
|
1231
1218
|
};
|
|
1232
1219
|
const handleCall = async (node) => {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1220
|
+
try {
|
|
1221
|
+
const { attrs } = node;
|
|
1222
|
+
const [infoChild] = getAllBinaryNodeChildren(node);
|
|
1223
|
+
if (!infoChild) {
|
|
1224
|
+
throw new Boom('Missing call info in call node');
|
|
1225
|
+
}
|
|
1226
|
+
const status = getCallStatusFromNode(infoChild);
|
|
1227
|
+
const callId = infoChild.attrs['call-id'];
|
|
1228
|
+
const from = infoChild.attrs.from || infoChild.attrs['call-creator'];
|
|
1229
|
+
const call = {
|
|
1230
|
+
chatId: attrs.from,
|
|
1231
|
+
from,
|
|
1232
|
+
callerPn: infoChild.attrs['caller_pn'],
|
|
1233
|
+
id: callId,
|
|
1234
|
+
date: new Date(+attrs.t * 1000),
|
|
1235
|
+
offline: !!attrs.offline,
|
|
1236
|
+
status
|
|
1237
|
+
};
|
|
1238
|
+
if (status === 'offer') {
|
|
1239
|
+
call.isVideo = !!getBinaryNodeChild(infoChild, 'video');
|
|
1240
|
+
call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid'];
|
|
1241
|
+
call.groupJid = infoChild.attrs['group-jid'];
|
|
1242
|
+
await callOfferCache.set(call.id, call);
|
|
1243
|
+
}
|
|
1244
|
+
const existingCall = await callOfferCache.get(call.id);
|
|
1245
|
+
// use existing call info to populate this event
|
|
1246
|
+
if (existingCall) {
|
|
1247
|
+
call.isVideo = existingCall.isVideo;
|
|
1248
|
+
call.isGroup = existingCall.isGroup;
|
|
1249
|
+
call.callerPn = call.callerPn || existingCall.callerPn;
|
|
1250
|
+
}
|
|
1251
|
+
// delete data once call has ended
|
|
1252
|
+
if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
|
|
1253
|
+
await callOfferCache.del(call.id);
|
|
1254
|
+
}
|
|
1255
|
+
ev.emit('call', [call]);
|
|
1255
1256
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
if (existingCall) {
|
|
1259
|
-
call.isVideo = existingCall.isVideo;
|
|
1260
|
-
call.isGroup = existingCall.isGroup;
|
|
1261
|
-
call.callerPn = call.callerPn || existingCall.callerPn;
|
|
1257
|
+
catch (error) {
|
|
1258
|
+
logger.error({ error, node: binaryNodeToString(node) }, 'error in handling call');
|
|
1262
1259
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
await callOfferCache.del(call.id);
|
|
1260
|
+
finally {
|
|
1261
|
+
await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack call'));
|
|
1266
1262
|
}
|
|
1267
|
-
ev.emit('call', [call]);
|
|
1268
|
-
await sendMessageAck(node);
|
|
1269
1263
|
};
|
|
1270
1264
|
const handleBadAck = async ({ attrs }) => {
|
|
1271
1265
|
const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id };
|
|
@@ -1314,58 +1308,26 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1314
1308
|
/// and adds the task to the existing buffer if we're buffering events
|
|
1315
1309
|
const processNodeWithBuffer = async (node, identifier, exec) => {
|
|
1316
1310
|
ev.buffer();
|
|
1317
|
-
|
|
1318
|
-
|
|
1311
|
+
try {
|
|
1312
|
+
await execTask();
|
|
1313
|
+
}
|
|
1314
|
+
finally {
|
|
1315
|
+
ev.flush();
|
|
1316
|
+
}
|
|
1319
1317
|
function execTask() {
|
|
1320
1318
|
return exec(node, false).catch(err => onUnexpectedError(err, identifier));
|
|
1321
1319
|
}
|
|
1322
1320
|
};
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
]);
|
|
1334
|
-
const nodes = [];
|
|
1335
|
-
let isProcessing = false;
|
|
1336
|
-
// Number of nodes to process before yielding to event loop
|
|
1337
|
-
const BATCH_SIZE = 10;
|
|
1338
|
-
const enqueue = (type, node) => {
|
|
1339
|
-
nodes.push({ type, node });
|
|
1340
|
-
if (isProcessing) {
|
|
1341
|
-
return;
|
|
1342
|
-
}
|
|
1343
|
-
isProcessing = true;
|
|
1344
|
-
const promise = async () => {
|
|
1345
|
-
let processedInBatch = 0;
|
|
1346
|
-
while (nodes.length && ws.isOpen) {
|
|
1347
|
-
const { type, node } = nodes.shift();
|
|
1348
|
-
const nodeProcessor = nodeProcessorMap.get(type);
|
|
1349
|
-
if (!nodeProcessor) {
|
|
1350
|
-
onUnexpectedError(new Error(`unknown offline node type: ${type}`), 'processing offline node');
|
|
1351
|
-
continue;
|
|
1352
|
-
}
|
|
1353
|
-
await nodeProcessor(node);
|
|
1354
|
-
processedInBatch++;
|
|
1355
|
-
// Yield to event loop after processing a batch
|
|
1356
|
-
// This prevents blocking the event loop for too long when there are many offline nodes
|
|
1357
|
-
if (processedInBatch >= BATCH_SIZE) {
|
|
1358
|
-
processedInBatch = 0;
|
|
1359
|
-
await yieldToEventLoop();
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
isProcessing = false;
|
|
1363
|
-
};
|
|
1364
|
-
promise().catch(error => onUnexpectedError(error, 'processing offline nodes'));
|
|
1365
|
-
};
|
|
1366
|
-
return { enqueue };
|
|
1367
|
-
};
|
|
1368
|
-
const offlineNodeProcessor = makeOfflineNodeProcessor();
|
|
1321
|
+
const offlineNodeProcessor = makeOfflineNodeProcessor(new Map([
|
|
1322
|
+
['message', handleMessage],
|
|
1323
|
+
['call', handleCall],
|
|
1324
|
+
['receipt', handleReceipt],
|
|
1325
|
+
['notification', handleNotification]
|
|
1326
|
+
]), {
|
|
1327
|
+
isWsOpen: () => ws.isOpen,
|
|
1328
|
+
onUnexpectedError,
|
|
1329
|
+
yieldToEventLoop: () => new Promise(resolve => setImmediate(resolve))
|
|
1330
|
+
});
|
|
1369
1331
|
const processNode = async (type, node, identifier, exec) => {
|
|
1370
1332
|
const isOffline = !!node.attrs.offline;
|
|
1371
1333
|
if (isOffline) {
|
package/lib/Socket/socket.js
CHANGED
|
@@ -123,12 +123,9 @@ export const makeSocket = (config) => {
|
|
|
123
123
|
node.attrs.id = generateMessageTag();
|
|
124
124
|
}
|
|
125
125
|
const msgId = node.attrs.id;
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
.then(async () => resolve(await result))
|
|
130
|
-
.catch(reject);
|
|
131
|
-
});
|
|
126
|
+
const resultPromise = waitForMessage(msgId, timeoutMs);
|
|
127
|
+
await sendNode(node);
|
|
128
|
+
const result = await resultPromise;
|
|
132
129
|
if (result && 'tag' in result) {
|
|
133
130
|
assertNodeErrorFree(result);
|
|
134
131
|
}
|
|
@@ -483,15 +480,18 @@ export const makeSocket = (config) => {
|
|
|
483
480
|
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed');
|
|
484
481
|
clearInterval(keepAliveReq);
|
|
485
482
|
clearTimeout(qrTimer);
|
|
486
|
-
|
|
487
|
-
ws.removeAllListeners(
|
|
488
|
-
ws.removeAllListeners('message');
|
|
483
|
+
// Clear all WS listeners (CB:*, TAG:*, frame, error, etc.) to prevent memory leaks
|
|
484
|
+
ws.removeAllListeners();
|
|
489
485
|
if (!ws.isClosed && !ws.isClosing) {
|
|
490
486
|
try {
|
|
491
487
|
await ws.close();
|
|
492
488
|
}
|
|
493
489
|
catch { }
|
|
494
490
|
}
|
|
491
|
+
// Clean up noise handler buffers
|
|
492
|
+
if (noise.destroy) {
|
|
493
|
+
noise.destroy();
|
|
494
|
+
}
|
|
495
495
|
ev.emit('connection.update', {
|
|
496
496
|
connection: 'close',
|
|
497
497
|
lastDisconnect: {
|
|
@@ -499,7 +499,11 @@ export const makeSocket = (config) => {
|
|
|
499
499
|
date: new Date()
|
|
500
500
|
}
|
|
501
501
|
});
|
|
502
|
+
// Remove only internal socket listeners — preserve user-registered listeners
|
|
502
503
|
ev.removeAllListeners('connection.update');
|
|
504
|
+
ev.removeAllListeners('creds.update');
|
|
505
|
+
ev.removeAllListeners('call');
|
|
506
|
+
ev.removeAllListeners('lid-mapping.update');
|
|
503
507
|
};
|
|
504
508
|
const waitForSocketOpen = async () => {
|
|
505
509
|
if (ws.isOpen) {
|
|
@@ -522,7 +526,8 @@ export const makeSocket = (config) => {
|
|
|
522
526
|
ws.off('error', onClose);
|
|
523
527
|
});
|
|
524
528
|
};
|
|
525
|
-
const startKeepAliveRequest = () =>
|
|
529
|
+
const startKeepAliveRequest = () => {
|
|
530
|
+
keepAliveReq = setInterval(() => {
|
|
526
531
|
if (!lastDateRecv) {
|
|
527
532
|
lastDateRecv = new Date();
|
|
528
533
|
}
|
|
@@ -552,7 +557,12 @@ export const makeSocket = (config) => {
|
|
|
552
557
|
else {
|
|
553
558
|
logger.warn('keep alive called when WS not open');
|
|
554
559
|
}
|
|
555
|
-
}, keepAliveIntervalMs)
|
|
560
|
+
}, keepAliveIntervalMs);
|
|
561
|
+
// unref so the interval doesn't prevent Node.js process from exiting
|
|
562
|
+
if (keepAliveReq?.unref) {
|
|
563
|
+
keepAliveReq.unref();
|
|
564
|
+
}
|
|
565
|
+
};
|
|
556
566
|
/** i have no idea why this exists. pls enlighten me */
|
|
557
567
|
const sendPassiveIq = (tag) => query({
|
|
558
568
|
tag: 'iq',
|
|
@@ -598,7 +608,7 @@ export const makeSocket = (config) => {
|
|
|
598
608
|
id: jidEncode(phoneNumber, 's.whatsapp.net'),
|
|
599
609
|
name: '~'
|
|
600
610
|
};
|
|
601
|
-
ev.emit('creds.update', authState.creds);
|
|
611
|
+
ev.emit('creds.update', { pairingCode: authState.creds.pairingCode, me: authState.creds.me });
|
|
602
612
|
await sendNode({
|
|
603
613
|
tag: 'iq',
|
|
604
614
|
attrs: {
|
|
@@ -810,7 +820,7 @@ export const makeSocket = (config) => {
|
|
|
810
820
|
const routingInfo = getBinaryNodeChild(edgeRoutingNode, 'routing_info');
|
|
811
821
|
if (routingInfo?.content) {
|
|
812
822
|
authState.creds.routingInfo = Buffer.from(routingInfo?.content);
|
|
813
|
-
ev.emit('creds.update', authState.creds);
|
|
823
|
+
ev.emit('creds.update', { routingInfo: authState.creds.routingInfo });
|
|
814
824
|
}
|
|
815
825
|
});
|
|
816
826
|
let didStartBuffer = false;
|
|
@@ -149,7 +149,7 @@ export const makeEventBuffer = (logger) => {
|
|
|
149
149
|
if (isBuffering && bufferCount === 1) {
|
|
150
150
|
flush();
|
|
151
151
|
}
|
|
152
|
-
},
|
|
152
|
+
}, 10); // Minimal delay to allow nested buffers without adding latency
|
|
153
153
|
}
|
|
154
154
|
return result;
|
|
155
155
|
}
|
|
@@ -161,7 +161,7 @@ export const makeEventBuffer = (logger) => {
|
|
|
161
161
|
if (bufferCount === 0) {
|
|
162
162
|
// Only schedule ONE timeout, not 10,000
|
|
163
163
|
if (!flushPendingTimeout) {
|
|
164
|
-
flushPendingTimeout = setTimeout(flush,
|
|
164
|
+
flushPendingTimeout = setTimeout(flush, 10);
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
}
|
package/lib/Utils/index.js
CHANGED
|
@@ -16,4 +16,5 @@ export * from './event-buffer.js';
|
|
|
16
16
|
export * from './process-message.js';
|
|
17
17
|
export * from './message-retry-manager.js';
|
|
18
18
|
export * from './browser-utils.js';
|
|
19
|
-
export * from './identity-change-handler.js';
|
|
19
|
+
export * from './identity-change-handler.js';
|
|
20
|
+
export * from './stanza-ack.js';
|
|
@@ -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 =
|
|
7
|
+
const PHONE_REQUEST_DELAY = 1000;
|
|
8
8
|
// Retry reason codes matching WhatsApp Web's Signal error codes.
|
|
9
9
|
export var RetryReason;
|
|
10
10
|
(function (RetryReason) {
|
|
@@ -195,6 +195,12 @@ export const makeNoiseHandler = ({ keyPair: { private: privateKey, public: publi
|
|
|
195
195
|
inBytes = Buffer.concat([inBytes, newData]);
|
|
196
196
|
}
|
|
197
197
|
await processData(onFrame);
|
|
198
|
+
},
|
|
199
|
+
/** Clean up internal buffers to prevent memory leaks on connection close */
|
|
200
|
+
destroy() {
|
|
201
|
+
inBytes = Buffer.alloc(0);
|
|
202
|
+
transport = null;
|
|
203
|
+
pendingOnFrame = null;
|
|
198
204
|
}
|
|
199
205
|
};
|
|
200
206
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a processor for offline stanza nodes that:
|
|
3
|
+
* - Queues nodes for sequential processing
|
|
4
|
+
* - Yields to the event loop periodically to avoid blocking
|
|
5
|
+
* - Catches handler errors to prevent the processing loop from crashing
|
|
6
|
+
*/
|
|
7
|
+
export function makeOfflineNodeProcessor(nodeProcessorMap, deps, batchSize = 10) {
|
|
8
|
+
const nodes = [];
|
|
9
|
+
let isProcessing = false;
|
|
10
|
+
const enqueue = (type, node) => {
|
|
11
|
+
nodes.push({ type, node });
|
|
12
|
+
if (isProcessing) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
isProcessing = true;
|
|
16
|
+
const promise = async () => {
|
|
17
|
+
let processedInBatch = 0;
|
|
18
|
+
while (nodes.length && deps.isWsOpen()) {
|
|
19
|
+
const { type, node } = nodes.shift();
|
|
20
|
+
const nodeProcessor = nodeProcessorMap.get(type);
|
|
21
|
+
if (!nodeProcessor) {
|
|
22
|
+
deps.onUnexpectedError(new Error(`unknown offline node type: ${type}`), 'processing offline node');
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
await nodeProcessor(node).catch(err => deps.onUnexpectedError(err, `processing offline ${type}`));
|
|
26
|
+
processedInBatch++;
|
|
27
|
+
// Yield to event loop after processing a batch
|
|
28
|
+
// This prevents blocking the event loop for too long when there are many offline nodes
|
|
29
|
+
if (processedInBatch >= batchSize) {
|
|
30
|
+
processedInBatch = 0;
|
|
31
|
+
await deps.yieldToEventLoop();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
isProcessing = false;
|
|
35
|
+
};
|
|
36
|
+
promise().catch(error => deps.onUnexpectedError(error, 'processing offline nodes'));
|
|
37
|
+
};
|
|
38
|
+
return { enqueue };
|
|
39
|
+
}
|