nexus-fca 2.1.2 → 2.1.5

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,13 +1,53 @@
1
1
  # Changelog
2
2
 
3
- ## [2.1.2] - Unreleased - CONTINUOUS IDLE RECOVERY
3
+ ## [2.1.5] - 2025-08-28 - PendingEdits & ACK Metrics
4
+ ### Added
5
+ - PendingEdits buffer with cap (default 200) + TTL (5m) + resend attempts (2) + ACK timeout (12s).
6
+ - Automatic edit resend watchdog with safe limits and metrics (editResends, editFailed, pendingEditsDropped, pendingEditsExpired).
7
+ - API: `api.setEditOptions({ maxPendingEdits, editTTLms, ackTimeoutMs, maxResendAttempts })`.
8
+ - Edit ACK integration: pending edit removed on ACK receipt.
9
+ - Health metrics: p95AckLatencyMs, editResends, editFailed.
10
+
11
+ ### Improved
12
+ - Safer edit pipeline: prevents uncontrolled retries, bounds memory, tracks expirations.
13
+ - Enhanced HealthMetrics with percentile latency sample retention (50-sample window).
14
+
15
+ ### Notes
16
+ - Durable outbound queue & metrics exporter planned next.
17
+
18
+ ---
19
+
20
+ ## [2.1.4] - Adaptive Backoff & Core Metrics
21
+ ### Added
22
+ - Adaptive reconnect backoff with jitter (caps at 5 minutes) for safer, stealthier recovery loops.
23
+ - Lazy preflight session validation (skips heavy validation if a recent successful connect occurred) toggle via `api.enableLazyPreflight()`.
24
+ - Health metrics collector (uptime, idle time, reconnect counts, failures, message/ack counters, synthetic keepalives) accessible with `api.getHealthMetrics()` and included in `api.healthCheck()`.
25
+ - Randomized synthetic keepalive interval (55-75s) to reduce detection patterns.
26
+ - Backoff configuration hook: `api.setBackoffOptions()`.
27
+
28
+ ### Improved
29
+ - Reduced noisy session validation on every `listenMqtt` invocation unless needed.
30
+ - More structured error classification feeding metrics (`session_invalid`, `timeout_no_t_ms`, `mqtt_error`, `message_parse`, `not_logged_in`).
31
+
32
+ ### Planned (Next)
33
+ - ACK tracking refinement & resend logic.
34
+ - Pending edits buffer with TTL and cap.
35
+ - Durable outbound queue & health exporter.
36
+
37
+ ---
38
+
39
+ ## [2.1.2] - CONTINUOUS IDLE RECOVERY
4
40
  ### Added
5
41
  - Soft-stale probing at 2 minutes idle (ping + conditional forced reconnect if no events within 5-8s)
6
42
  - Wrapper around `listenMqtt` to automatically feed events into safety heartbeat (`recordEvent`) for precise idle detection
43
+ - Ghost connection detection (10m silent but socket connected triggers forced reconnect after probe)
44
+ - Periodic connection recycle every ~6h ±30m to prevent long-lived silent degradation
45
+ - Force reconnect API: `globalSafety.forceReconnect(tag)`
7
46
 
8
47
  ### Improved
9
48
  - Faster recovery from silent idle states (previously required >5 min or external trigger)
10
49
  - Reduced chance of appearing online but unresponsive after short inactivity
50
+ - Added keepalive foreground_state publishes each heartbeat
11
51
 
12
52
  ---
13
53
 
package/README.md CHANGED
@@ -1,4 +1,7 @@
1
- # Nexus-FCA v2.1.0
1
+ # Nexus-FCA v2.1.5
2
+
3
+ <!-- 2.1.5 Highlights (PendingEdits & ACK Metrics) -->
4
+ > New in 2.1.5: PendingEdits buffer (cap + TTL + safe resend), edit ACK watchdog, p95 ACK latency & edit resend/failure metrics, configurable via `api.setEditOptions()`.
2
5
 
3
6
  <p align="center">
4
7
  <!-- Preview image wrapped in link (corrected ibb.co domain) -->
package/index.js CHANGED
@@ -10,6 +10,33 @@ const path = require('path');
10
10
  const models = require("./lib/database/models");
11
11
  const logger = require("./lib/logger");
12
12
  const { safeMode, ultraSafeMode, smartSafetyLimiter, isUserAllowed } = require('./utils'); // Enhanced safety system
13
+ // Minimal aesthetic banner system
14
+ let _fancyBannerPrinted = false;
15
+ const gradient = (() => { try { return require('gradient-string'); } catch(_) { return null; } })();
16
+ const pkgMeta = (() => { try { return require('./package.json'); } catch(_) { return { version: 'dev' }; } })();
17
+ function printFancyStartupBanner() {
18
+ if (_fancyBannerPrinted) return; _fancyBannerPrinted = true;
19
+ const lines = [
20
+ '╔══════════════════════════════════════════════════════╗',
21
+ '║ Nexus-FCA Secure Login ║',
22
+ '║ Advanced Stable Messenger Automation API ║',
23
+ '╚══════════════════════════════════════════════════════╝'
24
+ ];
25
+ if (gradient) console.log(gradient.cristal.multiline(lines.join('\n'))); else console.log(lines.join('\n'));
26
+ }
27
+ function printIdentityBanner(uid, name) {
28
+ const cleanName = name || 'Unknown';
29
+ const pad = (txt, len) => (txt.length < len ? txt + ' '.repeat(len - txt.length) : txt.substring(0, len));
30
+ const bodyLen = 54;
31
+ const line = (content) => `║ ${pad(content, bodyLen)} ║`;
32
+ const box = [
33
+ '╔════════════════ LOGGED IN IDENTITY ════════════════╗',
34
+ line(`UID : ${uid}`),
35
+ line(`Name: ${cleanName}`),
36
+ '╚════════════════════════════════════════════════════╝'
37
+ ];
38
+ if (gradient) console.log(gradient.atlas.multiline(box.join('\n'))); else console.log(box.join('\n'));
39
+ }
13
40
 
14
41
  // Enhanced imports - All new modules
15
42
  const { NexusClient } = require('./lib/compatibility/NexusClient');
@@ -208,8 +235,21 @@ function buildAPI(globalOptions, html, jar) {
208
235
  firstListen: true,
209
236
  fb_dtsg,
210
237
  wsReqNumber: 0,
211
- wsTaskNumber: 0
238
+ wsTaskNumber: 0,
239
+ // Provide safety module reference to lower layers (listenMqtt)
240
+ globalSafety,
241
+ // Pending edit tracking (Stage 2)
242
+ pendingEdits: new Map()
212
243
  };
