nodejs-insta-private-api-mqtt 1.3.20 → 1.3.22

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/dist/mqtt-shim.js CHANGED
@@ -34,34 +34,56 @@ class IllegalStateError extends Error {
34
34
 
35
35
  class PacketStream {
36
36
  constructor(buffer) {
37
- this.buffer = buffer;
37
+ this.buffer = buffer || Buffer.alloc(0);
38
38
  this.offset = 0;
39
39
  }
40
- readByte() {
41
- if (this.offset >= this.buffer.length) return 0;
42
- return this.buffer[this.offset++];
40
+ readByte() {
41
+ if (this.offset >= this.buffer.length) throw new Error('Unexpected end of stream');
42
+ return this.buffer[this.offset++];
43
43
  }
44
44
  readStringAsBuffer() {
45
45
  const len = this.readWord();
46
- if (this.offset + len > this.buffer.length) return Buffer.alloc(0);
46
+ if (this.offset + len > this.buffer.length) throw new Error('Unexpected end of stream');
47
47
  const buf = this.buffer.slice(this.offset, this.offset + len);
48
48
  this.offset += len;
49
49
  return buf;
50
50
  }
51
51
  readWord() {
52
- if (this.offset + 2 > this.buffer.length) return 0;
52
+ if (this.offset + 2 > this.buffer.length) throw new Error('Unexpected end of stream');
53
53
  const w = this.buffer.readUInt16BE(this.offset);
54
54
  this.offset += 2;
55
55
  return w;
56
56
  }
57
+ readRemaining() {
58
+ const buf = this.buffer.slice(this.offset);
59
+ this.offset = this.buffer.length;
60
+ return buf;
61
+ }
57
62
  }
58
63
 
59
64
  class PacketWriter {
60
65
  constructor() { this.chunks = []; }
61
66
  writeByte(b) { const buf = Buffer.alloc(1); buf.writeUInt8(b); this.chunks.push(buf); return this; }
62
67
  writeWord(w) { const buf = Buffer.alloc(2); buf.writeUInt16BE(w); this.chunks.push(buf); return this; }
63
- writeString(s) { this.writeWord(s.length); this.chunks.push(Buffer.from(s)); return this; }
64
- write(buf) { this.chunks.push(buf); return this; }
68
+ writeString(s) {
69
+ if (Buffer.isBuffer(s)) {
70
+ this.writeWord(s.length);
71
+ this.chunks.push(s);
72
+ } else {
73
+ const str = String(s || '');
74
+ const b = Buffer.from(str);
75
+ this.writeWord(b.length);
76
+ this.chunks.push(b);
77
+ }
78
+ return this;
79
+ }
80
+ write(buf) {
81
+ if (!buf) return this;
82
+ if (Buffer.isBuffer(buf)) this.chunks.push(buf);
83
+ else if (typeof buf === 'string') this.chunks.push(Buffer.from(buf));
84
+ else if (buf instanceof Uint8Array) this.chunks.push(Buffer.from(buf));
85
+ return this;
86
+ }
65
87
  toBuffer() { return Buffer.concat(this.chunks); }
66
88
  }
67
89
 
@@ -76,7 +98,39 @@ class MqttClient extends EventEmitter {
76
98
  this.packetIdCounter = 1;
77
99
  this.pendingPublishes = new Map(); // PacketID -> {resolve, reject, timer}
78
100
  this.pingTimer = null;
79
- this.keepAliveInterval = 60000;
101
+ this.keepAliveInterval = 30000;
102
+ this.recvBuffer = Buffer.alloc(0); // buffer pentru date primite (concatenează fragmente)
103
+ this._defaultReadMapInstalled = false;
104
+
105
+ // install defaults if user didn't provide
106
+ this._installDefaultReadMap();
107
+ }
108
+
109
+ _installDefaultReadMap() {
110
+ if (this._defaultReadMapInstalled) return;
111
+ // Only set a default ConnAck reader if user did not provide one
112
+ if (!this.readMap[PacketType.ConnAck]) {
113
+ this.readMap[PacketType.ConnAck] = (stream, length) => {
114
+ // read ackFlags and returnCode (minimum)
115
+ // Accept extra payload (Instagram often sends extra bytes)
116
+ let ackFlags = 0;
117
+ let returnCode = 255;
118
+ try {
119
+ ackFlags = stream.readByte();
120
+ } catch (e) {
121
+ throw new Error('Invalid CONNACK: missing ackFlags');
122
+ }
123
+ try {
124
+ returnCode = stream.readByte();
125
+ } catch (e) {
126
+ throw new Error('Invalid CONNACK: missing returnCode');
127
+ }
128
+ // remaining bytes as payload (could be protobuf or other)
129
+ const extra = stream.readRemaining();
130
+ return new ConnectResponsePacket(ackFlags, returnCode, extra);
131
+ };
132
+ }
133
+ this._defaultReadMapInstalled = true;
80
134
  }
81
135
 
82
136
  getNextPacketId() {
@@ -104,7 +158,7 @@ class MqttClient extends EventEmitter {
104
158
  ...this.options,
105
159
  ...connectOpts,
106
160
  payload: this.connectPayload || connectOpts.payload,
107
- keepAlive: 60
161
+ keepAlive: 30
108
162
  });
109
163
  const packetData = writer.toBuffer();
110
164
  let remLen = packetData.length;
@@ -116,7 +170,12 @@ class MqttClient extends EventEmitter {
116
170
  fixedHeader.push(byte);
117
171
  } while (remLen > 0);
118
172
 
119
- this.socket.write(Buffer.concat([Buffer.from(fixedHeader), packetData]));
173
+ // Ensure socket writable
174
+ if (this.socket && !this.socket.destroyed && this.socket.writable) {
175
+ this.socket.write(Buffer.concat([Buffer.from(fixedHeader), packetData]));
176
+ } else {
177
+ this.emit('error', new Error('Socket not writable when sending CONNECT'));
178
+ }
120
179
  } else {
121
180
  this.emit('error', new Error('No write handler for CONNECT'));
122
181
  }
@@ -158,6 +217,9 @@ class MqttClient extends EventEmitter {
158
217
  this.socket = tls.connect(port, host, { rejectUnauthorized: true }, onConnect);
159
218
  }
160
219
 
220
+ // reset recvBuffer on new socket
221
+ this.recvBuffer = Buffer.alloc(0);
222
+
161
223
  this.socket.on('data', (data) => this._handleData(data));
162
224
  this.socket.on('error', (err) => {
163
225
  this._clearPingTimer();
@@ -168,6 +230,7 @@ class MqttClient extends EventEmitter {
168
230
  this.emit('disconnect');
169
231
  });
170
232
 
233
+ // resolve when connect_success event fired (this is triggered by parsing CONNACK)
171
234
  this.once('connect_success', resolve);
172
235
  } catch (e) {
173
236
  reject(e);
@@ -178,8 +241,12 @@ class MqttClient extends EventEmitter {
178
241
  _setupPingTimer() {
179
242
  this._clearPingTimer();
180
243
  this.pingTimer = setInterval(() => {
181
- if (this.socket && !this.socket.destroyed) {
182
- this.socket.write(Buffer.from([0xC0, 0x00])); // PINGREQ (12 << 4), Length 0
244
+ if (this.socket && !this.socket.destroyed && this.socket.writable) {
245
+ try {
246
+ this.socket.write(Buffer.from([0xC0, 0x00])); // PINGREQ (12 << 4), Length 0
247
+ } catch (e) {
248
+ // ignore write errors here, they'll be handled by socket 'error'
249
+ }
183
250
  }
184
251
  }, this.keepAliveInterval);
185
252
  }
@@ -189,60 +256,140 @@ class MqttClient extends EventEmitter {
189
256
  this.pingTimer = null;
190
257
  }
191
258
 
259
+ /**
260
+ * Robust handler that collects incoming chunks into recvBuffer
261
+ * and parses complete MQTT frames. Handles variable Remaining Length
262
+ * encoding and fragmentation across TCP/TLS boundaries.
263
+ */
192
264
  _handleData(data) {
193
- let offset = 0;
194
- while(offset < data.length) {
195
- const first = data[offset++];
196
- const type = first >> 4;
265
+ // concat incoming chunk
266
+ this.recvBuffer = Buffer.concat([this.recvBuffer, data]);
267
+
268
+ // parse as many full packets as present
269
+ while (this.recvBuffer.length > 0) {
270
+ // need at least 2 bytes (1 for fixed header, at least 1 for remaining length byte)
271
+ if (this.recvBuffer.length < 2) break;
272
+
273
+ // first byte
274
+ const first = this.recvBuffer[0];
275
+ // parse Remaining Length (variable bytes, max 4)
197
276
  let multiplier = 1;
198
- let length = 0;
277
+ let value = 0;
278
+ let remLenBytes = 0;
279
+ let i = 1;
199
280
  let digit;
281
+ let incomplete = false;
200
282
  do {
201
- if (offset >= data.length) break;
202
- digit = data[offset++];
203
- length += (digit & 127) * multiplier;
204
- multiplier *= 128;
283
+ if (i >= this.recvBuffer.length) { incomplete = true; break; } // waiting for more bytes
284
+ digit = this.recvBuffer[i++];
285
+ remLenBytes++;
286
+ value += (digit & 127) * multiplier;
287
+ multiplier *= 128;
288
+ if (remLenBytes > 4) {
289
+ // malformed remaining length
290
+ this.emit('error', new Error('Malformed Remaining Length'));
291
+ return;
292
+ }
205
293
  } while ((digit & 128) !== 0);
206
-
207
- if (offset + length > data.length) break;
208
-
209
- const payload = data.slice(offset, offset + length);
210
- offset += length;
211
-
212
- if (type === PacketType.ConnAck) {
213
- const readFunc = this.readMap[PacketType.ConnAck];
214
- if (readFunc) {
215
- const stream = new PacketStream(payload);
216
- const packet = readFunc(stream, length);
217
- if (packet.isSuccess) this.emit('connect_success');
218
- this.emit('connect', { payload: packet.payload });
219
- }
220
- } else if (type === PacketType.Publish) {
221
- try {
222
- const topicLen = payload.readUInt16BE(0);
223
- const topic = payload.slice(2, 2 + topicLen).toString();
224
- const qos = (first & 0x06) >> 1;
225
- let msgPayload;
226
- if (qos > 0) {
227
- // Skip Packet ID (2 bytes)
228
- msgPayload = payload.slice(2 + topicLen + 2);
229
- } else {
230
- msgPayload = payload.slice(2 + topicLen);
294
+
295
+ if (incomplete) break; // wait more data
296
+
297
+ const headerLen = 1 + remLenBytes;
298
+ const totalLen = headerLen + value;
299
+
300
+ if (this.recvBuffer.length < totalLen) break; // wait full packet
301
+
302
+ // we have a full packet: extract it
303
+ const packet = this.recvBuffer.slice(0, totalLen);
304
+ // payload starts after headerLen
305
+ const payload = packet.slice(headerLen, totalLen);
306
+
307
+ // remove processed packet from buffer
308
+ this.recvBuffer = this.recvBuffer.slice(totalLen);
309
+
310
+ const type = first >> 4;
311
+
312
+ try {
313
+ if (type === PacketType.ConnAck) {
314
+ const readFunc = this.readMap[PacketType.ConnAck];
315
+ if (readFunc) {
316
+ const stream = new PacketStream(payload);
317
+ let packetObj;
318
+ try {
319
+ packetObj = readFunc(stream, value);
320
+ } catch (e) {
321
+ // If parser throws, emit error but continue parsing other packets
322
+ this.emit('error', e);
323
+ continue;
324
+ }
325
+ // Only emit success if return code indicates success
326
+ if (packetObj && packetObj.isSuccess) {
327
+ this.emit('connect_success');
328
+ } else if (packetObj && !packetObj.isSuccess) {
329
+ // emit error so upper layers can react
330
+ this.emit('error', new Error(packetObj.errorName || 'Connection refused'));
331
+ }
332
+ // emit connect event with raw payload (so caller can inspect extra data)
333
+ this.emit('connect', { payload: packetObj ? packetObj.payload : payload });
334
+ } else {
335
+ // no read handler, just emit raw connect
336
+ this.emit('connect', { payload });
337
+ }
338
+ } else if (type === PacketType.Publish) {
339
+ try {
340
+ // parse topic length safely
341
+ if (payload.length < 2) throw new Error('Publish payload too small for topic length');
342
+ const topicLen = payload.readUInt16BE(0);
343
+ if (payload.length < 2 + topicLen) throw new Error('Publish payload too small for topic');
344
+ const topic = payload.slice(2, 2 + topicLen).toString();
345
+ const qos = (first & 0x06) >> 1;
346
+ let msgPayload;
347
+ if (qos > 0) {
348
+ // Skip Packet ID (2 bytes)
349
+ msgPayload = payload.slice(2 + topicLen + 2);
350
+ } else {
351
+ msgPayload = payload.slice(2 + topicLen);
352
+ }
353
+ this.emit('message', { topic, payload: msgPayload, qosLevel: qos });
354
+ } catch (e) {
355
+ // ignore malformed publish frames but emit error
356
+ this.emit('error', e);
231
357
  }
232
- this.emit('message', { topic, payload: msgPayload, qosLevel: qos });
233
- } catch (e) {}
234
- } else if (type === PacketType.PubAck) {
235
- // Handle PubAck (QoS 1)
236
- if (payload.length >= 2) {
237
- const packetId = payload.readUInt16BE(0);
238
- const pending = this.pendingPublishes.get(packetId);
239
- if (pending) {
240
- pending.resolve();
241
- this.pendingPublishes.delete(packetId);
358
+ } else if (type === PacketType.PubAck) {
359
+ // Handle PubAck (QoS 1)
360
+ if (payload.length >= 2) {
361
+ const packetId = payload.readUInt16BE(0);
362
+ const pending = this.pendingPublishes.get(packetId);
363
+ if (pending) {
364
+ // clear timeout if present
365
+ if (pending.timer) clearTimeout(pending.timer);
366
+ pending.resolve();
367
+ this.pendingPublishes.delete(packetId);
368
+ }
369
+ }
370
+ } else if (type === PacketType.PingResp) {
371
+ // Pong received, good
372
+ // no-op
373
+ } else if (type === PacketType.SubAck) {
374
+ // for completeness - user can provide readMap[SubAck] if needed
375
+ const readFunc = this.readMap[PacketType.SubAck];
376
+ if (readFunc) {
377
+ try {
378
+ readFunc(new PacketStream(payload), payload.length);
379
+ } catch (e) { this.emit('error', e); }
380
+ }
381
+ } else {
382
+ // Unknown packet type - ignore or let readMap handle if provided
383
+ const readFunc = this.readMap[type];
384
+ if (readFunc) {
385
+ try {
386
+ readFunc(new PacketStream(payload), payload.length);
387
+ } catch (e) { this.emit('error', e); }
242
388
  }
243
389
  }
244
- } else if (type === PacketType.PingResp) {
245
- // Pong received, good
390
+ } catch (e) {
391
+ // catch-any to avoid breaking parse loop
392
+ this.emit('error', e);
246
393
  }
247
394
  }
248
395
  }
@@ -272,14 +419,16 @@ class MqttClient extends EventEmitter {
272
419
  try {
273
420
  this.socket.write(Buffer.concat([Buffer.from(fixedHeader), packetData]));
274
421
  if (msg.qosLevel > 0) {
275
- this.pendingPublishes.set(packetId, { resolve, reject });
276
- // Timeout fallback for ack?
277
- setTimeout(() => {
422
+ // set timeout for ack
423
+ const timer = setTimeout(() => {
278
424
  if (this.pendingPublishes.has(packetId)) {
425
+ const pending = this.pendingPublishes.get(packetId);
279
426
  this.pendingPublishes.delete(packetId);
280
- resolve(); // Resolve anyway to not block logic
427
+ // reject rather than resolve silently - let caller handle retry
428
+ pending.reject(new Error('PubAck timeout'));
281
429
  }
282
430
  }, 5000);
431
+ this.pendingPublishes.set(packetId, { resolve, reject, timer });
283
432
  } else {
284
433
  resolve();
285
434
  }
@@ -445,6 +445,81 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
445
445
 
446
446
  this.emit('connected');
447
447
 
448
+ // -------------------- WATCHDOG / KEEPALIVE / TRAFFIC MONITOR --------------------
449
+ // Track last message time for inactivity detection
450
+ try {
451
+ this._lastMessageAt = Date.now();
452
+
453
+ // update on common receive events
454
+ const updateLast = () => { try { this._lastMessageAt = Date.now(); } catch (e) {} };
455
+ this.on('receive', updateLast);
456
+ this.on('receiveRaw', updateLast);
457
+ this.on('message', updateLast);
458
+ this.on('iris', updateLast);
459
+
460
+ // Timings configurable via initOptions (fallback sensible defaults)
461
+ const KEEPALIVE_FOREGROUND_MS = (this.initOptions && this.initOptions.keepaliveForegroundMs) ? this.initOptions.keepaliveForegroundMs : 30000;
462
+ const MESSAGE_SYNC_REFRESH_MS = (this.initOptions && this.initOptions.messageSyncRefreshMs) ? this.initOptions.messageSyncRefreshMs : 60000;
463
+ const TRAFFIC_INACTIVITY_MS = (this.initOptions && this.initOptions.trafficInactivityMs) ? this.initOptions.trafficInactivityMs : 90000;
464
+
465
+ // Foreground keepalive: tell server we're foreground so it keeps delivering
466
+ try {
467
+ this._foregroundTimer = setInterval(async () => {
468
+ try {
469
+ if (!this.commands) return;
470
+ await this.commands.updateSubscriptions({
471
+ topic: constants_1.Topics.PUBSUB,
472
+ data: { foreground: true }
473
+ });
474
+ this.realtimeDebug('[WATCHDOG] Foreground keepalive sent.');
475
+ } catch (e) {
476
+ this.realtimeDebug('[WATCHDOG] Foreground keepalive failed:', e?.message || e);
477
+ }
478
+ }, KEEPALIVE_FOREGROUND_MS);
479
+ } catch (e) {
480
+ this.realtimeDebug('[WATCHDOG] Could not start foreground timer:', e?.message || e);
481
+ }
482
+
483
+ // Periodically refresh GraphQL/message_sync subscriptions to keep server sending events
484
+ try {
485
+ this._syncTimer = setInterval(async () => {
486
+ try {
487
+ if (!this.commands) return;
488
+ // Refresh the important graphQl subs (idempotent)
489
+ const subs = (this.initOptions && this.initOptions.graphQlSubs && this.initOptions.graphQlSubs.length) ? this.initOptions.graphQlSubs : ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'];
490
+ await this.graphQlSubscribe(subs);
491
+ this.realtimeDebug('[WATCHDOG] MESSAGE_SYNC refreshed.');
492
+ } catch (e) {
493
+ this.realtimeDebug('[WATCHDOG] MESSAGE_SYNC refresh failed:', e?.message || e);
494
+ }
495
+ }, MESSAGE_SYNC_REFRESH_MS);
496
+ } catch (e) {
497
+ this.realtimeDebug('[WATCHDOG] Could not start sync timer:', e?.message || e);
498
+ }
499
+
500
+ // Traffic watchdog: if no messages for a while -> reconnect
501
+ try {
502
+ this._trafficWatchdog = setInterval(async () => {
503
+ try {
504
+ const idle = Date.now() - (this._lastMessageAt || 0);
505
+ if (idle > TRAFFIC_INACTIVITY_MS) {
506
+ this.realtimeDebug(`[WATCHDOG] No traffic detected for ${idle}ms -> attempting reconnect`);
507
+ await this._attemptReconnectSafely();
508
+ }
509
+ } catch (e) {
510
+ this.realtimeDebug('[WATCHDOG] trafficWatchdog fault:', e?.message || e);
511
+ }
512
+ }, Math.min(KEEPALIVE_FOREGROUND_MS, 30000));
513
+ } catch (e) {
514
+ this.realtimeDebug('[WATCHDOG] Could not start traffic watchdog:', e?.message || e);
515
+ }
516
+
517
+ } catch (e) {
518
+ // non-fatal
519
+ this.realtimeDebug('[WATCHDOG] initialization error:', e?.message || e);
520
+ }
521
+ // -------------------- END WATCHDOG --------------------
522
+
448
523
  this._mqtt.on('message', async (msg) => {
449
524
  const topicMap = this.mqtt?.topicMap;
450
525
  const topic = topicMap?.get(msg.topic);
@@ -505,6 +580,34 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
505
580
  }
506
581
  this._setupMessageHandlers();
507
582
  }
583
+
584
+ /**
585
+ * Internal helper to attempt a reconnect safely (debounced)
586
+ */
587
+ async _attemptReconnectSafely() {
588
+ if (this._reconnectInProgress) return;
589
+ this._reconnectInProgress = true;
590
+ try {
591
+ // try graceful disconnect if possible
592
+ try {
593
+ await (this.mqtt?.disconnect?.() ?? Promise.resolve());
594
+ } catch (e) {
595
+ // ignore
596
+ }
597
+ // small delay to ensure socket closed
598
+ await (0, shared_1.delay)(500);
599
+ // reconnect using same initOptions
600
+ try {
601
+ await this.connect(this.initOptions);
602
+ this.realtimeDebug('[WATCHDOG] Reconnect succeeded.');
603
+ } catch (e) {
604
+ this.realtimeDebug('[WATCHDOG] Reconnect failed:', e?.message || e);
605
+ }
606
+ } finally {
607
+ this._reconnectInProgress = false;
608
+ }
609
+ }
610
+
508
611
  /**
509
612
  * Connect from saved session using MultiFileAuthState
510
613
  * @param {Object} authStateHelper - Result from useMultiFileAuthState()
@@ -522,12 +625,51 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
522
625
  this._attachedAuthState = authStateHelper;
523
626
  } catch (e) {}
524
627
 
525
- const savedOptions = authStateHelper.getMqttConnectOptions?.();
526
-
628
+ // Try to read any previously-saved connect options (may include graphQlSubs/irisData/skywalkerSubs)
629
+ const savedOptions = authStateHelper.getMqttConnectOptions?.() || {};
630
+
631
+ // Prefer options.irisData -> savedOptions.irisData -> freshly fetched inbox (guarantee)
632
+ let irisData = options.irisData || savedOptions.irisData || null;
633
+
634
+ // CRITICAL FIX: If irisData is missing, actively fetch a fresh IRIS snapshot from REST before connecting.
635
+ // This ensures IRIS seq_id / snapshot_at_ms are valid after restarts so the server will resume sending events.
636
+ // Additionally: even if savedOptions.irisData exists, we prefer to fetch a fresh snapshot to avoid using stale/expired snapshot.
637
+ let fetchedInbox = null;
638
+ const shouldForceFetch = true; // always attempt fresh fetch on saved-session connect to ensure live events
639
+ if (shouldForceFetch) {
640
+ const maxAttempts = 3;
641
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
642
+ try {
643
+ console.log(`[RealtimeClient] Attempting to fetch fresh IRIS inbox snapshot (attempt ${attempt}/${maxAttempts})...`);
644
+ fetchedInbox = await this.ig.direct.getInbox();
645
+ if (fetchedInbox) {
646
+ irisData = fetchedInbox;
647
+ console.log('[RealtimeClient] Fetched IRIS snapshot successfully.');
648
+ break;
649
+ }
650
+ } catch (e) {
651
+ console.warn(`[RealtimeClient] Failed to fetch IRIS snapshot (attempt ${attempt}):`, e?.message || e);
652
+ }
653
+ // small backoff
654
+ try {
655
+ await (0, shared_1.delay)(500 * attempt);
656
+ } catch (e) {}
657
+ }
658
+ if (!fetchedInbox) {
659
+ // If we couldn't fetch fresh inbox, fall back to savedOptions.irisData if present, otherwise proceed (connect may still work)
660
+ if (savedOptions.irisData) {
661
+ irisData = savedOptions.irisData;
662
+ console.warn('[RealtimeClient] Could not fetch fresh IRIS snapshot — falling back to saved irisData (may be stale).');
663
+ } else if (!irisData) {
664
+ console.warn('[RealtimeClient] No IRIS snapshot available (neither fetched nor saved). Proceeding without irisData — server may not replay missed events.');
665
+ }
666
+ }
667
+ }
668
+
527
669
  const connectOptions = {
528
- graphQlSubs: options.graphQlSubs || savedOptions?.graphQlSubs || ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'],
529
- skywalkerSubs: options.skywalkerSubs || savedOptions?.skywalkerSubs || ['presence_subscribe', 'typing_subscribe'],
530
- irisData: options.irisData || savedOptions?.irisData || null,
670
+ graphQlSubs: options.graphQlSubs || savedOptions.graphQlSubs || ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'],
671
+ skywalkerSubs: options.skywalkerSubs || savedOptions.skywalkerSubs || ['presence_subscribe', 'typing_subscribe'],
672
+ irisData,
531
673
  ...options
532
674
  };
533
675
 
@@ -576,6 +718,25 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
576
718
  }
577
719
  disconnect() {
578
720
  this.safeDisconnect = true;
721
+
722
+ // clear watchdog timers if present
723
+ try {
724
+ if (this._foregroundTimer) {
725
+ clearInterval(this._foregroundTimer);
726
+ this._foregroundTimer = null;
727
+ }
728
+ if (this._syncTimer) {
729
+ clearInterval(this._syncTimer);
730
+ this._syncTimer = null;
731
+ }
732
+ if (this._trafficWatchdog) {
733
+ clearInterval(this._trafficWatchdog);
734
+ this._trafficWatchdog = null;
735
+ }
736
+ } catch (e) {
737
+ // non-fatal
738
+ }
739
+
579
740
  return this.mqtt?.disconnect() ?? Promise.resolve();
580
741
  }
581
742
  graphQlSubscribe(sub) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodejs-insta-private-api-mqtt",
3
- "version": "1.3.20",
3
+ "version": "1.3.22",
4
4
  "description": "Complete Instagram MQTT protocol with FULL iOS + Android support. 33 device presets (21 iOS + 12 Android). iPhone 16/15/14/13/12, iPad Pro, Samsung, Pixel, Huawei. Real-time DM messaging, view-once media extraction, sub-500ms latency.",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {