@turquoisebay/mqtt 0.1.13 β 0.1.15
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 +14 -0
- package/README.md +9 -1
- package/dist/src/__mocks__/mqtt.d.ts +1 -0
- package/dist/src/__mocks__/mqtt.d.ts.map +1 -1
- package/dist/src/__mocks__/mqtt.js +5 -0
- package/dist/src/channel.js +8 -3
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +138 -57
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.15] - 2026-02-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Echo `correlationId` from inbound JSON in outbound replies
|
|
12
|
+
|
|
13
|
+
### Docs
|
|
14
|
+
- Clarify how `senderId` maps to sessions vs `correlationId`
|
|
15
|
+
|
|
16
|
+
## [0.1.14] - 2026-02-03
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Robust reconnection logic (handles broker down at startup and restarts)
|
|
20
|
+
- Clean reconnect scheduling + shutdown cleanup
|
|
21
|
+
|
|
8
22
|
## [0.1.13] - 2026-02-03
|
|
9
23
|
|
|
10
24
|
### Fixed
|
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ MQTT channel plugin for [OpenClaw](https://github.com/openclaw/openclaw) β bid
|
|
|
10
10
|
|
|
11
11
|
- π **Bidirectional messaging** β subscribe and publish to MQTT topics
|
|
12
12
|
- π **Home automation ready** β integrates with Home Assistant, Mosquitto, EMQX
|
|
13
|
+
- π **Robust reconnection** β recovers from broker restarts and cold starts
|
|
13
14
|
- π **TLS support** β secure connections to cloud brokers
|
|
14
15
|
- π **Service monitoring** β receive alerts from Uptime Kuma, healthchecks, etc.
|
|
15
16
|
- β‘ **QoS levels** β configurable delivery guarantees (0, 1, 2)
|
|
@@ -59,6 +60,13 @@ openclaw gateway restart
|
|
|
59
60
|
|
|
60
61
|
## Usage
|
|
61
62
|
|
|
63
|
+
### Sessions & correlation IDs (important)
|
|
64
|
+
|
|
65
|
+
- **Sessions are keyed by `senderId`** β OpenClaw uses `mqtt:{senderId}` as the SessionKey, so memory and conversation history are grouped by sender.
|
|
66
|
+
- **`correlationId` is requestβlevel only** β if you include it in inbound JSON, itβs echoed back in the outbound reply for client-side matching. It does **not** create a new session or change memory.
|
|
67
|
+
|
|
68
|
+
If you want separate conversations, use distinct `senderId`s.
|
|
69
|
+
|
|
62
70
|
### Receiving messages (inbound)
|
|
63
71
|
|
|
64
72
|
Messages published to your `inbound` topic will be processed by OpenClaw.
|
|
@@ -69,7 +77,7 @@ You can send either plain text or JSON (recommended):
|
|
|
69
77
|
mosquitto_pub -t "openclaw/inbound" -m "Alert: Service down on playground"
|
|
70
78
|
|
|
71
79
|
# JSON (recommended)
|
|
72
|
-
mosquitto_pub -t "openclaw/inbound" -m '{"senderId":"pg-cli","text":"hello"}'
|
|
80
|
+
mosquitto_pub -t "openclaw/inbound" -m '{"senderId":"pg-cli","text":"hello","correlationId":"abc-123"}'
|
|
73
81
|
```
|
|
74
82
|
|
|
75
83
|
### Sending messages (outbound)
|
|
@@ -21,6 +21,7 @@ export declare class MockMqttClient extends EventEmitter {
|
|
|
21
21
|
simulateMessage(topic: string, payload: Buffer | string): void;
|
|
22
22
|
simulateError(err: Error): void;
|
|
23
23
|
simulateDisconnect(): void;
|
|
24
|
+
reconnect(): this;
|
|
24
25
|
simulateReconnect(): void;
|
|
25
26
|
}
|
|
26
27
|
export declare function connect(url: string, opts?: unknown): MockMqttClient;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mqtt.d.ts","sourceRoot":"","sources":["../../../src/__mocks__/mqtt.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC;;GAEG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,SAAS,UAAS;IAClB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAa;IACxD,SAAS,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAM;IAEzE,SAAS,CACP,KAAK,EAAE,MAAM,EACb,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,EACrB,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI;IAOxC,OAAO,CACL,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,OAAO,EACb,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI;IAOxC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI;IAQxD,eAAe;IAKf,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;IAKvD,aAAa,CAAC,GAAG,EAAE,KAAK;IAIxB,kBAAkB;IAKlB,iBAAiB;CAGlB;AAKD,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,cAAc,CAKnE;AAGD,wBAAgB,aAAa,IAAI,cAAc,GAAG,IAAI,CAErD;AAGD,wBAAgB,SAAS,SAExB;;;;;;AAED,wBAAqD"}
|
|
1
|
+
{"version":3,"file":"mqtt.d.ts","sourceRoot":"","sources":["../../../src/__mocks__/mqtt.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC;;GAEG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,SAAS,UAAS;IAClB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAa;IACxD,SAAS,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAM;IAEzE,SAAS,CACP,KAAK,EAAE,MAAM,EACb,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,EACrB,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI;IAOxC,OAAO,CACL,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,OAAO,EACb,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI;IAOxC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI;IAQxD,eAAe;IAKf,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;IAKvD,aAAa,CAAC,GAAG,EAAE,KAAK;IAIxB,kBAAkB;IAKlB,SAAS;IAMT,iBAAiB;CAGlB;AAKD,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,cAAc,CAKnE;AAGD,wBAAgB,aAAa,IAAI,cAAc,GAAG,IAAI,CAErD;AAGD,wBAAgB,SAAS,SAExB;;;;;;AAED,wBAAqD"}
|
|
@@ -38,6 +38,11 @@ export class MockMqttClient extends EventEmitter {
|
|
|
38
38
|
this.connected = false;
|
|
39
39
|
this.emit("close");
|
|
40
40
|
}
|
|
41
|
+
reconnect() {
|
|
42
|
+
this.emit("reconnect");
|
|
43
|
+
setTimeout(() => this.simulateConnect(), 10);
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
41
46
|
simulateReconnect() {
|
|
42
47
|
this.emit("reconnect");
|
|
43
48
|
}
|
package/dist/src/channel.js
CHANGED
|
@@ -67,12 +67,12 @@ export const mqttPlugin = {
|
|
|
67
67
|
gateway: {
|
|
68
68
|
startAccount: async (ctx) => {
|
|
69
69
|
const { cfg, account, accountId, abortSignal, log } = ctx;
|
|
70
|
-
const runtime = getMqttRuntime();
|
|
71
70
|
const mqtt = cfg.channels?.mqtt;
|
|
72
71
|
if (!mqtt?.brokerUrl) {
|
|
73
72
|
log?.debug?.("MQTT channel not configured, skipping");
|
|
74
73
|
return;
|
|
75
74
|
}
|
|
75
|
+
const runtime = getMqttRuntime();
|
|
76
76
|
log?.info?.(`[${accountId}] starting MQTT provider (${mqtt.brokerUrl})`);
|
|
77
77
|
// Create and connect client
|
|
78
78
|
mqttClient = createMqttClient(mqtt, {
|
|
@@ -85,8 +85,7 @@ export const mqttPlugin = {
|
|
|
85
85
|
await mqttClient.connect();
|
|
86
86
|
}
|
|
87
87
|
catch (err) {
|
|
88
|
-
log?.error?.(`MQTT connection failed: ${err}`);
|
|
89
|
-
throw err;
|
|
88
|
+
log?.error?.(`MQTT connection failed (will keep retrying): ${err}`);
|
|
90
89
|
}
|
|
91
90
|
// Subscribe to inbound topic
|
|
92
91
|
const inboundTopic = mqtt.topics?.inbound ?? "openclaw/inbound";
|
|
@@ -145,6 +144,7 @@ async function handleInboundMessage(opts) {
|
|
|
145
144
|
// Extract message body and sender from payload
|
|
146
145
|
let messageBody;
|
|
147
146
|
let senderId;
|
|
147
|
+
let correlationId;
|
|
148
148
|
if (parsedPayload && typeof parsedPayload === "object") {
|
|
149
149
|
messageBody =
|
|
150
150
|
parsedPayload.message ??
|
|
@@ -160,6 +160,10 @@ async function handleInboundMessage(opts) {
|
|
|
160
160
|
parsedPayload.from ??
|
|
161
161
|
parsedPayload.service ??
|
|
162
162
|
topic.replace(/\//g, "-");
|
|
163
|
+
correlationId =
|
|
164
|
+
parsedPayload.correlationId ??
|
|
165
|
+
parsedPayload.requestId ??
|
|
166
|
+
undefined;
|
|
163
167
|
}
|
|
164
168
|
else {
|
|
165
169
|
messageBody = text;
|
|
@@ -203,6 +207,7 @@ async function handleInboundMessage(opts) {
|
|
|
203
207
|
text: payload.text,
|
|
204
208
|
kind: info.kind,
|
|
205
209
|
ts: Date.now(),
|
|
210
|
+
...(correlationId ? { correlationId } : {}),
|
|
206
211
|
});
|
|
207
212
|
await mqttClient.publish(outboundTopic, outboundPayload, qos);
|
|
208
213
|
log?.info?.(`MQTT: sent reply to ${outboundTopic}`);
|
package/dist/src/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGrD,MAAM,WAAW,iBAAiB;IAChC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IACxD,WAAW,IAAI,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAEtE,UAAU,MAAM;IACd,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGrD,MAAM,WAAW,iBAAiB;IAChC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IACxD,WAAW,IAAI,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAEtE,UAAU,MAAM;IACd,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAOD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,EAC9B,MAAM,EAAE,MAAM,GACb,iBAAiB,CA4QnB"}
|
package/dist/src/client.js
CHANGED
|
@@ -2,6 +2,8 @@ import mqtt from "mqtt";
|
|
|
2
2
|
import { mergeWithEnv } from "./env.js";
|
|
3
3
|
const DEFAULT_RECONNECT_MS = 5000;
|
|
4
4
|
const MAX_RECONNECT_MS = 60000;
|
|
5
|
+
const INITIAL_CONNECT_GRACE_MS = 5000;
|
|
6
|
+
const RECONNECT_JITTER = 0.2;
|
|
5
7
|
/**
|
|
6
8
|
* MQTT Client Manager
|
|
7
9
|
*
|
|
@@ -12,12 +14,15 @@ export function createMqttClient(rawConfig, logger) {
|
|
|
12
14
|
let client = null;
|
|
13
15
|
let messageHandlers = new Map();
|
|
14
16
|
let reconnectAttempts = 0;
|
|
17
|
+
let reconnectTimer = null;
|
|
18
|
+
let connectPromise = null;
|
|
19
|
+
let manualDisconnect = false;
|
|
15
20
|
function getClientOptions() {
|
|
16
21
|
const options = {
|
|
17
22
|
clientId: config.clientId ?? `openclaw-${Math.random().toString(36).slice(2, 10)}`,
|
|
18
23
|
clean: true,
|
|
19
24
|
connectTimeout: 10000,
|
|
20
|
-
reconnectPeriod:
|
|
25
|
+
reconnectPeriod: 0,
|
|
21
26
|
};
|
|
22
27
|
// Auth
|
|
23
28
|
if (config.username) {
|
|
@@ -36,80 +41,156 @@ export function createMqttClient(rawConfig, logger) {
|
|
|
36
41
|
}
|
|
37
42
|
return options;
|
|
38
43
|
}
|
|
44
|
+
function clearReconnectTimer() {
|
|
45
|
+
if (reconnectTimer) {
|
|
46
|
+
clearTimeout(reconnectTimer);
|
|
47
|
+
reconnectTimer = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function getBackoffDelay(attempt) {
|
|
51
|
+
const base = Math.min(DEFAULT_RECONNECT_MS * Math.pow(2, Math.max(0, attempt - 1)), MAX_RECONNECT_MS);
|
|
52
|
+
const jitter = base * RECONNECT_JITTER * Math.random();
|
|
53
|
+
return Math.round(base + jitter);
|
|
54
|
+
}
|
|
55
|
+
function scheduleReconnect(reason) {
|
|
56
|
+
if (manualDisconnect)
|
|
57
|
+
return;
|
|
58
|
+
if (reconnectTimer)
|
|
59
|
+
return;
|
|
60
|
+
reconnectAttempts += 1;
|
|
61
|
+
const delay = getBackoffDelay(reconnectAttempts);
|
|
62
|
+
logger.warn(`MQTT reconnect scheduled in ${delay}ms (${reason})`);
|
|
63
|
+
reconnectTimer = setTimeout(() => {
|
|
64
|
+
reconnectTimer = null;
|
|
65
|
+
if (manualDisconnect)
|
|
66
|
+
return;
|
|
67
|
+
if (!client) {
|
|
68
|
+
connect().catch((err) => logger.error(`MQTT reconnect failed: ${err}`));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
logger.info("MQTT reconnecting...");
|
|
73
|
+
client.reconnect();
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
logger.error(`MQTT reconnect error: ${err}`);
|
|
77
|
+
scheduleReconnect("reconnect error");
|
|
78
|
+
}
|
|
79
|
+
}, delay);
|
|
80
|
+
}
|
|
81
|
+
function attachClientHandlers(activeClient) {
|
|
82
|
+
activeClient.on("connect", () => {
|
|
83
|
+
logger.info("MQTT connected");
|
|
84
|
+
reconnectAttempts = 0;
|
|
85
|
+
clearReconnectTimer();
|
|
86
|
+
// Resubscribe to all topics
|
|
87
|
+
for (const topic of messageHandlers.keys()) {
|
|
88
|
+
activeClient.subscribe(topic, { qos: config.qos }, (err) => {
|
|
89
|
+
if (err) {
|
|
90
|
+
logger.error(`Failed to subscribe to ${topic}: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
logger.debug(`Subscribed to ${topic}`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
activeClient.on("message", (topic, payload) => {
|
|
99
|
+
logger.debug(`Received message on ${topic}: ${payload.length} bytes`);
|
|
100
|
+
const handlers = [...(messageHandlers.get(topic) ?? [])];
|
|
101
|
+
// Also check wildcard subscriptions (skip exact match to avoid duplicates)
|
|
102
|
+
for (const [pattern, patternHandlers] of messageHandlers) {
|
|
103
|
+
if (pattern === topic)
|
|
104
|
+
continue;
|
|
105
|
+
if (topicMatches(pattern, topic)) {
|
|
106
|
+
handlers.push(...patternHandlers);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const handler of handlers) {
|
|
110
|
+
try {
|
|
111
|
+
handler(topic, payload);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
logger.error(`Message handler error: ${err}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
activeClient.on("error", (err) => {
|
|
119
|
+
logger.error(`MQTT error: ${err.message}`);
|
|
120
|
+
scheduleReconnect("error");
|
|
121
|
+
});
|
|
122
|
+
activeClient.on("close", () => {
|
|
123
|
+
logger.warn("MQTT connection closed");
|
|
124
|
+
scheduleReconnect("close");
|
|
125
|
+
});
|
|
126
|
+
activeClient.on("reconnect", () => {
|
|
127
|
+
logger.info("MQTT reconnect event");
|
|
128
|
+
});
|
|
129
|
+
activeClient.on("offline", () => {
|
|
130
|
+
logger.warn("MQTT client offline");
|
|
131
|
+
scheduleReconnect("offline");
|
|
132
|
+
});
|
|
133
|
+
}
|
|
39
134
|
async function connect() {
|
|
40
135
|
if (client?.connected) {
|
|
41
136
|
logger.debug("MQTT already connected");
|
|
42
137
|
return;
|
|
43
138
|
}
|
|
44
|
-
|
|
139
|
+
if (connectPromise) {
|
|
140
|
+
return connectPromise;
|
|
141
|
+
}
|
|
142
|
+
manualDisconnect = false;
|
|
143
|
+
if (!client) {
|
|
45
144
|
logger.info(`Connecting to MQTT broker: ${config.brokerUrl}`);
|
|
46
145
|
const options = getClientOptions();
|
|
47
146
|
client = mqtt.connect(config.brokerUrl, options);
|
|
48
|
-
client
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
147
|
+
attachClientHandlers(client);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
logger.info("MQTT connect requested; reconnecting existing client");
|
|
151
|
+
try {
|
|
152
|
+
client.reconnect();
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
logger.error(`MQTT reconnect error: ${err}`);
|
|
156
|
+
scheduleReconnect("reconnect error");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
connectPromise = new Promise((resolve) => {
|
|
160
|
+
let settled = false;
|
|
161
|
+
const settle = () => {
|
|
162
|
+
if (settled)
|
|
163
|
+
return;
|
|
164
|
+
settled = true;
|
|
165
|
+
connectPromise = null;
|
|
62
166
|
resolve();
|
|
167
|
+
};
|
|
168
|
+
if (client?.connected) {
|
|
169
|
+
settle();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const timer = setTimeout(() => {
|
|
173
|
+
logger.warn(`MQTT initial connect not ready after ${INITIAL_CONNECT_GRACE_MS}ms; continuing retries in background`);
|
|
174
|
+
settle();
|
|
175
|
+
}, INITIAL_CONNECT_GRACE_MS);
|
|
176
|
+
client?.once("connect", () => {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
settle();
|
|
63
179
|
});
|
|
64
|
-
client.on("message", (topic, payload) => {
|
|
65
|
-
logger.debug(`Received message on ${topic}: ${payload.length} bytes`);
|
|
66
|
-
const handlers = [...(messageHandlers.get(topic) ?? [])];
|
|
67
|
-
// Also check wildcard subscriptions (skip exact match to avoid duplicates)
|
|
68
|
-
for (const [pattern, patternHandlers] of messageHandlers) {
|
|
69
|
-
if (pattern === topic)
|
|
70
|
-
continue;
|
|
71
|
-
if (topicMatches(pattern, topic)) {
|
|
72
|
-
handlers.push(...patternHandlers);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
for (const handler of handlers) {
|
|
76
|
-
try {
|
|
77
|
-
handler(topic, payload);
|
|
78
|
-
}
|
|
79
|
-
catch (err) {
|
|
80
|
-
logger.error(`Message handler error: ${err}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
client.on("error", (err) => {
|
|
85
|
-
logger.error(`MQTT error: ${err.message}`);
|
|
86
|
-
reject(err);
|
|
87
|
-
});
|
|
88
|
-
client.on("close", () => {
|
|
89
|
-
logger.warn("MQTT connection closed");
|
|
90
|
-
});
|
|
91
|
-
client.on("reconnect", () => {
|
|
92
|
-
reconnectAttempts++;
|
|
93
|
-
const backoff = Math.min(DEFAULT_RECONNECT_MS * Math.pow(2, reconnectAttempts), MAX_RECONNECT_MS);
|
|
94
|
-
logger.info(`MQTT reconnecting (attempt ${reconnectAttempts}, backoff ${backoff}ms)`);
|
|
95
|
-
});
|
|
96
|
-
client.on("offline", () => {
|
|
97
|
-
logger.warn("MQTT client offline");
|
|
98
|
-
});
|
|
99
|
-
// Timeout for initial connection
|
|
100
|
-
setTimeout(() => {
|
|
101
|
-
if (!client?.connected) {
|
|
102
|
-
reject(new Error("MQTT connection timeout"));
|
|
103
|
-
}
|
|
104
|
-
}, 15000);
|
|
105
180
|
});
|
|
181
|
+
return connectPromise;
|
|
106
182
|
}
|
|
107
183
|
async function disconnect() {
|
|
108
184
|
if (!client)
|
|
109
185
|
return;
|
|
186
|
+
manualDisconnect = true;
|
|
187
|
+
clearReconnectTimer();
|
|
188
|
+
reconnectAttempts = 0;
|
|
189
|
+
connectPromise = null;
|
|
110
190
|
return new Promise((resolve) => {
|
|
111
191
|
logger.info("Disconnecting from MQTT broker");
|
|
112
192
|
client?.end(false, {}, () => {
|
|
193
|
+
client?.removeAllListeners();
|
|
113
194
|
client = null;
|
|
114
195
|
messageHandlers.clear();
|
|
115
196
|
logger.info("MQTT disconnected");
|