@kangwifi-pro/waliwa 1.0.0
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/LICENSE +21 -0
- package/README.md +600 -0
- package/dist/advanced/features.d.ts +211 -0
- package/dist/advanced/features.d.ts.map +1 -0
- package/dist/advanced/features.js +671 -0
- package/dist/advanced/features.js.map +1 -0
- package/dist/auth/auth-state.d.ts +89 -0
- package/dist/auth/auth-state.d.ts.map +1 -0
- package/dist/auth/auth-state.js +330 -0
- package/dist/auth/auth-state.js.map +1 -0
- package/dist/auth/pairing-code.d.ts +78 -0
- package/dist/auth/pairing-code.d.ts.map +1 -0
- package/dist/auth/pairing-code.js +191 -0
- package/dist/auth/pairing-code.js.map +1 -0
- package/dist/auth/qr-code.d.ts +60 -0
- package/dist/auth/qr-code.d.ts.map +1 -0
- package/dist/auth/qr-code.js +151 -0
- package/dist/auth/qr-code.js.map +1 -0
- package/dist/calls/handler.d.ts +61 -0
- package/dist/calls/handler.d.ts.map +1 -0
- package/dist/calls/handler.js +117 -0
- package/dist/calls/handler.js.map +1 -0
- package/dist/core/binary.d.ts +74 -0
- package/dist/core/binary.d.ts.map +1 -0
- package/dist/core/binary.js +448 -0
- package/dist/core/binary.js.map +1 -0
- package/dist/core/crypto.d.ts +60 -0
- package/dist/core/crypto.d.ts.map +1 -0
- package/dist/core/crypto.js +170 -0
- package/dist/core/crypto.js.map +1 -0
- package/dist/core/noise.d.ts +106 -0
- package/dist/core/noise.d.ts.map +1 -0
- package/dist/core/noise.js +307 -0
- package/dist/core/noise.js.map +1 -0
- package/dist/events/emitter.d.ts +44 -0
- package/dist/events/emitter.d.ts.map +1 -0
- package/dist/events/emitter.js +79 -0
- package/dist/events/emitter.js.map +1 -0
- package/dist/features/index.d.ts +342 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +755 -0
- package/dist/features/index.js.map +1 -0
- package/dist/fixes/index.d.ts +333 -0
- package/dist/fixes/index.d.ts.map +1 -0
- package/dist/fixes/index.js +762 -0
- package/dist/fixes/index.js.map +1 -0
- package/dist/groups/management.d.ts +86 -0
- package/dist/groups/management.d.ts.map +1 -0
- package/dist/groups/management.js +443 -0
- package/dist/groups/management.js.map +1 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +236 -0
- package/dist/index.js.map +1 -0
- package/dist/messages/media.d.ts +93 -0
- package/dist/messages/media.d.ts.map +1 -0
- package/dist/messages/media.js +252 -0
- package/dist/messages/media.js.map +1 -0
- package/dist/messages/message-encoder.d.ts +70 -0
- package/dist/messages/message-encoder.d.ts.map +1 -0
- package/dist/messages/message-encoder.js +453 -0
- package/dist/messages/message-encoder.js.map +1 -0
- package/dist/messages/send.d.ts +101 -0
- package/dist/messages/send.d.ts.map +1 -0
- package/dist/messages/send.js +409 -0
- package/dist/messages/send.js.map +1 -0
- package/dist/recovery/index.d.ts +125 -0
- package/dist/recovery/index.d.ts.map +1 -0
- package/dist/recovery/index.js +584 -0
- package/dist/recovery/index.js.map +1 -0
- package/dist/skdm/index.d.ts +220 -0
- package/dist/skdm/index.d.ts.map +1 -0
- package/dist/skdm/index.js +600 -0
- package/dist/skdm/index.js.map +1 -0
- package/dist/socket/hybrid.d.ts +118 -0
- package/dist/socket/hybrid.d.ts.map +1 -0
- package/dist/socket/hybrid.js +352 -0
- package/dist/socket/hybrid.js.map +1 -0
- package/dist/socket/wa-socket.d.ts +300 -0
- package/dist/socket/wa-socket.d.ts.map +1 -0
- package/dist/socket/wa-socket.js +1094 -0
- package/dist/socket/wa-socket.js.map +1 -0
- package/dist/socket/ws-socket.d.ts +96 -0
- package/dist/socket/ws-socket.d.ts.map +1 -0
- package/dist/socket/ws-socket.js +302 -0
- package/dist/socket/ws-socket.js.map +1 -0
- package/dist/types/index.d.ts +444 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/jid.d.ts +46 -0
- package/dist/utils/jid.d.ts.map +1 -0
- package/dist/utils/jid.js +152 -0
- package/dist/utils/jid.js.map +1 -0
- package/dist/utils/logger.d.ts +45 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +79 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/retry.d.ts +43 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +175 -0
- package/dist/utils/retry.js.map +1 -0
- package/docs/API.md +745 -0
- package/docs/ARCHITECTURE.md +307 -0
- package/docs/BAILEYS_FIXES.md +360 -0
- package/docs/FEATURES.md +532 -0
- package/docs/PROTOCOL.md +489 -0
- package/docs/RECOVERY.md +409 -0
- package/docs/SKDM.md +233 -0
- package/examples/ai-bot/index.ts +479 -0
- package/examples/ai-bot/package.json +17 -0
- package/examples/echo-bot/index.ts +171 -0
- package/examples/echo-bot/package.json +18 -0
- package/examples/group-bot/index.ts +557 -0
- package/examples/group-bot/package.json +17 -0
- package/examples/rest-gateway/index.ts +499 -0
- package/examples/rest-gateway/package.json +19 -0
- package/package.json +75 -0
- package/src/advanced/features.ts +817 -0
- package/src/auth/auth-state.ts +342 -0
- package/src/auth/pairing-code.ts +246 -0
- package/src/auth/qr-code.ts +191 -0
- package/src/calls/handler.ts +153 -0
- package/src/core/binary.ts +464 -0
- package/src/core/crypto.ts +189 -0
- package/src/core/noise.ts +406 -0
- package/src/events/emitter.ts +88 -0
- package/src/features/index.ts +921 -0
- package/src/fixes/index.ts +882 -0
- package/src/groups/management.ts +497 -0
- package/src/index.ts +274 -0
- package/src/messages/media.ts +372 -0
- package/src/messages/message-encoder.ts +520 -0
- package/src/messages/send.ts +521 -0
- package/src/recovery/index.ts +704 -0
- package/src/skdm/index.ts +693 -0
- package/src/socket/hybrid.ts +414 -0
- package/src/socket/wa-socket.ts +1347 -0
- package/src/socket/ws-socket.ts +355 -0
- package/src/types/index.ts +457 -0
- package/src/utils/jid.ts +156 -0
- package/src/utils/logger.ts +84 -0
- package/src/utils/retry.ts +205 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Waliwa - Baileys Issues Fixes
|
|
4
|
+
*
|
|
5
|
+
* Module yang berisi fix-fix untuk issue-issue yang sering dikeluhkan di Baileys:
|
|
6
|
+
*
|
|
7
|
+
* 1. Memory leak dari event listeners → cleanup otomatis dengan weak references
|
|
8
|
+
* 2. Connection hang → heartbeat dengan proper dead detection
|
|
9
|
+
* 3. Duplicate messages → idempotency cache dengan TTL
|
|
10
|
+
* 4. Group events missing → event coalescing
|
|
11
|
+
* 5. History sync incomplete → sync manager dengan resume support
|
|
12
|
+
* 6. Decryption failure → key sync + retry queue
|
|
13
|
+
* 7. Pre-key exhaustion → handled di SKDM (lihat src/skdm/index.ts)
|
|
14
|
+
* 8. Race condition → serialized message queue
|
|
15
|
+
* 9. Media upload timeout → chunked upload dengan resume
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.SmarterReconnector = exports.ChunkedMediaUploader = exports.MessageQueue = exports.DecryptionRetryQueue = exports.HistorySyncManager = exports.GroupEventCoalescer = exports.IdempotencyCache = exports.ConnectionHeartbeat = exports.WeakEventEmitter = void 0;
|
|
19
|
+
const events_1 = require("events");
|
|
20
|
+
const logger_js_1 = require("../utils/logger.js");
|
|
21
|
+
const index_js_1 = require("../skdm/index.js");
|
|
22
|
+
const jid_js_1 = require("../utils/jid.js");
|
|
23
|
+
// ============================================================
|
|
24
|
+
// FIX 1: Memory Leak - Auto-cleanup event listeners
|
|
25
|
+
// ============================================================
|
|
26
|
+
/**
|
|
27
|
+
* WeakEventEmitter - auto-cleanup listeners saat reference hilang.
|
|
28
|
+
* Fix Baileys issue: listeners gak di-cleanup properly, leak memory.
|
|
29
|
+
*
|
|
30
|
+
* Implementation: gunakan FinalizationRegistry untuk auto-remove listeners
|
|
31
|
+
* saat object yang subscribe di-GC'd.
|
|
32
|
+
*/
|
|
33
|
+
class WeakEventEmitter extends events_1.EventEmitter {
|
|
34
|
+
registry;
|
|
35
|
+
listenerMap = new Map();
|
|
36
|
+
nextId = 0;
|
|
37
|
+
constructor() {
|
|
38
|
+
super();
|
|
39
|
+
this.registry = new FinalizationRegistry((id) => {
|
|
40
|
+
const entry = this.listenerMap.get(id);
|
|
41
|
+
if (entry) {
|
|
42
|
+
this.removeListener('*', entry.listener);
|
|
43
|
+
this.listenerMap.delete(id);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Subscribe dengan auto-cleanup saat target di-GC
|
|
49
|
+
*/
|
|
50
|
+
subscribeWeak(target, event, callback) {
|
|
51
|
+
const id = `weak-${this.nextId++}`;
|
|
52
|
+
const listener = (payload) => callback(payload);
|
|
53
|
+
this.on(event, listener);
|
|
54
|
+
this.listenerMap.set(id, { target, listener });
|
|
55
|
+
this.registry.register(target, id);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Manual cleanup untuk semua listeners dari target
|
|
59
|
+
*/
|
|
60
|
+
cleanupTarget(target) {
|
|
61
|
+
for (const [id, entry] of this.listenerMap.entries()) {
|
|
62
|
+
if (entry.target === target) {
|
|
63
|
+
this.removeListener('*', entry.listener);
|
|
64
|
+
this.listenerMap.delete(id);
|
|
65
|
+
this.registry.unregister(target);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.WeakEventEmitter = WeakEventEmitter;
|
|
71
|
+
// ============================================================
|
|
72
|
+
// FIX 2: Connection Hang - Heartbeat dengan proper detection
|
|
73
|
+
// ============================================================
|
|
74
|
+
/**
|
|
75
|
+
* ConnectionHeartbeat - detect dead connections properly.
|
|
76
|
+
* Fix Baileys issue: connection hang, gak detect disconnect.
|
|
77
|
+
*
|
|
78
|
+
* Strategy:
|
|
79
|
+
* 1. Send ping setiap N seconds
|
|
80
|
+
* 2. Jika gak dapat pong dalam timeout, mark sebagai dead
|
|
81
|
+
* 3. Trigger reconnect
|
|
82
|
+
*/
|
|
83
|
+
class ConnectionHeartbeat {
|
|
84
|
+
logger;
|
|
85
|
+
pingIntervalMs;
|
|
86
|
+
pongTimeoutMs;
|
|
87
|
+
sendPing;
|
|
88
|
+
onDead;
|
|
89
|
+
pingTimer = null;
|
|
90
|
+
pongTimer = null;
|
|
91
|
+
lastPongTime = Date.now();
|
|
92
|
+
missedPongs = 0;
|
|
93
|
+
maxMissedPongs;
|
|
94
|
+
isRunning = false;
|
|
95
|
+
constructor(opts) {
|
|
96
|
+
this.sendPing = opts.sendPing;
|
|
97
|
+
this.onDead = opts.onDead;
|
|
98
|
+
this.logger = opts.logger ?? new logger_js_1.SilentLogger();
|
|
99
|
+
this.pingIntervalMs = opts.pingIntervalMs ?? 20000; // 20s
|
|
100
|
+
this.pongTimeoutMs = opts.pongTimeoutMs ?? 10000; // 10s timeout
|
|
101
|
+
this.maxMissedPongs = opts.maxMissedPongs ?? 3;
|
|
102
|
+
}
|
|
103
|
+
start() {
|
|
104
|
+
if (this.isRunning)
|
|
105
|
+
return;
|
|
106
|
+
this.isRunning = true;
|
|
107
|
+
this.missedPongs = 0;
|
|
108
|
+
this.lastPongTime = Date.now();
|
|
109
|
+
this.pingTimer = setInterval(() => {
|
|
110
|
+
this.sendPingAndWatch().catch(err => {
|
|
111
|
+
this.logger.error(`Ping failed: ${err.message}`);
|
|
112
|
+
});
|
|
113
|
+
}, this.pingIntervalMs);
|
|
114
|
+
}
|
|
115
|
+
async sendPingAndWatch() {
|
|
116
|
+
// Set pong timeout
|
|
117
|
+
let gotPong = false;
|
|
118
|
+
this.pongTimer = setTimeout(() => {
|
|
119
|
+
if (!gotPong) {
|
|
120
|
+
this.missedPongs++;
|
|
121
|
+
this.logger.warn(`Missed pong ${this.missedPongs}/${this.maxMissedPongs}`);
|
|
122
|
+
if (this.missedPongs >= this.maxMissedPongs) {
|
|
123
|
+
this.logger.error(`Connection dead (missed ${this.missedPongs} pongs), triggering reconnect`);
|
|
124
|
+
this.onDead();
|
|
125
|
+
this.stop();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}, this.pongTimeoutMs);
|
|
129
|
+
try {
|
|
130
|
+
await this.sendPing();
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
this.logger.error(`Ping send failed: ${err.message}`);
|
|
134
|
+
this.onDead();
|
|
135
|
+
this.stop();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Mark bahwa pong diterima (reset missed counter)
|
|
140
|
+
*/
|
|
141
|
+
receivedPong() {
|
|
142
|
+
this.lastPongTime = Date.now();
|
|
143
|
+
this.missedPongs = 0;
|
|
144
|
+
if (this.pongTimer) {
|
|
145
|
+
clearTimeout(this.pongTimer);
|
|
146
|
+
this.pongTimer = null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
stop() {
|
|
150
|
+
this.isRunning = false;
|
|
151
|
+
if (this.pingTimer) {
|
|
152
|
+
clearInterval(this.pingTimer);
|
|
153
|
+
this.pingTimer = null;
|
|
154
|
+
}
|
|
155
|
+
if (this.pongTimer) {
|
|
156
|
+
clearTimeout(this.pongTimer);
|
|
157
|
+
this.pongTimer = null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
getLastPongTime() {
|
|
161
|
+
return this.lastPongTime;
|
|
162
|
+
}
|
|
163
|
+
isHealthy() {
|
|
164
|
+
return this.missedPongs < this.maxMissedPongs;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
exports.ConnectionHeartbeat = ConnectionHeartbeat;
|
|
168
|
+
// ============================================================
|
|
169
|
+
// FIX 3: Duplicate Messages - Idempotency cache
|
|
170
|
+
// ============================================================
|
|
171
|
+
/**
|
|
172
|
+
* IdempotencyCache - deduplicate incoming messages.
|
|
173
|
+
* Fix Baileys issue: messages received multiple times.
|
|
174
|
+
*
|
|
175
|
+
* Strategy:
|
|
176
|
+
* - Track last N message IDs per chat
|
|
177
|
+
* - Skip processing jika ID sudah pernah dilihat
|
|
178
|
+
* - TTL untuk auto-cleanup old IDs
|
|
179
|
+
*/
|
|
180
|
+
class IdempotencyCache {
|
|
181
|
+
cache;
|
|
182
|
+
ttlMs;
|
|
183
|
+
logger;
|
|
184
|
+
cleanupTimer = null;
|
|
185
|
+
constructor(maxSize = 10000, ttlMs = 24 * 60 * 60 * 1000, logger) {
|
|
186
|
+
this.cache = new index_js_1.LRUCache(maxSize);
|
|
187
|
+
this.ttlMs = ttlMs;
|
|
188
|
+
this.logger = logger ?? new logger_js_1.SilentLogger();
|
|
189
|
+
// Cleanup expired entries every hour
|
|
190
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), 60 * 60 * 1000);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Check if message ID already seen.
|
|
194
|
+
* Returns true jika duplicate (sudah pernah dilihat).
|
|
195
|
+
*/
|
|
196
|
+
isDuplicate(messageId) {
|
|
197
|
+
const key = messageId;
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
const seen = this.cache.get(key);
|
|
200
|
+
if (seen !== undefined && (now - seen) < this.ttlMs) {
|
|
201
|
+
this.logger.debug(`Duplicate message detected: ${messageId}`);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
// Mark as seen
|
|
205
|
+
this.cache.set(key, now);
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Check multiple messages - filter out duplicates
|
|
210
|
+
*/
|
|
211
|
+
filterDuplicates(messages) {
|
|
212
|
+
return messages.filter(msg => !this.isDuplicate(msg.key.id));
|
|
213
|
+
}
|
|
214
|
+
cleanup() {
|
|
215
|
+
// LRU cache auto-evicts, but we need to clean by TTL
|
|
216
|
+
// Iterate and remove expired
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
let cleaned = 0;
|
|
219
|
+
// Note: actual LRU handles size, TTL handled implicitly by LRU eviction
|
|
220
|
+
this.logger.debug(`Idempotency cache size: ${this.cache.size()}`);
|
|
221
|
+
}
|
|
222
|
+
destroy() {
|
|
223
|
+
if (this.cleanupTimer) {
|
|
224
|
+
clearInterval(this.cleanupTimer);
|
|
225
|
+
this.cleanupTimer = null;
|
|
226
|
+
}
|
|
227
|
+
this.cache.clear();
|
|
228
|
+
}
|
|
229
|
+
getStats() {
|
|
230
|
+
return this.cache.getStats();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
exports.IdempotencyCache = IdempotencyCache;
|
|
234
|
+
// ============================================================
|
|
235
|
+
// FIX 4: Group Events Missing - Event Coalescing
|
|
236
|
+
// ============================================================
|
|
237
|
+
/**
|
|
238
|
+
* GroupEventCoalescer - merge rapid group events.
|
|
239
|
+
* Fix Baileys issue: group participant events fired multiple times rapidly.
|
|
240
|
+
*
|
|
241
|
+
* Strategy: coalesce events dalam window 100ms, fire single event
|
|
242
|
+
*/
|
|
243
|
+
class GroupEventCoalescer {
|
|
244
|
+
pending = new Map();
|
|
245
|
+
timer = null;
|
|
246
|
+
windowMs;
|
|
247
|
+
onFlush;
|
|
248
|
+
logger;
|
|
249
|
+
constructor(onFlush, windowMs = 100, logger) {
|
|
250
|
+
this.onFlush = onFlush;
|
|
251
|
+
this.windowMs = windowMs;
|
|
252
|
+
this.logger = logger ?? new logger_js_1.SilentLogger();
|
|
253
|
+
}
|
|
254
|
+
addEvent(event) {
|
|
255
|
+
const key = `${(0, jid_js_1.jidToString)(event.id)}:${event.action}`;
|
|
256
|
+
const existing = this.pending.get(key);
|
|
257
|
+
if (existing) {
|
|
258
|
+
// Merge participants
|
|
259
|
+
for (const p of event.participants) {
|
|
260
|
+
existing.participants.add((0, jid_js_1.jidToString)(p));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
this.pending.set(key, {
|
|
265
|
+
participants: new Set(event.participants.map(p => (0, jid_js_1.jidToString)(p))),
|
|
266
|
+
action: event.action,
|
|
267
|
+
author: event.author ? (0, jid_js_1.jidToString)(event.author) : undefined,
|
|
268
|
+
timestamp: event.timestamp
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// Schedule flush if not already
|
|
272
|
+
if (!this.timer) {
|
|
273
|
+
this.timer = setTimeout(() => this.flush(), this.windowMs);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
flush() {
|
|
277
|
+
this.timer = null;
|
|
278
|
+
const events = [];
|
|
279
|
+
for (const [key, pending] of this.pending.entries()) {
|
|
280
|
+
const [groupJid] = key.split(':');
|
|
281
|
+
events.push({
|
|
282
|
+
id: groupJid,
|
|
283
|
+
participants: Array.from(pending.participants),
|
|
284
|
+
action: pending.action,
|
|
285
|
+
author: pending.author,
|
|
286
|
+
timestamp: pending.timestamp
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
this.pending.clear();
|
|
290
|
+
if (events.length > 0) {
|
|
291
|
+
this.logger.debug(`Coalesced ${events.length} group events`);
|
|
292
|
+
this.onFlush(events);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
destroy() {
|
|
296
|
+
if (this.timer) {
|
|
297
|
+
clearTimeout(this.timer);
|
|
298
|
+
this.timer = null;
|
|
299
|
+
}
|
|
300
|
+
this.pending.clear();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
exports.GroupEventCoalescer = GroupEventCoalescer;
|
|
304
|
+
// ============================================================
|
|
305
|
+
// FIX 5: History Sync Incomplete - Resume support
|
|
306
|
+
// ============================================================
|
|
307
|
+
/**
|
|
308
|
+
* HistorySyncManager - manage history sync dengan resume support.
|
|
309
|
+
* Fix Baileys issue: history sync incomplete, messages lost.
|
|
310
|
+
*
|
|
311
|
+
* Strategy:
|
|
312
|
+
* - Track sync progress per chat
|
|
313
|
+
* - Persist sync state ke file
|
|
314
|
+
* - Resume from last successful sync point
|
|
315
|
+
*/
|
|
316
|
+
class HistorySyncManager {
|
|
317
|
+
logger;
|
|
318
|
+
progress = new Map();
|
|
319
|
+
totalSynced = 0;
|
|
320
|
+
isSyncing = false;
|
|
321
|
+
onSyncComplete;
|
|
322
|
+
syncStartTime = 0;
|
|
323
|
+
constructor(logger, onSyncComplete) {
|
|
324
|
+
this.logger = logger ?? new logger_js_1.SilentLogger();
|
|
325
|
+
this.onSyncComplete = onSyncComplete;
|
|
326
|
+
}
|
|
327
|
+
startSync() {
|
|
328
|
+
this.isSyncing = true;
|
|
329
|
+
this.syncStartTime = Date.now();
|
|
330
|
+
this.totalSynced = 0;
|
|
331
|
+
this.logger.info('History sync started');
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Track sync progress per chat
|
|
335
|
+
*/
|
|
336
|
+
trackProgress(chatJid, messageId, timestamp) {
|
|
337
|
+
const existing = this.progress.get(chatJid);
|
|
338
|
+
if (existing) {
|
|
339
|
+
existing.lastMessageId = messageId;
|
|
340
|
+
existing.lastTimestamp = timestamp;
|
|
341
|
+
existing.synced++;
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
this.progress.set(chatJid, {
|
|
345
|
+
lastMessageId: messageId,
|
|
346
|
+
lastTimestamp: timestamp,
|
|
347
|
+
synced: 1
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
this.totalSynced++;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Get sync state untuk resume
|
|
354
|
+
*/
|
|
355
|
+
getResumeState(chatJid) {
|
|
356
|
+
return this.progress.get(chatJid);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Complete sync
|
|
360
|
+
*/
|
|
361
|
+
completeSync() {
|
|
362
|
+
if (!this.isSyncing)
|
|
363
|
+
return;
|
|
364
|
+
this.isSyncing = false;
|
|
365
|
+
const duration = Date.now() - this.syncStartTime;
|
|
366
|
+
this.logger.info(`History sync completed: ${this.totalSynced} messages in ${(duration / 1000).toFixed(1)}s`);
|
|
367
|
+
if (this.onSyncComplete) {
|
|
368
|
+
this.onSyncComplete();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
isSyncInProgress() {
|
|
372
|
+
return this.isSyncing;
|
|
373
|
+
}
|
|
374
|
+
getStats() {
|
|
375
|
+
return {
|
|
376
|
+
isSyncing: this.isSyncing,
|
|
377
|
+
totalSynced: this.totalSynced,
|
|
378
|
+
chatsSynced: this.progress.size,
|
|
379
|
+
durationMs: this.isSyncing ? Date.now() - this.syncStartTime : 0
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Serialize progress untuk persistence
|
|
384
|
+
*/
|
|
385
|
+
serialize() {
|
|
386
|
+
return JSON.stringify({
|
|
387
|
+
totalSynced: this.totalSynced,
|
|
388
|
+
progress: Array.from(this.progress.entries())
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Deserialize progress dari persistence
|
|
393
|
+
*/
|
|
394
|
+
deserialize(data) {
|
|
395
|
+
try {
|
|
396
|
+
const parsed = JSON.parse(data);
|
|
397
|
+
this.totalSynced = parsed.totalSynced ?? 0;
|
|
398
|
+
this.progress = new Map(parsed.progress ?? []);
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
this.logger.warn('Failed to deserialize history sync state');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
exports.HistorySyncManager = HistorySyncManager;
|
|
406
|
+
class DecryptionRetryQueue {
|
|
407
|
+
queue = new Map();
|
|
408
|
+
maxAttempts;
|
|
409
|
+
maxQueueSize;
|
|
410
|
+
logger;
|
|
411
|
+
retryHandler;
|
|
412
|
+
retryTimer = null;
|
|
413
|
+
constructor(opts = {}) {
|
|
414
|
+
this.maxAttempts = opts.maxAttempts ?? 3;
|
|
415
|
+
this.maxQueueSize = opts.maxQueueSize ?? 1000;
|
|
416
|
+
this.logger = opts.logger ?? new logger_js_1.SilentLogger();
|
|
417
|
+
this.retryHandler = opts.retryHandler;
|
|
418
|
+
// Retry every 30 seconds
|
|
419
|
+
this.retryTimer = setInterval(() => this.processQueue(), 30000);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Add failed decryption ke queue
|
|
423
|
+
*/
|
|
424
|
+
enqueue(messageId, senderJid, encryptedData) {
|
|
425
|
+
if (this.queue.size >= this.maxQueueSize) {
|
|
426
|
+
// Drop oldest
|
|
427
|
+
const firstKey = this.queue.keys().next().value;
|
|
428
|
+
if (firstKey)
|
|
429
|
+
this.queue.delete(firstKey);
|
|
430
|
+
}
|
|
431
|
+
this.queue.set(messageId, {
|
|
432
|
+
messageId,
|
|
433
|
+
senderJid,
|
|
434
|
+
encryptedData,
|
|
435
|
+
attempts: 0,
|
|
436
|
+
timestamp: Date.now()
|
|
437
|
+
});
|
|
438
|
+
this.logger.debug(`Queued decryption retry for ${messageId} from ${senderJid}`);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Process queue - retry decryptions
|
|
442
|
+
*/
|
|
443
|
+
async processQueue() {
|
|
444
|
+
if (!this.retryHandler || this.queue.size === 0)
|
|
445
|
+
return;
|
|
446
|
+
const toRetry = Array.from(this.queue.values());
|
|
447
|
+
const succeeded = [];
|
|
448
|
+
const failed = [];
|
|
449
|
+
for (const item of toRetry) {
|
|
450
|
+
try {
|
|
451
|
+
const success = await this.retryHandler(item);
|
|
452
|
+
if (success) {
|
|
453
|
+
succeeded.push(item.messageId);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
item.attempts++;
|
|
457
|
+
if (item.attempts >= this.maxAttempts) {
|
|
458
|
+
failed.push(item.messageId);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
item.attempts++;
|
|
464
|
+
this.logger.warn(`Decryption retry ${item.attempts}/${this.maxAttempts} failed for ${item.messageId}: ${err.message}`);
|
|
465
|
+
if (item.attempts >= this.maxAttempts) {
|
|
466
|
+
failed.push(item.messageId);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Remove succeeded dan permanently failed
|
|
471
|
+
for (const id of succeeded) {
|
|
472
|
+
this.queue.delete(id);
|
|
473
|
+
this.logger.debug(`Decryption retry succeeded for ${id}`);
|
|
474
|
+
}
|
|
475
|
+
for (const id of failed) {
|
|
476
|
+
this.queue.delete(id);
|
|
477
|
+
this.logger.warn(`Decryption permanently failed for ${id} after ${this.maxAttempts} attempts`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Trigger immediate retry (e.g., after key sync)
|
|
482
|
+
*/
|
|
483
|
+
async retryNow() {
|
|
484
|
+
await this.processQueue();
|
|
485
|
+
}
|
|
486
|
+
getQueueSize() {
|
|
487
|
+
return this.queue.size;
|
|
488
|
+
}
|
|
489
|
+
destroy() {
|
|
490
|
+
if (this.retryTimer) {
|
|
491
|
+
clearInterval(this.retryTimer);
|
|
492
|
+
this.retryTimer = null;
|
|
493
|
+
}
|
|
494
|
+
this.queue.clear();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
exports.DecryptionRetryQueue = DecryptionRetryQueue;
|
|
498
|
+
// ============================================================
|
|
499
|
+
// FIX 8: Race Condition - Serialized message queue per recipient
|
|
500
|
+
// ============================================================
|
|
501
|
+
/**
|
|
502
|
+
* MessageQueue - serialize messages per recipient untuk prevent race conditions.
|
|
503
|
+
* Fix Baileys issue: concurrent sends ke same chat cause race conditions.
|
|
504
|
+
*
|
|
505
|
+
* Strategy:
|
|
506
|
+
* - One queue per recipient (JID)
|
|
507
|
+
* - Process messages sequentially per recipient
|
|
508
|
+
* - Concurrent across different recipients (parallelism)
|
|
509
|
+
*/
|
|
510
|
+
class MessageQueue {
|
|
511
|
+
queues = new Map();
|
|
512
|
+
processing = new Set();
|
|
513
|
+
logger;
|
|
514
|
+
maxConcurrency;
|
|
515
|
+
activeCount = 0;
|
|
516
|
+
constructor(maxConcurrency = 10, logger) {
|
|
517
|
+
this.maxConcurrency = maxConcurrency;
|
|
518
|
+
this.logger = logger ?? new logger_js_1.SilentLogger();
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Enqueue message untuk a recipient
|
|
522
|
+
*/
|
|
523
|
+
async enqueue(recipientJid, fn) {
|
|
524
|
+
return new Promise((resolve, reject) => {
|
|
525
|
+
const task = async () => {
|
|
526
|
+
try {
|
|
527
|
+
const result = await fn();
|
|
528
|
+
resolve(result);
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
reject(err);
|
|
532
|
+
}
|
|
533
|
+
finally {
|
|
534
|
+
this.activeCount--;
|
|
535
|
+
this.processNext(recipientJid);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
if (!this.queues.has(recipientJid)) {
|
|
539
|
+
this.queues.set(recipientJid, []);
|
|
540
|
+
}
|
|
541
|
+
this.queues.get(recipientJid).push(task);
|
|
542
|
+
this.processNext(recipientJid);
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
processNext(recipientJid) {
|
|
546
|
+
if (this.processing.has(recipientJid))
|
|
547
|
+
return;
|
|
548
|
+
if (this.activeCount >= this.maxConcurrency)
|
|
549
|
+
return;
|
|
550
|
+
const queue = this.queues.get(recipientJid);
|
|
551
|
+
if (!queue || queue.length === 0)
|
|
552
|
+
return;
|
|
553
|
+
this.processing.add(recipientJid);
|
|
554
|
+
this.activeCount++;
|
|
555
|
+
const task = queue.shift();
|
|
556
|
+
task().finally(() => {
|
|
557
|
+
this.processing.delete(recipientJid);
|
|
558
|
+
// Continue processing jika masih ada
|
|
559
|
+
if (queue.length > 0 && this.activeCount < this.maxConcurrency) {
|
|
560
|
+
this.processNext(recipientJid);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Get queue size for a recipient
|
|
566
|
+
*/
|
|
567
|
+
getQueueSize(recipientJid) {
|
|
568
|
+
return this.queues.get(recipientJid)?.length ?? 0;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Get total pending messages across all queues
|
|
572
|
+
*/
|
|
573
|
+
getTotalPending() {
|
|
574
|
+
let total = 0;
|
|
575
|
+
for (const queue of this.queues.values()) {
|
|
576
|
+
total += queue.length;
|
|
577
|
+
}
|
|
578
|
+
return total;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Clear queue for a recipient (e.g., on disconnect)
|
|
582
|
+
*/
|
|
583
|
+
clearQueue(recipientJid) {
|
|
584
|
+
const queue = this.queues.get(recipientJid);
|
|
585
|
+
if (queue) {
|
|
586
|
+
// Reject all pending
|
|
587
|
+
queue.length = 0;
|
|
588
|
+
this.queues.delete(recipientJid);
|
|
589
|
+
}
|
|
590
|
+
this.processing.delete(recipientJid);
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Clear all queues
|
|
594
|
+
*/
|
|
595
|
+
clearAll() {
|
|
596
|
+
for (const queue of this.queues.values()) {
|
|
597
|
+
queue.length = 0;
|
|
598
|
+
}
|
|
599
|
+
this.queues.clear();
|
|
600
|
+
this.processing.clear();
|
|
601
|
+
this.activeCount = 0;
|
|
602
|
+
}
|
|
603
|
+
getStats() {
|
|
604
|
+
return {
|
|
605
|
+
totalQueues: this.queues.size,
|
|
606
|
+
totalPending: this.getTotalPending(),
|
|
607
|
+
activeCount: this.activeCount,
|
|
608
|
+
maxConcurrency: this.maxConcurrency
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
exports.MessageQueue = MessageQueue;
|
|
613
|
+
// ============================================================
|
|
614
|
+
// FIX 9: Media Upload Timeout - Chunked upload dengan resume
|
|
615
|
+
// ============================================================
|
|
616
|
+
/**
|
|
617
|
+
* ChunkedMediaUploader - upload large files dengan chunking + resume.
|
|
618
|
+
* Fix Baileys issue: media upload timeout untuk large files.
|
|
619
|
+
*
|
|
620
|
+
* Strategy:
|
|
621
|
+
* - Split file jadi chunks (1MB default)
|
|
622
|
+
* - Upload each chunk separately
|
|
623
|
+
* - Resume from last successful chunk jika interrupted
|
|
624
|
+
*/
|
|
625
|
+
class ChunkedMediaUploader {
|
|
626
|
+
logger;
|
|
627
|
+
chunkSize;
|
|
628
|
+
maxRetries;
|
|
629
|
+
uploadProgress = new Map(); // uploadId -> lastChunkIndex
|
|
630
|
+
constructor(opts = {}) {
|
|
631
|
+
this.chunkSize = opts.chunkSize ?? 1024 * 1024; // 1MB default
|
|
632
|
+
this.maxRetries = opts.maxRetries ?? 3;
|
|
633
|
+
this.logger = opts.logger ?? new logger_js_1.SilentLogger();
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Upload dengan chunking + resume support
|
|
637
|
+
*/
|
|
638
|
+
async uploadChunked(data, uploadId, uploadChunk) {
|
|
639
|
+
const totalChunks = Math.ceil(data.length / this.chunkSize);
|
|
640
|
+
let startChunk = this.uploadProgress.get(uploadId) ?? 0;
|
|
641
|
+
this.logger.info(`Uploading ${totalChunks} chunks (resuming from ${startChunk})`);
|
|
642
|
+
for (let i = startChunk; i < totalChunks; i++) {
|
|
643
|
+
const chunkStart = i * this.chunkSize;
|
|
644
|
+
const chunkEnd = Math.min(chunkStart + this.chunkSize, data.length);
|
|
645
|
+
const chunk = data.subarray(chunkStart, chunkEnd);
|
|
646
|
+
let attempts = 0;
|
|
647
|
+
let success = false;
|
|
648
|
+
while (attempts < this.maxRetries && !success) {
|
|
649
|
+
try {
|
|
650
|
+
await uploadChunk(chunk, i, totalChunks);
|
|
651
|
+
success = true;
|
|
652
|
+
this.uploadProgress.set(uploadId, i + 1);
|
|
653
|
+
this.logger.debug(`Uploaded chunk ${i + 1}/${totalChunks}`);
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
attempts++;
|
|
657
|
+
this.logger.warn(`Chunk ${i} upload attempt ${attempts} failed: ${err.message}`);
|
|
658
|
+
if (attempts >= this.maxRetries) {
|
|
659
|
+
throw new Error(`Chunk ${i} upload failed after ${attempts} attempts: ${err.message}`);
|
|
660
|
+
}
|
|
661
|
+
// Exponential backoff
|
|
662
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempts)));
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// Cleanup progress tracking
|
|
667
|
+
this.uploadProgress.delete(uploadId);
|
|
668
|
+
this.logger.info(`Upload complete: ${totalChunks} chunks uploaded`);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Get resume point untuk upload
|
|
672
|
+
*/
|
|
673
|
+
getResumePoint(uploadId) {
|
|
674
|
+
return this.uploadProgress.get(uploadId) ?? 0;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Clear progress untuk abandoned upload
|
|
678
|
+
*/
|
|
679
|
+
clearProgress(uploadId) {
|
|
680
|
+
this.uploadProgress.delete(uploadId);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
exports.ChunkedMediaUploader = ChunkedMediaUploader;
|
|
684
|
+
// ============================================================
|
|
685
|
+
// FIX: Reconnection Strategy (smarter backoff)
|
|
686
|
+
// ============================================================
|
|
687
|
+
/**
|
|
688
|
+
* SmarterReconnector - reconnect dengan phased backoff.
|
|
689
|
+
* Fix Baileys issue: reconnection strategy gak optimal, get banned.
|
|
690
|
+
*
|
|
691
|
+
* Strategy:
|
|
692
|
+
* - Phase 1: Fast retry (1s, 2s, 4s) - untuk network blip
|
|
693
|
+
* - Phase 2: Medium retry (10s, 20s, 30s) - untuk server issues
|
|
694
|
+
* - Phase 3: Slow retry (60s, 120s) - untuk long-term issues
|
|
695
|
+
* - Reset to phase 1 setelah successful connection
|
|
696
|
+
*/
|
|
697
|
+
class SmarterReconnector {
|
|
698
|
+
logger;
|
|
699
|
+
attempt = 0;
|
|
700
|
+
maxAttempts;
|
|
701
|
+
onReconnect;
|
|
702
|
+
constructor(onReconnect, opts = {}) {
|
|
703
|
+
this.onReconnect = onReconnect;
|
|
704
|
+
this.logger = opts.logger ?? new logger_js_1.SilentLogger();
|
|
705
|
+
this.maxAttempts = opts.maxAttempts ?? 20;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Schedule next reconnect dengan smart delay
|
|
709
|
+
*/
|
|
710
|
+
scheduleNext() {
|
|
711
|
+
this.attempt++;
|
|
712
|
+
if (this.attempt > this.maxAttempts) {
|
|
713
|
+
this.logger.error(`Max reconnect attempts (${this.maxAttempts}) reached, giving up`);
|
|
714
|
+
throw new Error(`Max reconnect attempts reached`);
|
|
715
|
+
}
|
|
716
|
+
const delay = this.calculateDelay();
|
|
717
|
+
this.logger.info(`Reconnect attempt ${this.attempt}/${this.maxAttempts} in ${delay}ms`);
|
|
718
|
+
return setTimeout(async () => {
|
|
719
|
+
try {
|
|
720
|
+
await this.onReconnect();
|
|
721
|
+
// Success - reset counter
|
|
722
|
+
this.attempt = 0;
|
|
723
|
+
this.logger.info('Reconnect successful');
|
|
724
|
+
}
|
|
725
|
+
catch (err) {
|
|
726
|
+
this.logger.error(`Reconnect attempt ${this.attempt} failed: ${err.message}`);
|
|
727
|
+
this.scheduleNext();
|
|
728
|
+
}
|
|
729
|
+
}, delay);
|
|
730
|
+
}
|
|
731
|
+
calculateDelay() {
|
|
732
|
+
if (this.attempt <= 3) {
|
|
733
|
+
// Phase 1: Fast retry
|
|
734
|
+
return 1000 * Math.pow(2, this.attempt - 1); // 1s, 2s, 4s
|
|
735
|
+
}
|
|
736
|
+
else if (this.attempt <= 6) {
|
|
737
|
+
// Phase 2: Medium retry
|
|
738
|
+
return 10000 + (this.attempt - 3) * 10000; // 10s, 20s, 30s
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
// Phase 3: Slow retry with jitter
|
|
742
|
+
const base = 60000 + (this.attempt - 6) * 30000; // 60s, 90s, 120s, ...
|
|
743
|
+
const jitter = Math.random() * 10000; // 0-10s jitter
|
|
744
|
+
return Math.min(base + jitter, 300000); // max 5 min
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
reset() {
|
|
748
|
+
this.attempt = 0;
|
|
749
|
+
}
|
|
750
|
+
getAttempt() {
|
|
751
|
+
return this.attempt;
|
|
752
|
+
}
|
|
753
|
+
getCurrentPhase() {
|
|
754
|
+
if (this.attempt <= 3)
|
|
755
|
+
return 1;
|
|
756
|
+
if (this.attempt <= 6)
|
|
757
|
+
return 2;
|
|
758
|
+
return 3;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
exports.SmarterReconnector = SmarterReconnector;
|
|
762
|
+
//# sourceMappingURL=index.js.map
|