@machhub-dev/node-red-nodes 1.0.9-test → 1.0.11-test

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.
@@ -15,11 +15,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.MQTTService = void 0;
16
16
  const mqtt_1 = __importDefault(require("mqtt"));
17
17
  class MQTTService {
18
+ constructor() {
19
+ this.subscribedTopics = [];
20
+ }
18
21
  static getInstance(settings, statusCallback) {
19
22
  return __awaiter(this, void 0, void 0, function* () {
20
23
  // If we already have an instance, return it
21
24
  if (this.instance && this.instance.client && this.instance.client.connected) {
22
- console.log("Returning existing MQTT instance");
23
25
  if (statusCallback) {
24
26
  statusCallback({ fill: "green", shape: "dot", text: "MQTT Connected" });
25
27
  }
@@ -39,6 +41,7 @@ class MQTTService {
39
41
  this.instancePromise = this.createInstance(settings, statusCallback);
40
42
  try {
41
43
  this.instance = yield this.instancePromise;
44
+ this.instancePromise = null; // Clear so future calls re-evaluate fresh state
42
45
  return this.instance;
43
46
  }
44
47
  catch (error) {
@@ -106,17 +109,8 @@ class MQTTService {
106
109
  clientId: settings.clientId,
107
110
  username: settings.clientId,
108
111
  password: settings.clientSecret,
109
- reconnectPeriod: 0, // disable MQTT.js auto-reconnect; we manage reconnection via getInstance()
110
112
  });
111
113
  instance.attachMessageListener();
112
- // Re-subscribe any topics registered before this connection was established
113
- for (const sub of MQTTService.subscribedTopics) {
114
- instance.client.subscribe(sub.topic, { qos: 2 }, (err) => {
115
- if (err) {
116
- console.error(`Failed to re-subscribe to topic ${sub.topic}:`, err);
117
- }
118
- });
119
- }
120
114
  // console.log("MQTT connection established successfully");
121
115
  if (statusCallback) {
122
116
  statusCallback({ fill: "green", shape: "dot", text: "MQTT Connected" });
@@ -134,22 +128,19 @@ class MQTTService {
134
128
  return instance;
135
129
  });
136
130
  }
137
- // Method to reset the instance and all state (full shutdown / redeploy)
131
+ // Method to reset the instance
138
132
  static resetInstance() {
139
- const inst = this.instance;
140
- // Clear references first so the 'close' event handler guard doesn't re-null unnecessarily
133
+ if (this.instance && this.instance.client) {
134
+ this.instance.client.end();
135
+ }
141
136
  this.instance = null;
142
137
  this.instancePromise = null;
143
138
  this.stopConnecting = true;
144
- this.subscribedTopics = [];
145
- if (inst && inst.client) {
146
- inst.client.end();
147
- }
148
139
  }
149
140
  // Adds a topic and handler to the subscribed list
150
141
  addTopicHandler(topic, handler) {
151
142
  try {
152
- MQTTService.subscribedTopics.push({ topic, handler });
143
+ this.subscribedTopics.push({ topic, handler });
153
144
  // console.log("New Subscription Handler : ", topic)
154
145
  this.client.subscribe(topic, { qos: 2 }, (err) => {
155
146
  if (err) {
@@ -161,30 +152,13 @@ class MQTTService {
161
152
  console.error(`Failed to subscribe to topic ${topic}:`, e);
162
153
  }
163
154
  }
164
- // Removes a topic handler and unsubscribes from the broker if no other handler covers the same topic
165
- removeTopicHandler(topic, handler) {
166
- MQTTService.subscribedTopics = MQTTService.subscribedTopics.filter(st => !(st.topic === topic && st.handler === handler));
167
- const stillSubscribed = MQTTService.subscribedTopics.some(st => st.topic === topic);
168
- if (!stillSubscribed) {
169
- try {
170
- this.client.unsubscribe(topic, (err) => {
171
- if (err) {
172
- console.error(`Failed to unsubscribe from topic ${topic}:`, err);
173
- }
174
- });
175
- }
176
- catch (e) {
177
- console.error(`Failed to unsubscribe from topic ${topic}:`, e);
178
- }
179
- }
180
- }
181
155
  // Publishes a message to a specific topic
182
- publish(topic, message) {
156
+ publish(topic, message, qos = 2) {
183
157
  try {
184
158
  const payload = JSON.stringify(message);
185
159
  // console.log("Publishing to ", topic, ", with payload : ", payload)
186
160
  this.client.publish(topic, payload, {
187
- qos: 2, retain: true,
161
+ qos: qos, retain: true,
188
162
  properties: {
189
163
  contentType: "json",
190
164
  }
@@ -202,7 +176,7 @@ class MQTTService {
202
176
  }
203
177
  attachMessageListener() {
204
178
  this.client.on('message', (topic, message, packet) => {
205
- for (const subscribedTopic of MQTTService.subscribedTopics) {
179
+ for (const subscribedTopic of this.subscribedTopics) {
206
180
  if (this.matchesTopic(subscribedTopic.topic, topic)) {
207
181
  const parsedMessage = this.parseMessage(message, topic);
208
182
  subscribedTopic.handler(parsedMessage, packet.retain);
@@ -210,14 +184,6 @@ class MQTTService {
210
184
  }
211
185
  }
212
186
  });
213
- // When the broker closes the connection (e.g. session expired), self-reset so the
214
- // next getInstance() call creates a fresh client with an empty in-flight store.
215
- this.client.on('close', () => {
216
- if (MQTTService.instance === this) {
217
- MQTTService.instance = null;
218
- MQTTService.instancePromise = null;
219
- }
220
- });
221
187
  }
222
188
  // Matches MQTT topic patterns with wildcards (+ for single level, # for multi-level)
223
189
  matchesTopic(pattern, topic) {
@@ -266,4 +232,3 @@ class MQTTService {
266
232
  exports.MQTTService = MQTTService;
267
233
  MQTTService.instancePromise = null;
268
234
  MQTTService.stopConnecting = false;
269
- MQTTService.subscribedTopics = [];
@@ -4,6 +4,7 @@
4
4
  color: '#c777f1',
5
5
  defaults: {
6
6
  name: { value: "" },
7
+ qos: { value: 2 },
7
8
  useConfigNode: { value: false }, // Toggle between JSON file and config node
8
9
  machhub: {
9
10
  value: "",
@@ -55,11 +56,16 @@
55
56
  } else {
56
57
  $machhub.val(node.machhub);
57
58
  }
59
+
60
+ // Sync QoS dropdown to saved value
61
+ const savedQos = node.qos !== undefined ? node.qos : 2;
62
+ $("#node-input-qos").val(String(savedQos));
58
63
  },
59
64
 
60
65
  oneditsave: function () {
61
66
  this.useConfigNode = $("#node-input-useConfigNode").is(":checked");
62
67
  this.machhub = $("#node-input-machhub").val();
68
+ this.qos = parseInt(String($("#node-input-qos").val()), 10);
63
69
  }
64
70
  });
65
71
  </script>
@@ -69,6 +75,14 @@
69
75
  <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
70
76
  <input type="text" id="node-input-name" placeholder="Name">
71
77
  </div>
78
+ <div class="form-row">
79
+ <label for="node-input-qos"><i class="fa fa-signal"></i> QoS</label>
80
+ <select id="node-input-qos" style="width: auto;">
81
+ <option value="0">0 - At most once</option>
82
+ <option value="1">1 - At least once</option>
83
+ <option value="2" selected>2 - Exactly once</option>
84
+ </select>
85
+ </div>
72
86
  <div class="form-row">
73
87
  <label for="node-input-useConfigNode" style="width: auto; padding-right: 10px;">
74
88
  <i class="fa fa-cog"></i> Use Config Node
@@ -20,6 +20,7 @@ function default_1(RED) {
20
20
  let isInitializing = false;
21
21
  let initializationPromise = null;
22
22
  node.useConfigNode = config.useConfigNode || false;
23
+ node.qos = (config.qos !== undefined && config.qos !== null) ? Number(config.qos) : 2;
23
24
  // Determine configuration source
24
25
  let machhubConfig;
25
26
  if (node.useConfigNode) {
@@ -56,6 +57,7 @@ function default_1(RED) {
56
57
  }
57
58
  }
58
59
  node.on('close', () => {
60
+ mqtt_service_1.MQTTService.resetInstance();
59
61
  mqttService = null;
60
62
  initializationPromise = null;
61
63
  });
@@ -63,13 +65,18 @@ function default_1(RED) {
63
65
  return __awaiter(this, void 0, void 0, function* () {
64
66
  try {
65
67
  node.status({ fill: "orange", shape: "dot", text: "Sending Payload" });
66
- // Always fetch the current instance so we recover transparently after reconnects
67
- const service = yield mqtt_service_1.MQTTService.getInstance(machhubConfig, (status) => {
68
- node.status(status);
69
- });
68
+ // Wait for initialization if still in progress
69
+ if (isInitializing && initializationPromise) {
70
+ yield initializationPromise;
71
+ }
72
+ // Check if mqttService is initialized
73
+ if (!mqttService) {
74
+ node.status({ fill: "red", shape: "dot", text: "MQTT service not initialized" });
75
+ return;
76
+ }
70
77
  Object.keys(msg.payload).forEach(function (key) {
71
78
  try {
72
- service.publish(key, msg.payload[key]);
79
+ mqttService.publish(key, msg.payload[key], node.qos);
73
80
  }
74
81
  catch (error) {
75
82
  node.status({ fill: "red", shape: "dot", text: "Failed to publish to '" + key + "': " + error.toString() });
@@ -88,16 +95,9 @@ function default_1(RED) {
88
95
  isInitializing = true;
89
96
  node.status({ fill: "yellow", shape: "dot", text: "Connecting..." });
90
97
  // Use MACHHUB config to get MQTT service with status callback
91
- while (true) {
92
- // console.log("Getting MQTT instance");
93
- mqttService = yield mqtt_service_1.MQTTService.getInstance(machhubConfig, (status) => {
94
- node.status(status);
95
- });
96
- // console.log("MQTT Service obtained client:", mqttService.client);
97
- if (mqttService.client) {
98
- break;
99
- }
100
- }
98
+ mqttService = yield mqtt_service_1.MQTTService.getInstance(machhubConfig, (status) => {
99
+ node.status(status);
100
+ });
101
101
  node.status({ fill: "green", shape: "dot", text: "Ready to publish" });
102
102
  }
103
103
  catch (error) {
@@ -61,36 +61,22 @@ function default_1(RED) {
61
61
  isRetained: false,
62
62
  payload: undefined
63
63
  };
64
- let mqttService = null;
65
- let topicHandler = null;
66
64
  node.on('close', function () {
67
- if (topicHandler && mqttService) {
68
- mqttService.removeTopicHandler(node.selectedTag, topicHandler);
69
- }
70
- mqttService = null;
71
- topicHandler = null;
65
+ mqtt_service_1.MQTTService.resetInstance();
72
66
  });
73
67
  (() => __awaiter(this, void 0, void 0, function* () {
74
68
  try {
75
69
  node.status({ fill: "yellow", shape: "dot", text: "Connecting..." });
76
70
  // Use MACHHUB config from machhub.env.json to get MQTT service with status callback
77
- while (true) {
78
- // console.log("Getting MQTT instance");
79
- mqttService = yield mqtt_service_1.MQTTService.getInstance(machhubConfig, (status) => {
80
- node.status(status);
81
- });
82
- // console.log("MQTT Service obtained client:", mqttService.client);
83
- if (mqttService.client) {
84
- break;
85
- }
86
- }
87
- topicHandler = (message, isRetained) => {
71
+ const mqttService = yield mqtt_service_1.MQTTService.getInstance(machhubConfig, (status) => {
72
+ node.status(status);
73
+ });
74
+ mqttService.addTopicHandler(node.selectedTag, (message, isRetained) => {
88
75
  msg.payload = message;
89
76
  msg.isRetained = isRetained;
90
77
  node.send(msg);
91
78
  node.status({ fill: "green", shape: "dot", text: "Payload Received" });
92
- };
93
- mqttService.addTopicHandler(node.selectedTag, topicHandler);
79
+ });
94
80
  node.status({ fill: "green", shape: "dot", text: "MQTT Listening" });
95
81
  }
96
82
  catch (error) {
@@ -5,6 +5,7 @@
5
5
  defaults: {
6
6
  name: { value: "" },
7
7
  selectedTag: { value: "", required: true }, // Store selected tag
8
+ qos: { value: 2 },
8
9
  useConfigNode: { value: false }, // Toggle between JSON file and config node
9
10
  machhub: { value: "", type: "machhub-config" }
10
11
  },
@@ -50,6 +51,10 @@
50
51
  $machhub.val(node.machhub);
51
52
  }
52
53
 
54
+ // Sync QoS dropdown to saved value
55
+ const savedQos = node.qos !== undefined ? node.qos : 2;
56
+ $("#node-input-qos").val(String(savedQos));
57
+
53
58
  async function fetchTags() {
54
59
  try {
55
60
  // Get the selected machhub config
@@ -133,6 +138,7 @@
133
138
  this.useConfigNode = $("#node-input-useConfigNode").is(":checked");
134
139
  this.machhub = $("#node-input-machhub").val();
135
140
  this.selectedTag = $("#node-input-selectedTag").val(); // Save selected value
141
+ this.qos = parseInt(String($("#node-input-qos").val()), 10);
136
142
  }
137
143
  });
138
144
  </script>
@@ -147,6 +153,14 @@
147
153
  <input type="text" id="node-input-selectedTag" list="node-input-selectedTag-datalist" placeholder="Start typing or select a tag...">
148
154
  <datalist id="node-input-selectedTag-datalist"></datalist>
149
155
  </div>
156
+ <div class="form-row">
157
+ <label for="node-input-qos"><i class="fa fa-signal"></i> QoS</label>
158
+ <select id="node-input-qos" style="width: auto;">
159
+ <option value="0">0 - At most once</option>
160
+ <option value="1">1 - At least once</option>
161
+ <option value="2" selected>2 - Exactly once</option>
162
+ </select>
163
+ </div>
150
164
  <div class="form-row">
151
165
  <label for="node-input-useConfigNode" style="width: auto; padding-right: 10px;">
152
166
  <i class="fa fa-cog"></i> Use Config Node
@@ -22,6 +22,7 @@ function default_1(RED) {
22
22
  const node = this;
23
23
  node.name = config.name;
24
24
  node.selectedTag = config.selectedTag;
25
+ node.qos = (config.qos !== undefined && config.qos !== null) ? Number(config.qos) : 2;
25
26
  node.useConfigNode = config.useConfigNode || false;
26
27
  let machhubConfig;
27
28
  if (node.useConfigNode) {
@@ -58,17 +59,23 @@ function default_1(RED) {
58
59
  }
59
60
  }
60
61
  node.on('close', () => {
62
+ mqtt_service_1.MQTTService.resetInstance();
61
63
  mqttService = null;
62
64
  initializationPromise = null;
63
65
  });
64
66
  node.on('input', (msg) => __awaiter(this, void 0, void 0, function* () {
65
67
  try {
66
68
  node.status({ fill: "orange", shape: "dot", text: "Sending Payload" });
67
- // Always fetch the current instance so we recover transparently after reconnects
68
- const service = yield mqtt_service_1.MQTTService.getInstance(machhubConfig, (status) => {
69
- node.status(status);
70
- });
71
- yield service.publish(node.selectedTag, msg.payload);
69
+ // Wait for initialization if still in progress
70
+ if (isInitializing && initializationPromise) {
71
+ yield initializationPromise;
72
+ }
73
+ // Check if mqttService is initialized
74
+ if (!mqttService) {
75
+ node.status({ fill: "red", shape: "dot", text: "MQTT service not initialized" });
76
+ return;
77
+ }
78
+ yield mqttService.publish(node.selectedTag, msg.payload, node.qos);
72
79
  node.status({ fill: "green", shape: "dot", text: "Payload Sent" });
73
80
  }
74
81
  catch (err) {
@@ -80,16 +87,9 @@ function default_1(RED) {
80
87
  try {
81
88
  isInitializing = true;
82
89
  node.status({ fill: "yellow", shape: "dot", text: "Connecting..." });
83
- while (true) {
84
- // console.log("Getting MQTT instance");
85
- mqttService = yield mqtt_service_1.MQTTService.getInstance(machhubConfig, (status) => {
86
- node.status(status);
87
- });
88
- // console.log("MQTT Service obtained client:", mqttService.client);
89
- if (mqttService.client) {
90
- break;
91
- }
92
- }
90
+ mqttService = yield mqtt_service_1.MQTTService.getInstance(machhubConfig, (status) => {
91
+ node.status(status);
92
+ });
93
93
  node.status({ fill: "green", shape: "dot", text: "Ready to publish" });
94
94
  }
95
95
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@machhub-dev/node-red-nodes",
3
- "version": "1.0.9-test",
3
+ "version": "1.0.11-test",
4
4
  "description": "Node-RED API for MACHHUB EDGE",
5
5
  "scripts": {
6
6
  "build": "yarn clean && npx tsc && yarn copy-files",