@turquoisebay/mqtt 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ 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.14] - 2026-02-03
9
+
10
+ ### Fixed
11
+ - Robust reconnection logic (handles broker down at startup and restarts)
12
+ - Clean reconnect scheduling + shutdown cleanup
13
+
8
14
  ## [0.1.13] - 2026-02-03
9
15
 
10
16
  ### 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)
@@ -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
  }
@@ -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";
@@ -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;AAKD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,EAC9B,MAAM,EAAE,MAAM,GACb,iBAAiB,CAoLnB"}
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"}
@@ -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: DEFAULT_RECONNECT_MS,
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
- return new Promise((resolve, reject) => {
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.on("connect", () => {
49
- logger.info("MQTT connected");
50
- reconnectAttempts = 0;
51
- // Resubscribe to all topics
52
- for (const topic of messageHandlers.keys()) {
53
- client?.subscribe(topic, { qos: config.qos }, (err) => {
54
- if (err) {
55
- logger.error(`Failed to subscribe to ${topic}: ${err.message}`);
56
- }
57
- else {
58
- logger.debug(`Subscribed to ${topic}`);
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turquoisebay/mqtt",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "MQTT channel plugin for OpenClaw - bidirectional messaging via MQTT brokers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",