node-red-contrib-aedes 0.15.1 → 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/aedes.js CHANGED
@@ -17,17 +17,17 @@
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
- const aedes = require('aedes');
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');
27
25
  const https = require('https');
28
- const ws = require('websocket-stream');
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,222 +40,220 @@ 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
- function AedesBrokerNode (config) {
55
- RED.nodes.createNode(this, config);
56
- this.mqtt_port = parseInt(config.mqtt_port, 10);
57
- this.mqtt_ws_port = parseInt(config.mqtt_ws_port, 10);
58
- this.mqtt_ws_path = '' + config.mqtt_ws_path;
59
- this.mqtt_ws_bind = config.mqtt_ws_bind;
60
- this.usetls = config.usetls;
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
+ }
61
63
 
62
- const certPath = config.cert ? config.cert.trim() : '';
63
- const keyPath = config.key ? config.key.trim() : '';
64
- const caPath = config.ca ? config.ca.trim() : '';
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
+ }
65
79
 
66
- this.uselocalfiles = config.uselocalfiles;
67
- this.dburl = config.dburl;
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
+ }
68
90
 
69
- if (this.mqtt_ws_bind === 'path') {
70
- this.mqtt_ws_port = 0;
71
- } else {
72
- this.mqtt_ws_path = '';
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;
73
96
  }
97
+ return data;
98
+ }
74
99
 
75
- if (certPath.length > 0 || keyPath.length > 0 || caPath.length > 0) {
76
- if ((certPath.length > 0) !== (keyPath.length > 0)) {
77
- this.valid = false;
78
- this.error(RED._('tls.error.missing-file'));
79
- return;
80
- }
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);
81
108
  try {
82
- if (certPath) {
83
- this.cert = fs.readFileSync(certPath);
84
- }
85
- if (keyPath) {
86
- this.key = fs.readFileSync(keyPath);
87
- }
88
- if (caPath) {
89
- this.ca = fs.readFileSync(caPath);
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
+ });
90
119
  }
91
120
  } catch (err) {
92
- this.valid = false;
93
- this.error(err.toString());
94
- return;
95
- }
96
- } else {
97
- if (this.credentials) {
98
- this.cert = this.credentials.certdata || '';
99
- this.key = this.credentials.keydata || '';
100
- this.ca = this.credentials.cadata || '';
121
+ node.warn('aedes: failed to restore retained messages from snapshot: ' + err.message);
101
122
  }
123
+ node.debug('aedes: snapshot restore complete');
102
124
  }
103
- if (this.credentials) {
104
- this.username = this.credentials.username;
105
- this.password = this.credentials.password;
106
- }
125
+ }
107
126
 
108
- if (typeof this.usetls === 'undefined') {
109
- this.usetls = false;
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);
110
158
  }
159
+ }
111
160
 
