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.
@@ -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 client = mqtt.connect('ws://localhost:8080', { clientId: 'client', resubscribe: false, reconnectPeriod: -1 });
91
- client.on('error', function (err) {
92
- console.error('Error: ', err.toString());
93
- });
94
- client.on('connect', function () {
95
- // console.log('External client connected');
96
- });
97
- const n2 = helper.getNode('n2');
98
- const n5 = helper.getNode('n5');
99
- n2.on('input', function (msg) {
100
- if (msg.topic === 'subscribe') {
101
- client.publish('test1883', 'test');
102
- }
103
- });
104
- n5.on('input', function (msg) {
105
- // console.log(msg);
106
- msg.should.have.property('topic', 'test1883');
107
- client.end(function () {
108
- done();
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 client = mqtt.connect(helper.url().replace(/http/, 'ws') + '/mqtt', { clientId: 'client', resubscribe: false, reconnectPeriod: -1 });
154
- client.on('error', function (err) {
155
- console.error('Client on error: ', err.toString());
156
- });
157
- client.on('connect', function () {
158
- // console.log('External client connected');
159
- });
160
- const n2 = helper.getNode('n2');
161
- const n5 = helper.getNode('n5');
162
- n2.on('input', function (msg) {
163
- if (msg.topic === 'subscribe') {
164
- client.publish('test1883', 'test');
165
- }
166
- });
167
- n5.on('input', function (msg) {
168
- // console.log(msg);
169
- msg.should.have.property('topic', 'test1883');
170
- client.end(function () {
171
- done();
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.