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 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-11` dist-tag:
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].server.handleUpgrade(
43
+ listenerNodes[pathname]._wsPathServer.handleUpgrade(
44
44
  request,
45
45
  socket,
46
46
  head,
47
47
  function done (conn) {
48
- listenerNodes[pathname].server.emit('connection', conn, request);
48
+ listenerNodes[pathname]._wsPathServer.emit('connection', conn, request);
49
49
  }
50
50
  );
51
51
  }
52
52
  }
53
53
 
54
- async function initializeBroker (node, config, aedesSettings, serverOptions) {
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._server = server;
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
- RED.server.on('upgrade', handleServerUpgrade);
200
+ boundHandleServerUpgrade = handleServerUpgrade;
201
+ RED.server.on('upgrade', boundHandleServerUpgrade);
115
202
  serverUpgradeAdded = true;
116
203
  }
204
+ wsPathNodeCount++;
117
205
 
118
- let path = RED.settings.httpNodeRoot || '/';
119
- path =
120
- path +
121
- (path.slice(-1) === '/' ? '' : '/') +
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 = path;
213
+ node.fullPath = pathStr;
126
214
 
127
- if (Object.prototype.hasOwnProperty.call(listenerNodes, path)) {
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.server = new WebSocketServer(serverOptions_);
141
- node.server.on('connection', function (websocket, req) {
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: config.mqtt_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: config.mqtt_port, error: err.toString() })
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
- if (node.mqtt_port) {
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._server = null;
409
- node._wss = null;
410
- node._httpServer = null;
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 = initializeBroker(node, config, aedesSettings, serverOptions);
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', async function (removed, done) {
596
+ this.on('close', function (removed, done) {
419
597
  node._closing = true;
420
- if (removed) {
421
- node.debug('Node removed or disabled');
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._wss) {
438
- node._wss.close(function () {
439
- if (node._httpServer) {
440
- node._httpServer.close(function () { done(); });
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._server) {
447
- node._server.close(function () {
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 (node.server) {
451
- node.server.close(function () { wsClose(); });
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
  });
@@ -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",
@@ -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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-aedes",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Node Red MQTT broker node based on aedes.js",
5
5
  "dependencies": {
6
6
  "aedes": "^1.0.0",