244
+ // Default edit / resend safety settings
245
+ if(!globalOptions.editSettings){
246
+ globalOptions.editSettings = {
247
+ maxPendingEdits: 200,
248
+ editTTLms: 5*60*1000,
249
+ ackTimeoutMs: 12000,
250
+ maxResendAttempts: 2
251
+ };
252
+ }
213
253
  const api = {
214
254
  setOptions: setOptions.bind(null, globalOptions),
215
255
  getAppState: function getAppState() {
@@ -222,14 +262,18 @@ function buildAPI(globalOptions, html, jar) {
222
262
  );
223
263
  },
224
264
  healthCheck: function(callback) {
225
- // Simple health check: returns status and safeMode info
226
265
  callback(null, {
227
266
  status: 'ok',
228
267
  safeMode,
229
268
  time: new Date().toISOString(),
230
- userID: ctx.userID || null
269
+ userID: ctx.userID || null,
270
+ metrics: ctx.health ? ctx.health.snapshot() : null
231
271
  });
232
272
  },
273
+ getHealthMetrics: function(){ return ctx.health ? ctx.health.snapshot() : null; },
274
+ enableLazyPreflight(enable=true){ ctx.globalOptions.disablePreflight = !enable; },
275
+ setBackoffOptions(opts={}){ ctx.globalOptions.backoff = Object.assign(ctx.globalOptions.backoff||{}, opts); },
276
+ setEditOptions(opts={}){ Object.assign(ctx.globalOptions.editSettings, opts); }
233
277
  };
234
278
  const defaultFuncs = utils.makeDefaults(html, i_userID || userID, ctx);
235
279
  require("fs")
@@ -269,6 +313,59 @@ function buildAPI(globalOptions, html, jar) {
269
313
  console.error("An error occurred while refreshing fb_dtsg", err);
270
314
  });
271
315
  }, 1000 * 60 * 60 * 24);
316
+ // === Group Queue (No Cooldown, Sequential per group) ===
317
+ (function initGroupQueue(){
318
+ const groupQueues = new Map(); // threadID -> { q: [], sending: false }
319
+ const isGroupThread = (tid) => typeof tid === 'string' && tid.length >= 15; // simple heuristic
320
+ const DIRECT_FN = api.sendMessage; // original
321
+
322
+ api.enableGroupQueue = function(enable=true){
323
+ globalOptions.groupQueueEnabled = !!enable;
324
+ };
325
+ api.setGroupQueueCapacity = function(n){ globalOptions.groupQueueMax = n; };
326
+ api.enableGroupQueue(true);
327
+ api.setGroupQueueCapacity(100); // allow up to 100 pending per group
328
+
329
+ api._sendMessageDirect = DIRECT_FN;
330
+ api.sendMessage = function(message, threadID, cb){
331
+ if(!globalOptions.groupQueueEnabled || !isGroupThread(threadID)) {
332
+ return api._sendMessageDirect(message, threadID, cb);
333
+ }
334
+ let entry = groupQueues.get(threadID);
335
+ if(!entry){ entry = { q: [], sending: false }; groupQueues.set(threadID, entry); }
336
+ if(entry.q.length >= (globalOptions.groupQueueMax||100)) {
337
+ // drop oldest (keep newest) to avoid unbounded growth
338
+ entry.q.shift();
339
+ }
340
+ entry.q.push({ message, threadID, cb });
341
+ processQueue(threadID, entry);
342
+ };
343
+
344
+ function processQueue(threadID, entry){
345
+ if(entry.sending) return;
346
+ if(!entry.q.length) return;
347
+ entry.sending = true;
348
+ const { message, threadID: tid, cb } = entry.q.shift();
349
+ api._sendMessageDirect(message, tid, function(err, res){
350
+ try { if(!err) globalSafety.recordEvent(); } catch(_) {}
351
+ if(typeof cb === 'function') cb(err, res);
352
+ entry.sending = false;
353
+ // Immediately process next (no cooldown) to keep strict sequence
354
+ setImmediate(()=>processQueue(threadID, entry));
355
+ });
356
+ }
357
+
358
+ api._flushGroupQueue = function(threadID){
359
+ const entry = groupQueues.get(threadID);
360
+ if(!entry) return;
361
+ while(entry.q.length) {
362
+ const item = entry.q.shift();
363
+ api._sendMessageDirect(item.message, item.threadID, item.cb);
364
+ }
365
+ entry.sending = false;
366
+ };
367
+ })();
368
+ // === End Group Queue ===
272
369
  return {
273
370
  ctx,
274
371
  defaultFuncs,
@@ -364,12 +461,25 @@ function loginHelper(appState, email, password, globalOptions, callback, prCallb
364
461
  if (!safetyStatus.safe) {
365
462
  logger(`⚠️ Login safety warning: ${safetyStatus.reason}`, 'warn');
366
463
  }
367
-
368
464
  logger('✅ Session authenticated successfully', 'info');
369
-
370
465
  // Initialize safety monitoring
371
466
  globalSafety.startMonitoring(ctx, api);
372
-
467
+ // Post-login identity banner
468
+ try {
469
+ const uid = api.getCurrentUserID && api.getCurrentUserID();
470
+ if (api.getUserInfo && uid) {
471
+ api.getUserInfo(uid, (err, info) => {
472
+ if (!err && info) {
473
+ const userObj = info[uid] || info; // depending on structure
474
+ printIdentityBanner(uid, userObj.name || userObj.firstName || userObj.fullName);
475
+ } else {
476
+ printIdentityBanner(uid || 'N/A');
477
+ }
478
+ });
479
+ } else {
480
+ printIdentityBanner(uid || 'N/A');
481
+ }
482
+ } catch(_) { /* ignore */ }
373
483
  callback(null, api);
374
484
  })
