nexus-fca 3.1.4 → 3.1.6

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.
@@ -0,0 +1,50 @@
1
+ # Nexus-FCA Memory Usage Guide
2
+
3
+ ## Built-in guardrails
4
+
5
+ - **Outbound MQTT buffers** (`src/sendMessageMqtt.js`, `src/listenMqtt.js`)
6
+ - `_pendingOutbound` tracks only outstanding message IDs and is pruned immediately when an ACK lands.
7
+ - Health metrics (`lib/health/HealthMetrics.js`) cap ACK samples at 50 entries and keep averages via exponential smoothing, so telemetry never balloons in RAM.
8
+ - **Pending edit queue** (`src/editMessage.js`)
9
+ - `globalOptions.editSettings.maxPendingEdits` defaults to 200; oldest edits are dropped once the cap is hit, and TTL checks clear stale entries.
10
+ - Every ACK removes the corresponding entry, and `HealthMetrics` mirrors the size so you can alert if it starts climbing.
11
+ - **Group send queue** (`index.js`)
12
+ - Each group’s queue is capped (default 100 messages) and sweeps run every 5 minutes to drop idle or over-capacity queues.
13
+ - Sweeper stats surface through `ctx.health.recordGroupQueuePrune`, so you can confirm that cleanup is happening.
14
+ - **Performance caches** (`lib/performance/PerformanceManager.js`)
15
+ - Cache maps are bounded by `cacheSize` (default 1000) and enforce TTL, while request time windows keep only the last 100 samples.
16
+ - `PerformanceOptimizer` trims its request history to 1000 entries and halves the buffer on each cleanup pass.
17
+ - **Database write queue** (`lib/database/EnhancedDatabase.js`)
18
+ - Writes batch in chunks of 100 and the queue processor re-runs every second. Long outages are the only way to accumulate large queues.
19
+ - **Safety timers** (`lib/safety/FacebookSafety.js`)
20
+ - All recurring timers are stored and cleared before new ones are scheduled, preventing runaway intervals during reconnect churn.
21
+
22
+ ## Situations that can increase memory
23
+
24
+ | Area | Why it grows | Mitigation |
25
+ | --- | --- | --- |
26
+ | Database write queue | Target SQLite/SQL server offline → `writeQueue` keeps buffering | Monitor `writeQueue.length` or add alerts around `EnhancedDatabase.processQueue`; if storage is optional, disable DB integration entirely.
27
+ | Multiple PerformanceManager / PerformanceOptimizer instances | Each instance spawns its own metrics intervals and caches | Treat both managers as singletons; share them via dependency injection instead of `new`ing per feature.
28
+ | Elevated group queue caps | Setting `setGroupQueueCapacity` >> 100 multiplies per-thread memory | Keep caps small; rely on `_flushGroupQueue` for bursts instead of raising the ceiling.
29
+ | Pending edit saturation | Frequent edit retries without ACKs hit the 200-item cap | Investigate upstream failures (usually edit rights or MQTT drops). `api.getMemoryMetrics()` will show `pendingEditsDropped` climbing when this happens.
30
+ | Large custom caches | If you override `cacheSize` or TTLs to very large values, the Map will grow | Pick realistic TTLs; if the workload is mostly transient, disable cache (`enableCache: false`).
31
+
32
+ ## Monitoring checklist
33
+
34
+ 1. **Runtime snapshot** – call `api.getMemoryMetrics()` to read pending edit counts, outbound depth, and memory guard actions.
35
+ 2. **Health dashboard** – `ctx.health.snapshot()` (or the API wrapper `getHealthMetrics`) exposes ACK latency samples and queue stats.
36
+ 3. **Performance events** – `PerformanceManager` emits `metricsUpdate` every 30s; attach a listener and pipe to your logger or Prometheus bridge.
37
+ 4. **Node heap checks** – pair the built-in metrics with `process.memoryUsage()` or `--inspect` tooling if you suspect leaks from user code.
38
+
39
+ ## Configuration tips
40
+
41
+ - Tune `globalOptions.editSettings` if your bot edits aggressively; lower `maxPendingEdits` to 100 and `editTTLms` to 2–3 minutes for tighter control.
42
+ - Use `api.setGroupQueueCapacity(n)` to keep per-thread queues bounded; the sweeper already limits idle queues to 30 minutes, but lower values (10–20) reduce burst memory further.
43
+ - If you don’t need database analytics, avoid initializing `EnhancedDatabase`/`DatabaseManager`; the rest of the stack runs without it.
44
+ - Disable extra instrumentation when running on low-memory hardware:
45
+ ```js
46
+ const perfManager = new PerformanceManager({ enableMetrics: false, enableCache: false });
47
+ ```
48
+ - Always reuse the same `PerformanceOptimizer`/`PerformanceManager` instead of instantiating per request handler, so their intervals remain singular.
49
+
50
+ Following the defaults keeps Nexus-FCA comfortably under a few hundred megabytes even on small VPS nodes. When memory spikes appear, start by sampling `api.getMemoryMetrics()` and check the table above to see which subsystem is accumulating work. Adjust the related caps or temporarily disable the optional feature until the upstream issue (DB outage, repeated edit failures, etc.) is resolved.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-fca",
3
- "version": "3.1.4",
3
+ "version": "3.1.6",
4
4
  "description": "Nexus-FCA 3.1 – THE BEST, SAFEST, MOST STABLE Facebook Messenger API! Email/password + appState login, proxy support (HTTP/HTTPS/SOCKS5), random user agent, proactive cookie refresh, MQTT stability, session protection, and TypeScript support.",
