node-red-contrib-aedes 0.15.1 → 1.1.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/.github/workflows/nodejs.yml +1 -1
- package/CHANGELOG.md +10 -0
- package/README.md +9 -2
- package/aedes.html +29 -19
- package/aedes.js +181 -172
- package/locales/de/aedes.html +110 -0
- package/locales/de/aedes.json +50 -0
- package/locales/en-US/aedes.html +103 -12
- package/locales/en-US/aedes.json +10 -1
- package/package.json +6 -4
- package/test/aedes_last_will_spec.js +25 -48
- package/test/aedes_qos_spec.js +83 -77
- package/test/aedes_retain_spec.js +84 -78
- package/test/aedes_spec.js +260 -67
- package/test/aedes_ws_spec.js +112 -38
- package/test/test-utils.js +17 -0
- package/docs/DEV-SETUP.md +0 -86
- package/docs/MIGRATION-PLAN-0.51-TO-1.0.md +0 -349
- package/docs/MIGRATION-PLAN-NODE-RED-4.md +0 -107
package/test/aedes_ws_spec.js
CHANGED
|
@@ -17,6 +17,45 @@ describe('Aedes Broker Websocket tests', function () {
|
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
it('should connect an external ws mqtt client', function (done) {
|
|
21
|
+
this.timeout(10000);
|
|
22
|
+
const flow = [
|
|
23
|
+
{
|
|
24
|
+
id: 'n1',
|
|
25
|
+
type: 'aedes broker',
|
|
26
|
+
mqtt_port: '1883',
|
|
27
|
+
mqtt_ws_port: '8080',
|
|
28
|
+
name: 'Aedes 1883',
|
|
29
|
+
wires: [['n2'], []]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'n2',
|
|
33
|
+
type: 'helper'
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
helper.load(aedesNode, flow, function () {
|
|
37
|
+
const n1 = helper.getNode('n1');
|
|
38
|
+
n1._initPromise.then(function () {
|
|
39
|
+
const client = mqtt.connect('ws://localhost:8080', {
|
|
40
|
+
clientId: 'ws-client',
|
|
41
|
+
resubscribe: false,
|
|
42
|
+
reconnectPeriod: -1
|
|
43
|
+
});
|
|
44
|
+
client.on('error', function (err) {
|
|
45
|
+
console.error('Error: ', err.toString());
|
|
46
|
+
});
|
|
47
|
+
const n2 = helper.getNode('n2');
|
|
48
|
+
n2.on('input', function (msg) {
|
|
49
|
+
if (msg.topic === 'clientReady') {
|
|
50
|
+
client.end(function () {
|
|
51
|
+
done();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
20
59
|
it('should not throw an exception with 2 servers on the same ws port', function (done) {
|
|
21
60
|
this.timeout(10000); // have to wait for the inject with delay of two seconds
|
|
22
61
|
|
|
@@ -50,6 +89,35 @@ describe('Aedes Broker Websocket tests', function () {
|
|
|
50
89
|
});
|
|
51
90
|
});
|
|
52
91
|
|
|
92
|
+
it('should set error status when ws port is already in use', function (done) {
|
|
93
|
+
this.timeout(10000);
|
|
94
|
+
const net = require('net');
|
|
95
|
+
const blocker = net.createServer();
|
|
96
|
+
blocker.listen(8081, function () {
|
|
97
|
+
const flow = [{
|
|
98
|
+
id: 'n1',
|
|
99
|
+
type: 'aedes broker',
|
|
100
|
+
mqtt_port: '1883',
|
|
101
|
+
mqtt_ws_port: '8081',
|
|
102
|
+
name: 'Aedes WS Error',
|
|
103
|
+
wires: [[], []]
|
|
104
|
+
}];
|
|
105
|
+
helper.load(aedesNode, flow, function () {
|
|
106
|
+
const n1 = helper.getNode('n1');
|
|
107
|
+
n1.on('call:status', function (call) {
|
|
108
|
+
if (call.args[0].fill === 'red') {
|
|
109
|
+
call.args[0].should.have.property('fill', 'red');
|
|
110
|
+
call.args[0].should.have.property('shape', 'ring');
|
|
111
|
+
call.args[0].should.have.property('text', 'aedes-mqtt-broker.status.error');
|
|
112
|
+
blocker.close(function () {
|
|
113
|
+
done();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
53
121
|
it('a subscriber should receive a message from an external ws publisher', function (done) {
|
|
54
122
|
this.timeout(10000); // have to wait for the inject with delay of 10 seconds
|
|
55
123
|
const flow = [
|
|
@@ -87,25 +155,28 @@ describe('Aedes Broker Websocket tests', function () {
|
|
|
87
155
|
}
|
|
88
156
|
];
|
|
89
157
|
helper.load([aedesNode, mqttNode], flow, function () {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
158
|
+
const n1 = helper.getNode('n1');
|
|
159
|
+
n1._initPromise.then(function () {
|
|
160
|
+
const client = mqtt.connect('ws://localhost:8080', { clientId: 'client', resubscribe: false, reconnectPeriod: -1 });
|
|
161
|
+
client.on('error', function (err) {
|
|
162
|
+
console.error('Error: ', err.toString());
|
|
163
|
+
});
|
|
164
|
+
client.on('connect', function () {
|
|
165
|
+
// console.log('External client connected');
|
|
166
|
+
});
|
|
167
|
+
const n2 = helper.getNode('n2');
|
|
168
|
+
const n5 = helper.getNode('n5');
|
|
169
|
+
n2.on('input', function (msg) {
|
|
170
|
+
if (msg.topic === 'subscribe') {
|
|
171
|
+
client.publish('test1883', 'test');
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
n5.on('input', function (msg) {
|
|
175
|
+
// console.log(msg);
|
|
176
|
+
msg.should.have.property('topic', 'test1883');
|
|
177
|
+
client.end(function () {
|
|
178
|
+
done();
|
|
179
|
+
});
|
|
109
180
|
});
|
|
110
181
|
});
|
|
111
182
|
});
|
|
@@ -150,25 +221,28 @@ describe('Aedes Broker Websocket tests', function () {
|
|
|
150
221
|
];
|
|
151
222
|
|
|
152
223
|
helper.load([aedesNode, mqttNode], flow, function () {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
224
|
+
const n1 = helper.getNode('n1');
|
|
225
|
+
n1._initPromise.then(function () {
|
|
226
|
+
const client = mqtt.connect(helper.url().replace(/http/, 'ws') + '/mqtt', { clientId: 'client', resubscribe: false, reconnectPeriod: -1 });
|
|
227
|
+
client.on('error', function (err) {
|
|
228
|
+
console.error('Client on error: ', err.toString());
|
|
229
|
+
});
|
|
230
|
+
client.on('connect', function () {
|
|
231
|
+
// console.log('External client connected');
|
|
232
|
+
});
|
|
233
|
+
const n2 = helper.getNode('n2');
|
|
234
|
+
const n5 = helper.getNode('n5');
|
|
235
|
+
n2.on('input', function (msg) {
|
|
236
|
+
if (msg.topic === 'subscribe') {
|
|
237
|
+
client.publish('test1883', 'test');
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
n5.on('input', function (msg) {
|
|
241
|
+
// console.log(msg);
|
|
242
|
+
msg.should.have.property('topic', 'test1883');
|
|
243
|
+
client.end(function () {
|
|
244
|
+
done();
|
|
245
|
+
});
|
|
172
246
|
});
|
|
173
247
|
});
|
|
174
248
|
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* eslint no-console: ["error", { allow: ["log", "warn", "error"] }] */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Logs detailed error information including AggregateError support.
|
|
5
|
+
* Useful for debugging Node-RED and MQTT client errors in tests.
|
|
6
|
+
*/
|
|
7
|
+
function logError (err) {
|
|
8
|
+
if (err instanceof AggregateError) {
|
|
9
|
+
console.error('AggregateError:', err.message);
|
|
10
|
+
console.error('Name:', err.name);
|
|
11
|
+
console.error('Errors:', err.errors);
|
|
12
|
+
} else {
|
|
13
|
+
console.error('Error:', err.message || err.toString());
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { logError };
|
package/docs/DEV-SETUP.md
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# Developer Setup Note
|
|
2
|
-
|
|
3
|
-
After every `npm install` or `npm update`, re-link Node-RED:
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
sudo npm link node-red
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
`npm install` resets the `node_modules` directory and removes the symlink created by `npm link`. Without re-linking, the test helper cannot find Node-RED and tests will fail.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Backporting a fix to a legacy branch
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
# 1. Stash any WIP on current branch
|
|
17
|
-
git stash push -m "WIP before backport"
|
|
18
|
-
|
|
19
|
-
# 2. Switch to the legacy branch
|
|
20
|
-
git checkout v0.15.x # or: git checkout v11
|
|
21
|
-
|
|
22
|
-
# 3. Cherry-pick the fix commit from the source branch
|
|
23
|
-
git cherry-pick <commit-hash> # resolve conflicts if any
|
|
24
|
-
|
|
25
|
-
# 4. Run tests
|
|
26
|
-
npm test
|
|
27
|
-
|
|
28
|
-
# 5. Bump patch version and commit
|
|
29
|
-
npm run patch # bumps version in package.json (no git tag)
|
|
30
|
-
git add -A
|
|
31
|
-
git commit -m "Bump patch version"
|
|
32
|
-
|
|
33
|
-
# 6. Publish to npm (optional, with appropriate tag)
|
|
34
|
-
npm publish --tag <tag> # e.g. --tag legacy or --tag v11
|
|
35
|
-
|
|
36
|
-
# 7. Return to working branch and restore WIP
|
|
37
|
-
git checkout <working-branch>
|
|
38
|
-
git stash pop
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Notes:
|
|
42
|
-
- Find the commit hash with `git log --oneline <source-branch>`.
|
|
43
|
-
- If the cherry-pick has conflicts, resolve them, then `git cherry-pick --continue`.
|
|
44
|
-
- `npm run patch` runs `npm --no-git-tag-version version patch` — it bumps the patch version in `package.json` without creating a git tag.
|
|
45
|
-
- Use `--tag <tag>` with `npm publish` to avoid overwriting the `latest` tag on npm. Choose a tag that matches the branch (e.g. `legacy`, `v11`).
|
|
46
|
-
|
|
47
|
-
### Example: Backport Tasks 6 and 7.1
|
|
48
|
-
|
|
49
|
-
Each fix you want to backport needs its own separate commit on the source branch.
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
# 1. Commit each fix separately on the source branch
|
|
53
|
-
git add aedes.js
|
|
54
|
-
git commit -m "Fix: add removed parameter to close handler" # Task 7.1
|
|
55
|
-
|
|
56
|
-
git add aedes.js
|
|
57
|
-
git commit -m "Fix: iterate subscribe/unsubscribe arrays" # Task 6
|
|
58
|
-
|
|
59
|
-
# 2. Find the commit hashes
|
|
60
|
-
git log --oneline migrate-aedes-v1
|
|
61
|
-
|
|
62
|
-
# 3. Backport to v0.15.x
|
|
63
|
-
git stash push -m "WIP before backport"
|
|
64
|
-
git checkout v0.15.x
|
|
65
|
-
git cherry-pick <Task-7.1-commit> # close handler: add removed parameter
|
|
66
|
-
git cherry-pick <Task-6-commit> # fix subscribe/unsubscribe event handlers
|
|
67
|
-
npm test
|
|
68
|
-
npm run patch
|
|
69
|
-
git add -A && git commit -m "Bump patch version"
|
|
70
|
-
npm publish --tag v0.15.x
|
|
71
|
-
git push origin v0.15.x
|
|
72
|
-
|
|
73
|
-
# 4. Backport to v11
|
|
74
|
-
git checkout v11
|
|
75
|
-
git cherry-pick <Task-7.1-commit> # close handler: add removed parameter
|
|
76
|
-
git cherry-pick <Task-6-commit> # fix subscribe/unsubscribe event handlers
|
|
77
|
-
npm test
|
|
78
|
-
npm run patch
|
|
79
|
-
git add -A && git commit -m "Bump patch version"
|
|
80
|
-
npm publish --tag v11
|
|
81
|
-
git push origin v11
|
|
82
|
-
|
|
83
|
-
# 5. Return to working branch
|
|
84
|
-
git checkout migrate-aedes-v1
|
|
85
|
-
git stash pop
|
|
86
|
-
```
|
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
# Migration Plan: Aedes 0.51.x to 1.0.0
|
|
2
|
-
|
|
3
|
-
## Context
|
|
4
|
-
|
|
5
|
-
Aedes v1.0.0 is ESM-only with async broker creation and replaces `websocket-stream` with native `ws`. This plan migrates node-red-contrib-aedes from Aedes ^0.51.3 to ^1.0.0.
|
|
6
|
-
|
|
7
|
-
## Decision: Stay CJS
|
|
8
|
-
|
|
9
|
-
The project stays CommonJS (`module.exports = function(RED) {...}`). Node-RED has no native ESM support for custom nodes yet. Only `aedes` (ESM-only) needs dynamic `await import()`. All other dependencies (`aedes-persistence-mongodb` v9.4.1 = CJS, `ws` v8 = dual CJS/ESM, Node.js built-ins = both) work fine with `require()`.
|
|
10
|
-
|
|
11
|
-
## Breaking Changes
|
|
12
|
-
|
|
13
|
-
| Area | v0.51.3 (Current) | v1.0.0 (Target) |
|
|
14
|
-
|------|-------------------|------------------|
|
|
15
|
-
| Module system | CJS `require('aedes')` | ESM only -- use `await import('aedes')` |
|
|
16
|
-
| Broker creation | `aedes.createBroker(opts)` sync | `await Aedes.createBroker(opts)` async, named export |
|
|
17
|
-
| WebSocket lib | `websocket-stream` package | Native `ws` with `WebSocketServer` + `createWebSocketStream` |
|
|
18
|
-
| Default export | Callable function | Throws error -- must use named `{ Aedes }` |
|
|
19
|
-
| Subscribe event | Array (but code treats as single object -- latent bug) | Array -- must iterate |
|
|
20
|
-
| Unsubscribe event | Array of strings (but code treats as object -- latent bug) | Array of strings -- must iterate |
|
|
21
|
-
|
|
22
|
-
## Files to Modify
|
|
23
|
-
|
|
24
|
-
- `package.json` -- dependency updates
|
|
25
|
-
- `aedes.js` -- core broker logic (main work)
|
|
26
|
-
- `test/aedes_spec.js` -- basic TCP tests
|
|
27
|
-
- `test/aedes_ws_spec.js` -- WebSocket tests
|
|
28
|
-
- `test/aedes_secure_spec.js` -- TLS tests
|
|
29
|
-
- `test/aedes_qos_spec.js` -- QoS tests
|
|
30
|
-
- `test/aedes_retain_spec.js` -- retain tests
|
|
31
|
-
- `test/aedes_last_will_spec.js` -- last will tests
|
|
32
|
-
|
|
33
|
-
No changes: `aedes.html`, `locales/`, `examples/`, `icons/`
|
|
34
|
-
|
|
35
|
-
## Unchanged APIs (no migration needed)
|
|
36
|
-
|
|
37
|
-
- `broker.handle(conn, req)` -- same signature in v1
|
|
38
|
-
- `broker.authenticate(client, username, password, callback)` -- same in v1
|
|
39
|
-
- `broker.close(callback)` -- still callback-based in v1
|
|
40
|
-
- All broker events -- same names and signatures
|
|
41
|
-
- `broker.connectedClients` -- still a property
|
|
42
|
-
- `aedes-persistence-mongodb` v9.4.1 -- compatible (peer dep min ^9.3.1)
|
|
43
|
-
- `handleServerUpgrade` function -- uses standard `ws` API already
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## TASK 0: Create migration branch
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
git checkout -b migrate-aedes-v1
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
All migration work happens on this branch. Merge to master after tests pass.
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
## TASK 1: Update package.json dependencies
|
|
58
|
-
|
|
59
|
-
**File:** `package.json`
|
|
60
|
-
|
|
61
|
-
### 1.1 Update aedes version
|
|
62
|
-
- Change `"aedes": "^0.51.3"` to `"aedes": "^1.0.0"`
|
|
63
|
-
|
|
64
|
-
### 1.2 Replace websocket-stream with ws
|
|
65
|
-
- Remove `"websocket-stream": "^5.5.2"`
|
|
66
|
-
- Add `"ws": "^8.18.0"`
|
|
67
|
-
|
|
68
|
-
### 1.3 Keep aedes-persistence-mongodb
|
|
69
|
-
- No change needed: `"aedes-persistence-mongodb": "^9.4.1"` is compatible
|
|
70
|
-
|
|
71
|
-
---
|
|
72
|
-
|
|
73
|
-
## TASK 2: Update imports in aedes.js
|
|
74
|
-
|
|
75
|
-
**File:** `aedes.js` lines 17-28
|
|
76
|
-
|
|
77
|
-
### 2.1 Remove aedes require
|
|
78
|
-
- Delete line 22: `const aedes = require('aedes');`
|
|
79
|
-
- Aedes will be dynamically imported inside async init (ESM-only, cannot require)
|
|
80
|
-
|
|
81
|
-
### 2.2 Replace websocket-stream require
|
|
82
|
-
- Delete line 28: `const ws = require('websocket-stream');`
|
|
83
|
-
- Add: `const { WebSocketServer, createWebSocketStream } = require('ws');`
|
|
84
|
-
- Note: `ws` v8 supports CJS require
|
|
85
|
-
|
|
86
|
-
---
|
|
87
|
-
|
|
88
|
-
## TASK 3: Async broker initialization pattern
|
|
89
|
-
|
|
90
|
-
**File:** `aedes.js` -- constructor `AedesBrokerNode` (line 54)
|
|
91
|
-
|
|
92
|
-
The constructor must stay synchronous (Node-RED requirement). Use "init promise" pattern.
|
|
93
|
-
|
|
94
|
-
### 3.1 Add state tracking to constructor
|
|
95
|
-
After synchronous config parsing (lines 54-133), add:
|
|
96
|
-
```javascript
|
|
97
|
-
node._closing = false;
|
|
98
|
-
node._broker = null;
|
|
99
|
-
node._server = null;
|
|
100
|
-
node._wss = null;
|
|
101
|
-
node._httpServer = null;
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### 3.2 Extract async init function
|
|
105
|
-
Create `async function initializeBroker(node, config, aedesSettings, serverOptions)` containing all logic currently at lines 135-408:
|
|
106
|
-
```javascript
|
|
107
|
-
async function initializeBroker (node, config, aedesSettings, serverOptions) {
|
|
108
|
-
const { Aedes } = await import('aedes');
|
|
109
|
-
const broker = await Aedes.createBroker(aedesSettings);
|
|
110
|
-
if (node._closing) { broker.close(); return; }
|
|
111
|
-
node._broker = broker;
|
|
112
|
-
// ... TCP/TLS server setup (TASK 3.3)
|
|
113
|
-
// ... WebSocket port setup (TASK 4)
|
|
114
|
-
// ... WebSocket path setup (TASK 5)
|
|
115
|
-
// ... server.listen (existing logic)
|
|
116
|
-
// ... authentication setup (existing logic, unchanged)
|
|
117
|
-
// ... event handlers (TASK 6)
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### 3.3 Call init from constructor
|
|
122
|
-
Replace lines 135-408 with:
|
|
123
|
-
```javascript
|
|
124
|
-
node._initPromise = initializeBroker(node, config, aedesSettings, serverOptions);
|
|
125
|
-
node._initPromise.catch(function (err) {
|
|
126
|
-
node.error('Failed to initialize Aedes broker: ' + err.toString());
|
|
127
|
-
node.status({ fill: 'red', shape: 'ring', text: 'initialization failed' });
|
|
128
|
-
});
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### 3.4 TCP/TLS server creation inside initializeBroker
|
|
132
|
-
Move lines 136-141 into initializeBroker. No API changes needed:
|
|
133
|
-
```javascript
|
|
134
|
-
let server;
|
|
135
|
-
if (node.usetls) {
|
|
136
|
-
server = tls.createServer(serverOptions, broker.handle);
|
|
137
|
-
} else {
|
|
138
|
-
server = net.createServer(broker.handle);
|
|
139
|
-
}
|
|
140
|
-
node._server = server;
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
---
|
|
144
|
-
|
|
145
|
-
## TASK 4: Rewrite WebSocket port mode
|
|
146
|
-
|
|
147
|
-
**File:** `aedes.js` lines 146-188
|
|
148
|
-
|
|
149
|
-
### 4.1 Replace ws.createServer with WebSocketServer
|
|
150
|
-
**Old (lines 173-178):**
|
|
151
|
-
```javascript
|
|
152
|
-
wss = ws.createServer({ server: httpServer }, broker.handle);
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
**New:**
|
|
156
|
-
```javascript
|
|
157
|
-
const wss = new WebSocketServer({ server: httpServer });
|
|
158
|
-
wss.on('connection', function (websocket, req) {
|
|
159
|
-
const stream = createWebSocketStream(websocket);
|
|
160
|
-
broker.handle(stream, req);
|
|
161
|
-
});
|
|
162
|
-
node._wss = wss;
|
|
163
|
-
node._httpServer = httpServer;
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### 4.2 Promisify port availability test
|
|
167
|
-
Wrap the testServer logic (lines 148-187) in a Promise for clean async flow inside initializeBroker. Keep same EADDRINUSE error handling.
|
|
168
|
-
|
|
169
|
-
---
|
|
170
|
-
|
|
171
|
-
## TASK 5: Rewrite WebSocket path mode
|
|
172
|
-
|
|
173
|
-
**File:** `aedes.js` lines 190-227
|
|
174
|
-
|
|
175
|
-
### 5.1 Replace ws.createServer noServer with WebSocketServer
|
|
176
|
-
**Old (lines 219-224):**
|
|
177
|
-
```javascript
|
|
178
|
-
node.server = ws.createServer({ noServer: true }, broker.handle);
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
**New:**
|
|
182
|
-
```javascript
|
|
183
|
-
node.server = new WebSocketServer({ noServer: true });
|
|
184
|
-
node.server.on('connection', function (websocket, req) {
|
|
185
|
-
const stream = createWebSocketStream(websocket);
|
|
186
|
-
broker.handle(stream, req);
|
|
187
|
-
});
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### 5.2 handleServerUpgrade -- NO CHANGES
|
|
191
|
-
Function at lines 40-52 already uses standard `ws` API pattern (`handleUpgrade` + `emit('connection')`). Works as-is with `WebSocketServer`.
|
|
192
|
-
|
|
193
|
-
---
|
|
194
|
-
|
|
195
|
-
## TASK 6: Fix subscribe/unsubscribe event handlers
|
|
196
|
-
|
|
197
|
-
**File:** `aedes.js` lines 368-390
|
|
198
|
-
|
|
199
|
-
### 6.1 Fix subscribe handler (lines 368-378)
|
|
200
|
-
The first argument is an **array** of `{topic, qos}` objects. Iterate it:
|
|
201
|
-
```javascript
|
|
202
|
-
broker.on('subscribe', function (subscriptions, client) {
|
|
203
|
-
for (const subscription of subscriptions) {
|
|
204
|
-
node.send([{
|
|
205
|
-
topic: 'subscribe',
|
|
206
|
-
payload: { topic: subscription.topic, qos: subscription.qos, client }
|
|
207
|
-
}, null]);
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
### 6.2 Fix unsubscribe handler (lines 380-390)
|
|
213
|
-
The first argument is an **array of topic strings** (not objects). Iterate it:
|
|
214
|
-
```javascript
|
|
215
|
-
broker.on('unsubscribe', function (unsubscriptions, client) {
|
|
216
|
-
for (const topic of unsubscriptions) {
|
|
217
|
-
node.send([{
|
|
218
|
-
topic: 'unsubscribe',
|
|
219
|
-
payload: { topic: topic, client }
|
|
220
|
-
}, null]);
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
---
|
|
226
|
-
|
|
227
|
-
## TASK 7: Restructure close handler
|
|
228
|
-
|
|
229
|
-
**File:** `aedes.js` lines 410-452
|
|
230
|
-
|
|
231
|
-
### 7.1 Replace close handler with async-safe version
|
|
232
|
-
Carries over the `removed` parameter from the Node-RED 4 migration (task 2.2):
|
|
233
|
-
```javascript
|
|
234
|
-
this.on('close', async function (removed, done) {
|
|
235
|
-
node._closing = true;
|
|
236
|
-
if (removed) {
|
|
237
|
-
node.debug('Node removed or disabled');
|
|
238
|
-
} else {
|
|
239
|
-
node.debug('Node restarting');
|
|
240
|
-
}
|
|
241
|
-
try {
|
|
242
|
-
await node._initPromise;
|
|
243
|
-
closeBroker(node, config, done);
|
|
244
|
-
} catch (e) {
|
|
245
|
-
done();
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### 7.2 Create closeBroker function
|
|
251
|
-
Extract cleanup into a standalone function. Access resources via `node._broker`, `node._server`, `node._wss`, `node._httpServer` with null checks:
|
|
252
|
-
```javascript
|
|
253
|
-
function closeBroker (node, config, done) {
|
|
254
|
-
process.nextTick(function () {
|
|
255
|
-
function wsClose () {
|
|
256
|
-
if (node._wss) {
|
|
257
|
-
node._wss.close(function () {
|
|
258
|
-
if (node._httpServer) {
|
|
259
|
-
node._httpServer.close(function () { done(); });
|
|
260
|
-
} else { done(); }
|
|
261
|
-
});
|
|
262
|
-
} else { done(); }
|
|
263
|
-
}
|
|
264
|
-
function serverClose () {
|
|
265
|
-
if (node._server) {
|
|
266
|
-
node._server.close(function () {
|
|
267
|
-
if (node.mqtt_ws_path !== '' && node.fullPath) {
|
|
268
|
-
delete listenerNodes[node.fullPath];
|
|
269
|
-
if (node.server) {
|
|
270
|
-
node.server.close(function () { wsClose(); });
|
|
271
|
-
} else { wsClose(); }
|
|
272
|
-
} else { wsClose(); }
|
|
273
|
-
});
|
|
274
|
-
} else { wsClose(); }
|
|
275
|
-
}
|
|
276
|
-
if (node._broker) {
|
|
277
|
-
node._broker.close(function () { serverClose(); });
|
|
278
|
-
} else { serverClose(); }
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
---
|
|
284
|
-
|
|
285
|
-
## TASK 8: Update test files for async init
|
|
286
|
-
|
|
287
|
-
**Files:** all 6 files in `test/`
|
|
288
|
-
|
|
289
|
-
### 8.1 Add _initPromise wait pattern
|
|
290
|
-
In every test that connects an MQTT client, use the async `helper.load()` (available since `node-red-node-test-helper` v0.3.5) and `await` the init promise:
|
|
291
|
-
```javascript
|
|
292
|
-
await helper.load(aedesNode, flow);
|
|
293
|
-
const n1 = helper.getNode('n1');
|
|
294
|
-
await n1._initPromise;
|
|
295
|
-
// ... existing test logic ...
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
This replaces the nested callback + `.then()` pattern with flat async/await. Each `it()` callback must be declared `async`.
|
|
299
|
-
|
|
300
|
-
### 8.2 Tests that do NOT need changes
|
|
301
|
-
Tests that only check `n1.name` or node existence ("should be loaded") work without waiting -- the name is set synchronously in the constructor.
|
|
302
|
-
|
|
303
|
-
### 8.3 Test files to update
|
|
304
|
-
- `test/aedes_spec.js` -- all tests except "should be loaded"
|
|
305
|
-
- `test/aedes_ws_spec.js` -- all tests except "should be loaded"
|
|
306
|
-
- `test/aedes_secure_spec.js` -- all tests except "should be loaded"
|
|
307
|
-
- `test/aedes_qos_spec.js` -- all tests
|
|
308
|
-
- `test/aedes_retain_spec.js` -- all tests
|
|
309
|
-
- `test/aedes_last_will_spec.js` -- all tests except "should be loaded"
|
|
310
|
-
|
|
311
|
-
---
|
|
312
|
-
|
|
313
|
-
## TASK 9: Install and verify
|
|
314
|
-
|
|
315
|
-
### 9.1 Install new dependencies
|
|
316
|
-
```bash
|
|
317
|
-
npm install
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### 9.2 Run full test suite
|
|
321
|
-
```bash
|
|
322
|
-
npm test
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
### 9.3 Verify all scenarios
|
|
326
|
-
- TCP plain connection
|
|
327
|
-
- TCP with TLS (port 8883)
|
|
328
|
-
- WebSocket via standalone port
|
|
329
|
-
- WebSocket via Node-RED path
|
|
330
|
-
- Secure WebSocket (WSS) on port
|
|
331
|
-
- Secure WebSocket (WSS) on path
|
|
332
|
-
- QoS 1 and QoS 2 persistence
|
|
333
|
-
- Retained messages
|
|
334
|
-
- Last will and testament
|
|
335
|
-
- Authentication (valid + invalid credentials)
|
|
336
|
-
- Multiple brokers on different ports
|
|
337
|
-
- Port conflict detection
|
|
338
|
-
|
|
339
|
-
---
|
|
340
|
-
|
|
341
|
-
## Legacy branch relevance (v11, v0.15.x)
|
|
342
|
-
|
|
343
|
-
Both legacy branches use Aedes 0.49.0. We checked whether Tasks 6 and 7.1 also apply there:
|
|
344
|
-
|
|
345
|
-
- **Task 6 (subscribe/unsubscribe handlers) — YES, relevant for both branches.**
|
|
346
|
-
Both `v11` and `v0.15.x` have the same latent bug: the handlers treat the first argument as a single object, but the Aedes API (even in 0.49.0) passes an array. This fix should be backported.
|
|
347
|
-
|
|
348
|
-
- **Task 7.1 (close handler `removed` parameter) — NO, not relevant.**
|
|
349
|
-
Both legacy branches use `this.on('close', function (done) {...})` with one parameter. Node-RED handles this via arity checking — if the function declares 1 param, Node-RED passes only `done`, so the handler works correctly on all Node-RED versions. The `removed` param is a nice-to-have, and the async-safe parts (`_initPromise`, `_closing`) only apply to aedes v1's async init.
|