nexus-fca 2.1.0 → 2.1.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.1.1] - 2025-08-27 - ADVANCED SESSION STABILITY
4
+ ### 🛠 Added
5
+ - Adaptive safe session refresh interval (dynamic based on risk level)
6
+ - Heartbeat + watchdog timers to detect stale MQTT connections early
7
+ - Progressive backoff with jitter for MQTT reconnect attempts
8
+ - Layered post-refresh health checks (1s / 10s / 30s) to catch silent drops
9
+ - Abortable refresh with timeout safeguard (25s) to prevent hangs
10
+ - Automatic reconnection trigger if no events within thresholds (2m soft, 15m hard)
11
+ - `destroy()` method to cleanup timers/listeners (prevents memory leaks)
12
+
13
+ ### 🔄 Changed
14
+ - Safe refresh now records in‑flight ID and supersedes outdated checks
15
+ - Reconnect logic centralized in `_reconnectMqttWithBackoff`
16
+
17
+ ### ✅ Improved
18
+ - Stability after long runtimes / multiple token refresh cycles
19
+ - Reduced risk of listener not resuming after refresh
20
+
21
+ ---
22
+
3
23
  ## [2.1.0] - 2025-08-20 - SESSION RELIABILITY & PROMISE LOGIN
4
24
  ### 🚀 Highlights
5
25
  Stability-focused release improving long‑running bot sessions, reducing false `not_logged_in` events, and modernizing the login flow.
@@ -53,6 +53,20 @@ class FacebookSafety {
53
53
  riskLevel: 'low'
54
54
  };
55
55
 
56
+ // Track last incoming event time to detect stale / dead connections
57
+ this._lastEventTs = Date.now();
58
+ this._reconnecting = false;
59
+ this._activeListenerStop = null; // store stop function from listenMqtt if we attach
60
+ this._safeRefreshInterval = null; // guard for multiple intervals
61
+ this._safeRefreshTimer = null; // for dynamic timeout pattern
62
+ // New stability / heartbeat fields
63
+ this._heartbeatTimer = null;
64
+ this._watchdogTimer = null;
65
+ this._backoff = { attempt: 0, next: 0 };
66
+ this._destroyed = false;
67
+ this._postRefreshChecks = [];
68
+ this._inFlightRefreshId = 0;
69
+
56
70
  this.initSafety();
57
71
  }
58
72
 
