@dongdev/fca-unofficial 3.0.0 → 3.0.3
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 +3 -0
- package/DOCS.md +501 -3
- package/index.d.ts +746 -700
- package/package.json +1 -1
- package/src/api/action/enableAutoSaveAppState.js +73 -0
- package/src/api/messaging/scheduler.js +264 -0
- package/src/api/socket/core/connectMqtt.js +17 -5
- package/src/api/socket/core/emitAuth.js +54 -0
- package/src/api/socket/listenMqtt.js +108 -1
- package/src/api/socket/middleware/index.js +216 -0
- package/src/api/users/getUserInfo.js +8 -1
- package/src/core/sendReqMqtt.js +47 -14
- package/src/utils/format.js +6 -2
package/package.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const logger = require("../../../func/logger");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Enable automatic AppState saving
|
|
8
|
+
* @param {Object} options - Options for auto-save
|
|
9
|
+
* @param {string} [options.filePath] - Path to save AppState (default: "appstate.json")
|
|
10
|
+
* @param {number} [options.interval] - Save interval in milliseconds (default: 10 minutes)
|
|
11
|
+
* @param {boolean} [options.saveOnLogin] - Save immediately on login (default: true)
|
|
12
|
+
* @returns {Function} Function to disable auto-save
|
|
13
|
+
*/
|
|
14
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
15
|
+
return function enableAutoSaveAppState(options = {}) {
|
|
16
|
+
const filePath = options.filePath || path.join(process.cwd(), "appstate.json");
|
|
17
|
+
const interval = options.interval || 10 * 60 * 1000; // 10 minutes default
|
|
18
|
+
const saveOnLogin = options.saveOnLogin !== false; // default true
|
|
19
|
+
|
|
20
|
+
// Save function
|
|
21
|
+
function saveAppState() {
|
|
22
|
+
try {
|
|
23
|
+
const appState = api.getAppState();
|
|
24
|
+
if (!appState || !appState.appState || appState.appState.length === 0) {
|
|
25
|
+
logger("AppState is empty, skipping save", "warn");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const data = JSON.stringify(appState, null, 2);
|
|
30
|
+
fs.writeFileSync(filePath, data, "utf8");
|
|
31
|
+
logger(`AppState saved to ${filePath}`, "info");
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger(`Error saving AppState: ${error && error.message ? error.message : String(error)}`, "error");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Save immediately if requested
|
|
38
|
+
let immediateSaveTimer = null;
|
|
39
|
+
if (saveOnLogin) {
|
|
40
|
+
// Delay a bit to ensure login is complete
|
|
41
|
+
immediateSaveTimer = setTimeout(() => {
|
|
42
|
+
saveAppState();
|
|
43
|
+
immediateSaveTimer = null;
|
|
44
|
+
}, 2000);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Set up interval
|
|
48
|
+
const intervalId = setInterval(saveAppState, interval);
|
|
49
|
+
logger(`Auto-save AppState enabled: ${filePath} (every ${Math.round(interval / 1000 / 60)} minutes)`, "info");
|
|
50
|
+
|
|
51
|
+
// Store interval ID for cleanup
|
|
52
|
+
if (!ctx._autoSaveInterval) {
|
|
53
|
+
ctx._autoSaveInterval = [];
|
|
54
|
+
}
|
|
55
|
+
ctx._autoSaveInterval.push(intervalId);
|
|
56
|
+
|
|
57
|
+
// Return disable function
|
|
58
|
+
return function disableAutoSaveAppState() {
|
|
59
|
+
// Clear immediate save timer if still pending
|
|
60
|
+
if (immediateSaveTimer) {
|
|
61
|
+
clearTimeout(immediateSaveTimer);
|
|
62
|
+
immediateSaveTimer = null;
|
|
63
|
+
}
|
|
64
|
+
// Clear interval
|
|
65
|
+
clearInterval(intervalId);
|
|
66
|
+
const index = ctx._autoSaveInterval ? ctx._autoSaveInterval.indexOf(intervalId) : -1;
|
|
67
|
+
if (index !== -1) {
|
|
68
|
+
ctx._autoSaveInterval.splice(index, 1);
|
|
69
|
+
}
|
|
70
|
+
logger("Auto-save AppState disabled", "info");
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const logger = require("../../../func/logger");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Message Scheduler System
|
|
6
|
+
* Allows scheduling messages to be sent at a specific time in the future
|
|
7
|
+
*/
|
|
8
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
9
|
+
// Initialize scheduler on first call
|
|
10
|
+
if (!ctx._scheduler) {
|
|
11
|
+
ctx._scheduler = createSchedulerInstance(api);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return ctx._scheduler;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function createSchedulerInstance(api) {
|
|
18
|
+
const scheduledMessages = new Map(); // Map<id, ScheduledMessage>
|
|
19
|
+
let nextId = 1;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Scheduled Message Object
|
|
23
|
+
* @typedef {Object} ScheduledMessage
|
|
24
|
+
* @property {string} id - Unique identifier
|
|
25
|
+
* @property {string|Object} message - Message content
|
|
26
|
+
* @property {string|string[]} threadID - Target thread ID(s)
|
|
27
|
+
* @property {number} timestamp - When to send (Unix timestamp in ms)
|
|
28
|
+
* @property {number} createdAt - When scheduled (Unix timestamp in ms)
|
|
29
|
+
* @property {Object} options - Additional options (replyMessageID, isGroup, etc.)
|
|
30
|
+
* @property {Function} callback - Optional callback when sent
|
|
31
|
+
* @property {NodeJS.Timeout} timeout - Timeout reference
|
|
32
|
+
* @property {boolean} cancelled - Whether cancelled
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Schedule a message to be sent at a specific time
|
|
37
|
+
* @param {string|Object} message - Message content
|
|
38
|
+
* @param {string|string[]} threadID - Target thread ID(s)
|
|
39
|
+
* @param {Date|number|string} when - When to send (Date, timestamp, or ISO string)
|
|
40
|
+
* @param {Object} [options] - Additional options
|
|
41
|
+
* @param {string} [options.replyMessageID] - Message ID to reply to
|
|
42
|
+
* @param {boolean} [options.isGroup] - Whether it's a group chat
|
|
43
|
+
* @param {Function} [options.callback] - Callback when sent
|
|
44
|
+
* @returns {string} Scheduled message ID
|
|
45
|
+
*/
|
|
46
|
+
function scheduleMessage(message, threadID, when, options = {}) {
|
|
47
|
+
let timestamp;
|
|
48
|
+
|
|
49
|
+
// Parse when parameter
|
|
50
|
+
if (when instanceof Date) {
|
|
51
|
+
timestamp = when.getTime();
|
|
52
|
+
} else if (typeof when === "number") {
|
|
53
|
+
timestamp = when;
|
|
54
|
+
} else if (typeof when === "string") {
|
|
55
|
+
timestamp = new Date(when).getTime();
|
|
56
|
+
} else {
|
|
57
|
+
throw new Error("Invalid 'when' parameter. Must be Date, number (timestamp), or ISO string");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate timestamp
|
|
61
|
+
if (isNaN(timestamp)) {
|
|
62
|
+
throw new Error("Invalid date/time");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
if (timestamp <= now) {
|
|
67
|
+
throw new Error("Scheduled time must be in the future");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const id = `scheduled_${nextId++}_${Date.now()}`;
|
|
71
|
+
const delay = timestamp - now;
|
|
72
|
+
|
|
73
|
+
// Create scheduled message object
|
|
74
|
+
const scheduled = {
|
|
75
|
+
id,
|
|
76
|
+
message,
|
|
77
|
+
threadID,
|
|
78
|
+
timestamp,
|
|
79
|
+
createdAt: now,
|
|
80
|
+
options: {
|
|
81
|
+
replyMessageID: options.replyMessageID,
|
|
82
|
+
isGroup: options.isGroup,
|
|
83
|
+
callback: options.callback
|
|
84
|
+
},
|
|
85
|
+
cancelled: false
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Set timeout to send message
|
|
89
|
+
scheduled.timeout = setTimeout(() => {
|
|
90
|
+
if (scheduled.cancelled) return;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
logger(`Sending scheduled message ${id}`, "info");
|
|
94
|
+
|
|
95
|
+
// Send message
|
|
96
|
+
api.sendMessage(
|
|
97
|
+
message,
|
|
98
|
+
threadID,
|
|
99
|
+
scheduled.options.callback || (() => {}),
|
|
100
|
+
scheduled.options.replyMessageID,
|
|
101
|
+
scheduled.options.isGroup
|
|
102
|
+
).then(() => {
|
|
103
|
+
logger(`Scheduled message ${id} sent successfully`, "info");
|
|
104
|
+
scheduledMessages.delete(id);
|
|
105
|
+
}).catch(err => {
|
|
106
|
+
logger(`Error sending scheduled message ${id}: ${err && err.message ? err.message : String(err)}`, "error");
|
|
107
|
+
if (scheduled.options.callback) {
|
|
108
|
+
scheduled.options.callback(err);
|
|
109
|
+
}
|
|
110
|
+
scheduledMessages.delete(id);
|
|
111
|
+
});
|
|
112
|
+
} catch (err) {
|
|
113
|
+
logger(`Error in scheduled message ${id}: ${err && err.message ? err.message : String(err)}`, "error");
|
|
114
|
+
scheduledMessages.delete(id);
|
|
115
|
+
}
|
|
116
|
+
}, delay);
|
|
117
|
+
|
|
118
|
+
scheduledMessages.set(id, scheduled);
|
|
119
|
+
logger(`Message scheduled: ${id} (in ${Math.round(delay / 1000)}s)`, "info");
|
|
120
|
+
|
|
121
|
+
return id;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Cancel a scheduled message
|
|
126
|
+
* @param {string} id - Scheduled message ID
|
|
127
|
+
* @returns {boolean} True if cancelled, false if not found
|
|
128
|
+
*/
|
|
129
|
+
function cancelScheduledMessage(id) {
|
|
130
|
+
const scheduled = scheduledMessages.get(id);
|
|
131
|
+
if (!scheduled) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (scheduled.cancelled) {
|
|
136
|
+
return false; // Already cancelled
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
clearTimeout(scheduled.timeout);
|
|
140
|
+
scheduled.cancelled = true;
|
|
141
|
+
scheduledMessages.delete(id);
|
|
142
|
+
logger(`Scheduled message ${id} cancelled`, "info");
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get scheduled message info
|
|
148
|
+
* @param {string} id - Scheduled message ID
|
|
149
|
+
* @returns {ScheduledMessage|null} Scheduled message or null if not found
|
|
150
|
+
*/
|
|
151
|
+
function getScheduledMessage(id) {
|
|
152
|
+
const scheduled = scheduledMessages.get(id);
|
|
153
|
+
if (!scheduled || scheduled.cancelled) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Return a copy without internal properties
|
|
158
|
+
return {
|
|
159
|
+
id: scheduled.id,
|
|
160
|
+
message: scheduled.message,
|
|
161
|
+
threadID: scheduled.threadID,
|
|
162
|
+
timestamp: scheduled.timestamp,
|
|
163
|
+
createdAt: scheduled.createdAt,
|
|
164
|
+
options: { ...scheduled.options },
|
|
165
|
+
timeUntilSend: scheduled.timestamp - Date.now()
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* List all scheduled messages
|
|
171
|
+
* @returns {ScheduledMessage[]} Array of scheduled messages
|
|
172
|
+
*/
|
|
173
|
+
function listScheduledMessages() {
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
const list = [];
|
|
176
|
+
|
|
177
|
+
for (const scheduled of scheduledMessages.values()) {
|
|
178
|
+
if (scheduled.cancelled) continue;
|
|
179
|
+
|
|
180
|
+
list.push({
|
|
181
|
+
id: scheduled.id,
|
|
182
|
+
message: scheduled.message,
|
|
183
|
+
threadID: scheduled.threadID,
|
|
184
|
+
timestamp: scheduled.timestamp,
|
|
185
|
+
createdAt: scheduled.createdAt,
|
|
186
|
+
options: { ...scheduled.options },
|
|
187
|
+
timeUntilSend: scheduled.timestamp - now
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Sort by timestamp
|
|
192
|
+
return list.sort((a, b) => a.timestamp - b.timestamp);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Cancel all scheduled messages
|
|
197
|
+
* @returns {number} Number of cancelled messages
|
|
198
|
+
*/
|
|
199
|
+
function cancelAllScheduledMessages() {
|
|
200
|
+
let count = 0;
|
|
201
|
+
for (const id of scheduledMessages.keys()) {
|
|
202
|
+
if (cancelScheduledMessage(id)) {
|
|
203
|
+
count++;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
logger(`Cancelled ${count} scheduled messages`, "info");
|
|
207
|
+
return count;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get count of scheduled messages
|
|
212
|
+
* @returns {number} Count
|
|
213
|
+
*/
|
|
214
|
+
function getScheduledCount() {
|
|
215
|
+
return scheduledMessages.size;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Clear expired/cancelled messages from memory
|
|
220
|
+
*/
|
|
221
|
+
function cleanup() {
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
let cleaned = 0;
|
|
224
|
+
|
|
225
|
+
for (const [id, scheduled] of scheduledMessages.entries()) {
|
|
226
|
+
if (scheduled.cancelled || scheduled.timestamp < now) {
|
|
227
|
+
scheduledMessages.delete(id);
|
|
228
|
+
cleaned++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (cleaned > 0) {
|
|
233
|
+
logger(`Cleaned up ${cleaned} scheduled messages`, "info");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Auto cleanup every 5 minutes
|
|
238
|
+
const cleanupInterval = setInterval(cleanup, 5 * 60 * 1000);
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Destroy scheduler and cleanup all resources
|
|
242
|
+
* @returns {number} Number of cancelled messages
|
|
243
|
+
*/
|
|
244
|
+
function destroy() {
|
|
245
|
+
clearInterval(cleanupInterval);
|
|
246
|
+
const count = cancelAllScheduledMessages();
|
|
247
|
+
logger("Scheduler destroyed and all resources cleaned up", "info");
|
|
248
|
+
return count;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Return scheduler API
|
|
252
|
+
return {
|
|
253
|
+
scheduleMessage,
|
|
254
|
+
cancelScheduledMessage,
|
|
255
|
+
getScheduledMessage,
|
|
256
|
+
listScheduledMessages,
|
|
257
|
+
cancelAllScheduledMessages,
|
|
258
|
+
getScheduledCount,
|
|
259
|
+
cleanup,
|
|
260
|
+
destroy,
|
|
261
|
+
// Cleanup interval reference for manual cleanup if needed
|
|
262
|
+
_cleanupInterval: cleanupInterval
|
|
263
|
+
};
|
|
264
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const { formatID } = require("../../../utils/format");
|
|
3
|
-
const uuid = require("uuid");
|
|
4
|
-
"use strict";
|
|
5
3
|
module.exports = function createListenMqtt(deps) {
|
|
6
4
|
const { WebSocket, mqtt, HttpsProxyAgent, buildStream, buildProxy,
|
|
7
5
|
topics, parseDelta, getTaskResponseData, logger, emitAuth
|
|
@@ -87,7 +85,11 @@ module.exports = function createListenMqtt(deps) {
|
|
|
87
85
|
options
|
|
88
86
|
);
|
|
89
87
|
const mqttClient = ctx.mqttClient;
|
|
90
|
-
global
|
|
88
|
+
// Remove global reference to prevent memory leak
|
|
89
|
+
// Only set if needed for debugging, but clear on cleanup
|
|
90
|
+
if (process.env.DEBUG_MQTT) {
|
|
91
|
+
global.mqttClient = mqttClient;
|
|
92
|
+
}
|
|
91
93
|
|
|
92
94
|
mqttClient.on("error", function (err) {
|
|
93
95
|
const msg = String(err && err.message ? err.message : err || "");
|
|
@@ -145,7 +147,8 @@ module.exports = function createListenMqtt(deps) {
|
|
|
145
147
|
mqttClient.publish("/foreground_state", JSON.stringify({ foreground: chatOn }), { qos: 1 });
|
|
146
148
|
mqttClient.publish("/set_client_settings", JSON.stringify({ make_user_available_when_in_foreground: true }), { qos: 1 });
|
|
147
149
|
const d = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
|
|
148
|
-
|
|
150
|
+
let rTimeout = setTimeout(function () {
|
|
151
|
+
rTimeout = null;
|
|
149
152
|
if (ctx._ending) {
|
|
150
153
|
logger("mqtt t_ms timeout skipped - ending", "warn");
|
|
151
154
|
return;
|
|
@@ -159,8 +162,17 @@ module.exports = function createListenMqtt(deps) {
|
|
|
159
162
|
scheduleReconnect(d);
|
|
160
163
|
}, 5000);
|
|
161
164
|
|
|
165
|
+
// Store timeout reference for cleanup
|
|
166
|
+
ctx._rTimeout = rTimeout;
|
|
167
|
+
|
|
162
168
|
ctx.tmsWait = function () {
|
|
163
|
-
|
|
169
|
+
if (rTimeout) {
|
|
170
|
+
clearTimeout(rTimeout);
|
|
171
|
+
rTimeout = null;
|
|
172
|
+
}
|
|
173
|
+
if (ctx._rTimeout) {
|
|
174
|
+
delete ctx._rTimeout;
|
|
175
|
+
}
|
|
164
176
|
if (ctx.globalOptions.emitReady) globalCallback({ type: "ready", error: null });
|
|
165
177
|
delete ctx.tmsWait;
|
|
166
178
|
};
|
|
@@ -33,6 +33,60 @@ module.exports = function createEmitAuth({ logger }) {
|
|
|
33
33
|
ctx.mqttClient = undefined;
|
|
34
34
|
ctx.loggedIn = false;
|
|
35
35
|
|
|
36
|
+
// Clean up timeout references
|
|
37
|
+
try {
|
|
38
|
+
if (ctx._rTimeout) {
|
|
39
|
+
clearTimeout(ctx._rTimeout);
|
|
40
|
+
ctx._rTimeout = null;
|
|
41
|
+
}
|
|
42
|
+
} catch (_) { }
|
|
43
|
+
|
|
44
|
+
// Clean up tasks Map to prevent memory leak
|
|
45
|
+
try {
|
|
46
|
+
if (ctx.tasks && ctx.tasks instanceof Map) {
|
|
47
|
+
ctx.tasks.clear();
|
|
48
|
+
}
|
|
49
|
+
} catch (_) { }
|
|
50
|
+
|
|
51
|
+
// Clean up userInfo intervals
|
|
52
|
+
try {
|
|
53
|
+
if (ctx._userInfoIntervals && Array.isArray(ctx._userInfoIntervals)) {
|
|
54
|
+
ctx._userInfoIntervals.forEach(interval => {
|
|
55
|
+
try {
|
|
56
|
+
clearInterval(interval);
|
|
57
|
+
} catch (_) { }
|
|
58
|
+
});
|
|
59
|
+
ctx._userInfoIntervals = [];
|
|
60
|
+
}
|
|
61
|
+
} catch (_) { }
|
|
62
|
+
|
|
63
|
+
// Clean up autoSave intervals
|
|
64
|
+
try {
|
|
65
|
+
if (ctx._autoSaveInterval && Array.isArray(ctx._autoSaveInterval)) {
|
|
66
|
+
ctx._autoSaveInterval.forEach(interval => {
|
|
67
|
+
try {
|
|
68
|
+
clearInterval(interval);
|
|
69
|
+
} catch (_) { }
|
|
70
|
+
});
|
|
71
|
+
ctx._autoSaveInterval = [];
|
|
72
|
+
}
|
|
73
|
+
} catch (_) { }
|
|
74
|
+
|
|
75
|
+
// Clean up scheduler
|
|
76
|
+
try {
|
|
77
|
+
if (ctx._scheduler && typeof ctx._scheduler.destroy === "function") {
|
|
78
|
+
ctx._scheduler.destroy();
|
|
79
|
+
ctx._scheduler = undefined;
|
|
80
|
+
}
|
|
81
|
+
} catch (_) { }
|
|
82
|
+
|
|
83
|
+
// Clear global mqttClient reference if set
|
|
84
|
+
try {
|
|
85
|
+
if (global.mqttClient) {
|
|
86
|
+
delete global.mqttClient;
|
|
87
|
+
}
|
|
88
|
+
} catch (_) { }
|
|
89
|
+
|
|
36
90
|
const msg = detail || reason;
|
|
37
91
|
logger(`auth change -> ${reason}: ${msg}`, "error");
|
|
38
92
|
|
|
@@ -13,6 +13,7 @@ const createGetSeqID = require("./core/getSeqID");
|
|
|
13
13
|
const markDelivery = require("./core/markDelivery");
|
|
14
14
|
const getTaskResponseData = require("./core/getTaskResponseData");
|
|
15
15
|
const createEmitAuth = require("./core/emitAuth");
|
|
16
|
+
const createMiddlewareSystem = require("./middleware");
|
|
16
17
|
const parseDelta = createParseDelta({ markDelivery, parseAndCheckLogin });
|
|
17
18
|
// Create emitAuth first so it can be injected into both factories
|
|
18
19
|
const emitAuth = createEmitAuth({ logger });
|
|
@@ -32,6 +33,12 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
32
33
|
const identity = function () { };
|
|
33
34
|
let globalCallback = identity;
|
|
34
35
|
|
|
36
|
+
// Initialize middleware system if not already initialized
|
|
37
|
+
if (!ctx._middleware) {
|
|
38
|
+
ctx._middleware = createMiddlewareSystem();
|
|
39
|
+
}
|
|
40
|
+
const middleware = ctx._middleware;
|
|
41
|
+
|
|
35
42
|
function installPostGuard() {
|
|
36
43
|
if (ctx._postGuarded) return defaultFuncs.post;
|
|
37
44
|
const rawPost = defaultFuncs.post && defaultFuncs.post.bind(defaultFuncs);
|
|
@@ -160,6 +167,43 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
160
167
|
clearTimeout(ctx._reconnectTimer);
|
|
161
168
|
ctx._reconnectTimer = null;
|
|
162
169
|
}
|
|
170
|
+
if (ctx._rTimeout) {
|
|
171
|
+
clearTimeout(ctx._rTimeout);
|
|
172
|
+
ctx._rTimeout = null;
|
|
173
|
+
}
|
|
174
|
+
// Clean up tasks Map to prevent memory leak
|
|
175
|
+
if (ctx.tasks && ctx.tasks instanceof Map) {
|
|
176
|
+
ctx.tasks.clear();
|
|
177
|
+
}
|
|
178
|
+
// Clean up userInfo intervals
|
|
179
|
+
if (ctx._userInfoIntervals && Array.isArray(ctx._userInfoIntervals)) {
|
|
180
|
+
ctx._userInfoIntervals.forEach(interval => {
|
|
181
|
+
try {
|
|
182
|
+
clearInterval(interval);
|
|
183
|
+
} catch (_) { }
|
|
184
|
+
});
|
|
185
|
+
ctx._userInfoIntervals = [];
|
|
186
|
+
}
|
|
187
|
+
// Clean up autoSave intervals
|
|
188
|
+
if (ctx._autoSaveInterval && Array.isArray(ctx._autoSaveInterval)) {
|
|
189
|
+
ctx._autoSaveInterval.forEach(interval => {
|
|
190
|
+
try {
|
|
191
|
+
clearInterval(interval);
|
|
192
|
+
} catch (_) { }
|
|
193
|
+
});
|
|
194
|
+
ctx._autoSaveInterval = [];
|
|
195
|
+
}
|
|
196
|
+
// Clean up scheduler
|
|
197
|
+
if (ctx._scheduler && typeof ctx._scheduler.destroy === "function") {
|
|
198
|
+
try {
|
|
199
|
+
ctx._scheduler.destroy();
|
|
200
|
+
} catch (_) { }
|
|
201
|
+
ctx._scheduler = undefined;
|
|
202
|
+
}
|
|
203
|
+
// Clear global mqttClient reference if set
|
|
204
|
+
if (global.mqttClient) {
|
|
205
|
+
delete global.mqttClient;
|
|
206
|
+
}
|
|
163
207
|
next && next();
|
|
164
208
|
};
|
|
165
209
|
try {
|
|
@@ -224,11 +268,20 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
224
268
|
|
|
225
269
|
const msgEmitter = new MessageEmitter();
|
|
226
270
|
|
|
227
|
-
|
|
271
|
+
// Original callback without middleware
|
|
272
|
+
const originalCallback = callback || function (error, message) {
|
|
228
273
|
if (error) { logger("mqtt emit error", "error"); return msgEmitter.emit("error", error); }
|
|
229
274
|
msgEmitter.emit("message", message);
|
|
230
275
|
};
|
|
231
276
|
|
|
277
|
+
// Only wrap callback with middleware if middleware exists
|
|
278
|
+
// If no middleware, use callback directly for better performance
|
|
279
|
+
if (middleware.count > 0) {
|
|
280
|
+
globalCallback = middleware.wrapCallback(originalCallback);
|
|
281
|
+
} else {
|
|
282
|
+
globalCallback = originalCallback;
|
|
283
|
+
}
|
|
284
|
+
|
|
232
285
|
conf = mqttConf(ctx, conf);
|
|
233
286
|
|
|
234
287
|
installPostGuard();
|
|
@@ -253,6 +306,60 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
253
306
|
|
|
254
307
|
api.stopListening = msgEmitter.stopListening;
|
|
255
308
|
api.stopListeningAsync = msgEmitter.stopListeningAsync;
|
|
309
|
+
|
|
310
|
+
// Store original callback for re-wrapping when middleware is added/removed
|
|
311
|
+
let currentOriginalCallback = originalCallback;
|
|
312
|
+
let currentGlobalCallback = globalCallback;
|
|
313
|
+
|
|
314
|
+
// Function to re-wrap callback when middleware changes
|
|
315
|
+
function rewrapCallbackIfNeeded() {
|
|
316
|
+
if (!ctx.mqttClient || ctx._ending) return; // Not listening or ending
|
|
317
|
+
|
|
318
|
+
const hasMiddleware = middleware.count > 0;
|
|
319
|
+
const isWrapped = currentGlobalCallback !== currentOriginalCallback;
|
|
320
|
+
|
|
321
|
+
// If middleware exists but callback is not wrapped, wrap it
|
|
322
|
+
if (hasMiddleware && !isWrapped) {
|
|
323
|
+
currentGlobalCallback = middleware.wrapCallback(currentOriginalCallback);
|
|
324
|
+
globalCallback = currentGlobalCallback;
|
|
325
|
+
logger("Middleware added - callback re-wrapped", "info");
|
|
326
|
+
}
|
|
327
|
+
// If no middleware but callback is wrapped, unwrap it
|
|
328
|
+
else if (!hasMiddleware && isWrapped) {
|
|
329
|
+
currentGlobalCallback = currentOriginalCallback;
|
|
330
|
+
globalCallback = currentGlobalCallback;
|
|
331
|
+
logger("All middleware removed - callback unwrapped", "info");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Expose middleware API with re-wrapping support
|
|
336
|
+
api.useMiddleware = function (middlewareFn, fn) {
|
|
337
|
+
const result = middleware.use(middlewareFn, fn);
|
|
338
|
+
rewrapCallbackIfNeeded();
|
|
339
|
+
return result;
|
|
340
|
+
};
|
|
341
|
+
api.removeMiddleware = function (identifier) {
|
|
342
|
+
const result = middleware.remove(identifier);
|
|
343
|
+
rewrapCallbackIfNeeded();
|
|
344
|
+
return result;
|
|
345
|
+
};
|
|
346
|
+
api.clearMiddleware = function () {
|
|
347
|
+
const result = middleware.clear();
|
|
348
|
+
rewrapCallbackIfNeeded();
|
|
349
|
+
return result;
|
|
350
|
+
};
|
|
351
|
+
api.listMiddleware = function () {
|
|
352
|
+
return middleware.list();
|
|
353
|
+
};
|
|
354
|
+
api.setMiddlewareEnabled = function (name, enabled) {
|
|
355
|
+
const result = middleware.setEnabled(name, enabled);
|
|
356
|
+
rewrapCallbackIfNeeded();
|
|
357
|
+
return result;
|
|
358
|
+
};
|
|
359
|
+
Object.defineProperty(api, "middlewareCount", {
|
|
360
|
+
get: function () { return middleware.count; }
|
|
361
|
+
});
|
|
362
|
+
|
|
256
363
|
return msgEmitter;
|
|
257
364
|
};
|
|
258
365
|
};
|