node-red-contrib-aedes 1.1.0 → 1.2.0
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 +4 -0
- package/README.md +3 -2
- package/aedes.html +15 -0
- package/aedes.js +296 -103
- package/locales/de/aedes.json +5 -1
- package/locales/en-US/aedes.json +5 -1
- package/package.json +1 -1
- package/test/aedes_persist_spec.js +777 -0
- package/test/aedes_qos_spec.js +5 -12
- package/test/aedes_retain_spec.js +5 -138
- package/test/aedes_spec.js +3 -6
- package/test/aedes_ws_spec.js +4 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# node-red-contrib-aedes Changelog
|
|
2
2
|
|
|
3
|
+
## Feb 19, 2026, Version 1.2.0
|
|
4
|
+
### Notable changes
|
|
5
|
+
- Added file-based snapshot persistence with automatic recovery on Node-RED restart
|
|
6
|
+
|
|
3
7
|
## Feb 15, 2026, Version 1.1.0
|
|
4
8
|
### Notable changes
|
|
5
9
|
- Improved help documentation
|
package/README.md
CHANGED
|
@@ -31,7 +31,8 @@ Just put this node on Node-RED and hit the deploy button. The MQTT Broker will r
|
|
|
31
31
|
- WebSocket Support via port or path
|
|
32
32
|
- SSL / TLS
|
|
33
33
|
- Message Persistence (In-memory or MongoDB)
|
|
34
|
-
|
|
34
|
+
- File-based snapshot persistence
|
|
35
|
+
|
|
35
36
|
For more information see [Aedes](https://github.com/moscajs/aedes/blob/master/README.md#features).
|
|
36
37
|
|
|
37
38
|
## Server without public IP or behind firewall
|
|
@@ -45,7 +46,7 @@ You can also bind the WebSocket to the root `"/"` path and having `wss://yourser
|
|
|
45
46
|
|
|
46
47
|
The current version is based on **Aedes version 1.0**, which introduces several breaking changes. If your environment requires Aedes version 0.51, you can switch to version 0.15.x of this package.
|
|
47
48
|
|
|
48
|
-
To install the compatible version using the `version-
|
|
49
|
+
To install the compatible version using the `version-15` dist-tag:
|
|
49
50
|
```sh
|
|
50
51
|
npm install node-red-contrib-aedes@version-15
|
|
51
52
|
```
|
package/aedes.html
CHANGED
|
@@ -136,6 +136,13 @@
|
|
|
136
136
|
data-i18n="aedes-mqtt-broker.label.dburl"></span></label>
|
|
137
137
|
<input type="text" id="node-input-dburl" data-i18n="[placeholder]aedes-mqtt-broker.placeholder.dburl">
|
|
138
138
|
</div>
|
|
139
|
+
<div class="form-row hide" id="node-row-persist-to-file">
|
|
140
|
+
<input type="checkbox" id="node-input-persist_to_file"
|
|
141
|
+
style="display: inline-block; width: auto; vertical-align: top;">
|
|
142
|
+
<label for="node-input-persist_to_file" style="width: auto"
|
|
143
|
+
data-i18n="aedes-mqtt-broker.label.persist_to_file"></label>
|
|
144
|
+
<div class="form-tips" data-i18n="aedes-mqtt-broker.tip.persist_to_file" style="margin-top: 4px;"></div>
|
|
145
|
+
</div>
|
|
139
146
|
</div>
|
|
140
147
|
<div id="aedes-broker-tab-security" style="display:none">
|
|
141
148
|
<div class="form-row">
|
|
@@ -195,6 +202,7 @@
|
|
|
195
202
|
caname: {value:""},
|
|
196
203
|
persistence_bind: { value: 'memory', required: true },
|
|
197
204
|
dburl: { value: '', required: false },
|
|
205
|
+
persist_to_file: { value: false },
|
|
198
206
|
usetls: { value: false }
|
|
199
207
|
},
|
|
200
208
|
credentials: {
|
|
@@ -264,31 +272,38 @@
|
|
|
264
272
|
|
|
265
273
|
const persistenceInput = $('#node-input-persistence_bind');
|
|
266
274
|
const dburlRow = $('#node-row-dburl');
|
|
275
|
+
const persistFileRow = $('#node-row-persist-to-file');
|
|
267
276
|
const dburl = this.dburl;
|
|
268
277
|
persistenceInput.on('change', function () {
|
|
269
278
|
switch ($(this).val()) {
|
|
270
279
|
case 'memory':
|
|
271
280
|
dburlRow.hide();
|
|
281
|
+
persistFileRow.show();
|
|
272
282
|
break;
|
|
273
283
|
case 'mongodb':
|
|
274
284
|
dburlRow.show();
|
|
285
|
+
persistFileRow.hide();
|
|
275
286
|
break;
|
|
276
287
|
case 'level':
|
|
277
288
|
dburlRow.hide();
|
|
289
|
+
persistFileRow.hide();
|
|
278
290
|
break;
|
|
279
291
|
case null:
|
|
280
292
|
if (dburl) {
|
|
281
293
|
this.persistence_bind = 'mongodb';
|
|
282
294
|
persistenceInput.val('mongodb');
|
|
283
295
|
dburlRow.show();
|
|
296
|
+
persistFileRow.hide();
|
|
284
297
|
} else {
|
|
285
298
|
this.persistence_bind = 'memory';
|
|
286
299
|
persistenceInput.val('memory');
|
|
287
300
|
dburlRow.hide();
|
|
301
|
+
persistFileRow.show();
|
|
288
302
|
}
|
|
289
303
|
break;
|
|
290
304
|
}
|
|
291
305
|
});
|
|
306
|
+
persistenceInput.trigger('change');
|
|
292
307
|
|
|
293
308
|
if (typeof this.usetls === 'undefined') {
|
|
294
309
|
this.usetls = false;
|
package/aedes.js
CHANGED
|
@@ -17,10 +17,8 @@
|
|
|
17
17
|
module.exports = function (RED) {
|
|
18
18
|
'use strict';
|
|
19
19
|
const MongoPersistence = require('aedes-persistence-mongodb');
|
|
20
|
-
// const { Level } = require('level');
|
|
21
|
-
// const LevelPersistence = require('aedes-persistence-level');
|
|
22
|
-
// aedes is ESM-only in v1 -- dynamically imported in initializeBroker()
|
|
23
20
|
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
24
22
|
const net = require('net');
|
|
25
23
|
const tls = require('tls');
|
|
26
24
|
const http = require('http');
|
|
@@ -28,6 +26,8 @@ module.exports = function (RED) {
|
|
|
28
26
|
const { WebSocketServer, createWebSocketStream } = require('ws');
|
|
29
27
|
|
|
30
28
|
let serverUpgradeAdded = false;
|
|
29
|
+
let wsPathNodeCount = 0;
|
|
30
|
+
let boundHandleServerUpgrade = null;
|
|
31
31
|
const listenerNodes = {};
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -40,91 +40,179 @@ module.exports = function (RED) {
|
|
|
40
40
|
function handleServerUpgrade (request, socket, head) {
|
|
41
41
|
const pathname = new URL(request.url, 'http://example.org').pathname;
|
|
42
42
|
if (Object.prototype.hasOwnProperty.call(listenerNodes, pathname)) {
|
|
43
|
-
listenerNodes[pathname].
|
|
43
|
+
listenerNodes[pathname]._wsPathServer.handleUpgrade(
|
|
44
44
|
request,
|
|
45
45
|
socket,
|
|
46
46
|
head,
|
|
47
47
|
function done (conn) {
|
|
48
|
-
listenerNodes[pathname].
|
|
48
|
+
listenerNodes[pathname]._wsPathServer.emit('connection', conn, request);
|
|
49
49
|
}
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
function checkWritable (dirPath, node) {
|
|
55
|
+
try {
|
|
56
|
+
fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
57
|
+
return true;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
node.warn('aedes: userDir is not writable (' + dirPath + ') – file persistence disabled: ' + err.message);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readSnapshotSync (filePath, node) {
|
|
65
|
+
if (!fs.existsSync(filePath)) {
|
|
66
|
+
node.debug('aedes: no snapshot found at ' + filePath);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
72
|
+
} catch (readErr) {
|
|
73
|
+
node.warn(
|
|
74
|
+
'aedes: could not read snapshot, starting with empty state: ' +
|
|
75
|
+
readErr.message
|
|
76
|
+
);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let data;
|
|
81
|
+
try {
|
|
82
|
+
data = JSON.parse(raw);
|
|
83
|
+
} catch (parseErr) {
|
|
84
|
+
node.warn(
|
|
85
|
+
'aedes: snapshot file is corrupt, starting with empty state: ' +
|
|
86
|
+
parseErr.message
|
|
87
|
+
);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
92
|
+
node.warn(
|
|
93
|
+
'aedes: snapshot file has unexpected format, starting with empty state'
|
|
94
|
+
);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return data;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function restoreRetained (broker, data, node) {
|
|
101
|
+
if (!data) {
|
|
102
|
+
node.debug('aedes: no snapshot data to restore');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
node.debug('aedes: restoring snapshot - retained messages: ' + Object.keys(data.retained || {}).length);
|
|
106
|
+
if (data.retained && typeof data.retained === 'object') {
|
|
107
|
+
const topics = Object.keys(data.retained);
|
|
108
|
+
try {
|
|
109
|
+
for (let i = 0; i < topics.length; i++) {
|
|
110
|
+
const packet = data.retained[topics[i]];
|
|
111
|
+
if (!packet.topic) continue;
|
|
112
|
+
await broker.persistence.storeRetained({
|
|
113
|
+
topic: packet.topic,
|
|
114
|
+
payload: Buffer.from(packet.payload || '', 'base64'),
|
|
115
|
+
qos: packet.qos || 0,
|
|
116
|
+
retain: true,
|
|
117
|
+
cmd: 'publish'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
node.warn('aedes: failed to restore retained messages from snapshot: ' + err.message);
|
|
122
|
+
}
|
|
123
|
+
node.debug('aedes: snapshot restore complete');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function saveSnapshot (broker, filePath, node) {
|
|
128
|
+
try {
|
|
129
|
+
node.debug('aedes: saving snapshot to ' + filePath);
|
|
130
|
+
// 1. Collect retained messages via public stream API
|
|
131
|
+
const retained = {};
|
|
132
|
+
const stream = broker.persistence.createRetainedStreamCombi(['#']);
|
|
133
|
+
node.debug('aedes: snapshot - collecting retained messages');
|
|
134
|
+
node.debug('aedes: snapshot - stream is readable: ' + stream.readable);
|
|
135
|
+
for await (const packet of stream) {
|
|
136
|
+
node.debug('aedes: snapshot - processing retained message: ' + packet.topic);
|
|
137
|
+
if (!packet.payload || packet.payload.length === 0) continue;
|
|
138
|
+
retained[packet.topic] = {
|
|
139
|
+
topic: packet.topic,
|
|
140
|
+
payload: Buffer.from(packet.payload).toString('base64'),
|
|
141
|
+
qos: packet.qos,
|
|
142
|
+
retain: true,
|
|
143
|
+
cmd: 'publish'
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 2. Atomic write: temp file + rename
|
|
148
|
+
const tmpFile = filePath + '.tmp';
|
|
149
|
+
await fs.promises.writeFile(
|
|
150
|
+
tmpFile,
|
|
151
|
+
JSON.stringify({ retained }, null, 2),
|
|
152
|
+
'utf8'
|
|
153
|
+
);
|
|
154
|
+
await fs.promises.rename(tmpFile, filePath);
|
|
155
|
+
node.debug('aedes: snapshot saved to ' + filePath);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
node.warn('aedes: could not save snapshot: ' + err.message);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function createBroker (node, config, aedesSettings, serverOptions) {
|
|
55
162
|
const { Aedes } = await import('aedes');
|
|
56
163
|
const broker = await Aedes.createBroker(aedesSettings);
|
|
57
164
|
if (node._closing) { broker.close(); return; }
|
|
58
165
|
node._broker = broker;
|
|
59
166
|
|
|
167
|
+
if (config.persistence_bind !== 'mongodb' && config.persist_to_file === true) {
|
|
168
|
+
const persistFile = path.join(RED.settings.userDir, 'aedes-persist-' + node.id + '.json');
|
|
169
|
+
node._persistFile = persistFile;
|
|
170
|
+
|
|
171
|
+
if (checkWritable(RED.settings.userDir, node)) {
|
|
172
|
+
node._persistEnabled = true;
|
|
173
|
+
|
|
174
|
+
// Restore retained messages from snapshot data (already read synchronously in constructor)
|
|
175
|
+
if (node._snapshotData) {
|
|
176
|
+
await restoreRetained(broker, node._snapshotData, node);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Periodic save every 60 seconds (with guard against concurrent saves)
|
|
180
|
+
node._saving = false;
|
|
181
|
+
node._snapshotInterval = setInterval(function () {
|
|
182
|
+
if (node._saving) return;
|
|
183
|
+
node._saving = true;
|
|
184
|
+
saveSnapshot(node._broker, persistFile, node)
|
|
185
|
+
.finally(function () { node._saving = false; });
|
|
186
|
+
}, 60000);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
60
190
|
let server;
|
|
61
191
|
if (node.usetls) {
|
|
62
192
|
server = tls.createServer(serverOptions, broker.handle);
|
|
63
193
|
} else {
|
|
64
194
|
server = net.createServer(broker.handle);
|
|
65
195
|
}
|
|
66
|
-
node.
|
|
67
|
-
|
|
68
|
-
if (node.mqtt_ws_port) {
|
|
69
|
-
// Awkward check since http or ws do not fire an error event in case the port is in use
|
|
70
|
-
const testServer = net.createServer();
|
|
71
|
-
testServer.once('error', function (err) {
|
|
72
|
-
if (err.code === 'EADDRINUSE') {
|
|
73
|
-
node.error(
|
|
74
|
-
RED._('aedes-mqtt-broker.error.port-in-use', { port: config.mqtt_ws_port })
|
|
75
|
-
);
|
|
76
|
-
} else {
|
|
77
|
-
node.error(
|
|
78
|
-
RED._('aedes-mqtt-broker.error.server-error', { port: config.mqtt_ws_port, error: err.toString() })
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
node.status({ fill: 'red', shape: 'ring', text: 'aedes-mqtt-broker.status.error' });
|
|
82
|
-
});
|
|
83
|
-
testServer.once('listening', function () {
|
|
84
|
-
testServer.close();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
testServer.once('close', function () {
|
|
88
|
-
let httpServer;
|
|
89
|
-
if (node.usetls) {
|
|
90
|
-
httpServer = https.createServer(serverOptions);
|
|
91
|
-
} else {
|
|
92
|
-
httpServer = http.createServer();
|
|
93
|
-
}
|
|
94
|
-
node._httpServer = httpServer;
|
|
95
|
-
const wss = new WebSocketServer({ server: httpServer });
|
|
96
|
-
wss.on('connection', function (websocket, req) {
|
|
97
|
-
const stream = createWebSocketStream(websocket);
|
|
98
|
-
broker.handle(stream, req);
|
|
99
|
-
});
|
|
100
|
-
node._wss = wss;
|
|
101
|
-
httpServer.listen(config.mqtt_ws_port, function () {
|
|
102
|
-
node.log(
|
|
103
|
-
'Binding aedes mqtt server on ws port: ' + config.mqtt_ws_port
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
testServer.listen(config.mqtt_ws_port, function () {
|
|
108
|
-
node.log('Checking ws port: ' + config.mqtt_ws_port);
|
|
109
|
-
});
|
|
110
|
-
}
|
|
196
|
+
node._netServer = server;
|
|
111
197
|
|
|
112
198
|
if (node.mqtt_ws_path !== '') {
|
|
113
199
|
if (!serverUpgradeAdded) {
|
|
114
|
-
|
|
200
|
+
boundHandleServerUpgrade = handleServerUpgrade;
|
|
201
|
+
RED.server.on('upgrade', boundHandleServerUpgrade);
|
|
115
202
|
serverUpgradeAdded = true;
|
|
116
203
|
}
|
|
204
|
+
wsPathNodeCount++;
|
|
117
205
|
|
|
118
|
-
let
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
(
|
|
206
|
+
let pathStr = RED.settings.httpNodeRoot || '/';
|
|
207
|
+
pathStr =
|
|
208
|
+
pathStr +
|
|
209
|
+
(pathStr.slice(-1) === '/' ? '' : '/') +
|
|
122
210
|
(node.mqtt_ws_path.charAt(0) === '/'
|
|
123
211
|
? node.mqtt_ws_path.substring(1)
|
|
124
212
|
: node.mqtt_ws_path);
|
|
125
|
-
node.fullPath =
|
|
213
|
+
node.fullPath = pathStr;
|
|
126
214
|
|
|
127
|
-
if (Object.prototype.hasOwnProperty.call(listenerNodes,
|
|
215
|
+
if (Object.prototype.hasOwnProperty.call(listenerNodes, pathStr)) {
|
|
128
216
|
node.error(
|
|
129
217
|
RED._('websocket.errors.duplicate-path', { path: node.mqtt_ws_path })
|
|
130
218
|
);
|
|
@@ -137,8 +225,8 @@ module.exports = function (RED) {
|
|
|
137
225
|
serverOptions_.verifyClient = RED.settings.webSocketNodeVerifyClient;
|
|
138
226
|
}
|
|
139
227
|
|
|
140
|
-
node.
|
|
141
|
-
node.
|
|
228
|
+
node._wsPathServer = new WebSocketServer(serverOptions_);
|
|
229
|
+
node._wsPathServer.on('connection', function (websocket, req) {
|
|
142
230
|
const stream = createWebSocketStream(websocket);
|
|
143
231
|
broker.handle(stream, req);
|
|
144
232
|
});
|
|
@@ -150,11 +238,11 @@ module.exports = function (RED) {
|
|
|
150
238
|
server.once('error', function (err) {
|
|
151
239
|
if (err.code === 'EADDRINUSE') {
|
|
152
240
|
node.error(
|
|
153
|
-
RED._('aedes-mqtt-broker.error.port-in-use', { port:
|
|
241
|
+
RED._('aedes-mqtt-broker.error.port-in-use', { port: node.mqtt_port })
|
|
154
242
|
);
|
|
155
243
|
} else {
|
|
156
244
|
node.error(
|
|
157
|
-
RED._('aedes-mqtt-broker.error.server-error', { port:
|
|
245
|
+
RED._('aedes-mqtt-broker.error.server-error', { port: node.mqtt_port, error: err.toString() })
|
|
158
246
|
);
|
|
159
247
|
}
|
|
160
248
|
node.status({
|
|
@@ -164,17 +252,7 @@ module.exports = function (RED) {
|
|
|
164
252
|
});
|
|
165
253
|
});
|
|
166
254
|
|
|
167
|
-
|
|
168
|
-
server.listen(node.mqtt_port, function () {
|
|
169
|
-
node.log('Binding aedes mqtt server on port: ' + config.mqtt_port);
|
|
170
|
-
node.status({
|
|
171
|
-
fill: 'green',
|
|
172
|
-
shape: 'dot',
|
|
173
|
-
text: 'node-red:common.status.connected'
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
255
|
+
// Set up authentication handler BEFORE starting the server
|
|
178
256
|
if (node.credentials && node.username && node.password) {
|
|
179
257
|
broker.authenticate = function (client, username, password, callback) {
|
|
180
258
|
const authorized =
|
|
@@ -205,6 +283,7 @@ module.exports = function (RED) {
|
|
|
205
283
|
client
|
|
206
284
|
}
|
|
207
285
|
};
|
|
286
|
+
node.send([msg, null]);
|
|
208
287
|
node.status({
|
|
209
288
|
fill: 'green',
|
|
210
289
|
shape: 'dot',
|
|
@@ -212,7 +291,6 @@ module.exports = function (RED) {
|
|
|
212
291
|
count: broker.connectedClients
|
|
213
292
|
})
|
|
214
293
|
});
|
|
215
|
-
node.send([msg, null]);
|
|
216
294
|
});
|
|
217
295
|
|
|
218
296
|
broker.on('clientDisconnect', function (client) {
|
|
@@ -322,6 +400,92 @@ module.exports = function (RED) {
|
|
|
322
400
|
});
|
|
323
401
|
}
|
|
324
402
|
|
|
403
|
+
async function startListening (node, config, serverOptions) {
|
|
404
|
+
if (node.mqtt_ws_port) {
|
|
405
|
+
// Awkward check since http or ws do not fire an error event in case the port is in use
|
|
406
|
+
const testServer = net.createServer();
|
|
407
|
+
testServer.once('error', function (err) {
|
|
408
|
+
if (err.code === 'EADDRINUSE') {
|
|
409
|
+
node.error(
|
|
410
|
+
RED._('aedes-mqtt-broker.error.port-in-use', { port: config.mqtt_ws_port })
|
|
411
|
+
);
|
|
412
|
+
} else {
|
|
413
|
+
node.error(
|
|
414
|
+
RED._('aedes-mqtt-broker.error.server-error', { port: config.mqtt_ws_port, error: err.toString() })
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
node.status({ fill: 'red', shape: 'ring', text: 'aedes-mqtt-broker.status.error' });
|
|
418
|
+
});
|
|
419
|
+
testServer.once('listening', function () {
|
|
420
|
+
testServer.close();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
testServer.once('close', function () {
|
|
424
|
+
let httpServer;
|
|
425
|
+
if (node.usetls) {
|
|
426
|
+
httpServer = https.createServer(serverOptions);
|
|
427
|
+
} else {
|
|
428
|
+
httpServer = http.createServer();
|
|
429
|
+
}
|
|
430
|
+
node._wsHttpServer = httpServer;
|
|
431
|
+
const wss = new WebSocketServer({ server: httpServer });
|
|
432
|
+
wss.on('connection', function (websocket, req) {
|
|
433
|
+
const stream = createWebSocketStream(websocket);
|
|
434
|
+
node._broker.handle(stream, req);
|
|
435
|
+
});
|
|
436
|
+
node._wsServer = wss;
|
|
437
|
+
httpServer.listen(config.mqtt_ws_port, function () {
|
|
438
|
+
node.log(
|
|
439
|
+
'Binding aedes mqtt server on ws port: ' + config.mqtt_ws_port
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
testServer.listen(config.mqtt_ws_port, function () {
|
|
444
|
+
node.log('Checking ws port: ' + config.mqtt_ws_port);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (node.mqtt_port) {
|
|
449
|
+
node._netServer.listen(node.mqtt_port, function () {
|
|
450
|
+
node.log('Binding aedes mqtt server on port: ' + node.mqtt_port);
|
|
451
|
+
node.status({
|
|
452
|
+
fill: 'green',
|
|
453
|
+
shape: 'dot',
|
|
454
|
+
text: 'node-red:common.status.connected'
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function shutdownBroker (node, done) {
|
|
461
|
+
try {
|
|
462
|
+
await node._initPromise;
|
|
463
|
+
// Stop periodic snapshot interval
|
|
464
|
+
if (node._snapshotInterval) {
|
|
465
|
+
clearInterval(node._snapshotInterval);
|
|
466
|
+
node._snapshotInterval = null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Save final snapshot on shutdown (wait if an interval save is in progress)
|
|
470
|
+
if (node._persistEnabled && node._broker) {
|
|
471
|
+
// Wait for any in-progress interval save to complete
|
|
472
|
+
const waitForSave = new Promise(function (resolve) {
|
|
473
|
+
const check = setInterval(function () {
|
|
474
|
+
if (!node._saving) {
|
|
475
|
+
clearInterval(check);
|
|
476
|
+
resolve();
|
|
477
|
+
}
|
|
478
|
+
}, 50);
|
|
479
|
+
});
|
|
480
|
+
await waitForSave;
|
|
481
|
+
await saveSnapshot(node._broker, node._persistFile, node);
|
|
482
|
+
}
|
|
483
|
+
closeBroker(node, done);
|
|
484
|
+
} catch (e) {
|
|
485
|
+
done();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
325
489
|
function AedesBrokerNode (config) {
|
|
326
490
|
RED.nodes.createNode(this, config);
|
|
327
491
|
this.mqtt_port = parseInt(config.mqtt_port, 10);
|
|
@@ -390,13 +554,10 @@ module.exports = function (RED) {
|
|
|
390
554
|
url: config.dburl
|
|
391
555
|
});
|
|
392
556
|
node.log('Start persistence to MongoDB');
|
|
393
|
-
/*
|
|
394
|
-
} else if (config.persistence_bind === 'level') {
|
|
395
|
-
aedesSettings.persistence = LevelPersistence(new Level('leveldb'));
|
|
396
|
-
node.log('Start persistence to LevelDB');
|
|
397
|
-
*/
|
|
398
557
|
}
|
|
399
558
|
|
|
559
|
+
// File persistence (only for in-memory mode with persist_to_file enabled)
|
|
560
|
+
|
|
400
561
|
if (this.cert && this.key && this.usetls) {
|
|
401
562
|
serverOptions.cert = this.cert;
|
|
402
563
|
serverOptions.key = this.key;
|
|
@@ -405,50 +566,82 @@ module.exports = function (RED) {
|
|
|
405
566
|
|
|
406
567
|
node._closing = false;
|
|
407
568
|
node._broker = null;
|
|
408
|
-
node.
|
|
409
|
-
node.
|
|
410
|
-
node.
|
|
569
|
+
node._netServer = null;
|
|
570
|
+
node._wsServer = null;
|
|
571
|
+
node._wsHttpServer = null;
|
|
572
|
+
node._persistEnabled = false;
|
|
573
|
+
node._snapshotInterval = null;
|
|
574
|
+
node._persistFile = null;
|
|
575
|
+
node._snapshotData = null;
|
|
576
|
+
node._trackedSubs = null;
|
|
577
|
+
node._saving = false;
|
|
578
|
+
|
|
579
|
+
// Read snapshot file synchronously before async initialization
|
|
580
|
+
if (config.persistence_bind !== 'mongodb' && config.persist_to_file === true) {
|
|
581
|
+
const persistFile = path.join(RED.settings.userDir, 'aedes-persist-' + node.id + '.json');
|
|
582
|
+
if (checkWritable(RED.settings.userDir, node)) {
|
|
583
|
+
node._snapshotData = readSnapshotSync(persistFile, node);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
411
586
|
|
|
412
|
-
node._initPromise =
|
|
587
|
+
node._initPromise = (async function () {
|
|
588
|
+
await createBroker(node, config, aedesSettings, serverOptions);
|
|
589
|
+
await startListening(node, config, serverOptions);
|
|
590
|
+
}());
|
|
413
591
|
node._initPromise.catch(function (err) {
|
|
414
592
|
node.error(RED._('aedes-mqtt-broker.error.init-failed', { error: err.toString() }));
|
|
415
593
|
node.status({ fill: 'red', shape: 'ring', text: 'aedes-mqtt-broker.status.init-failed' });
|
|
416
594
|
});
|
|
417
595
|
|
|
418
|
-
this.on('close',
|
|
596
|
+
this.on('close', function (removed, done) {
|
|
419
597
|
node._closing = true;
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
} else {
|
|
423
|
-
node.debug('Node restarting');
|
|
424
|
-
}
|
|
425
|
-
try {
|
|
426
|
-
await node._initPromise;
|
|
427
|
-
closeBroker(node, done);
|
|
428
|
-
} catch (e) {
|
|
429
|
-
done();
|
|
430
|
-
}
|
|
598
|
+
node.debug(removed ? 'Node removed or disabled' : 'Node restarting');
|
|
599
|
+
shutdownBroker(node, done);
|
|
431
600
|
});
|
|
432
601
|
}
|
|
433
602
|
|
|
434
603
|
function closeBroker (node, done) {
|
|
435
604
|
process.nextTick(function () {
|
|
436
605
|
function wsClose () {
|
|
437
|
-
if (node.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
606
|
+
if (node._wsServer) {
|
|
607
|
+
// Terminate all existing WebSocket connections so close() callback fires promptly
|
|
608
|
+
node.log('Unbinding aedes mqtt server from ws port: ' + node.mqtt_ws_port);
|
|
609
|
+
node._wsServer.clients.forEach(function (ws) {
|
|
610
|
+
ws.terminate();
|
|
611
|
+
});
|
|
612
|
+
node._wsServer.close(function () {
|
|
613
|
+
if (node._wsHttpServer) {
|
|
614
|
+
node._wsHttpServer.close(function () { done(); });
|
|
441
615
|
} else { done(); }
|
|
442
616
|
});
|
|
443
617
|
} else { done(); }
|
|
444
618
|
}
|
|
445
619
|
function serverClose () {
|
|
446
|
-
if (node.
|
|
447
|
-
node.
|
|
620
|
+
if (node._netServer) {
|
|
621
|
+
node.log('Unbinding aedes mqtt server from port: ' + node.mqtt_port);
|
|
622
|
+
node.status({
|
|
623
|
+
fill: 'red',
|
|
624
|
+
shape: 'ring',
|
|
625
|
+
text: 'node-red:common.status.disconnected'
|
|
626
|
+
});
|
|
627
|
+
node._netServer.close(function () {
|
|
448
628
|
if (node.mqtt_ws_path !== '' && node.fullPath) {
|
|
629
|
+
node.log('Unbinding aedes mqtt server from ws path: ' + node.fullPath);
|
|
449
630
|
delete listenerNodes[node.fullPath];
|
|
450
|
-
if
|
|
451
|
-
|
|
631
|
+
// Remove upgrade listener if this is the last WS-path node
|
|
632
|
+
wsPathNodeCount--;
|
|
633
|
+
if (wsPathNodeCount <= 0 && serverUpgradeAdded && boundHandleServerUpgrade) {
|
|
634
|
+
RED.server.removeListener('upgrade', boundHandleServerUpgrade);
|
|
635
|
+
serverUpgradeAdded = false;
|
|
636
|
+
boundHandleServerUpgrade = null;
|
|
637
|
+
wsPathNodeCount = 0;
|
|
638
|
+
}
|
|
639
|
+
if (node._wsPathServer) {
|
|
640
|
+
// Terminate all existing WebSocket connections so close() callback fires promptly
|
|
641
|
+
node._wsPathServer.clients.forEach(function (ws) {
|
|
642
|
+
ws.terminate();
|
|
643
|
+
});
|
|
644
|
+
node._wsPathServer.close(function () { wsClose(); });
|
|
452
645
|
} else { wsClose(); }
|
|
453
646
|
} else { wsClose(); }
|
|
454
647
|
});
|
package/locales/de/aedes.json
CHANGED
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
"persistence_memory": "Speicher",
|
|
29
29
|
"persistence_mongodb": "MongoDB",
|
|
30
30
|
"persistence_level": "LevelDB",
|
|
31
|
-
"dburl": "DB-URL"
|
|
31
|
+
"dburl": "DB-URL",
|
|
32
|
+
"persist_to_file": "Zustand über Neustarts hinweg speichern"
|
|
33
|
+
},
|
|
34
|
+
"tip": {
|
|
35
|
+
"persist_to_file": "Speichert Retained Messages und Subscriptions als JSON-Datei im Node-RED userDir. Wird beim Herunterfahren und alle 60 s geschrieben. QoS 1/2 In-Flight-Nachrichten werden nicht gespeichert. Wird automatisch deaktiviert, wenn userDir nicht beschreibbar ist."
|
|
32
36
|
},
|
|
33
37
|
"placeholder": {
|
|
34
38
|
"mqtt_port": "MQTT-Port eingeben",
|
package/locales/en-US/aedes.json
CHANGED
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
"persistence_memory": "Memory",
|
|
29
29
|
"persistence_mongodb": "MongoDB",
|
|
30
30
|
"persistence_level": "LevelDB",
|
|
31
|
-
"dburl": "DB Url"
|
|
31
|
+
"dburl": "DB Url",
|
|
32
|
+
"persist_to_file": "Persist state across restarts"
|
|
33
|
+
},
|
|
34
|
+
"tip": {
|
|
35
|
+
"persist_to_file": "Saves retained messages and subscriptions as a JSON file in the Node-RED userDir. Written on shutdown and every 60 s. QoS 1/2 in-flight messages are not persisted. Disabled automatically if userDir is not writable."
|
|
32
36
|
},
|
|
33
37
|
"placeholder": {
|
|
34
38
|
"mqtt_port": "Enter MQTT port",
|