nexus-fca 2.1.3 → 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 +53 -1
- package/README.md +4 -1
- package/index.d.ts +5 -0
- package/index.js +64 -5
- package/lib/health/HealthMetrics.js +122 -0
- package/package.json +1 -1
- package/src/editMessage.js +66 -42
- package/src/listenMqtt.js +119 -89
- package/src/sendMessageMqtt.js +15 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [2.1.
|
|
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
|
+
|
|
19
|
+
## [2.1.5] - 2025-08-28 - PendingEdits & ACK Metrics
|
|
20
|
+
### Added
|
|
21
|
+
- PendingEdits buffer with cap (default 200) + TTL (5m) + resend attempts (2) + ACK timeout (12s).
|
|
22
|
+
- Automatic edit resend watchdog with safe limits and metrics (editResends, editFailed, pendingEditsDropped, pendingEditsExpired).
|
|
23
|
+
- API: `api.setEditOptions({ maxPendingEdits, editTTLms, ackTimeoutMs, maxResendAttempts })`.
|
|
24
|
+
- Edit ACK integration: pending edit removed on ACK receipt.
|
|
25
|
+
- Health metrics: p95AckLatencyMs, editResends, editFailed.
|
|
26
|
+
|
|
27
|
+
### Improved
|
|
28
|
+
- Safer edit pipeline: prevents uncontrolled retries, bounds memory, tracks expirations.
|
|
29
|
+
- Enhanced HealthMetrics with percentile latency sample retention (50-sample window).
|
|
30
|
+
|
|
31
|
+
### Notes
|
|
32
|
+
- Durable outbound queue & metrics exporter planned next.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## [2.1.4] - Adaptive Backoff & Core Metrics
|
|
37
|
+
### Added
|
|
38
|
+
- Adaptive reconnect backoff with jitter (caps at 5 minutes) for safer, stealthier recovery loops.
|
|
39
|
+
- Lazy preflight session validation (skips heavy validation if a recent successful connect occurred) toggle via `api.enableLazyPreflight()`.
|
|
40
|
+
- Health metrics collector (uptime, idle time, reconnect counts, failures, message/ack counters, synthetic keepalives) accessible with `api.getHealthMetrics()` and included in `api.healthCheck()`.
|
|
41
|
+
- Randomized synthetic keepalive interval (55-75s) to reduce detection patterns.
|
|
42
|
+
- Backoff configuration hook: `api.setBackoffOptions()`.
|
|
43
|
+
|
|
44
|
+
### Improved
|
|
45
|
+
- Reduced noisy session validation on every `listenMqtt` invocation unless needed.
|
|
46
|
+
- More structured error classification feeding metrics (`session_invalid`, `timeout_no_t_ms`, `mqtt_error`, `message_parse`, `not_logged_in`).
|
|
47
|
+
|
|
48
|
+
### Planned (Next)
|
|
49
|
+
- ACK tracking refinement & resend logic.
|
|
50
|
+
- Pending edits buffer with TTL and cap.
|
|
51
|
+
- Durable outbound queue & health exporter.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## [2.1.2] - CONTINUOUS IDLE RECOVERY
|
|
4
56
|
### Added
|
|
5
57
|
- Soft-stale probing at 2 minutes idle (ping + conditional forced reconnect if no events within 5-8s)
|
|
6
58
|
- Wrapper around `listenMqtt` to automatically feed events into safety heartbeat (`recordEvent`) for precise idle detection
|
package/README.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
# Nexus-FCA v2.1.
|
|
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.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
|
@@ -237,8 +237,19 @@ function buildAPI(globalOptions, html, jar) {
|
|
|
237
237
|
wsReqNumber: 0,
|
|
238
238
|
wsTaskNumber: 0,
|
|
239
239
|
// Provide safety module reference to lower layers (listenMqtt)
|
|
240
|
-
globalSafety
|
|
240
|
+
globalSafety,
|
|
241
|
+
// Pending edit tracking (Stage 2)
|
|
242
|
+
pendingEdits: new Map()
|
|
241
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
|
+
}
|
|
242
253
|
const api = {
|
|
243
254
|
setOptions: setOptions.bind(null, globalOptions),
|
|
244
255
|
getAppState: function getAppState() {
|
|
@@ -251,14 +262,31 @@ function buildAPI(globalOptions, html, jar) {
|
|
|
251
262
|
);
|
|
252
263
|
},
|
|
253
264
|
healthCheck: function(callback) {
|
|
254
|
-
// Simple health check: returns status and safeMode info
|
|
255
265
|
callback(null, {
|
|
256
266
|
status: 'ok',
|
|
257
267
|
safeMode,
|
|
258
268
|
time: new Date().toISOString(),
|
|
259
|
-
userID: ctx.userID || null
|
|
269
|
+
userID: ctx.userID || null,
|
|
270
|
+
metrics: ctx.health ? ctx.health.snapshot() : null
|
|
260
271
|
});
|
|
261
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); },
|
|
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
|
+
}
|
|
262
290
|
};
|
|
263
291
|
const defaultFuncs = utils.makeDefaults(html, i_userID || userID, ctx);
|
|
264
292
|
require("fs")
|
|
@@ -300,7 +328,7 @@ function buildAPI(globalOptions, html, jar) {
|
|
|
300
328
|
}, 1000 * 60 * 60 * 24);
|
|
301
329
|
// === Group Queue (No Cooldown, Sequential per group) ===
|
|
302
330
|
(function initGroupQueue(){
|
|
303
|
-
const groupQueues = new Map(); // threadID -> { q: [], sending: false }
|
|
331
|
+
const groupQueues = new Map(); // threadID -> { q: [], sending: false, lastActive: number }
|
|
304
332
|
const isGroupThread = (tid) => typeof tid === 'string' && tid.length >= 15; // simple heuristic
|
|
305
333
|
const DIRECT_FN = api.sendMessage; // original
|
|
306
334
|
|
|
@@ -310,6 +338,8 @@ function buildAPI(globalOptions, html, jar) {
|
|
|
310
338
|
api.setGroupQueueCapacity = function(n){ globalOptions.groupQueueMax = n; };
|
|
311
339
|
api.enableGroupQueue(true);
|
|
312
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
|
|
313
343
|
|
|
314
344
|
api._sendMessageDirect = DIRECT_FN;
|
|
315
345
|
api.sendMessage = function(message, threadID, cb){
|
|
@@ -317,10 +347,12 @@ function buildAPI(globalOptions, html, jar) {
|
|
|
317
347
|
return api._sendMessageDirect(message, threadID, cb);
|
|
318
348
|
}
|
|
319
349
|
let entry = groupQueues.get(threadID);
|
|
320
|
-
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();
|
|
321
352
|
if(entry.q.length >= (globalOptions.groupQueueMax||100)) {
|
|
322
353
|
// drop oldest (keep newest) to avoid unbounded growth
|
|
323
354
|
entry.q.shift();
|
|
355
|
+
if(ctx.health) ctx.health.recordGroupQueuePrune(0,0,1);
|
|
324
356
|
}
|
|
325
357
|
entry.q.push({ message, threadID, cb });
|
|
326
358
|
processQueue(threadID, entry);
|
|
@@ -349,6 +381,33 @@ function buildAPI(globalOptions, html, jar) {
|
|
|
349
381
|
}
|
|
350
382
|
entry.sending = false;
|
|
351
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
|
+
}
|
|
352
411
|
})();
|
|
353
412
|
// === End Group Queue ===
|
|
354
413
|
return {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Lightweight health & metrics tracker for Nexus-FCA (extended Stage 2 + Memory Guard)
|
|
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
|
+
// 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;
|
|
37
|
+
}
|
|
38
|
+
onConnect() { this.lastConnectTs = Date.now(); this.consecutiveFailures = 0; }
|
|
39
|
+
onDisconnect() { this.lastDisconnectTs = Date.now(); }
|
|
40
|
+
onMessage() { this.messagesReceived++; this.lastMessageTs = Date.now(); }
|
|
41
|
+
onSynthetic() { this.syntheticKeepAlives++; }
|
|
42
|
+
onAck(latencyMs){
|
|
43
|
+
this.acksReceived++;
|
|
44
|
+
if(typeof latencyMs === 'number'){
|
|
45
|
+
this.lastAckLatencyMs = latencyMs;
|
|
46
|
+
if(this.avgAckLatencyMs == null) this.avgAckLatencyMs = latencyMs; else this.avgAckLatencyMs = Math.round(this.avgAckLatencyMs*0.8 + latencyMs*0.2);
|
|
47
|
+
// track distribution (cap list length for memory safety)
|
|
48
|
+
this._ackLatencySamples.push(latencyMs);
|
|
49
|
+
if(this._ackLatencySamples.length > 50) this._ackLatencySamples.shift();
|
|
50
|
+
this._recalcP95();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
_recalcP95(){
|
|
54
|
+
if(!this._ackLatencySamples.length){ this.p95AckLatencyMs = null; return; }
|
|
55
|
+
const sorted = [...this._ackLatencySamples].sort((a,b)=>a-b);
|
|
56
|
+
const idx = Math.min(sorted.length-1, Math.floor(sorted.length*0.95));
|
|
57
|
+
this.p95AckLatencyMs = sorted[idx];
|
|
58
|
+
}
|
|
59
|
+
onError(type){ this.lastErrorTs = Date.now(); this.lastErrorType = type || 'unknown'; }
|
|
60
|
+
onReconnectScheduled(delay){ this.reconnects++; this.currentBackoffDelay = delay; if(delay > (this.maxObservedBackoff||0)) this.maxObservedBackoff = delay; }
|
|
61
|
+
trackOutbound(depth){ this.outboundQueueDepth = depth; }
|
|
62
|
+
incOutboundDropped(){ this.outboundQueueDropped++; }
|
|
63
|
+
addPendingEdit(mid, text){ this.pendingEditMap.set(mid, { ts: Date.now(), text, attempts: 0 }); this.pendingEdits = this.pendingEditMap.size; }
|
|
64
|
+
markEditResent(mid){ const rec = this.pendingEditMap.get(mid); if(rec){ rec.attempts++; this.editResends++; } }
|
|
65
|
+
markEditFailed(mid){ if(this.pendingEditMap.delete(mid)) { this.pendingEdits = this.pendingEditMap.size; this.editFailed++; } }
|
|
66
|
+
removePendingEdit(mid){ if(this.pendingEditMap.delete(mid)) this.pendingEdits = this.pendingEditMap.size; }
|
|
67
|
+
incPendingEditDropped(){ this.pendingEditsDropped++; }
|
|
68
|
+
incPendingEditExpired(n=1){ this.pendingEditsExpired += n; }
|
|
69
|
+
sweepPendingEdits(ttlMs){
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
let expired = 0;
|
|
72
|
+
for(const [mid, val] of this.pendingEditMap.entries()){
|
|
73
|
+
if(now - val.ts > ttlMs){ this.pendingEditMap.delete(mid); expired++; }
|
|
74
|
+
}
|
|
75
|
+
if(expired) { this.incPendingEditExpired(expired); this.pendingEditSweeps++; }
|
|
76
|
+
this.pendingEdits = this.pendingEditMap.size;
|
|
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
|
+
}
|
|
85
|
+
snapshot(){
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
const idleMs = now - (this.lastMessageTs || this.lastConnectTs || now);
|
|
88
|
+
return {
|
|
89
|
+
uptimeMs: now - this.uptimeStart,
|
|
90
|
+
idleMs,
|
|
91
|
+
reconnects: this.reconnects,
|
|
92
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
93
|
+
lastErrorType: this.lastErrorType,
|
|
94
|
+
lastErrorAgoMs: this.lastErrorTs ? now - this.lastErrorTs : null,
|
|
95
|
+
currentBackoffDelay: this.currentBackoffDelay||0,
|
|
96
|
+
maxObservedBackoff: this.maxObservedBackoff||0,
|
|
97
|
+
messagesReceived: this.messagesReceived,
|
|
98
|
+
syntheticKeepAlives: this.syntheticKeepAlives,
|
|
99
|
+
acksReceived: this.acksReceived,
|
|
100
|
+
lastAckLatencyMs: this.lastAckLatencyMs,
|
|
101
|
+
avgAckLatencyMs: this.avgAckLatencyMs,
|
|
102
|
+
p95AckLatencyMs: this.p95AckLatencyMs,
|
|
103
|
+
pendingEdits: this.pendingEdits,
|
|
104
|
+
pendingEditsDropped: this.pendingEditsDropped,
|
|
105
|
+
pendingEditsExpired: this.pendingEditsExpired,
|
|
106
|
+
editResends: this.editResends,
|
|
107
|
+
editFailed: this.editFailed,
|
|
108
|
+
outboundQueueDepth: this.outboundQueueDepth,
|
|
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,
|
|
117
|
+
healthy: this.isHealthy(idleMs)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
isHealthy(idleMs){ if (this.consecutiveFailures >= 10) return false; if (this.messagesReceived < 5 && idleMs > 5*60*1000) return false; return true; }
|
|
121
|
+
}
|
|
122
|
+
module.exports = { HealthMetrics };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-fca",
|
|
3
|
-
"version": "2.1.
|
|
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": {
|
package/src/editMessage.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
54
|
-
qos:
|
|
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
|
-
//
|
|
132
|
-
if
|
|
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:
|
|
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
|
-
},
|
|
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,40 @@ 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
|
-
|
|
287
|
+
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
|
|
246
288
|
} else {
|
|
247
289
|
utils.checkLiveCookie(ctx, defaultFuncs)
|
|
248
|
-
.then(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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();
|
|
263
312
|
if (ctx.globalSafety) { try { ctx.globalSafety.recordEvent(); } catch(_) {} }
|
|
264
313
|
if (process.env.OnStatus === undefined) {
|
|
265
314
|
logger("Nexus-FCA premium features works only with Nexus-Bot framework(Kidding)", "info");
|
|
@@ -296,9 +345,11 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
296
345
|
JSON.stringify({ make_user_available_when_in_foreground: true }),
|
|
297
346
|
{ qos: 1 }
|
|
298
347
|
);
|
|
348
|
+
// Replace fixed rTimeout reconnect with health-driven logic
|
|
299
349
|
const rTimeout = setTimeout(function () {
|
|
350
|
+
ctx.health.onError('timeout_no_t_ms');
|
|
300
351
|
mqttClient.end();
|
|
301
|
-
|
|
352
|
+
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
|
|
302
353
|
}, 5000);
|
|
303
354
|
ctx.tmsWait = function () {
|
|
304
355
|
clearTimeout(rTimeout);
|
|
@@ -312,15 +363,35 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
312
363
|
};
|
|
313
364
|
});
|
|
314
365
|
mqttClient.on("message", function (topic, message, _packet) {
|
|
366
|
+
ctx.health.onMessage();
|
|
315
367
|
if (ctx.globalSafety) { try { ctx.globalSafety.recordEvent(); } catch(_) {} }
|
|
316
368
|
try {
|
|
317
369
|
let jsonMessage = Buffer.isBuffer(message)
|
|
318
370
|
? Buffer.from(message).toString()
|
|
319
371
|
: message;
|
|
320
|
-
try {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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();
|
|
324
395
|
}
|
|
325
396
|
if (jsonMessage.type === "jewel_requests_add") {
|
|
326
397
|
globalCallback(null, {
|
|
@@ -335,53 +406,15 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
335
406
|
timestamp: Date.now().toString(),
|
|
336
407
|
});
|
|
337
408
|
} else if (topic === "/t_ms") {
|
|
338
|
-
if (ctx.tmsWait && typeof ctx.tmsWait == "function") {
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
if (jsonMessage.lastIssuedSeqId) {
|
|
346
|
-
ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
|
|
347
|
-
}
|
|
348
|
-
for (const i in jsonMessage.deltas) {
|
|
349
|
-
const delta = jsonMessage.deltas[i];
|
|
350
|
-
parseDelta(defaultFuncs, api, ctx, globalCallback, {
|
|
351
|
-
delta: delta,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
} else if (
|
|
355
|
-
topic === "/thread_typing" ||
|
|
356
|
-
topic === "/orca_typing_notifications"
|
|
357
|
-
) {
|
|
358
|
-
const typ = {
|
|
359
|
-
type: "typ",
|
|
360
|
-
isTyping: !!jsonMessage.state,
|
|
361
|
-
from: jsonMessage.sender_fbid.toString(),
|
|
362
|
-
threadID: utils.formatID(
|
|
363
|
-
(jsonMessage.thread || jsonMessage.sender_fbid).toString()
|
|
364
|
-
),
|
|
365
|
-
};
|
|
366
|
-
(function () {
|
|
367
|
-
globalCallback(null, typ);
|
|
368
|
-
})();
|
|
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); })();
|
|
369
416
|
} else if (topic === "/orca_presence") {
|
|
370
|
-
if (!ctx.globalOptions.updatePresence) {
|
|
371
|
-
for (const i in jsonMessage.list) {
|
|
372
|
-
const data = jsonMessage.list[i];
|
|
373
|
-
const userID = data["u"];
|
|
374
|
-
const presence = {
|
|
375
|
-
type: "presence",
|
|
376
|
-
userID: userID.toString(),
|
|
377
|
-
timestamp: data["l"] * 1000,
|
|
378
|
-
statuses: data["p"],
|
|
379
|
-
};
|
|
380
|
-
(function () {
|
|
381
|
-
globalCallback(null, presence);
|
|
382
|
-
})();
|
|
383
|
-
}
|
|
384
|
-
}
|
|
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); })(); } }
|
|
385
418
|
} else if (topic == "/ls_resp") {
|
|
386
419
|
const parsedPayload = JSON.parse(jsonMessage.payload);
|
|
387
420
|
const reqID = jsonMessage.request_id;
|
|
@@ -389,39 +422,36 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
389
422
|
const taskData = ctx["tasks"].get(reqID);
|
|
390
423
|
const { type: taskType, callback: taskCallback } = taskData;
|
|
391
424
|
const taskRespData = getTaskResponseData(taskType, parsedPayload);
|
|
392
|
-
if (taskRespData == null) {
|
|
393
|
-
taskCallback("error", null);
|
|
394
|
-
} else {
|
|
395
|
-
taskCallback(null, {
|
|
396
|
-
type: taskType,
|
|
397
|
-
reqID: reqID,
|
|
398
|
-
...taskRespData,
|
|
399
|
-
});
|
|
400
|
-
}
|
|
425
|
+
if (taskRespData == null) { taskCallback("error", null); } else { taskCallback(null, { type: taskType, reqID: reqID, ...taskRespData, }); }
|
|
401
426
|
}
|
|
402
427
|
}
|
|
403
428
|
} catch (ex) {
|
|
429
|
+
ctx.health.onError('message_parse');
|
|
404
430
|
console.error("Message parsing error:", ex);
|
|
405
431
|
if (ex.stack) console.error(ex.stack);
|
|
406
432
|
return;
|
|
407
433
|
}
|
|
408
434
|
});
|
|
409
|
-
mqttClient.on("close", function () { if (ctx.globalSafety) { try { ctx.globalSafety._ensureMqttAlive(); } catch(_) {} } });
|
|
410
|
-
mqttClient.on("disconnect", () => { if (ctx.globalSafety) { try { ctx.globalSafety._ensureMqttAlive(); } catch(_) {} } });
|
|
411
|
-
//
|
|
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
|
|
412
438
|
if (!ctx._syntheticKeepAliveInterval) {
|
|
413
439
|
ctx._syntheticKeepAliveInterval = setInterval(() => {
|
|
414
440
|
if (!ctx.mqttClient || !ctx.mqttClient.connected) return;
|
|
415
441
|
if (ctx.globalSafety) {
|
|
416
442
|
const idle = Date.now() - ctx.globalSafety._lastEventTs;
|
|
417
|
-
|
|
418
|
-
if (idle > 65 * 1000) {
|
|
419
|
-
ctx.globalSafety.recordEvent();
|
|
420
|
-
}
|
|
443
|
+
if (idle > 65 * 1000) { ctx.globalSafety.recordEvent(); ctx.health.onSynthetic(); }
|
|
421
444
|
}
|
|
422
|
-
},
|
|
445
|
+
}, 55000 + Math.floor(Math.random()*20000));
|
|
423
446
|
}
|
|
424
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);
|
|
454
|
+
}
|
|
425
455
|
function getTaskResponseData(taskType, payload) {
|
|
426
456
|
try {
|
|
427
457
|
switch (taskType) {
|
package/src/sendMessageMqtt.js
CHANGED
|
@@ -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
|
-
|
|
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
|
});
|