375
485
  .catch(e => {
@@ -893,6 +1003,7 @@ class IntegratedNexusLoginSystem {
893
1003
 
894
1004
  // Integrated Nexus Login wrapper for easy usage
895
1005
  async function integratedNexusLogin(credentials = null, options = {}) {
1006
+ printFancyStartupBanner();
896
1007
  const loginSystem = new IntegratedNexusLoginSystem(options);
897
1008
 
898
1009
  // Professional logging system
@@ -1008,6 +1119,7 @@ async function integratedNexusLogin(credentials = null, options = {}) {
1008
1119
  * - Appstate only: Uses existing session directly
1009
1120
  */
1010
1121
  async function login(loginData, options = {}, callback) {
1122
+ printFancyStartupBanner();
1011
1123
  // Support multiple callback signatures
1012
1124
  if (typeof options === 'function') {
1013
1125
  callback = options;
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ // Lightweight health & metrics tracker for Nexus-FCA (extended Stage 2)
3
+ class HealthMetrics {
4
+ constructor() {
5
+ const now = Date.now();
6
+ this.uptimeStart = now;
7
+ this.lastConnectTs = 0;
8
+ this.lastDisconnectTs = 0;
9
+ this.lastMessageTs = 0;
10
+ this.lastErrorTs = 0;
11
+ this.lastErrorType = null;
12
+ this.reconnects = 0;
13
+ this.consecutiveFailures = 0;
14
+ this.messagesReceived = 0;
15
+ this.syntheticKeepAlives = 0;
16
+ this.acksReceived = 0;
17
+ this.pendingEdits = 0;
18
+ this.pendingEditMap = new Map(); // messageID -> { ts, text, attempts }
19
+ this.pendingEditsDropped = 0; // dropped due to cap
20
+ this.pendingEditsExpired = 0; // expired via TTL sweep
21
+ this.outboundQueueDepth = 0;
22
+ this.outboundQueueDropped = 0;
23
+ this.lastAckLatencyMs = null;
24
+ this.avgAckLatencyMs = null;
25
+ this._ackLatencySamples = [];
26
+ this.p95AckLatencyMs = null;
27
+ this.editResends = 0;
28
+ this.editFailed = 0;
29
+ }
30
+ onConnect() { this.lastConnectTs = Date.now(); this.consecutiveFailures = 0; }
31
+ onDisconnect() { this.lastDisconnectTs = Date.now(); }
32
+ onMessage() { this.messagesReceived++; this.lastMessageTs = Date.now(); }
33
+ onSynthetic() { this.syntheticKeepAlives++; }
34
+ onAck(latencyMs){
35
+ this.acksReceived++;
36
+ if(typeof latencyMs === 'number'){
37
+ this.lastAckLatencyMs = latencyMs;
38
+ if(this.avgAckLatencyMs == null) this.avgAckLatencyMs = latencyMs; else this.avgAckLatencyMs = Math.round(this.avgAckLatencyMs*0.8 + latencyMs*0.2);
39
+ // track distribution (cap list length for memory safety)
40
+ this._ackLatencySamples.push(latencyMs);
41
+ if(this._ackLatencySamples.length > 50) this._ackLatencySamples.shift();
42
+ this._recalcP95();
43
+ }
44
+ }
45
+ _recalcP95(){
46
+ if(!this._ackLatencySamples.length){ this.p95AckLatencyMs = null; return; }
47
+ const sorted = [...this._ackLatencySamples].sort((a,b)=>a-b);
48
+ const idx = Math.min(sorted.length-1, Math.floor(sorted.length*0.95));
49
+ this.p95AckLatencyMs = sorted[idx];
50
+ }
51
+ onError(type){ this.lastErrorTs = Date.now(); this.lastErrorType = type || 'unknown'; }
52
+ onReconnectScheduled(delay){ this.reconnects++; this.currentBackoffDelay = delay; if(delay > (this.maxObservedBackoff||0)) this.maxObservedBackoff = delay; }
53
+ trackOutbound(depth){ this.outboundQueueDepth = depth; }
54
+ incOutboundDropped(){ this.outboundQueueDropped++; }
55
+ addPendingEdit(mid, text){ this.pendingEditMap.set(mid, { ts: Date.now(), text, attempts: 0 }); this.pendingEdits = this.pendingEditMap.size; }
56
+ markEditResent(mid){ const rec = this.pendingEditMap.get(mid); if(rec){ rec.attempts++; this.editResends++; } }
57
+ markEditFailed(mid){ if(this.pendingEditMap.delete(mid)) { this.pendingEdits = this.pendingEditMap.size; this.editFailed++; } }
58
+ removePendingEdit(mid){ if(this.pendingEditMap.delete(mid)) this.pendingEdits = this.pendingEditMap.size; }
59
+ incPendingEditDropped(){ this.pendingEditsDropped++; }
60
+ incPendingEditExpired(n=1){ this.pendingEditsExpired += n; }
61
+ sweepPendingEdits(ttlMs){
62
+ const now = Date.now();
63
+ let expired = 0;
64
+ for(const [mid, val] of this.pendingEditMap.entries()){
65
+ if(now - val.ts > ttlMs){ this.pendingEditMap.delete(mid); expired++; }
66
+ }
67
+ if(expired) { this.incPendingEditExpired(expired); }
68
+ this.pendingEdits = this.pendingEditMap.size;
69
+ }
70
+ snapshot(){
71
+ const now = Date.now();
72
+ const idleMs = now - (this.lastMessageTs || this.lastConnectTs || now);
73
+ return {
74
+ uptimeMs: now - this.uptimeStart,
75
+ idleMs,
76
+ reconnects: this.reconnects,
77
+ consecutiveFailures: this.consecutiveFailures,
78
+ lastErrorType: this.lastErrorType,
79
+ lastErrorAgoMs: this.lastErrorTs ? now - this.lastErrorTs : null,
80
+ currentBackoffDelay: this.currentBackoffDelay||0,
81
+ maxObservedBackoff: this.maxObservedBackoff||0,
82
+ messagesReceived: this.messagesReceived,
83
+ syntheticKeepAlives: this.syntheticKeepAlives,
84
+ acksReceived: this.acksReceived,
85
+ lastAckLatencyMs: this.lastAckLatencyMs,
86
+ avgAckLatencyMs: this.avgAckLatencyMs,
87
+ p95AckLatencyMs: this.p95AckLatencyMs,
88
+ pendingEdits: this.pendingEdits,
89
+ pendingEditsDropped: this.pendingEditsDropped,
90
+ pendingEditsExpired: this.pendingEditsExpired,
91
+ editResends: this.editResends,
92
+ editFailed: this.editFailed,
93
+ outboundQueueDepth: this.outboundQueueDepth,
94
+ outboundQueueDropped: this.outboundQueueDropped,
95
+ healthy: this.isHealthy(idleMs)
96
+ };
97
+ }
98
+ isHealthy(idleMs){ if (this.consecutiveFailures >= 10) return false; if (this.messagesReceived < 5 && idleMs > 5*60*1000) return false; return true; }
99
+ }
100
+ module.exports = { HealthMetrics };
@@ -68,6 +68,10 @@ class FacebookSafety {
68
68
  this._inFlightRefreshId = 0;
69
69
  // New: probing guard to avoid overlapping soft-stale probes
70
70
  this._probing = false;
71
+ // Ghost detection guard
72
+ this._ghostChecking = false;
73
+ // Periodic recycle timer
74
+ this._periodicRecycleTimer = null;
71
75
 
72
76
  this.initSafety();
73
77
  }
@@ -80,6 +84,7 @@ class FacebookSafety {
80
84
 
81
85
  // Setup session monitoring
82
86
  this.setupSessionMonitoring();
87
+ this._schedulePeriodicRecycle();
83
88
  }
84
89
 
85
90
  /**
@@ -245,18 +250,19 @@ class FacebookSafety {
245
250
  clearTimeout(this._safeRefreshTimer);
246
251
  this._safeRefreshTimer = null;
247
252
  }
248
- // Use recursive timeout with randomization each cycle (more human-like)
253
+ // Stealth+Resilient profile refresh policy:
254
+ // risk low: 50-60m, medium: 40-50m, high: 25-35m (random inside band)
249
255
  const schedule = () => {
250
256
  if (this._destroyed) return;
251
- // Adaptive interval: shorter if high risk (to revalidate), longer if stable
252
- const base = this.sessionMetrics.riskLevel === 'high' ? 25 : this.sessionMetrics.riskLevel === 'medium' ? 35 : 45; // minutes
253
- const baseInterval = base * 60 * 1000;
254
- const randomVariation = (Math.random() * 16 - 8) * 60 * 1000; // ±8 min
255
- const interval = baseInterval + randomVariation;
257
+ let minM, maxM;
258
+ if (this.sessionMetrics.riskLevel === 'high') { minM = 25; maxM = 35; }
259
+ else if (this.sessionMetrics.riskLevel === 'medium') { minM = 40; maxM = 50; }
260
+ else { minM = 50; maxM = 60; }
261
+ const interval = (minM * 60 * 1000) + Math.random() * ((maxM - minM) * 60 * 1000);
256
262
  this._safeRefreshTimer = setTimeout(async () => {
257
263
  await this.refreshSafeSession();
258
264
  schedule();
259
- }, Math.max(10 * 60 * 1000, interval)); // never below 10 min
265
+ }, interval);
260
266
  };
261
267
  schedule();
262
268
  }
@@ -311,40 +317,27 @@ class FacebookSafety {
311
317
  const now = Date.now();
312
318
  const disconnected = !this.ctx || !this.ctx.mqttClient || !this.ctx.mqttClient.connected;
313
319
  const idle = now - this._lastEventTs;
314
- const softStale = idle > 2 * 60 * 1000; // >2 min no events
315
- const hardStale = idle > 5 * 60 * 1000; // >5 min no events (legacy threshold)
316
- const stale = hardStale; // backwards compat naming
317
-
318
- // If totally disconnected or hard stale -> reconnect immediately
320
+ const softStale = idle > (2.5 * 60 * 1000); // Stealth profile: 2m30s
321
+ const hardStale = idle > 8 * 60 * 1000; // escalate earlier than watchdog hard (8m)
322
+ const stale = hardStale;
319
323
  if (disconnected || stale) {
320
324
  await this._reconnectMqttWithBackoff(disconnected ? 'disconnected' : 'hard-stale');
321
325
  return;
322
326
  }
323
-
324
- // Soft-stale probing: connection claims to be open but no events for 2-5 minutes.
325
- // We issue a ping and if still no events after probe window, we force a reconnect.
326
327
  if (softStale && !this._probing) {
327
328
  this._probing = true;
328
329
  const prevTs = this._lastEventTs;
329
- try {
330
- if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
331
- if (typeof this.ctx.mqttClient.ping === 'function') {
332
- try { this.ctx.mqttClient.ping(); } catch(_) {}
333
- }
334
- }
335
- } catch(_) {}
330
+ try { if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected && typeof this.ctx.mqttClient.ping === 'function') this.ctx.mqttClient.ping(); } catch(_) {}
336
331
  setTimeout(() => {
337
332
  if (this._destroyed) return;
338
- // If no new events arrived since probe start, treat as latent-dead connection
339
333
  if (this._lastEventTs <= prevTs) {
340
- // Reset backoff to allow immediate reconnect (latency sensitive)
341
334
  this._backoff.attempt = 0;
342
335
  this._reconnectMqttWithBackoff('soft-stale');
343
336
  }
344
337
  this._probing = false;
345
- }, 5000 + Math.random() * 3000); // 5-8s probe window
338
+ }, 6000 + Math.random() * 2000); // 6-8s probe window (Stealth+Resilient)
346
339
  }
347
- } catch (_) { /* swallow */ }
340
+ } catch(_) {}
348
341
  }
349
342
 
350
343
  // Progressive backoff + jitter reconnect
@@ -353,38 +346,47 @@ class FacebookSafety {
353
346
  this._reconnecting = true;
354
347
  try {
355
348
  const now = Date.now();
356
- if (now < this._backoff.next) {
357
- return; // respect backoff window
358
- }
349
+ if (now < this._backoff.next) { return; }
359
350
  const attempt = ++this._backoff.attempt;
360
- const baseDelay = Math.min(30000, 1000 * Math.pow(2, Math.min(attempt, 5))); // cap 30s
361
- const jitter = Math.random() * 400;
351
+ // Stealth backoff: 1.2s * 1.8^n capped ~20s, add jitter 0-500ms
352
+ const baseDelay = Math.min(20000, 1200 * Math.pow(1.8, Math.min(attempt, 6)));
353
+ const jitter = Math.random() * 500;
362
354
  const delay = baseDelay + jitter;
363
355
  this._backoff.next = now + delay;
364
356
  await new Promise(r => setTimeout(r, delay));
365
- // Graceful stop old listener
366
- if (this._activeListenerStop && typeof this._activeListenerStop === 'function') {
367
- try { this._activeListenerStop(); } catch (_) {}
368
- }
357
+ if (this._activeListenerStop && typeof this._activeListenerStop === 'function') { try { this._activeListenerStop(); } catch(_) {} }
369
358
  if (this.api && typeof this.api.listenMqtt === 'function' && !this._destroyed) {
370
- const stop = this.api.listenMqtt((err, event) => {
371
- if (!err && event) this.recordEvent();
372
- });
359
+ const stop = this.api.listenMqtt((err, event) => { if (!err && event) this.recordEvent(); });
373
360
  this._activeListenerStop = stop;
374
- if (attempt > 1) this.safetyEmit('mqttBackoff', { attempt, delay, reason });
375
- else this.safetyEmit('mqttReconnect', { success: true, reason });
361
+ this.safetyEmit('mqttReconnect', { success: true, reason, attempt, delay });
376
362
  }
377
- // Reset backoff on success detection soon after
378
363
  setTimeout(() => {
379
- if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
380
- this._backoff.attempt = 0;
381
- }
364
+ if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) { this._backoff.attempt = 0; }
382
365
  }, 5000);
383
- } catch (e) {
384
- this.safetyEmit('mqttReconnect', { success: false, error: e.message });
385
- } finally {
386
- this._reconnecting = false;
387
- }
366
+ } catch(e) {
367
+ this.safetyEmit('mqttReconnect', { success: false, error: e.message, reason });
368
+ } finally { this._reconnecting = false; }
369
+ }
370
+
371
+ // Public force reconnect (bypass backoff)
372
+ forceReconnect(tag = 'manual') {
373
+ if (this._destroyed) return;
374
+ this._backoff.attempt = 0;
375
+ return this._reconnectMqttWithBackoff('force-' + tag);
376
+ }
377
+
378
+ // Schedule periodic recycle (connection rejuvenation) every 6h ±30m jitter
379
+ _schedulePeriodicRecycle() {
380
+ if (this._periodicRecycleTimer) clearTimeout(this._periodicRecycleTimer);
381
+ if (this._destroyed) return;
382
+ const base = 6 * 60 * 60 * 1000; // 6h
383
+ const jitter = (Math.random() * 60 - 30) * 60 * 1000; // ±30m
384
+ const delay = base + jitter;
385
+ this._periodicRecycleTimer = setTimeout(() => {
386
+ if (this._destroyed) return;
387
+ this.forceReconnect('periodic');
388
+ this._schedulePeriodicRecycle();
389
+ }, delay);
388
390
  }
389
391
 
390
392
  // Heartbeat ping & watchdog
@@ -392,26 +394,38 @@ class FacebookSafety {
392
394
  if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
393
395
  if (this._watchdogTimer) clearInterval(this._watchdogTimer);
394
396
  if (this._destroyed) return;
397
+ // Stealth profile heartbeat: 80–100s random
395
398
  this._heartbeatTimer = setInterval(() => {
396
399
  if (this._destroyed) return;
397
400
  try {
398
401
  if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
399
402
  if (this.ctx.mqttClient.ping) this.ctx.mqttClient.ping();
403
+ try { this.ctx.mqttClient.publish('/foreground_state', JSON.stringify({ foreground: true })); } catch(_) {}
400
404
  this.safetyEmit('heartbeat', { ts: Date.now() });
401
405
  }
402
- } catch (_) {}
403
- }, 60 * 1000 + Math.random() * 5000); // 60s ±5s
406
+ } catch(_) {}
407
+ }, (80 + Math.random()*20) * 1000);
404
408
  this._watchdogTimer = setInterval(() => {
405
409
  if (this._destroyed) return;
406
410
  const idle = Date.now() - this._lastEventTs;
407
- if (idle > 2 * 60 * 1000) { // 2 min no events -> soft check
408
- this._ensureMqttAlive();
411
+ // Soft escalate already handled inside _ensureMqttAlive at 2m30s
412
+ // Ghost detection earlier: 9m
413
+ if (idle > 9 * 60 * 1000 && !this._ghostChecking && this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
414
+ this._ghostChecking = true;
415
+ const before = this._lastEventTs;
416
+ try { if (this.ctx.mqttClient.ping) this.ctx.mqttClient.ping(); } catch(_) {}
417
+ setTimeout(() => {
418
+ if (this._destroyed) return;
419
+ if (this._lastEventTs <= before) { this.forceReconnect('ghost'); }
420
+ setTimeout(() => { this._ghostChecking = false; }, 5 * 60 * 1000);
421
+ }, 6000 + Math.random()*2000);
409
422
  }
410
- if (idle > 15 * 60 * 1000) { // 15 min -> force reconnect attempt ignoring backoff
411
- this._backoff.attempt = 0; // reset to allow immediate
423
+ // Hard watchdog escalate: 12m
424
+ if (idle > 12 * 60 * 1000) {
425
+ this._backoff.attempt = 0;
412
426
  this._ensureMqttAlive();
413
427
  }
414
- }, 30 * 1000); // watchdog every 30s
428
+ }, 35 * 1000); // slight change to avoid pattern
415
429
  }