@@ -220,14 +234,29 @@ class FacebookSafety {
220
234
  * Setup safe token refresh intervals
221
235
  */
222
236
  setupSafeRefresh() {
223
- // Refresh tokens every 45 minutes with randomization
224
- const baseInterval = 45 * 60 * 1000; // 45 minutes
225
- const randomVariation = Math.random() * 10 * 60 * 1000; // ±10 minutes
226
- const interval = baseInterval + randomVariation;
227
-
228
- setInterval(() => {
229
- this.refreshSafeSession();
230
- }, interval);
237
+ // Replace previous interval/timer to avoid stacking
238
+ if (this._safeRefreshInterval) {
239
+ clearInterval(this._safeRefreshInterval);
240
+ this._safeRefreshInterval = null;
241
+ }
242
+ if (this._safeRefreshTimer) {
243
+ clearTimeout(this._safeRefreshTimer);
244
+ this._safeRefreshTimer = null;
245
+ }
246
+ // Use recursive timeout with randomization each cycle (more human-like)
247
+ const schedule = () => {
248
+ if (this._destroyed) return;
249
+ // Adaptive interval: shorter if high risk (to revalidate), longer if stable
250
+ const base = this.sessionMetrics.riskLevel === 'high' ? 25 : this.sessionMetrics.riskLevel === 'medium' ? 35 : 45; // minutes
251
+ const baseInterval = base * 60 * 1000;
252
+ const randomVariation = (Math.random() * 16 - 8) * 60 * 1000; // ±8 min
253
+ const interval = baseInterval + randomVariation;
254
+ this._safeRefreshTimer = setTimeout(async () => {
255
+ await this.refreshSafeSession();
256
+ schedule();
257
+ }, Math.max(10 * 60 * 1000, interval)); // never below 10 min
258
+ };
259
+ schedule();
231
260
  }
232
261
 
233
262
  /**
@@ -265,18 +294,107 @@ class FacebookSafety {
265
294
  if (isError) {
266
295
  this.sessionMetrics.errorCount++;
267
296
  }
297
+ this._lastEventTs = Date.now();
298
+ }
299
+
300
+ // Expose a method for external caller (e.g., main listener) to update last event timestamp
301
+ recordEvent() {
302
+ this._lastEventTs = Date.now();
303
+ }
304
+
305
+ // Internal helper to ensure MQTT connection stays alive / auto-recover if dead after refresh
306
+ async _ensureMqttAlive() {
307
+ if (!this.api || this._destroyed) return;
308
+ try {
309
+ const disconnected = !this.ctx || !this.ctx.mqttClient || !this.ctx.mqttClient.connected;
310
+ const stale = Date.now() - this._lastEventTs > 5 * 60 * 1000; // >5 min no events
311
+ if (disconnected || stale) {
312
+ await this._reconnectMqttWithBackoff(disconnected ? 'disconnected' : 'stale');
313
+ }
314
+ } catch (_) { /* swallow */ }
315
+ }
316
+
317
+ // Progressive backoff + jitter reconnect
318
+ async _reconnectMqttWithBackoff(reason) {
319
+ if (this._reconnecting || this._destroyed) return;
320
+ this._reconnecting = true;
321
+ try {
322
+ const now = Date.now();
323
+ if (now < this._backoff.next) {
324
+ return; // respect backoff window
325
+ }
326
+ const attempt = ++this._backoff.attempt;
327
+ const baseDelay = Math.min(30000, 1000 * Math.pow(2, Math.min(attempt, 5))); // cap 30s
328
+ const jitter = Math.random() * 400;
329
+ const delay = baseDelay + jitter;
330
+ this._backoff.next = now + delay;
331
+ await new Promise(r => setTimeout(r, delay));
332
+ // Graceful stop old listener
333
+ if (this._activeListenerStop && typeof this._activeListenerStop === 'function') {
334
+ try { this._activeListenerStop(); } catch (_) {}
335
+ }
336
+ if (this.api && typeof this.api.listenMqtt === 'function' && !this._destroyed) {
337
+ const stop = this.api.listenMqtt((err, event) => {
338
+ if (!err && event) this.recordEvent();
339
+ });
340
+ this._activeListenerStop = stop;
341
+ if (attempt > 1) this.safetyEmit('mqttBackoff', { attempt, delay, reason });
342
+ else this.safetyEmit('mqttReconnect', { success: true, reason });
343
+ }
344
+ // Reset backoff on success detection soon after
345
+ setTimeout(() => {
346
+ if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
347
+ this._backoff.attempt = 0;
348
+ }
349
+ }, 5000);
350
+ } catch (e) {
351
+ this.safetyEmit('mqttReconnect', { success: false, error: e.message });
352
+ } finally {
353
+ this._reconnecting = false;
354
+ }
355
+ }
356
+
357
+ // Heartbeat ping & watchdog
358
+ _startHeartbeat() {
359
+ if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
360
+ if (this._watchdogTimer) clearInterval(this._watchdogTimer);
361
+ if (this._destroyed) return;
362
+ this._heartbeatTimer = setInterval(() => {
363
+ if (this._destroyed) return;
364
+ try {
365
+ if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
366
+ if (this.ctx.mqttClient.ping) this.ctx.mqttClient.ping();
367
+ this.safetyEmit('heartbeat', { ts: Date.now() });
368
+ }
369
+ } catch (_) {}
370
+ }, 60 * 1000 + Math.random() * 5000); // 60s ±5s
371
+ this._watchdogTimer = setInterval(() => {
372
+ if (this._destroyed) return;
373
+ const idle = Date.now() - this._lastEventTs;
374
+ if (idle > 2 * 60 * 1000) { // 2 min no events -> soft check
375
+ this._ensureMqttAlive();
376
+ }
377
+ if (idle > 15 * 60 * 1000) { // 15 min -> force reconnect attempt ignoring backoff
378
+ this._backoff.attempt = 0; // reset to allow immediate
379
+ this._ensureMqttAlive();
380
+ }
381
+ }, 30 * 1000); // watchdog every 30s
268
382
  }
269
383
 
270
384
  /**
271
385
  * Start safety monitoring for session
272
386
  */