5
5
  "main": "index.js",
6
6
  "repository": {
package/src/listenMqtt.js CHANGED
@@ -93,19 +93,34 @@ function fetchSeqID(defaultFuncs, api, ctx, callback) {
93
93
 
94
94
  // Adaptive backoff state (per-process singleton like) - tie to ctx
95
95
  function getBackoffState(ctx){
96
+ const envBase = parseInt(process.env.NEXUS_MQTT_BACKOFF_BASE, 10) || 1000;
97
+ const envMax = parseInt(process.env.NEXUS_MQTT_BACKOFF_MAX, 10) || (5 * 60 * 1000);
98
+ const envFactor = parseFloat(process.env.NEXUS_MQTT_BACKOFF_FACTOR) || 2;
99
+ const backoffOverrides = (ctx.globalOptions && ctx.globalOptions.backoff) || {};
100
+ const resolved = {
101
+ base: Number.isFinite(backoffOverrides.baseMs) ? backoffOverrides.baseMs : envBase,
102
+ max: Number.isFinite(backoffOverrides.maxMs) ? backoffOverrides.maxMs : envMax,
103
+ factor: Number.isFinite(backoffOverrides.factor) ? backoffOverrides.factor : envFactor,
104
+ jitter: typeof backoffOverrides.jitter === 'number' ? backoffOverrides.jitter : 0.25,
105
+ resetAfterMs: Number.isFinite(backoffOverrides.resetAfterMs) ? backoffOverrides.resetAfterMs : (3 * 60 * 1000)
106
+ };
96
107
  if(!ctx._adaptiveReconnect){
97
- const envBase = parseInt(process.env.NEXUS_MQTT_BACKOFF_BASE, 10) || 1000;
98
- const envMax = parseInt(process.env.NEXUS_MQTT_BACKOFF_MAX, 10) || (5 * 60 * 1000);
99
- const envFactor = parseFloat(process.env.NEXUS_MQTT_BACKOFF_FACTOR) || 2;
100
108
  ctx._adaptiveReconnect = {
101
- base: envBase, // 1s default
102
- max: envMax, // 5m default
103
- factor: envFactor,
104
- jitter: 0.25, // 25% random
109
+ base: resolved.base,
110
+ max: resolved.max,
111
+ factor: resolved.factor,
112
+ jitter: resolved.jitter,
113
+ resetAfterMs: resolved.resetAfterMs,
105
114
  current: 0,
106
115
  lastResetTs: 0,
107
116
  consecutiveFails: 0 // Track consecutive failures
108
117
  };
118
+ } else {
119
+ ctx._adaptiveReconnect.base = resolved.base;
120
+ ctx._adaptiveReconnect.max = resolved.max;
121
+ ctx._adaptiveReconnect.factor = resolved.factor;
122
+ ctx._adaptiveReconnect.jitter = resolved.jitter;
123
+ ctx._adaptiveReconnect.resetAfterMs = resolved.resetAfterMs;
109
124
  }
110
125
  return ctx._adaptiveReconnect;
111
126
  }
@@ -121,6 +136,126 @@ function resetBackoff(state){
121
136
  state.current = 0;
122
137
  state.lastResetTs = Date.now();
123
138
  }
139
+ function getForegroundState(ctx){
140
+ if (ctx && ctx.globalOptions) {
141
+ if (ctx.globalOptions.foreground === false) return false;
142
+ if (ctx.globalOptions.online === false) return false;
143
+ }
144
+ return true;
145
+ }
146
+ function startForegroundRefresh(ctx){
147
+ if (ctx._foregroundRefreshInterval) {
148
+ clearInterval(ctx._foregroundRefreshInterval);
149
+ ctx._foregroundRefreshInterval = null;
150
+ }
151
+ const options = ctx.globalOptions || {};
152
+ if (options.foregroundRefreshEnabled === false) return;
153
+ const minutes = Number.isFinite(options.foregroundRefreshMinutes)
154
+ ? Math.max(1, options.foregroundRefreshMinutes)
155
+ : 15;
156
+ ctx._foregroundRefreshInterval = setInterval(() => {
157
+ if (!ctx.mqttClient || !ctx.mqttClient.connected) return;
158
+ try {
159
+ const foreground = getForegroundState(ctx);
160
+ ctx.mqttClient.publish("/foreground_state", JSON.stringify({ foreground }), { qos: 0, retain: false });
161
+ ctx.mqttClient.publish(
162
+ "/set_client_settings",
163
+ JSON.stringify({ make_user_available_when_in_foreground: true }),
164
+ { qos: 0, retain: false }
165
+ );
166
+ } catch (err) {
167
+ if (typeof log.verbose === 'function') {
168
+ log.verbose("listenMqtt", `Foreground refresh publish failed: ${err.message || err}`);
169
+ }
170
+ }
171
+ }, minutes * 60 * 1000);
172
+ }
173
+ function stopForegroundRefresh(ctx){
174
+ if (ctx && ctx._foregroundRefreshInterval) {
175
+ clearInterval(ctx._foregroundRefreshInterval);
176
+ ctx._foregroundRefreshInterval = null;
177
+ }
178
+ }
179
+ function getStormGuard(ctx){
180
+ if(!ctx._mqttStorm){
181
+ ctx._mqttStorm = {
182
+ events: [],
183
+ windowMs: 30 * 60 * 1000,
184
+ threshold: 12,
185
+ quietMs: 90 * 1000,
186
+ extraDelayMs: 120 * 1000,
187
+ quietUntil: 0,
188
+ active: false,
189
+ lastLogTs: 0
190
+ };
191
+ }
192
+ return ctx._mqttStorm;
193
+ }
194
+ function noteStormEvent(ctx){
195
+ const guard = getStormGuard(ctx);
196
+ const now = Date.now();
197
+ guard.events.push(now);
198
+ guard.events = guard.events.filter(ts => now - ts <= guard.windowMs);
199
+ if (guard.events.length >= guard.threshold) {
200
+ guard.active = true;
201
+ guard.quietUntil = now + guard.quietMs;
202
+ } else if (guard.active && now > guard.quietUntil) {
203
+ guard.active = false;
204
+ }
205
+ return guard;
206
+ }
207
+ function resetStormGuard(ctx){
208
+ if(ctx._mqttStorm){
209
+ ctx._mqttStorm.events = [];
210
+ ctx._mqttStorm.active = false;
211
+ ctx._mqttStorm.quietUntil = 0;
212
+ ctx._mqttStorm.lastLogTs = 0;
213
+ }
214
+ }
215
+ function shouldLogStorm(guard){
216
+ if(!guard || !guard.active) return true;
217
+ const now = Date.now();
218
+ if(!guard.lastLogTs || now - guard.lastLogTs > guard.quietMs){
219
+ guard.lastLogTs = now;
220
+ return true;
221
+ }
222
+ return false;
223
+ }
224
+ async function runStormRecovery(ctx, api, defaultFuncs){
225
+ if(ctx._stormRecoveryRunning) return;
226
+ ctx._stormRecoveryRunning = true;
227
+ try {
228
+ log.warn('listenMqtt', 'Storm recovery triggered: validating session & refreshing tokens.');
229
+ try {
230
+ await utils.validateSession(ctx, defaultFuncs, { retries: 1, delayMs: 1000 });
231
+ } catch (err) {
232
+ log.warn('listenMqtt', `Storm validateSession failed: ${err.message || err}`);
233
+ }
234
+ if (api && typeof api.refreshFb_dtsg === 'function') {
235
+ try {
236
+ await api.refreshFb_dtsg();
237
+ } catch (err) {
238
+ log.warn('listenMqtt', `Storm fb_dtsg refresh failed: ${err.message || err}`);
239
+ }
240
+ }
241
+ if (ctx.globalSafety && typeof ctx.globalSafety.refreshSafeSession === 'function') {
242
+ try {
243
+ await ctx.globalSafety.refreshSafeSession();
244
+ } catch (err) {
245
+ log.warn('listenMqtt', `Storm safe session refresh failed: ${err.message || err}`);
246
+ }
247
+ }
248
+ } finally {
249
+ ctx._stormRecoveryRunning = false;
250
+ }
251
+ }
252
+ function scheduleStormRecovery(ctx, api, defaultFuncs, guard){
253
+ if(!guard || !guard.active) return;
254
+ const now = Date.now();
255
+ if(ctx._nextStormRecovery && now < ctx._nextStormRecovery) return;
256
+ ctx._nextStormRecovery = now + guard.quietMs;
257
+ runStormRecovery(ctx, api, defaultFuncs);
258
+ }
124
259
  // Build lazy preflight gating
125
260
  function shouldRunPreflight(ctx){
126
261
  if(ctx.globalOptions.disablePreflight) return false;
@@ -272,8 +407,8 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
272
407
  }
273
408
  })();