112
- const node = this;
161
+ async function createBroker (node, config, aedesSettings, serverOptions) {
162
+ const { Aedes } = await import('aedes');
163
+ const broker = await Aedes.createBroker(aedesSettings);
164
+ if (node._closing) { broker.close(); return; }
165
+ node._broker = broker;
113
166
 
114
- const aedesSettings = {};
115
- const serverOptions = {};
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;
116
170
 
117
- if (config.persistence_bind === 'mongodb' && config.dburl) {
118
- aedesSettings.persistence = MongoPersistence({
119
- url: config.dburl
120
- });
121
- node.log('Start persistence to MongeDB');
122
- /*
123
- } else if (config.persistence_bind === 'level') {
124
- aedesSettings.persistence = LevelPersistence(new Level('leveldb'));
125
- node.log('Start persistence to LevelDB');
126
- */
127
- }
171
+ if (checkWritable(RED.settings.userDir, node)) {
172
+ node._persistEnabled = true;
128
173
 
129
- if (this.cert && this.key && this.usetls) {
130
- serverOptions.cert = this.cert;
131
- serverOptions.key = this.key;
132
- serverOptions.ca = this.ca;
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
+ }
133
188
  }
134
189
 
135
- const broker = aedes.createBroker(aedesSettings);
136
190
  let server;
137
- if (this.usetls) {
191
+ if (node.usetls) {
138
192
  server = tls.createServer(serverOptions, broker.handle);
139
193
  } else {
140
194
  server = net.createServer(broker.handle);
141
195
  }
196
+ node._netServer = server;
142
197
 
143
- let wss = null;
144
- let httpServer = null;
145
-
146
- if (this.mqtt_ws_port) {
147
- // Awkward check since http or ws do not fire an error event in case the port is in use
148
- const testServer = net.createServer();
149
- testServer.once('error', function (err) {
150
- if (err.code === 'EADDRINUSE') {
151
- node.error(
152
- 'Error: Port ' + config.mqtt_ws_port + ' is already in use'
153
- );
154
- } else {
155
- node.error(
156
- 'Error creating net server on port ' +
157
- config.mqtt_ws_port +
158
- ', ' +
159
- err.toString()
160
- );
161
- }
162
- });
163
- testServer.once('listening', function () {
164
- testServer.close();
165
- });
166
-
167
- testServer.once('close', function () {
168
- if (node.usetls) {
169
- httpServer = https.createServer(serverOptions);
170
- } else {
171
- httpServer = http.createServer();
172
- }
173
- wss = ws.createServer(
174
- {
175
- server: httpServer
176
- },
177
- broker.handle
178
- );
179
- httpServer.listen(config.mqtt_ws_port, function () {
180
- node.log(
181
- 'Binding aedes mqtt server on ws port: ' + config.mqtt_ws_port
182
- );
183
- });
184
- });
185
- testServer.listen(config.mqtt_ws_port, function () {
186
- node.log('Checking ws port: ' + config.mqtt_ws_port);
187
- });
188
- }
189
-
190
- if (this.mqtt_ws_path !== '') {
198
+ if (node.mqtt_ws_path !== '') {
191
199
  if (!serverUpgradeAdded) {
192
- RED.server.on('upgrade', handleServerUpgrade);
200
+ boundHandleServerUpgrade = handleServerUpgrade;
201
+ RED.server.on('upgrade', boundHandleServerUpgrade);
193
202
  serverUpgradeAdded = true;
194
203
  }
204
+ wsPathNodeCount++;
195
205
 
196
- let path = RED.settings.httpNodeRoot || '/';
197
- path =
198
- path +
199
- (path.slice(-1) === '/' ? '' : '/') +
206
+ let pathStr = RED.settings.httpNodeRoot || '/';
207
+ pathStr =
208
+ pathStr +
209
+ (pathStr.slice(-1) === '/' ? '' : '/') +
200
210
  (node.mqtt_ws_path.charAt(0) === '/'
201
211
  ? node.mqtt_ws_path.substring(1)
202
212
  : node.mqtt_ws_path);
203
- node.fullPath = path;
213
+ node.fullPath = pathStr;
204
214
 
205
- if (Object.prototype.hasOwnProperty.call(listenerNodes, path)) {
215
+ if (Object.prototype.hasOwnProperty.call(listenerNodes, pathStr)) {
206
216
  node.error(
207
217
  RED._('websocket.errors.duplicate-path', { path: node.mqtt_ws_path })
208
218
  );
209
- return;
210
- }
211
- listenerNodes[node.fullPath] = node;
212
- const serverOptions_ = {
213
- noServer: true
214
- };
215
- if (RED.settings.webSocketNodeVerifyClient) {
216
- serverOptions_.verifyClient = RED.settings.webSocketNodeVerifyClient;
217
- }
218
-
219
- node.server = ws.createServer(
220
- {
219
+ } else {
220
+ listenerNodes[node.fullPath] = node;
221
+ const serverOptions_ = {
221
222
  noServer: true
222
- },
223
- broker.handle
224
- );
223
+ };
224
+ if (RED.settings.webSocketNodeVerifyClient) {
225
+ serverOptions_.verifyClient = RED.settings.webSocketNodeVerifyClient;
226
+ }
227
+
228
+ node._wsPathServer = new WebSocketServer(serverOptions_);
229
+ node._wsPathServer.on('connection', function (websocket, req) {
230
+ const stream = createWebSocketStream(websocket);
231
+ broker.handle(stream, req);
232
+ });
225
233
 
226
- node.log('Binding aedes mqtt server on ws path: ' + node.fullPath);
234
+ node.log('Binding aedes mqtt server on ws path: ' + node.fullPath);
235
+ }
227
236
  }
228
237
 
229
238
  server.once('error', function (err) {
230
239
  if (err.code === 'EADDRINUSE') {
231
- node.error('Error: Port ' + config.mqtt_port + ' is already in use');
232
- node.status({
233
- fill: 'red',
234
- shape: 'ring',
235
- text: 'node-red:common.status.disconnected'
236
- });
240
+ node.error(
241
+ RED._('aedes-mqtt-broker.error.port-in-use', { port: node.mqtt_port })
242
+ );
237
243
  } else {
238
- node.error('Error: Port ' + config.mqtt_port + ' ' + err.toString());
239
- node.status({
240
- fill: 'red',
241
- shape: 'ring',
242
- text: 'node-red:common.status.disconnected'
243
- });
244
+ node.error(
245
+ RED._('aedes-mqtt-broker.error.server-error', { port: node.mqtt_port, error: err.toString() })
246
+ );
244
247
  }
245
- });
246
-
247
- if (this.mqtt_port) {
248
- server.listen(this.mqtt_port, function () {
249
- node.log('Binding aedes mqtt server on port: ' + config.mqtt_port);
250
- node.status({
251
- fill: 'green',
252
- shape: 'dot',
253
- text: 'node-red:common.status.connected'
254
- });
248
+ node.status({
249
+ fill: 'red',
250
+ shape: 'ring',
251
+ text: 'aedes-mqtt-broker.status.error'
255
252
  });
256
- }
253
+ });
257
254
 
258
- if (this.credentials && this.username && this.password) {
255
+ // Set up authentication handler BEFORE starting the server
256
+ if (node.credentials && node.username && node.password) {
259
257
  broker.authenticate = function (client, username, password, callback) {
260
258
  const authorized =
261
259
  username === node.username &&
@@ -285,6 +283,7 @@ module.exports = function (RED) {
285
283
  client
286
284
  }
287
285
  };
286
+ node.send([msg, null]);
288
287
  node.status({
289
288
  fill: 'green',
290
289
  shape: 'dot',
@@ -292,7 +291,6 @@ module.exports = function (RED) {
292
291
  count: broker.connectedClients
293
292
  })
294
293
  });
295
- node.send([msg, null]);
296
294
  });
297
295
 
298
296
  broker.on('clientDisconnect', function (client) {
@@ -383,7 +381,7 @@ module.exports = function (RED) {
383
381
  }
384
382
  });
385
383
 
386
- if (this.wires && this.wires[1] && this.wires[1].length > 0) {
384
+ if (node.wires && node.wires[1] && node.wires[1].length > 0) {
387
385
  node.log('Publish output wired. Enable broker publish event messages.');
388
386
  broker.on('publish', function (packet, client) {
389
387
  const msg = {
@@ -400,54 +398,258 @@ module.exports = function (RED) {
400
398
  broker.on('closed', function () {
401
399
  node.debug('Closed event');
402
400
  });
401
+ }
403
402
 
404
- this.on('close', function (removed, done) {
405
- if (removed) {
406
- node.debug('Node removed or disabled');
407
- } else {
408
- node.debug('Node restarting');
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;
409
467
  }
410
- process.nextTick(function onCloseDelayed () {
411
- function wsClose () {
412
- if (wss) {
413
- node.log(
414
- 'Unbinding aedes mqtt server from ws port: ' + config.mqtt_ws_port
415
- );
416
- wss.close(function () {
417
- node.debug('after wss.close(): ');
418
- httpServer.close(function () {
419
- node.debug('after httpServer.close(): ');
420
- done();
421
- });
422
- });
423
- } else {
424
- done();
425
- }
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
+
489
+ function AedesBrokerNode (config) {
490
+ RED.nodes.createNode(this, config);
491
+ this.mqtt_port = parseInt(config.mqtt_port, 10);
492
+ this.mqtt_ws_port = parseInt(config.mqtt_ws_port, 10);
493
+ this.mqtt_ws_path = '' + config.mqtt_ws_path;
494
+ this.mqtt_ws_bind = config.mqtt_ws_bind;
495
+ this.usetls = config.usetls;
496
+
497
+ const certPath = config.cert ? config.cert.trim() : '';
498
+ const keyPath = config.key ? config.key.trim() : '';
499
+ const caPath = config.ca ? config.ca.trim() : '';
500
+
501
+ this.uselocalfiles = config.uselocalfiles;
502
+ this.dburl = config.dburl;
503
+
504
+ if (this.mqtt_ws_bind === 'path') {
505
+ this.mqtt_ws_port = 0;
506
+ } else {
507
+ this.mqtt_ws_path = '';
508
+ }
509
+
510
+ if (certPath.length > 0 || keyPath.length > 0 || caPath.length > 0) {
511
+ if ((certPath.length > 0) !== (keyPath.length > 0)) {
512
+ this.valid = false;
513
+ this.error(RED._('tls.error.missing-file'));
514
+ return;
515
+ }
516
+ try {
517
+ if (certPath) {
518
+ this.cert = fs.readFileSync(certPath);
426
519
  }
520
+ if (keyPath) {
521
+ this.key = fs.readFileSync(keyPath);
522
+ }
523
+ if (caPath) {
524
+ this.ca = fs.readFileSync(caPath);
525
+ }
526
+ } catch (err) {
527
+ this.valid = false;
528
+ this.error(err.toString());
529
+ return;
530
+ }
531
+ } else {
532
+ if (this.credentials) {
533
+ this.cert = this.credentials.certdata || '';
534
+ this.key = this.credentials.keydata || '';
535
+ this.ca = this.credentials.cadata || '';
536
+ }
537
+ }
538
+ if (this.credentials) {
539
+ this.username = this.credentials.username;
540
+ this.password = this.credentials.password;
541
+ }
427
542
 
428
- function brokerClose () {
429
- broker.close(function () {
430
- node.log(
431
- 'Unbinding aedes mqtt server from port: ' + config.mqtt_port
432
- );
433
- server.close(function () {
434
- node.debug('after server.close(): ');
435
- if (node.mqtt_ws_path !== '') {
436
- node.log(
437
- 'Unbinding aedes mqtt server from ws path: ' + node.fullPath
438
- );
439
- delete listenerNodes[node.fullPath];
440
- node.server.close(function () {
441
- wsClose();
442
- });
443
- } else {
444
- wsClose();
543
+ if (typeof this.usetls === 'undefined') {
544
+ this.usetls = false;
545
+ }
546
+
547
+ const node = this;
548
+
549
+ const aedesSettings = {};
550
+ const serverOptions = {};
551
+
552
+ if (config.persistence_bind === 'mongodb' && config.dburl) {
553
+ aedesSettings.persistence = MongoPersistence({
554
+ url: config.dburl
555
+ });
556
+ node.log('Start persistence to MongoDB');
557
+ }
558
+
559
+ // File persistence (only for in-memory mode with persist_to_file enabled)
560
+
561
+ if (this.cert && this.key && this.usetls) {
562
+ serverOptions.cert = this.cert;
563
+ serverOptions.key = this.key;
564
+ serverOptions.ca = this.ca;
565
+ }
566
+
567
+ node._closing = false;
568
+ node._broker = 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
+ }
586
+
587
+ node._initPromise = (async function () {
588
+ await createBroker(node, config, aedesSettings, serverOptions);
589
+ await startListening(node, config, serverOptions);
590
+ }());
591
+ node._initPromise.catch(function (err) {
592
+ node.error(RED._('aedes-mqtt-broker.error.init-failed', { error: err.toString() }));
593
+ node.status({ fill: 'red', shape: 'ring', text: 'aedes-mqtt-broker.status.init-failed' });
594
+ });
595
+
596
+ this.on('close', function (removed, done) {
597
+ node._closing = true;
598
+ node.debug(removed ? 'Node removed or disabled' : 'Node restarting');
599
+ shutdownBroker(node, done);
600
+ });
601
+ }
602
+
603
+ function closeBroker (node, done) {
604
+ process.nextTick(function () {
605
+ function wsClose () {
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(); });
615
+ } else { done(); }
616
+ });
617
+ } else { done(); }
618
+ }
619
+ function serverClose () {
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 () {
628
+ if (node.mqtt_ws_path !== '' && node.fullPath) {
629
+ node.log('Unbinding aedes mqtt server from ws path: ' + node.fullPath);
630
+ delete listenerNodes[node.fullPath];
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;
445
638
  }
446
- });
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(); });
645
+ } else { wsClose(); }
646
+ } else { wsClose(); }
447
647
  });
448
- }
449
- brokerClose();
450
- });
648
+ } else { wsClose(); }
649
+ }
650
+ if (node._broker) {
651
+ node._broker.close(function () { serverClose(); });
652
+ } else { serverClose(); }
451
653
  });
452
654
  }
453
655