nexus-fca 2.1.5 → 2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.1.6] - 2025-08-31 - Memory Guard & Queue Sweeping
4
+ ### Added
5
+ - Central lightweight memory guard sweeps: group queue pruning (idle >30m, overflow trim) and pendingEdits TTL sweeper (every 4m).
6
+ - Health metrics extended: memoryGuardRuns, memoryGuardActions, groupQueueDroppedMessages, groupQueueExpiredQueues, groupQueuePrunedThreads, pendingEditSweeps.
7
+ - API: `api.getMemoryMetrics()` returns focused memory-related counters.
8
+ - Typings updated (`EditOptions`, new API methods) in `index.d.ts`.
9
+
10
+ ### Improved
11
+ - Group queue now tracks `lastActive` and enforces idle purge + overflow protection with metrics.
12
+ - Pending edits TTL enforcement separated from resend watchdog for deterministic expiry.
13
+
14
+ ### Notes
15
+ - All guards are low-frequency, low-impact; no change to delivery reliability or safety – only prevention of unbounded growth.
16
+
17
+ ---
18
+
3
19
  ## [2.1.5] - 2025-08-28 - PendingEdits & ACK Metrics
4
20
  ### Added
5
21
  - PendingEdits buffer with cap (default 200) + TTL (5m) + resend attempts (2) + ACK timeout (12s).
package/index.d.ts CHANGED
@@ -395,6 +395,11 @@ declare module 'nexus-fca' {
395
395
  setMessageReaction: (reaction: string, messageID: string, callback?: (err?: Error) => void, forceCustomReaction?: boolean) => Promise<void>,
396
396
  setMessageReactionMqtt: (reaction: string, messageID: string, threadID: string, callback?: (err?: Error) => void) => Promise<void>,
397
397
  setOptions: (options: Partial<IFCAU_Options>) => void,
398
+ setEditOptions: (opts: EditOptions) => void,
399
+ setBackoffOptions: (opts: { base?: number; max?: number; factor?: number; jitter?: number }) => void,
400
+ enableLazyPreflight: (enable?: boolean) => void,
401
+ getHealthMetrics: () => any,
402
+ getMemoryMetrics: () => { pendingEdits: number; pendingEditsDropped: number; pendingEditsExpired: number; outboundQueueDepth: number; groupQueueDroppedMessages: number; memoryGuardRuns: number; memoryGuardActions: number } | null,
398
403
  setTitle: (newTitle: string, threadID: string, callback?: (err?: Error) => void) => Promise<void>,
399
404
  setTheme: (themeID?: string, threadID: string, callback?: (err?: Error) => void) => Promise<void>,
400
405
  unsendMessage: (messageID: string, callback?: (err?: Error) => void) => Promise<void>,
package/index.js CHANGED
@@ -273,7 +273,20 @@ function buildAPI(globalOptions, html, jar) {
273
273
  getHealthMetrics: function(){ return ctx.health ? ctx.health.snapshot() : null; },
274
274
  enableLazyPreflight(enable=true){ ctx.globalOptions.disablePreflight = !enable; },
275
275
  setBackoffOptions(opts={}){ ctx.globalOptions.backoff = Object.assign(ctx.globalOptions.backoff||{}, opts); },
276
- setEditOptions(opts={}){ Object.assign(ctx.globalOptions.editSettings, opts); }
276
+ setEditOptions(opts={}){ Object.assign(ctx.globalOptions.editSettings, opts); },
277
+ getMemoryMetrics(){
278
+ if(!ctx.health) return null;
279
+ const snap = ctx.health.snapshot();
280
+ return {
281
+ pendingEdits: snap.pendingEdits,
282
+ pendingEditsDropped: snap.pendingEditsDropped,
283
+ pendingEditsExpired: snap.pendingEditsExpired,
284
+ outboundQueueDepth: snap.outboundQueueDepth,
285
+ groupQueueDroppedMessages: snap.groupQueueDroppedMessages,
286
+ memoryGuardRuns: snap.memoryGuardRuns,
287
+ memoryGuardActions: snap.memoryGuardActions
288
+ };
289
+ }
277
290
  };
278
291
  const defaultFuncs = utils.makeDefaults(html, i_userID || userID, ctx);
279
292
  require("fs")
@@ -315,7 +328,7 @@ function buildAPI(globalOptions, html, jar) {
315
328
  }, 1000 * 60 * 60 * 24);
316
329
  // === Group Queue (No Cooldown, Sequential per group) ===