274
409
  }
275
- const chatOn = ctx.globalOptions.online;
276
- const foreground = false;
410
+ const chatOn = (ctx.globalOptions && ctx.globalOptions.online === false) ? false : true;
411
+ const foreground = getForegroundState(ctx);
277
412
  const sessionID = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1;
278
413
  const GUID = utils.getGUID();
279
414
  const username = {
@@ -388,7 +523,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
388
523
  fetchSeqID(defaultFuncs, api, ctx, (err) => {
389
524
  if (err) {
390
525
  log.warn("listenMqtt", "Failed to refresh SeqID on error, falling back to adaptive reconnect...");
391
- scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
526
+ scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback, 'seqid-refresh-failed');
392
527
  } else {
393
528
  listenMqtt(defaultFuncs, api, ctx, globalCallback);
394
529
  }
@@ -403,48 +538,87 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
403
538
  mqttClient.on('close', function () {
404
539
  ctx.health.onDisconnect();
405
540
  if (ctx.health && typeof ctx.health.incFailure === 'function') { ctx.health.incFailure(); }
541
+ stopForegroundRefresh(ctx);
406
542
 
407
543
  const duration = Date.now() - attemptStartTs;
408
544
  const seconds = Math.floor(duration / 1000);
409
545
 
546
+ const shortWindowMs = 5 * 60 * 1000;
547
+ const guard = duration < shortWindowMs ? noteStormEvent(ctx) : getStormGuard(ctx);
548
+ const allowLog = shouldLogStorm(guard);
549
+ const stormSuffix = guard && guard.active ? ` [storm:${guard.events.length}/${Math.round(guard.windowMs/60000)}m]` : '';
550
+ if (guard && guard.active) {
551
+ scheduleStormRecovery(ctx, api, defaultFuncs, guard);
552
+ }
410
553
  // Treat long-lived connections as normal lifecycle, keep logs calm
411
554
  if (duration >= 30 * 60 * 1000) { // >= 30 minutes
412
- log.info('listenMqtt', `MQTT connection closed after ${seconds}s (normal lifecycle). Reconnecting...`);
555
+ if (allowLog) log.info('listenMqtt', `MQTT connection closed after ${seconds}s (normal lifecycle). Reconnecting...${stormSuffix}`);
413
556
  } else if (duration >= 5 * 60 * 1000) { // 5-30 minutes
414
- log.info('listenMqtt', `MQTT connection closed after ${seconds}s (remote close). Reconnecting with backoff...`);
557
+ if (allowLog) log.info('listenMqtt', `MQTT connection closed after ${seconds}s (remote close). Reconnecting with backoff...${stormSuffix}`);
558
+ } else if (duration >= 60 * 1000) {
559
+ if (allowLog) log.info('listenMqtt', `MQTT connection closed after ${seconds}s (short remote close). Reconnecting with backoff...${stormSuffix}`);
415
560
  } else {
416
- log.warn('listenMqtt', `MQTT bridge socket closed quickly after ${duration}ms (attempt=${ctx._mqttDiag.attempts}).`);
561
+ if (allowLog) log.warn('listenMqtt', `MQTT bridge socket closed quickly after ${duration}ms (attempt=${ctx._mqttDiag.attempts}).${stormSuffix}`);
417
562
  }
418
563
 
419
564
  if (!ctx.loggedIn) return; // avoid loops if logged out
420
565
  if (ctx.globalOptions.autoReconnect) {
421
- scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
566
+ const backoffState = getBackoffState(ctx);
567
+ const resetThreshold = backoffState.resetAfterMs || (3 * 60 * 1000);
568
+ let reconnectReason = 'close';
569
+ if (duration >= resetThreshold) {
570
+ log.info('listenMqtt', `Resetting MQTT backoff after ${seconds}s healthy session.`);
571
+ resetBackoff(backoffState);
572
+ resetStormGuard(ctx);
573
+ reconnectReason = 'close-long';
574
+ }
575
+ scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback, reconnectReason);
422
576
  }
423
577
  });
424
578
 
425
579
  mqttClient.on('disconnect', function(){
426
580
  ctx.health.onDisconnect();
427
581
  if (ctx.health && typeof ctx.health.incFailure === 'function') { ctx.health.incFailure(); }
582
+ stopForegroundRefresh(ctx);
428
583
 
429
584
  const duration = Date.now() - attemptStartTs;
430
585
  const seconds = Math.floor(duration / 1000);
431
586
 
587
+ const shortWindowMs = 5 * 60 * 1000;
588
+ const guard = duration < shortWindowMs ? noteStormEvent(ctx) : getStormGuard(ctx);
589
+ const allowLog = shouldLogStorm(guard);
590
+ const stormSuffix = guard && guard.active ? ` [storm:${guard.events.length}/${Math.round(guard.windowMs/60000)}m]` : '';
591
+ if (guard && guard.active) {
592
+ scheduleStormRecovery(ctx, api, defaultFuncs, guard);
593
+ }
432
594
  if (duration >= 30 * 60 * 1000) {
433
- log.info('listenMqtt', `MQTT disconnected after ${seconds}s (normal lifecycle). Reconnecting...`);
595
+ if (allowLog) log.info('listenMqtt', `MQTT disconnected after ${seconds}s (normal lifecycle). Reconnecting...${stormSuffix}`);
434
596
  } else if (duration >= 5 * 60 * 1000) {
435
- log.info('listenMqtt', `MQTT disconnected after ${seconds}s (remote close). Reconnecting with backoff...`);
597
+ if (allowLog) log.info('listenMqtt', `MQTT disconnected after ${seconds}s (remote close). Reconnecting with backoff...${stormSuffix}`);
598
+ } else if (duration >= 60 * 1000) {
599
+ if (allowLog) log.info('listenMqtt', `MQTT disconnected after ${seconds}s (short remote close). Reconnecting with backoff...${stormSuffix}`);
436
600
  } else {
437
- log.warn('listenMqtt', `MQTT bridge disconnect event after ${duration}ms (attempt=${ctx._mqttDiag.attempts}).`);
601
+ if (allowLog) log.warn('listenMqtt', `MQTT bridge disconnect event after ${duration}ms (attempt=${ctx._mqttDiag.attempts}).${stormSuffix}`);
438
602
  }
439
603
 
440
604
  if (!ctx.loggedIn) return;
441
605
  if (ctx.globalOptions.autoReconnect) {
442
- scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
606
+ const backoffState = getBackoffState(ctx);
607
+ const resetThreshold = backoffState.resetAfterMs || (3 * 60 * 1000);
608
+ let reconnectReason = 'disconnect';
609
+ if (duration >= resetThreshold) {
610
+ log.info('listenMqtt', `Resetting MQTT backoff after ${seconds}s healthy session.`);
611
+ resetBackoff(backoffState);
612
+ resetStormGuard(ctx);
613
+ reconnectReason = 'disconnect-long';
614
+ }
615
+ scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback, reconnectReason);
443
616
  }
444
617
  });