273
- startMonitoring(ctx, api) {
387
+ startMonitoring(ctx, api) { // added persistence of ctx/api so refresh can use them
274
388
  if (!ctx || !api) return;
275
-
276
- // Monitor for account issues
277
- setInterval(() => {
389
+ this.ctx = ctx; // persist for later safe refresh
390
+ this.api = api;
391
+ if (this._monitorInterval) clearInterval(this._monitorInterval);
392
+ this._monitorInterval = setInterval(() => {
278
393
  this.checkAccountHealth(ctx, api);
279
- }, 30000); // Check every 30 seconds
394
+ }, 30000);
395
+ // Attach lightweight hook if api emits events to update lastEventTs externally if user wires it
396
+ this.recordEvent();
397
+ this._startHeartbeat();
280
398
  }
281
399
 
282
400
  /**
@@ -290,7 +408,7 @@ class FacebookSafety {
290
408
  const userCookie = cookies.find(c => c.key === 'c_user');
291
409
 
292
410
  if (!userCookie) {
293
- this.emit('accountIssue', {
411
+ this.safetyEmit('accountIssue', {
294
412
  type: 'session_expired',
295
413
  message: 'User session cookie missing'
296
414
  });
@@ -301,7 +419,7 @@ class FacebookSafety {
301
419
 
302
420
  const safetyCheck = this.checkErrorSafety(error);
303
421
  if (!safetyCheck.safe) {
304
- this.emit('accountIssue', {
422
+ this.safetyEmit('accountIssue', {
305
423
  type: safetyCheck.danger,
306
424
  message: error.message,
307
425
  recommendation: safetyCheck.recommendation
@@ -314,8 +432,85 @@ class FacebookSafety {
314
432
  * Refresh session safely
315
433
  */
316
434
  async refreshSafeSession() {
317
- // Implement safe session refresh logic
318
- console.log('🔄 Performing safe session refresh...');
435
+ // Improved safe session refresh implementation
436
+ if (this._refreshing) return; // prevent concurrent refreshes
437
+ this._refreshing = true;
438
+ const refreshId = ++this._inFlightRefreshId;
439
+ const startedAt = Date.now();
440
+ let preMqttConnected = this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected;
441
+ let preLastEvent = this._lastEventTs;
442
+ try {
443
+ console.log('🔄 Performing safe session refresh...');
444
+ if (!this.api || typeof this.api.refreshFb_dtsg !== 'function') {
445
+ console.log('⚠️ Safe refresh skipped: api.refreshFb_dtsg not available');
446
+ return;
447
+ }
448
+ // Abort protection if takes too long (network hang)
449
+ const timeoutMs = 25 * 1000;
450
+ const controller = new AbortController();
451
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
452
+ let res;
453
+ try {
454
+ res = await this.api.refreshFb_dtsg({ signal: controller.signal });
455
+ } finally { clearTimeout(timeout); }
456
+ this.sessionMetrics.errorCount = Math.max(0, this.sessionMetrics.errorCount - 1);
457
+ this.sessionMetrics.lastActivity = Date.now();
458
+ this.safetyEmit('safeRefresh', {
459
+ ok: true,
460
+ fb_dtsg: this.ctx && this.ctx.fb_dtsg,
461
+ jazoest: this.ctx && this.ctx.jazoest,
462
+ durationMs: Date.now() - startedAt,
463
+ message: 'Session tokens refreshed'
464
+ });
465
+ // Immediate MQTT health ensure
466
+ await this._ensureMqttAlive();
467
+ // Schedule layered post-refresh checks (1s, 10s, 30s) to catch silent drops
468
+ const checksAt = [1000, 10000, 30000];
469
+ checksAt.forEach(delay => {
470
+ const handle = setTimeout(() => {
471
+ if (this._destroyed) return;
472
+ if (refreshId !== this._inFlightRefreshId) return; // newer refresh superseded
473
+ this._ensureMqttAlive();
474
+ }, delay);
475
+ this._postRefreshChecks.push(handle);
476
+ });
477
+ // If previously connected and now no events for >1 min after refresh -> reconnect
478
+ setTimeout(() => {
479
+ if (this._destroyed) return;
480
+ if (preMqttConnected && Date.now() - Math.max(this._lastEventTs, preLastEvent) > 60 * 1000) {
481
+ this._backoff.attempt = 0; // reset backoff for immediate action
482
+ this._ensureMqttAlive();
483
+ }
484
+ }, 60 * 1000);
485
+ } catch (e) {
486
+ this.recordRequest(true);
487
+ this.safetyEmit('safeRefresh', {
488
+ ok: false,
489
+ error: e.message,
490
+ durationMs: Date.now() - startedAt
491
+ });
492
+ if (this.sessionMetrics.errorCount > 3) {
493
+ this.sessionMetrics.riskLevel = 'high';
494
+ }
495
+ // Force reconnection attempt if refresh failed & potential token invalidation
496
+ this._backoff.attempt = 0;
497
+ await this._ensureMqttAlive();
498
+ } finally {
499
+ this._refreshing = false;
500
+ }
501
+ }
502
+
503
+ // Cleanup / destroy resources (to prevent dangling timers)
504
+ destroy() {
505
+ this._destroyed = true;
506
+ const timers = [this._safeRefreshInterval, this._safeRefreshTimer, this._heartbeatTimer, this._watchdogTimer];
507
+ timers.forEach(t => t && clearTimeout(t));
508
+ if (this._activeListenerStop) {
509
+ try { this._activeListenerStop(); } catch (_) {}
510
+ this._activeListenerStop = null;
511
+ }
512
+ this._postRefreshChecks.forEach(h => clearTimeout(h));
513
+ this._postRefreshChecks = [];
319
514
  }
320
515
 
321
516
  /**
@@ -351,7 +546,7 @@ class FacebookSafety {
351
546
  /**
352
547
  * Emit safety events
353
548
  */
354
- emit(event, data) {
549
+ safetyEmit(event, data) {
355
550
  if (typeof this.onSafetyEvent === 'function') {
356
551
  this.onSafetyEvent(event, data);
357
552
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-fca",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "A modern, safe, and advanced Facebook Chat API for Node.js with fully integrated Nexus Login System. NPM-ready with ID/password/2FA support, ultra-low ban rate protection, and zero external dependencies.",
5
5
  "main": "index.js",
6
6
  "repository": {