416
430
 
417
431
  /**
@@ -536,7 +550,7 @@ class FacebookSafety {
536
550
  // Cleanup / destroy resources (to prevent dangling timers)
537
551
  destroy() {
538
552
  this._destroyed = true;
539
- const timers = [this._safeRefreshInterval, this._safeRefreshTimer, this._heartbeatTimer, this._watchdogTimer];
553
+ const timers = [this._safeRefreshInterval, this._safeRefreshTimer, this._heartbeatTimer, this._watchdogTimer, this._periodicRecycleTimer];
540
554
  timers.forEach(t => t && clearTimeout(t));
541
555
  if (this._activeListenerStop) {
542
556
  try { this._activeListenerStop(); } catch (_) {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-fca",
3
- "version": "2.1.2",
3
+ "version": "2.1.5",
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": {
@@ -1,57 +1,81 @@
1
1
  // Nexus-FCA: Advanced and Safe Facebook Chat API
2
- // editMessage.js - Edit a sent message via MQTT
2
+ // editMessage.js - Edit a sent message via MQTT with PendingEdits buffer & safe resend
3
3
 
4
4
  const { generateOfflineThreadingID } = require('../utils');
5
5
 
6
- function canBeCalled(func) {
7
- try {
8
- Reflect.apply(func, null, []);
9
- return true;
10
- } catch (error) {
11
- return false;
12
- }
13
- }
14
-
15
6
  module.exports = function (defaultFuncs, api, ctx) {
16
7
  return function editMessage(text, messageID, callback) {
8
+ callback = callback || function(){};
17
9
  if (!ctx.mqttClient) {
18
- throw new Error('Not connected to MQTT');
10
+ return callback(new Error('Not connected to MQTT'));
11
+ }
12
+ if(!messageID || typeof text !== 'string') {
13
+ return callback(new Error('Invalid arguments for editMessage'));
14
+ }
15
+ // Safety: manage pending edits buffer with cap & TTL
16
+ const settings = ctx.globalOptions.editSettings || { maxPendingEdits:200, editTTLms:300000, ackTimeoutMs:12000, maxResendAttempts:2 };
17
+ // Drop oldest if capacity reached
18
+ if(ctx.pendingEdits.size >= settings.maxPendingEdits){
19
+ const firstKey = ctx.pendingEdits.keys().next().value;
20
+ if(firstKey){ ctx.pendingEdits.delete(firstKey); if(ctx.health) ctx.health.incPendingEditDropped(); }
19
21
  }
22
+ const now = Date.now();
23
+ ctx.pendingEdits.set(messageID, { text, ts: now, attempts: 0 });
24
+ if(ctx.health) ctx.health.addPendingEdit(messageID, text);
20
25
 
21
26
  ctx.wsReqNumber += 1;
22
27
  ctx.wsTaskNumber += 1;
23
28
 
24
- const queryPayload = {
25
- message_id: messageID,
26
- text: text
27
- };
28
-
29
- const query = {
30
- failure_count: null,
31
- label: '742',
32
- payload: JSON.stringify(queryPayload),
33
- queue_name: 'edit_message',
34
- task_id: ctx.wsTaskNumber
35
- };
36
-
37
- const context = {
38
- app_id: '2220391788200892',
39
- payload: JSON.stringify({
40
- data_trace_id: null,
41
- epoch_id: parseInt(generateOfflineThreadingID()),
42
- tasks: [query],
43
- version_id: '6903494529735864'
44
- }),
45
- request_id: ctx.wsReqNumber,
46
- type: 3
47
- };
48
-
49
- // if (canBeCalled(callback)) {
50
- // ctx.reqCallbacks[ctx.wsReqNumber] = callback;
51
- // }
29
+ const queryPayload = { message_id: messageID, text };
30
+ const query = { failure_count: null, label: '742', payload: JSON.stringify(queryPayload), queue_name: 'edit_message', task_id: ctx.wsTaskNumber };
31
+ const context = { app_id: '2220391788200892', payload: JSON.stringify({ data_trace_id: null, epoch_id: parseInt(generateOfflineThreadingID()), tasks: [query], version_id: '6903494529735864' }), request_id: ctx.wsReqNumber, type: 3 };
52
32
 
53
- ctx.mqttClient.publish('/ls_req', JSON.stringify(context), {
54
- qos: 1, retain: false
55
- });
33
+ try {
34
+ ctx.mqttClient.publish('/ls_req', JSON.stringify(context), { qos:1, retain:false }, (err)=>{
35
+ if(err){
36
+ if(ctx.health) ctx.health.onError('edit_publish_fail');
37
+ return callback(err);
38
+ }
39
+ // Schedule ACK / resend watchdog
40
+ scheduleEditAckWatch(messageID, settings, ctx, api, callback);
41
+ callback(null, { queued:true, messageID });
42
+ });
43
+ } catch (e) {
44
+ if(ctx.health) ctx.health.onError('edit_exception');
45
+ return callback(e);
46
+ }
56
47
  };
57
48
  };
49
+
50
+ function scheduleEditAckWatch(messageID, settings, ctx, api, originalCb){
51
+ if(!settings || !ctx) return;
52
+ const { ackTimeoutMs=12000, maxResendAttempts=2, editTTLms=300000 } = settings;
53
+ setTimeout(()=>{
54
+ const rec = ctx.pendingEdits.get(messageID);
55
+ if(!rec) return; // already acked or removed
56
+ const age = Date.now() - rec.ts;
57
+ if(age > editTTLms){
58
+ ctx.pendingEdits.delete(messageID);
59
+ if(ctx.health){ ctx.health.removePendingEdit(messageID); ctx.health.incPendingEditExpired(); }
60
+ return;
61
+ }
62
+ if(rec.attempts >= maxResendAttempts){
63
+ ctx.pendingEdits.delete(messageID);
64
+ if(ctx.health) ctx.health.markEditFailed(messageID);
65
+ return;
66
+ }
67
+ // Resend
68
+ try {
69
+ rec.attempts++;
70
+ if(ctx.health) ctx.health.markEditResent(messageID);
71
+ const queryPayload = { message_id: messageID, text: rec.text };
72
+ const resend = { failure_count:null, label:'742', payload: JSON.stringify(queryPayload), queue_name:'edit_message', task_id: ++ctx.wsTaskNumber };
73
+ const context = { app_id: '2220391788200892', payload: JSON.stringify({ data_trace_id:null, epoch_id: parseInt(generateOfflineThreadingID()), tasks:[resend], version_id:'6903494529735864' }), request_id: ++ctx.wsReqNumber, type:3 };
74
+ ctx.mqttClient.publish('/ls_req', JSON.stringify(context), { qos:1, retain:false });
75
+ // Chain another watch if still pending
76
+ scheduleEditAckWatch(messageID, settings, ctx, api, originalCb);
77
+ } catch(e){
78
+ if(ctx.health) ctx.health.onError('edit_resend_exception');
79
+ }
80
+ }, ackTimeoutMs);
81
+ }
package/src/listenMqtt.js CHANGED
@@ -11,6 +11,7 @@ var identity = function () {};
11
11
  var form = {};
12
12
  var getSeqID = function () {};
13
13
  const logger = require("../lib/logger.js");
14
+ const { HealthMetrics } = require("../lib/health/HealthMetrics");
14
15
 
15
16
  // Enhanced imports
16
17
  const MqttManager = require("../lib/mqtt/MqttManager");
@@ -38,6 +39,43 @@ const topics = [
38
39
  "/webrtc_response",
39
40
  ];
40
41
  let WebSocket_Global;
42
+ // Adaptive backoff state (per-process singleton like) - tie to ctx
43
+ function getBackoffState(ctx){
44
+ if(!ctx._adaptiveReconnect){
45
+ ctx._adaptiveReconnect = {
46
+ base: 1000, // 1s
47
+ max: 5 * 60 * 1000, // 5m
48
+ factor: 2,
49
+ jitter: 0.25, // 25% random
50
+ current: 0,
51
+ lastResetTs: 0
52
+ };
53
+ }
54
+ return ctx._adaptiveReconnect;
55
+ }
56
+ function computeNextDelay(state){
57
+ if(!state.current) state.current = state.base;
58
+ else state.current = Math.min(state.max, state.current * state.factor);
59
+ // jitter
60
+ const rand = (Math.random() * 2 - 1) * state.jitter; // -j..+j
61
+ const delay = Math.max(500, Math.round(state.current * (1 + rand)));
62
+ return delay;
63
+ }
64
+ function resetBackoff(state){
65
+ state.current = 0;
66
+ state.lastResetTs = Date.now();
67
+ }
68
+ // Build lazy preflight gating
69
+ function shouldRunPreflight(ctx){
70
+ if(ctx.globalOptions.disablePreflight) return false;
71
+ // If we connected successfully within last 10 minutes, skip heavy preflight to reduce surface.
72
+ const now = Date.now();
73
+ const metrics = ctx.health;
74
+ if(metrics && metrics.lastConnectTs && (now - metrics.lastConnectTs) < 10*60*1000){
75
+ return false;
76
+ }
77
+ return true;
78
+ }
41
79
  function buildProxy() {
42
80
  const Proxy = new Transform({
43
81
  objectMode: false,
@@ -128,20 +166,23 @@ function buildStream(options, WebSocket, Proxy) {
128
166
  return Stream;
129
167
  }
130
168
  function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
131
- // Improved preflight with option to disable
132
- if (!ctx.globalOptions.disablePreflight) {
169
+ // Attach health metrics container lazily
170
+ if(!ctx.health) ctx.health = new (require('../lib/health/HealthMetrics').HealthMetrics)();
171
+ const backoff = getBackoffState(ctx);
172
+ const runPreflight = shouldRunPreflight(ctx);
173
+ if (runPreflight) {
133
174
  (async () => {
134
175
  try {
135
- await utils.validateSession(ctx, defaultFuncs, { retries: 2, delayMs: 1000 });
176
+ await utils.validateSession(ctx, defaultFuncs, { retries: 1, delayMs: 1000 });
136
177
  } catch (e) {
137
- // Suppress first failure; only emit if still bad after short grace period
138
178
  setTimeout(() => {
139
179
  utils.validateSession(ctx, defaultFuncs, { retries: 0 }).catch(err2 => {
140
180
  log.error("listenMqtt", "Session invalid after retry: Not logged in.");
141
181
  ctx.loggedIn = false;
182
+ ctx.health.onError('session_invalid');
142
183
  globalCallback({ type: "not_logged_in", error: "Session invalid (post-retry)." });
143
184
  });
144
- }, 2000);
185
+ }, 1500);
145
186
  }
146
187
  })();
147
188
  }
@@ -170,6 +211,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
170
211
  p: null,
171
212
  php_override: ""
172
213
  };
214
+ // jitter user agent keep consistent
173
215
  const cookies = ctx.jar.getCookies("https://www.facebook.com").join("; ");
174
216
  let host;
175
217
  if (ctx.mqttEndpoint) {
@@ -233,33 +275,41 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
233
275
  const mqttClient = ctx.mqttClient;
234
276
  global.mqttClient = mqttClient;
235
277
  mqttClient.on('error', function (err) {
236
- log.error("listenMqtt", err);
237
- mqttClient.end();
238
- // classify redirect/login errors surfaced through upstream logic
239
278
  const errMsg = (err && (err.error || err.message || "")).toString();
279
+ ctx.health.onError(errMsg.includes('not logged in') ? 'not_logged_in' : 'mqtt_error');
280
+ log.error("listenMqtt", errMsg);
281
+ try { mqttClient.end(true); } catch(_){ }
240
282
  if (/not logged in|login_redirect|html_login_page/i.test(errMsg)) {
241
283
  ctx.loggedIn = false;
242
284
  return globalCallback({ type: "not_logged_in", error: errMsg });
243
285
  }
244
286
  if (ctx.globalOptions.autoReconnect) {
245
- listenMqtt(defaultFuncs, api, ctx, globalCallback);
287
+ scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
246
288
  } else {
247
289
  utils.checkLiveCookie(ctx, defaultFuncs)
248
- .then(res => {
249
- globalCallback({
250
- type: "stop_listen",
251
- error: "Connection refused: Server unavailable"
252
- }, null);
253
- })
254
- .catch(err => {
255
- globalCallback({
256
- type: "account_inactive",
257
- error: "Maybe your account is blocked by facebook, please login and check at https://facebook.com"
258
- }, null);
259
- });
290
+ .then(() => globalCallback({ type: "stop_listen", error: "Connection refused: Server unavailable" }))
291
+ .catch(() => globalCallback({ type: "account_inactive", error: "Maybe your account is blocked by facebook, please login and check at https://facebook.com" }));
292
+ }
293
+ });
294
+ // Ensure reconnection also triggers on unexpected close without prior error
295
+ mqttClient.on('close', function () {
296
+ ctx.health.onDisconnect();
297
+ if (!ctx.loggedIn) return; // avoid loops if logged out
298
+ if (ctx.globalOptions.autoReconnect) {
299
+ scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
300
+ }
301
+ });
302
+ mqttClient.on('disconnect', function(){
303
+ ctx.health.onDisconnect();
304
+ if (!ctx.loggedIn) return;
305
+ if (ctx.globalOptions.autoReconnect) {
306
+ scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
260
307
  }
261
308
  });
262
309
  mqttClient.on("connect", function () {
310
+ resetBackoff(backoff);
311
+ ctx.health.onConnect();
312
+ if (ctx.globalSafety) { try { ctx.globalSafety.recordEvent(); } catch(_) {} }
263
313
  if (process.env.OnStatus === undefined) {
264
314
  logger("Nexus-FCA premium features works only with Nexus-Bot framework(Kidding)", "info");
265
315
  process.env.OnStatus = true;
@@ -295,9 +345,11 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
295
345
  JSON.stringify({ make_user_available_when_in_foreground: true }),
296
346
  { qos: 1 }
297
347
  );
348
+ // Replace fixed rTimeout reconnect with health-driven logic
298
349
  const rTimeout = setTimeout(function () {
350
+ ctx.health.onError('timeout_no_t_ms');
299
351
  mqttClient.end();
300
- listenMqtt(defaultFuncs, api, ctx, globalCallback);
352
+ scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
301
353
  }, 5000);
302
354
  ctx.tmsWait = function () {
303
355
  clearTimeout(rTimeout);
@@ -311,14 +363,35 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
311
363
  };
312
364
  });
313
365
  mqttClient.on("message", function (topic, message, _packet) {
366
+ ctx.health.onMessage();
367
+ if (ctx.globalSafety) { try { ctx.globalSafety.recordEvent(); } catch(_) {} }
314
368
  try {
315
369
  let jsonMessage = Buffer.isBuffer(message)
316
370
  ? Buffer.from(message).toString()
317
371
  : message;
318
- try {
319
- jsonMessage = JSON.parse(jsonMessage);
320
- } catch (e) {
321
- jsonMessage = {};
372
+ try { jsonMessage = JSON.parse(jsonMessage); } catch (e) { jsonMessage = {}; }
373
+ // ACK tracking: detect send acknowledgements with latency hint if present
374
+ if (jsonMessage?.message_ack) {
375
+ const ack = jsonMessage.message_ack;
376
+ const mid = ack.message_id || ack.mid;
377
+ if(mid && ctx._pendingOutbound && ctx._pendingOutbound.has(mid)){
378
+ const started = ctx._pendingOutbound.get(mid);
379
+ ctx._pendingOutbound.delete(mid);
380
+ const latency = Date.now() - started;
381
+ ctx.health.onAck(latency);
382
+ } else {
383
+ ctx.health.onAck();
384
+ }
385
+ // If this ACK corresponds to an edit, clear from pendingEdits
386
+ if(mid && ctx.pendingEdits && ctx.pendingEdits.has(mid)){
387
+ ctx.pendingEdits.delete(mid);
388
+ if(ctx.health) ctx.health.removePendingEdit(mid);
389
+ }
390
+ }
391
+ if (jsonMessage?.type === 'ack') { ctx.health.onAck(); }
392
+ // lightweight ack detection heuristic
393
+ if (jsonMessage?.type === 'ack' || jsonMessage?.message_ack) {
394
+ ctx.health.onAck();
322
395
  }
323
396
  if (jsonMessage.type === "jewel_requests_add") {
324
397
  globalCallback(null, {
@@ -333,53 +406,15 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
333
406
  timestamp: Date.now().toString(),
334
407
  });
335
408
  } else if (topic === "/t_ms") {
336
- if (ctx.tmsWait && typeof ctx.tmsWait == "function") {
337
- ctx.tmsWait();
338
- }
339
- if (jsonMessage.firstDeltaSeqId && jsonMessage.syncToken) {
340
- ctx.lastSeqId = jsonMessage.firstDeltaSeqId;
341
- ctx.syncToken = jsonMessage.syncToken;
342
- }
343
- if (jsonMessage.lastIssuedSeqId) {
344
- ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
345
- }
346
- for (const i in jsonMessage.deltas) {
347
- const delta = jsonMessage.deltas[i];
348
- parseDelta(defaultFuncs, api, ctx, globalCallback, {
349
- delta: delta,
350
- });
351
- }
352
- } else if (
353
- topic === "/thread_typing" ||
354
- topic === "/orca_typing_notifications"
355
- ) {
356
- const typ = {
357
- type: "typ",
358
- isTyping: !!jsonMessage.state,
359
- from: jsonMessage.sender_fbid.toString(),
360
- threadID: utils.formatID(
361
- (jsonMessage.thread || jsonMessage.sender_fbid).toString()
362
- ),
363
- };
364
- (function () {
365
- globalCallback(null, typ);
366
- })();
409
+ if (ctx.tmsWait && typeof ctx.tmsWait == "function") { ctx.tmsWait(); }
410
+ if (jsonMessage.firstDeltaSeqId && jsonMessage.syncToken) { ctx.lastSeqId = jsonMessage.firstDeltaSeqId; ctx.syncToken = jsonMessage.syncToken; }
411
+ if (jsonMessage.lastIssuedSeqId) { ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId); }
412
+ for (const i in jsonMessage.deltas) { const delta = jsonMessage.deltas[i]; parseDelta(defaultFuncs, api, ctx, globalCallback, { delta: delta, }); }
413
+ } else if ( topic === "/thread_typing" || topic === "/orca_typing_notifications" ) {
414
+ const typ = { type: "typ", isTyping: !!jsonMessage.state, from: jsonMessage.sender_fbid.toString(), threadID: utils.formatID( (jsonMessage.thread || jsonMessage.sender_fbid).toString() ), };
415
+ (function () { globalCallback(null, typ); })();
367
416
  } else if (topic === "/orca_presence") {
368
- if (!ctx.globalOptions.updatePresence) {
369
- for (const i in jsonMessage.list) {
370
- const data = jsonMessage.list[i];
371
- const userID = data["u"];
372
- const presence = {
373
- type: "presence",
374
- userID: userID.toString(),
375
- timestamp: data["l"] * 1000,
376
- statuses: data["p"],
377
- };
378
- (function () {
379
- globalCallback(null, presence);
380
- })();
381
- }
382
- }
417
+ if (!ctx.globalOptions.updatePresence) { for (const i in jsonMessage.list) { const data = jsonMessage.list[i]; const userID = data["u"]; const presence = { type: "presence", userID: userID.toString(), timestamp: data["l"] * 1000, statuses: data["p"], }; (function () { globalCallback(null, presence); })(); } }
383
418
  } else if (topic == "/ls_resp") {
384
419
  const parsedPayload = JSON.parse(jsonMessage.payload);
385
420
  const reqID = jsonMessage.request_id;
@@ -387,25 +422,35 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
387
422
  const taskData = ctx["tasks"].get(reqID);
388
423
  const { type: taskType, callback: taskCallback } = taskData;
389
424
  const taskRespData = getTaskResponseData(taskType, parsedPayload);
390
- if (taskRespData == null) {
391
- taskCallback("error", null);
392
- } else {
393
- taskCallback(null, {
394
- type: taskType,
395
- reqID: reqID,
396
- ...taskRespData,
397
- });
398
- }
425
+ if (taskRespData == null) { taskCallback("error", null); } else { taskCallback(null, { type: taskType, reqID: reqID, ...taskRespData, }); }
399
426
  }
400
427
  }
401
428
  } catch (ex) {
429
+ ctx.health.onError('message_parse');
402
430
  console.error("Message parsing error:", ex);
403
431
  if (ex.stack) console.error(ex.stack);
404
432
  return;
405
433
  }
406
434
  });
407
- mqttClient.on("close", function () {});
408
- mqttClient.on("disconnect", () => {});
435
+ mqttClient.on("close", function () { ctx.health.onDisconnect(); if (ctx.globalSafety) { try { ctx.globalSafety._ensureMqttAlive(); } catch(_) {} } });
436
+ mqttClient.on("disconnect", () => { ctx.health.onDisconnect(); if (ctx.globalSafety) { try { ctx.globalSafety._ensureMqttAlive(); } catch(_) {} } });
437
+ // Synthetic keepalive with randomized cadence (55-75s) to appear human and keep state alive
438
+ if (!ctx._syntheticKeepAliveInterval) {
439
+ ctx._syntheticKeepAliveInterval = setInterval(() => {
440
+ if (!ctx.mqttClient || !ctx.mqttClient.connected) return;
441
+ if (ctx.globalSafety) {
442
+ const idle = Date.now() - ctx.globalSafety._lastEventTs;
443
+ if (idle > 65 * 1000) { ctx.globalSafety.recordEvent(); ctx.health.onSynthetic(); }
444
+ }
445
+ }, 55000 + Math.floor(Math.random()*20000));
446
+ }
447
+ }
448
+ function scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback){
449
+ const state = getBackoffState(ctx);
450
+ const delay = computeNextDelay(state);
451
+ ctx.health.onReconnectScheduled(delay);
452
+ log.warn('listenMqtt', `Reconnecting in ${delay} ms (adaptive backoff)`);
453
+ setTimeout(()=>listenMqtt(defaultFuncs, api, ctx, globalCallback), delay);
409
454
  }
410
455
  function getTaskResponseData(taskType, payload) {
411
456
  try {
@@ -205,14 +205,26 @@ module.exports = function (defaultFuncs, api, ctx) {
205
205
  task.payload = JSON.stringify(task.payload);
206
206
  });
207
207
  form.payload = JSON.stringify(form.payload);
208
- console.log(global.jsonStringifyColor(form, null, 2));
208
+ try { console.log(global.jsonStringifyColor(form, null, 2)); } catch (_) { }
209
+
210
+ // Stage 2: outbound tracking for ACK latency
211
+ if (!ctx._pendingOutbound) ctx._pendingOutbound = new Map();
212
+ let midCandidate = null;
213
+ try {
214
+ const parsed = JSON.parse(form.payload);
215
+ const firstTask = parsed.tasks && parsed.tasks[0] && JSON.parse(parsed.tasks[0].payload);
216
+ if (firstTask && firstTask.otid) midCandidate = String(firstTask.otid);
217
+ } catch (_) { }
218
+ if (midCandidate) {
219
+ ctx._pendingOutbound.set(midCandidate, Date.now());
220
+ if (ctx.health) ctx.health.trackOutbound(ctx._pendingOutbound.size);
221
+ }
209
222
 
210
223
  return mqttClient.publish("/ls_req", JSON.stringify(form), function (err, data) {
211
224
  if (err) {
212
- console.error('Error publishing message: ', err);
225
+ if (ctx.health) ctx.health.onError('publish_fail');
213
226
  callback(err);
214
227
  } else {
215
- console.log('Message published successfully with data: ', data);
216
228
  callback(null, data);
217
229
  }
218
230
  });