445
618
 
446
619
  mqttClient.on("connect", function () {
447
620
  resetBackoff(backoff);
621
+ resetStormGuard(ctx);
448
622
  backoff.consecutiveFails = 0; // Reset consecutive failures on successful connect
449
623
  // Reset or wrap MQTT attempt counter so long-lived sessions don't look scary
450
624
  ctx._mqttDiag = ctx._mqttDiag || {};
@@ -518,11 +692,12 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
518
692
  JSON.stringify({ make_user_available_when_in_foreground: true }),
519
693
  { qos: 1 }
520
694
  );
695
+ startForegroundRefresh(ctx);
521
696
  // Replace fixed rTimeout reconnect with health-driven logic
522
697
  const rTimeout = setTimeout(function () {
523
698
  ctx.health.onError('timeout_no_t_ms');
524
699
  mqttClient.end();
525
- scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
700
+ scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback, 'tms-timeout');
526
701
  }, 5000);
527
702
  ctx.tmsWait = function () {
528
703
  clearTimeout(rTimeout);
@@ -619,11 +794,19 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
619
794
  }, 55000 + Math.floor(Math.random()*20000));
620
795
  }
621
796
  }
622
- function scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback){
797
+ function scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback, reason){
623
798
  const state = getBackoffState(ctx);
624
- const delay = computeNextDelay(state);
799
+ const guard = getStormGuard(ctx);
800
+ let delay = computeNextDelay(state);
801
+ if (guard && guard.active) {
802
+ delay = Math.min(state.max, delay + guard.extraDelayMs);
803
+ }
625
804
  ctx.health.onReconnectScheduled(delay);
626
- log.warn('listenMqtt', `Reconnecting in ${delay} ms (adaptive backoff)`);
805
+ const tags = [];
806
+ if (reason) tags.push(reason);
807
+ if (guard && guard.active) tags.push('storm');
808
+ const suffix = tags.length ? ` (${tags.join(', ')})` : '';
809
+ log.warn('listenMqtt', `Reconnecting in ${delay} ms (adaptive backoff)${suffix}`);
627
810
  setTimeout(()=>listenMqtt(defaultFuncs, api, ctx, globalCallback), delay);
628
811
  }
629
812
  function getTaskResponseData(taskType, payload) {