@dongdev/fca-unofficial 3.0.27 → 3.0.28

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.
@@ -1,372 +1,371 @@
1
- "use strict";
2
- const mqtt = require("mqtt");
3
- const WebSocket = require("ws");
4
- const HttpsProxyAgent = require("https-proxy-agent");
5
- const EventEmitter = require("events");
6
- const logger = require("../../../func/logger");
7
- const { parseAndCheckLogin } = require("../../utils/client");
8
- const { buildProxy, buildStream } = require("./detail/buildStream");
9
- const { topics } = require("./detail/constants");
10
- const createParseDelta = require("./core/parseDelta");
11
- const createListenMqtt = require("./core/connectMqtt");
12
- const createGetSeqID = require("./core/getSeqID");
13
- const markDelivery = require("./core/markDelivery");
14
- const getTaskResponseData = require("./core/getTaskResponseData");
15
- const createEmitAuth = require("./core/emitAuth");
16
- const createMiddlewareSystem = require("./middleware");
17
- const parseDelta = createParseDelta({ markDelivery, parseAndCheckLogin });
18
- // Create emitAuth first so it can be injected into both factories
19
- const emitAuth = createEmitAuth({ logger });
20
- // Pass emitAuth into connectMqtt so errors there can signal auth state
21
- const listenMqtt = createListenMqtt({ WebSocket, mqtt, HttpsProxyAgent, buildStream, buildProxy, topics, parseDelta, getTaskResponseData, logger, emitAuth });
22
- // Inject emitAuth into getSeqID so its catch handler can notify properly
23
- const getSeqIDFactory = createGetSeqID({ parseAndCheckLogin, listenMqtt, logger, emitAuth });
24
-
25
- const MQTT_DEFAULTS = { cycleMs: 60 * 60 * 1000, reconnectDelayMs: 2000, autoReconnect: true, reconnectAfterStop: false };
26
- function mqttConf(ctx, overrides) {
27
- ctx._mqttOpt = Object.assign({}, MQTT_DEFAULTS, ctx._mqttOpt || {}, overrides || {});
28
- if (typeof ctx._mqttOpt.autoReconnect === "boolean") ctx.globalOptions.autoReconnect = ctx._mqttOpt.autoReconnect;
29
- return ctx._mqttOpt;
30
- }
31
-
32
- module.exports = function (defaultFuncs, api, ctx, opts) {
33
- const identity = function () { };
34
- let globalCallback = identity;
35
-
36
- // Initialize middleware system if not already initialized
37
- if (!ctx._middleware) {
38
- ctx._middleware = createMiddlewareSystem();
39
- }
40
- const middleware = ctx._middleware;
41
-
42
- function installPostGuard() {
43
- if (ctx._postGuarded) return defaultFuncs.post;
44
- const rawPost = defaultFuncs.post && defaultFuncs.post.bind(defaultFuncs);
45
- if (!rawPost) return defaultFuncs.post;
46
-
47
- function postSafe(...args) {
48
- return rawPost(...args).catch(err => {
49
- const msg = (err && err.error) || (err && err.message) || String(err || "");
50
- if (/Not logged in|blocked the login/i.test(msg)) {
51
- emitAuth(
52
- ctx,
53
- api,
54
- globalCallback,
55
- /blocked/i.test(msg) ? "login_blocked" : "not_logged_in",
56
- msg
57
- );
58
- }
59
- throw err;
60
- });
61
- }
62
- defaultFuncs.post = postSafe;
63
- ctx._postGuarded = true;
64
- return postSafe;
65
- }
66
-
67
- let conf = mqttConf(ctx, opts);
68
-
69
- function getSeqIDWrapper() {
70
- if (ctx._ending && !ctx._cycling) {
71
- logger("mqtt getSeqID skipped - ending", "warn");
72
- return Promise.resolve();
73
- }
74
- const form = {
75
- av: ctx.globalOptions.pageID,
76
- queries: JSON.stringify({
77
- o0: {
78
- doc_id: "3336396659757871",
79
- query_params: {
80
- limit: 1, before: null, tags: ["INBOX"],
81
- includeDeliveryReceipts: false, includeSeqID: true
82
- }
83
- }
84
- })
85
- };
86
- logger("mqtt getSeqID call", "info");
87
- return getSeqIDFactory(defaultFuncs, api, ctx, globalCallback, form)
88
- .then(() => {
89
- logger("mqtt getSeqID done", "info");
90
- ctx._cycling = false;
91
- })
92
- .catch(e => {
93
- ctx._cycling = false;
94
- const errMsg = e && e.message ? e.message : String(e || "Unknown error");
95
- logger(`mqtt getSeqID error: ${errMsg}`, "error");
96
- // Don't reconnect if we're ending
97
- if (ctx._ending) return;
98
- // Retry after delay if autoReconnect is enabled
99
- if (ctx.globalOptions.autoReconnect) {
100
- const d = conf.reconnectDelayMs;
101
- logger(`mqtt getSeqID will retry in ${d}ms`, "warn");
102
- setTimeout(() => {
103
- if (!ctx._ending) getSeqIDWrapper();
104
- }, d);
105
- }
106
- });
107
- }
108
-
109
- function isConnected() {
110
- return !!(ctx.mqttClient && ctx.mqttClient.connected);
111
- }
112
-
113
- function unsubAll(cb) {
114
- if (!isConnected()) {
115
- if (cb) setTimeout(cb, 0);
116
- return;
117
- }
118
- let pending = topics.length;
119
- if (!pending) {
120
- if (cb) setTimeout(cb, 0);
121
- return;
122
- }
123
- let fired = false;
124
- const timeout = setTimeout(() => {
125
- if (!fired) {
126
- fired = true;
127
- logger("unsubAll timeout, proceeding anyway", "warn");
128
- if (cb) cb();
129
- }
130
- }, 5000); // 5 second timeout
131
-
132
- topics.forEach(t => {
133
- try {
134
- ctx.mqttClient.unsubscribe(t, () => {
135
- if (--pending === 0 && !fired) {
136
- clearTimeout(timeout);
137
- fired = true;
138
- if (cb) cb();
139
- }
140
- });
141
- } catch (err) {
142
- logger(`unsubAll error for topic ${t}: ${err && err.message ? err.message : String(err)}`, "warn");
143
- if (--pending === 0 && !fired) {
144
- clearTimeout(timeout);
145
- fired = true;
146
- if (cb) cb();
147
- }
148
- }
149
- });
150
- }
151
-
152
- function endQuietly(next) {
153
- const finish = () => {
154
- try {
155
- if (ctx.mqttClient) {
156
- ctx.mqttClient.removeAllListeners();
157
- }
158
- } catch (_) { }
159
- ctx.mqttClient = undefined;
160
- ctx.lastSeqId = null;
161
- ctx.syncToken = undefined;
162
- ctx.t_mqttCalled = false;
163
- ctx._ending = false;
164
- ctx._cycling = false;
165
- if (ctx._reconnectTimer) {
166
- clearTimeout(ctx._reconnectTimer);
167
- ctx._reconnectTimer = null;
168
- }
169
- if (ctx._rTimeout) {
170
- clearTimeout(ctx._rTimeout);
171
- ctx._rTimeout = null;
172
- }
173
- // Clean up tasks Map to prevent memory leak
174
- if (ctx.tasks && ctx.tasks instanceof Map) {
175
- ctx.tasks.clear();
176
- }
177
- // Clean up userInfo intervals
178
- if (ctx._userInfoIntervals && Array.isArray(ctx._userInfoIntervals)) {
179
- ctx._userInfoIntervals.forEach(interval => {
180
- try {
181
- clearInterval(interval);
182
- } catch (_) { }
183
- });
184
- ctx._userInfoIntervals = [];
185
- }
186
- // Clean up autoSave intervals
187
- if (ctx._autoSaveInterval && Array.isArray(ctx._autoSaveInterval)) {
188
- ctx._autoSaveInterval.forEach(interval => {
189
- try {
190
- clearInterval(interval);
191
- } catch (_) { }
192
- });
193
- ctx._autoSaveInterval = [];
194
- }
195
- // Clean up scheduler
196
- if (ctx._scheduler && typeof ctx._scheduler.destroy === "function") {
197
- try {
198
- ctx._scheduler.destroy();
199
- } catch (_) { }
200
- ctx._scheduler = undefined;
201
- }
202
- next && next();
203
- };
204
- try {
205
- if (ctx.mqttClient) {
206
- if (isConnected()) {
207
- try {
208
- ctx.mqttClient.publish("/browser_close", "{}", { qos: 0 });
209
- } catch (_) { }
210
- }
211
- ctx.mqttClient.end(true, finish);
212
- } else finish();
213
- } catch (_) { finish(); }
214
- }
215
-
216
- function delayedReconnect() {
217
- const d = conf.reconnectDelayMs;
218
- logger(`mqtt reconnect in ${d}ms`, "info");
219
- setTimeout(() => getSeqIDWrapper(), d);
220
- }
221
-
222
- function forceCycle() {
223
- if (ctx._cycling) {
224
- logger("mqtt force cycle already in progress", "warn");
225
- return;
226
- }
227
- ctx._cycling = true;
228
- ctx._ending = true;
229
- logger("mqtt force cycle begin", "warn");
230
- unsubAll(() => endQuietly(() => delayedReconnect()));
231
- }
232
-
233
- return function (callback) {
234
- class MessageEmitter extends EventEmitter {
235
- stopListening(callback2) {
236
- const cb = callback2 || function () { };
237
- logger("mqtt stop requested", "info");
238
- globalCallback = identity;
239
-
240
- if (ctx._autoCycleTimer) {
241
- clearInterval(ctx._autoCycleTimer);
242
- ctx._autoCycleTimer = null;
243
- logger("mqtt auto-cycle cleared", "info");
244
- }
245
-
246
- if (ctx._reconnectTimer) {
247
- clearTimeout(ctx._reconnectTimer);
248
- ctx._reconnectTimer = null;
249
- }
250
-
251
- ctx._ending = true;
252
- unsubAll(() => endQuietly(() => {
253
- logger("mqtt stopped", "info");
254
- cb();
255
- conf = mqttConf(ctx, conf);
256
- if (conf.reconnectAfterStop) delayedReconnect();
257
- }));
258
- }
259
- async stopListeningAsync() {
260
- return new Promise(resolve => { this.stopListening(resolve); });
261
- }
262
- }
263
-
264
- const msgEmitter = new MessageEmitter();
265
-
266
- // Original callback without middleware
267
- const originalCallback = callback || function (error, message) {
268
- if (error) { logger("mqtt emit error", "error"); return msgEmitter.emit("error", error); }
269
- msgEmitter.emit("message", message);
270
- };
271
-
272
- // Only wrap callback with middleware if middleware exists
273
- // If no middleware, use callback directly for better performance
274
- if (middleware.count > 0) {
275
- globalCallback = middleware.wrapCallback(originalCallback);
276
- } else {
277
- globalCallback = originalCallback;
278
- }
279
-
280
- conf = mqttConf(ctx, conf);
281
-
282
- installPostGuard();
283
-
284
- if (!ctx.firstListen) ctx.lastSeqId = null;
285
- ctx.syncToken = undefined;
286
- ctx.t_mqttCalled = false;
287
-
288
- if (ctx._autoCycleTimer) { clearInterval(ctx._autoCycleTimer); ctx._autoCycleTimer = null; }
289
- if (conf.cycleMs && conf.cycleMs > 0) {
290
- ctx._autoCycleTimer = setInterval(forceCycle, conf.cycleMs);
291
- logger(`mqtt auto-cycle enabled ${conf.cycleMs}ms`, "info");
292
- } else {
293
- logger("mqtt auto-cycle disabled", "info");
294
- }
295
-
296
- if (!ctx.firstListen || !ctx.lastSeqId) getSeqIDWrapper();
297
- else {
298
- logger("mqtt starting listenMqtt", "info");
299
- listenMqtt(defaultFuncs, api, ctx, globalCallback);
300
- }
301
-
302
- api.stopListening = msgEmitter.stopListening;
303
- api.stopListeningAsync = msgEmitter.stopListeningAsync;
304
-
305
- // Store original callback for re-wrapping when middleware is added/removed
306
- let currentOriginalCallback = originalCallback;
307
- let currentGlobalCallback = globalCallback;
308
-
309
- // Function to re-wrap callback when middleware changes
310
- function rewrapCallbackIfNeeded() {
311
- if (!ctx.mqttClient || ctx._ending) return; // Not listening or ending
312
-
313
- const hasMiddleware = middleware.count > 0;
314
- const isWrapped = currentGlobalCallback !== currentOriginalCallback;
315
-
316
- // If middleware exists but callback is not wrapped, wrap it
317
- if (hasMiddleware && !isWrapped) {
318
- currentGlobalCallback = middleware.wrapCallback(currentOriginalCallback);
319
- globalCallback = currentGlobalCallback;
320
- logger("Middleware added - callback re-wrapped", "info");
321
- }
322
- // If no middleware but callback is wrapped, unwrap it
323
- else if (!hasMiddleware && isWrapped) {
324
- currentGlobalCallback = currentOriginalCallback;
325
- globalCallback = currentGlobalCallback;
326
- logger("All middleware removed - callback unwrapped", "info");
327
- }
328
- }
329
-
330
- // Expose middleware API with re-wrapping support
331
- api.useMiddleware = function (middlewareFn, fn) {
332
- const result = middleware.use(middlewareFn, fn);
333
- rewrapCallbackIfNeeded();
334
- return result;
335
- };
336
- api.removeMiddleware = function (identifier) {
337
- const result = middleware.remove(identifier);
338
- rewrapCallbackIfNeeded();
339
- return result;
340
- };
341
- api.clearMiddleware = function () {
342
- const result = middleware.clear();
343
- rewrapCallbackIfNeeded();
344
- return result;
345
- };
346
- api.listMiddleware = function () {
347
- return middleware.list();
348
- };
349
- api.setMiddlewareEnabled = function (name, enabled) {
350
- const result = middleware.setEnabled(name, enabled);
351
- rewrapCallbackIfNeeded();
352
- return result;
353
- };
354
- // Avoid crashing on restart: defineProperty throws if already defined and non-configurable.
355
- const existingMiddlewareCount = Object.getOwnPropertyDescriptor(api, "middlewareCount");
356
- if (!existingMiddlewareCount) {
357
- Object.defineProperty(api, "middlewareCount", {
358
- configurable: true,
359
- enumerable: false,
360
- get: function () { return (ctx._middleware && ctx._middleware.count) || 0; }
361
- });
362
- } else if (existingMiddlewareCount.configurable) {
363
- Object.defineProperty(api, "middlewareCount", {
364
- configurable: true,
365
- enumerable: existingMiddlewareCount.enumerable,
366
- get: function () { return (ctx._middleware && ctx._middleware.count) || 0; }
367
- });
368
- }
369
-
370
- return msgEmitter;
371
- };
372
- };
1
+ "use strict";
2
+ const mqtt = require("mqtt");
3
+ const WebSocket = require("ws");
4
+ const HttpsProxyAgent = require("https-proxy-agent");
5
+ const EventEmitter = require("events");
6
+ const logger = require("../../../func/logger");
7
+ const { parseAndCheckLogin } = require("../../utils/client");
8
+ const { buildProxy, buildStream } = require("./detail/buildStream");
9
+ const { topics } = require("./detail/constants");
10
+ const createParseDelta = require("./core/parseDelta");
11
+ const createListenMqtt = require("./core/connectMqtt");
12
+ const createGetSeqID = require("./core/getSeqID");
13
+ const getTaskResponseData = require("./core/getTaskResponseData");
14
+ const createEmitAuth = require("./core/emitAuth");
15
+ const createMiddlewareSystem = require("./middleware");
16
+ const parseDelta = createParseDelta({ parseAndCheckLogin });
17
+ // Create emitAuth first so it can be injected into both factories
18
+ const emitAuth = createEmitAuth({ logger });
19
+ // Pass emitAuth into connectMqtt so errors there can signal auth state
20
+ const listenMqtt = createListenMqtt({ WebSocket, mqtt, HttpsProxyAgent, buildStream, buildProxy, topics, parseDelta, getTaskResponseData, logger, emitAuth });
21
+ // Inject emitAuth into getSeqID so its catch handler can notify properly
22
+ const getSeqIDFactory = createGetSeqID({ parseAndCheckLogin, listenMqtt, logger, emitAuth });
23
+
24
+ const MQTT_DEFAULTS = { cycleMs: 60 * 60 * 1000, reconnectDelayMs: 2000, autoReconnect: true, reconnectAfterStop: false };
25
+ function mqttConf(ctx, overrides) {
26
+ ctx._mqttOpt = Object.assign({}, MQTT_DEFAULTS, ctx._mqttOpt || {}, overrides || {});
27
+ if (typeof ctx._mqttOpt.autoReconnect === "boolean") ctx.globalOptions.autoReconnect = ctx._mqttOpt.autoReconnect;
28
+ return ctx._mqttOpt;
29
+ }
30
+
31
+ module.exports = function (defaultFuncs, api, ctx, opts) {
32
+ const identity = function () { };
33
+ let globalCallback = identity;
34
+
35
+ // Initialize middleware system if not already initialized
36
+ if (!ctx._middleware) {
37
+ ctx._middleware = createMiddlewareSystem();
38
+ }
39
+ const middleware = ctx._middleware;
40
+
41
+ function installPostGuard() {
42
+ if (ctx._postGuarded) return defaultFuncs.post;
43
+ const rawPost = defaultFuncs.post && defaultFuncs.post.bind(defaultFuncs);
44
+ if (!rawPost) return defaultFuncs.post;
45
+
46
+ function postSafe(...args) {
47
+ return rawPost(...args).catch(err => {
48
+ const msg = (err && err.error) || (err && err.message) || String(err || "");
49
+ if (/Not logged in|blocked the login/i.test(msg)) {
50
+ emitAuth(
51
+ ctx,
52
+ api,
53
+ globalCallback,
54
+ /blocked/i.test(msg) ? "login_blocked" : "not_logged_in",
55
+ msg
56
+ );
57
+ }
58
+ throw err;
59
+ });
60
+ }
61
+ defaultFuncs.post = postSafe;
62
+ ctx._postGuarded = true;
63
+ return postSafe;
64
+ }
65
+
66
+ let conf = mqttConf(ctx, opts);
67
+
68
+ function getSeqIDWrapper() {
69
+ if (ctx._ending && !ctx._cycling) {
70
+ logger("mqtt getSeqID skipped - ending", "warn");
71
+ return Promise.resolve();
72
+ }
73
+ const form = {
74
+ av: ctx.globalOptions.pageID,
75
+ queries: JSON.stringify({
76
+ o0: {
77
+ doc_id: "3336396659757871",
78
+ query_params: {
79
+ limit: 1, before: null, tags: ["INBOX"],
80
+ includeDeliveryReceipts: false, includeSeqID: true
81
+ }
82
+ }
83
+ })
84
+ };
85
+ logger("mqtt getSeqID call", "info");
86
+ return getSeqIDFactory(defaultFuncs, api, ctx, globalCallback, form)
87
+ .then(() => {
88
+ logger("mqtt getSeqID done", "info");
89
+ ctx._cycling = false;
90
+ })
91
+ .catch(e => {
92
+ ctx._cycling = false;
93
+ const errMsg = e && e.message ? e.message : String(e || "Unknown error");
94
+ logger(`mqtt getSeqID error: ${errMsg}`, "error");
95
+ // Don't reconnect if we're ending
96
+ if (ctx._ending) return;
97
+ // Retry after delay if autoReconnect is enabled
98
+ if (ctx.globalOptions.autoReconnect) {
99
+ const d = conf.reconnectDelayMs;
100
+ logger(`mqtt getSeqID will retry in ${d}ms`, "warn");
101
+ setTimeout(() => {
102
+ if (!ctx._ending) getSeqIDWrapper();
103
+ }, d);
104
+ }
105
+ });
106
+ }
107
+
108
+ function isConnected() {
109
+ return !!(ctx.mqttClient && ctx.mqttClient.connected);
110
+ }
111
+
112
+ function unsubAll(cb) {
113
+ if (!isConnected()) {
114
+ if (cb) setTimeout(cb, 0);
115
+ return;
116
+ }
117
+ let pending = topics.length;
118
+ if (!pending) {
119
+ if (cb) setTimeout(cb, 0);
120
+ return;
121
+ }
122
+ let fired = false;
123
+ const timeout = setTimeout(() => {
124
+ if (!fired) {
125
+ fired = true;
126
+ logger("unsubAll timeout, proceeding anyway", "warn");
127
+ if (cb) cb();
128
+ }
129
+ }, 5000); // 5 second timeout
130
+
131
+ topics.forEach(t => {
132
+ try {
133
+ ctx.mqttClient.unsubscribe(t, () => {
134
+ if (--pending === 0 && !fired) {
135
+ clearTimeout(timeout);
136
+ fired = true;
137
+ if (cb) cb();
138
+ }
139
+ });
140
+ } catch (err) {
141
+ logger(`unsubAll error for topic ${t}: ${err && err.message ? err.message : String(err)}`, "warn");
142
+ if (--pending === 0 && !fired) {
143
+ clearTimeout(timeout);
144
+ fired = true;
145
+ if (cb) cb();
146
+ }
147
+ }
148
+ });
149
+ }
150
+
151
+ function endQuietly(next) {
152
+ const finish = () => {
153
+ try {
154
+ if (ctx.mqttClient) {
155
+ ctx.mqttClient.removeAllListeners();
156
+ }
157
+ } catch (_) { }
158
+ ctx.mqttClient = undefined;
159
+ ctx.lastSeqId = null;
160
+ ctx.syncToken = undefined;
161
+ ctx.t_mqttCalled = false;
162
+ ctx._ending = false;
163
+ ctx._cycling = false;
164
+ if (ctx._reconnectTimer) {
165
+ clearTimeout(ctx._reconnectTimer);
166
+ ctx._reconnectTimer = null;
167
+ }
168
+ if (ctx._rTimeout) {
169
+ clearTimeout(ctx._rTimeout);
170
+ ctx._rTimeout = null;
171
+ }
172
+ // Clean up tasks Map to prevent memory leak
173
+ if (ctx.tasks && ctx.tasks instanceof Map) {
174
+ ctx.tasks.clear();
175
+ }
176
+ // Clean up userInfo intervals
177
+ if (ctx._userInfoIntervals && Array.isArray(ctx._userInfoIntervals)) {
178
+ ctx._userInfoIntervals.forEach(interval => {
179
+ try {
180
+ clearInterval(interval);
181
+ } catch (_) { }
182
+ });
183
+ ctx._userInfoIntervals = [];
184
+ }
185
+ // Clean up autoSave intervals
186
+ if (ctx._autoSaveInterval && Array.isArray(ctx._autoSaveInterval)) {
187
+ ctx._autoSaveInterval.forEach(interval => {
188
+ try {
189
+ clearInterval(interval);
190
+ } catch (_) { }
191
+ });
192
+ ctx._autoSaveInterval = [];
193
+ }
194
+ // Clean up scheduler
195
+ if (ctx._scheduler && typeof ctx._scheduler.destroy === "function") {
196
+ try {
197
+ ctx._scheduler.destroy();
198
+ } catch (_) { }
199
+ ctx._scheduler = undefined;
200
+ }
201
+ next && next();
202
+ };
203
+ try {
204
+ if (ctx.mqttClient) {
205
+ if (isConnected()) {
206
+ try {
207
+ ctx.mqttClient.publish("/browser_close", "{}", { qos: 0 });
208
+ } catch (_) { }
209
+ }
210
+ ctx.mqttClient.end(true, finish);
211
+ } else finish();
212
+ } catch (_) { finish(); }
213
+ }
214
+
215
+ function delayedReconnect() {
216
+ const d = conf.reconnectDelayMs;
217
+ logger(`mqtt reconnect in ${d}ms`, "info");
218
+ setTimeout(() => getSeqIDWrapper(), d);
219
+ }
220
+
221
+ function forceCycle() {
222
+ if (ctx._cycling) {
223
+ logger("mqtt force cycle already in progress", "warn");
224
+ return;
225
+ }
226
+ ctx._cycling = true;
227
+ ctx._ending = true;
228
+ logger("mqtt force cycle begin", "warn");
229
+ unsubAll(() => endQuietly(() => delayedReconnect()));
230
+ }
231
+
232
+ return function (callback) {
233
+ class MessageEmitter extends EventEmitter {
234
+ stopListening(callback2) {
235
+ const cb = callback2 || function () { };
236
+ logger("mqtt stop requested", "info");
237
+ globalCallback = identity;
238
+
239
+ if (ctx._autoCycleTimer) {
240
+ clearInterval(ctx._autoCycleTimer);
241
+ ctx._autoCycleTimer = null;
242
+ logger("mqtt auto-cycle cleared", "info");
243
+ }
244
+
245
+ if (ctx._reconnectTimer) {
246
+ clearTimeout(ctx._reconnectTimer);
247
+ ctx._reconnectTimer = null;
248
+ }
249
+
250
+ ctx._ending = true;
251
+ unsubAll(() => endQuietly(() => {
252
+ logger("mqtt stopped", "info");
253
+ cb();
254
+ conf = mqttConf(ctx, conf);
255
+ if (conf.reconnectAfterStop) delayedReconnect();
256
+ }));
257
+ }
258
+ async stopListeningAsync() {
259
+ return new Promise(resolve => { this.stopListening(resolve); });
260
+ }
261
+ }
262
+
263
+ const msgEmitter = new MessageEmitter();
264
+
265
+ // Original callback without middleware
266
+ const originalCallback = callback || function (error, message) {
267
+ if (error) { logger("mqtt emit error", "error"); return msgEmitter.emit("error", error); }
268
+ msgEmitter.emit("message", message);
269
+ };
270
+
271
+ // Only wrap callback with middleware if middleware exists
272
+ // If no middleware, use callback directly for better performance
273
+ if (middleware.count > 0) {
274
+ globalCallback = middleware.wrapCallback(originalCallback);
275
+ } else {
276
+ globalCallback = originalCallback;
277
+ }
278
+
279
+ conf = mqttConf(ctx, conf);
280
+
281
+ installPostGuard();
282
+
283
+ if (!ctx.firstListen) ctx.lastSeqId = null;
284
+ ctx.syncToken = undefined;
285
+ ctx.t_mqttCalled = false;
286
+
287
+ if (ctx._autoCycleTimer) { clearInterval(ctx._autoCycleTimer); ctx._autoCycleTimer = null; }
288
+ if (conf.cycleMs && conf.cycleMs > 0) {
289
+ ctx._autoCycleTimer = setInterval(forceCycle, conf.cycleMs);
290
+ logger(`mqtt auto-cycle enabled ${conf.cycleMs}ms`, "info");
291
+ } else {
292
+ logger("mqtt auto-cycle disabled", "info");
293
+ }
294
+
295
+ if (!ctx.firstListen || !ctx.lastSeqId) getSeqIDWrapper();
296
+ else {
297
+ logger("mqtt starting listenMqtt", "info");
298
+ listenMqtt(defaultFuncs, api, ctx, globalCallback);
299
+ }
300
+
301
+ api.stopListening = msgEmitter.stopListening;
302
+ api.stopListeningAsync = msgEmitter.stopListeningAsync;
303
+
304
+ // Store original callback for re-wrapping when middleware is added/removed
305
+ let currentOriginalCallback = originalCallback;
306
+ let currentGlobalCallback = globalCallback;
307
+
308
+ // Function to re-wrap callback when middleware changes
309
+ function rewrapCallbackIfNeeded() {
310
+ if (!ctx.mqttClient || ctx._ending) return; // Not listening or ending
311
+
312
+ const hasMiddleware = middleware.count > 0;
313
+ const isWrapped = currentGlobalCallback !== currentOriginalCallback;
314
+
315
+ // If middleware exists but callback is not wrapped, wrap it
316
+ if (hasMiddleware && !isWrapped) {
317
+ currentGlobalCallback = middleware.wrapCallback(currentOriginalCallback);
318
+ globalCallback = currentGlobalCallback;
319
+ logger("Middleware added - callback re-wrapped", "info");
320
+ }
321
+ // If no middleware but callback is wrapped, unwrap it
322
+ else if (!hasMiddleware && isWrapped) {
323
+ currentGlobalCallback = currentOriginalCallback;
324
+ globalCallback = currentGlobalCallback;
325
+ logger("All middleware removed - callback unwrapped", "info");
326
+ }
327
+ }
328
+
329
+ // Expose middleware API with re-wrapping support
330
+ api.useMiddleware = function (middlewareFn, fn) {
331
+ const result = middleware.use(middlewareFn, fn);
332
+ rewrapCallbackIfNeeded();
333
+ return result;
334
+ };
335
+ api.removeMiddleware = function (identifier) {
336
+ const result = middleware.remove(identifier);
337
+ rewrapCallbackIfNeeded();
338
+ return result;
339
+ };
340
+ api.clearMiddleware = function () {
341
+ const result = middleware.clear();
342
+ rewrapCallbackIfNeeded();
343
+ return result;
344
+ };
345
+ api.listMiddleware = function () {
346
+ return middleware.list();
347
+ };
348
+ api.setMiddlewareEnabled = function (name, enabled) {
349
+ const result = middleware.setEnabled(name, enabled);
350
+ rewrapCallbackIfNeeded();
351
+ return result;
352
+ };
353
+ // Avoid crashing on restart: defineProperty throws if already defined and non-configurable.
354
+ const existingMiddlewareCount = Object.getOwnPropertyDescriptor(api, "middlewareCount");
355
+ if (!existingMiddlewareCount) {
356
+ Object.defineProperty(api, "middlewareCount", {
357
+ configurable: true,
358
+ enumerable: false,
359
+ get: function () { return (ctx._middleware && ctx._middleware.count) || 0; }
360
+ });
361
+ } else if (existingMiddlewareCount.configurable) {
362
+ Object.defineProperty(api, "middlewareCount", {
363
+ configurable: true,
364
+ enumerable: existingMiddlewareCount.enumerable,
365
+ get: function () { return (ctx._middleware && ctx._middleware.count) || 0; }
366
+ });
367
+ }
368
+
369
+ return msgEmitter;
370
+ };
371
+ };