317
330
  (function initGroupQueue(){
318
- const groupQueues = new Map(); // threadID -> { q: [], sending: false }
331
+ const groupQueues = new Map(); // threadID -> { q: [], sending: false, lastActive: number }
319
332
  const isGroupThread = (tid) => typeof tid === 'string' && tid.length >= 15; // simple heuristic
320
333
  const DIRECT_FN = api.sendMessage; // original
321
334
 
@@ -325,6 +338,8 @@ function buildAPI(globalOptions, html, jar) {
325
338
  api.setGroupQueueCapacity = function(n){ globalOptions.groupQueueMax = n; };
326
339
  api.enableGroupQueue(true);
327
340
  api.setGroupQueueCapacity(100); // allow up to 100 pending per group
341
+ // New: group queue retention policy
342
+ globalOptions.groupQueueIdleMs = 30*60*1000; // 30m idle purge
328
343
 
329
344
  api._sendMessageDirect = DIRECT_FN;
330
345
  api.sendMessage = function(message, threadID, cb){
@@ -332,10 +347,12 @@ function buildAPI(globalOptions, html, jar) {
332
347
  return api._sendMessageDirect(message, threadID, cb);
333
348
  }
334
349
  let entry = groupQueues.get(threadID);
335
- if(!entry){ entry = { q: [], sending: false }; groupQueues.set(threadID, entry); }
350
+ if(!entry){ entry = { q: [], sending: false, lastActive: Date.now() }; groupQueues.set(threadID, entry); }
351
+ entry.lastActive = Date.now();
336
352
  if(entry.q.length >= (globalOptions.groupQueueMax||100)) {
337
353
  // drop oldest (keep newest) to avoid unbounded growth
338
354
  entry.q.shift();
355
+ if(ctx.health) ctx.health.recordGroupQueuePrune(0,0,1);
339
356
  }
340
357
  entry.q.push({ message, threadID, cb });
341
358
  processQueue(threadID, entry);
@@ -364,6 +381,33 @@ function buildAPI(globalOptions, html, jar) {
364
381
  }
365
382
  entry.sending = false;
366
383
  };
384
+
385
+ // Memory guard sweeper (lightweight)
386
+ if(!globalOptions._groupQueueSweeper){
387
+ globalOptions._groupQueueSweeper = setInterval(()=>{
388
+ const now = Date.now();
389
+ let prunedThreads = 0; let expiredQueues = 0; let dropped = 0; let actions = 0;
390
+ for(const [tid, entry] of groupQueues.entries()){
391
+ // Idle purge
392
+ if(now - entry.lastActive > (globalOptions.groupQueueIdleMs||1800000) && !entry.sending){
393
+ if(entry.q.length){ dropped += entry.q.length; }
394
+ groupQueues.delete(tid); expiredQueues++; actions++;
395
+ continue;
396
+ }
397
+ // Hard cap queue length (just in case capacity changed lower)
398
+ const cap = globalOptions.groupQueueMax||100;
399
+ if(entry.q.length > cap){
400
+ const overflow = entry.q.length - cap;
401
+ entry.q.splice(0, overflow); // drop oldest overflow
402
+ dropped += overflow; actions++;
403
+ }
404
+ }
405
+ if((prunedThreads||expiredQueues||dropped) && ctx.health){
406
+ ctx.health.recordGroupQueuePrune(prunedThreads, expiredQueues, dropped);
407
+ ctx.health.recordMemoryGuardRun(actions);
408
+ }
409
+ }, 5*60*1000); // every 5 minutes
410
+ }
367
411
  })();
368
412
  // === End Group Queue ===
369
413
  return {
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- // Lightweight health & metrics tracker for Nexus-FCA (extended Stage 2)
2
+ // Lightweight health & metrics tracker for Nexus-FCA (extended Stage 2 + Memory Guard)
3
3
  class HealthMetrics {
4
4
  constructor() {
5
5
  const now = Date.now();
@@ -26,6 +26,14 @@ class HealthMetrics {
26
26
  this.p95AckLatencyMs = null;
27
27
  this.editResends = 0;
28
28
  this.editFailed = 0;
29
+ // Memory / queue guard metrics (Stage 3)
30
+ this.memoryGuardRuns = 0;
31
+ this.memoryGuardLastRun = 0;
32
+ this.memoryGuardActions = 0;
33
+ this.groupQueuePrunedThreads = 0;
34
+ this.groupQueueExpiredQueues = 0;
35
+ this.groupQueueDroppedMessages = 0;
36
+ this.pendingEditSweeps = 0;
29
37
  }
30
38
  onConnect() { this.lastConnectTs = Date.now(); this.consecutiveFailures = 0; }
31
39
  onDisconnect() { this.lastDisconnectTs = Date.now(); }
@@ -64,9 +72,16 @@ class HealthMetrics {
64
72
  for(const [mid, val] of this.pendingEditMap.entries()){
65
73
  if(now - val.ts > ttlMs){ this.pendingEditMap.delete(mid); expired++; }
66
74
  }
67
- if(expired) { this.incPendingEditExpired(expired); }
75
+ if(expired) { this.incPendingEditExpired(expired); this.pendingEditSweeps++; }
68
76
  this.pendingEdits = this.pendingEditMap.size;
69
77
  }
78
+ // Memory guard helpers
79
+ recordMemoryGuardRun(actions=0){ this.memoryGuardRuns++; this.memoryGuardLastRun = Date.now(); this.memoryGuardActions += actions; }
80
+ recordGroupQueuePrune(threads, expiredQueues, droppedMsgs){
81
+ if(threads) this.groupQueuePrunedThreads += threads;
82
+ if(expiredQueues) this.groupQueueExpiredQueues += expiredQueues;
83
+ if(droppedMsgs) this.groupQueueDroppedMessages += droppedMsgs;
84
+ }
70
85
  snapshot(){
71
86
  const now = Date.now();
72
87
  const idleMs = now - (this.lastMessageTs || this.lastConnectTs || now);
@@ -92,6 +107,13 @@ class HealthMetrics {
92
107
  editFailed: this.editFailed,
93
108
  outboundQueueDepth: this.outboundQueueDepth,
94
109
  outboundQueueDropped: this.outboundQueueDropped,
110
+ memoryGuardRuns: this.memoryGuardRuns,
111
+ memoryGuardLastRun: this.memoryGuardLastRun,
112
+ memoryGuardActions: this.memoryGuardActions,
113
+ groupQueuePrunedThreads: this.groupQueuePrunedThreads,
114
+ groupQueueExpiredQueues: this.groupQueueExpiredQueues,
115
+ groupQueueDroppedMessages: this.groupQueueDroppedMessages,
116
+ pendingEditSweeps: this.pendingEditSweeps,
95
117
  healthy: this.isHealthy(idleMs)
96
118
  };
97
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-fca",
3
- "version": "2.1.5",
3
+ "version": "2.1.6",
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": {