mm_mqtt_server 2.0.0 → 2.0.1

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.
Files changed (2) hide show
  1. package/lib/mqtt_server.js +127 -38
  2. package/package.json +1 -1
@@ -5,6 +5,12 @@ const { Aedes } = require('aedes');
5
5
  const { Mod } = require('mm_mod');
6
6
  const { eventAdmin } = require('mm_event');
7
7
 
8
+ /**
9
+ * 代理自发布标记:打在发布报文上,用于在 broker 的 publish 事件中避免回环。
10
+ * 使用 Symbol 避免污染报文序列化,aedes 转投给订阅者时按字段复制也不会带上该标记。
11
+ */
12
+ const SELF_PUBLISH = Symbol('mm_mqtt_server.self_publish');
13
+
8
14
  /**
9
15
  * MqttServer —— 最薄的 MQTT 代理传输层(入站适配器模块)。
10
16
  *
@@ -33,9 +39,10 @@ class MqttServer extends Mod {
33
39
  title: 'MQTT代理服务器模块',
34
40
  description: 'MQTT 传输层:消息直达事件管理器,不做路由与插件',
35
41
  version: '2.0.0',
42
+ allow_anonymous: true,
43
+ scope: 'mqtt',
36
44
  host: '0.0.0.0',
37
45
  port: 1883,
38
- allow_anonymous: true,
39
46
  username: '',
40
47
  password: '',
41
48
  options: {},
@@ -80,22 +87,41 @@ class MqttServer extends Mod {
80
87
  */
81
88
  constructor(config, server) {
82
89
  super(config, server);
83
- this.#events = eventAdmin('mqtt');
90
+ this.#events = eventAdmin(this.config.scope);
84
91
  }
85
92
 
86
93
  // ---------- 生命周期钩子 ----------
87
94
  /**
88
- * 启动钩子:创建 broker 与 TCP 服务器并开始监听
95
+ * 启动钩子:创建 broker 与 TCP 服务器并开始监听,失败时回滚已创建的资源
89
96
  * @returns {Promise<void>} 无返回值
90
97
  */
91
98
  async onStart() {
92
- const broker = await this._createBroker();
93
- this.#broker = broker;
94
- this._bindBrokerEvents(broker);
95
- const server = net.createServer((stream) => broker.handle(stream));
96
- this.#server = server;
97
- await this._listen(server);
98
- server.on('error', (err) => this._emitError(err));
99
+ try {
100
+ const broker = await this._createBroker();
101
+ this.#broker = broker;
102
+ this._bindBrokerEvents(broker);
103
+ const server = net.createServer((stream) => broker.handle(stream));
104
+ this.#server = server;
105
+ await this._listen(server);
106
+ server.on('error', (err) => this._emitError(err));
107
+ } catch (err) {
108
+ await this._rollbackStart();
109
+ throw err;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * 启动失败时回滚已创建的资源(服务器与 broker 的内部定时器)
115
+ * @returns {Promise<void>} 无返回值
116
+ */
117
+ async _rollbackStart() {
118
+ try {
119
+ this._closeAllClients();
120
+ await this._closeServer();
121
+ await this._closeBroker();
122
+ } catch (err) {
123
+ this._emitError(err);
124
+ }
99
125
  }
100
126
 
101
127
  /**
@@ -197,14 +223,19 @@ class MqttServer extends Mod {
197
223
  const broker = this.#broker;
198
224
  if (!broker) return Promise.resolve(false);
199
225
  const packet = this._buildPacket(topic, payload, opts);
226
+ packet[SELF_PUBLISH] = true;
200
227
  return new Promise((resolve, reject) => {
201
- broker.publish(packet, (err) => {
202
- if (err) {
203
- reject(err);
204
- return;
205
- }
206
- resolve(true);
207
- });
228
+ try {
229
+ broker.publish(packet, (err) => {
230
+ if (err) {
231
+ reject(err);
232
+ return;
233
+ }
234
+ resolve(true);
235
+ });
236
+ } catch (err) {
237
+ reject(err);
238
+ }
208
239
  });
209
240
  }
210
241
 
@@ -284,12 +315,10 @@ class MqttServer extends Mod {
284
315
  * @param {Object} broker aedes broker 实例
285
316
  */
286
317
  _bindBrokerEvents(broker) {
318
+ broker.on('error', (err) => this._emitError(err));
287
319
  broker.on('client', (client) => this._registerClient(client));
288
- broker.on('clientDisconnect', (client) => this.#clients.delete(client.id));
289
- broker.on('publish', (packet, client) => {
290
- if (!client) return;
291
- this._handlePublish(client, packet);
292
- });
320
+ broker.on('clientDisconnect', (client) => this._unregisterClient(client));
321
+ broker.on('publish', (packet, client) => this._dispatchPublish(packet, client));
293
322
  broker.on('clientError', (client, err) => this._emitError(err));
294
323
  broker.on('connectionError', (client, err) => this._emitError(err));
295
324
  }
@@ -308,21 +337,92 @@ class MqttServer extends Mod {
308
337
  }
309
338
 
310
339
  /**
311
- * 处理入站发布消息:交给事件管理器,返回发布描述时由代理重发
340
+ * 注销客户端:仅当连接表中记录的仍是该实例时才删除,避免误删同 ID 的新连接
312
341
  * @param {Object} client aedes client 实例
342
+ */
343
+ _unregisterClient(client) {
344
+ const entry = this.#clients.get(client.id);
345
+ if (entry && entry.client === client) this.#clients.delete(client.id);
346
+ }
347
+
348
+ /**
349
+ * 分发 broker 发布事件:过滤系统主题与代理自发布后转交事件管理器
350
+ * @param {Object} packet aedes 发布报文
351
+ * @param {Object|null} client 发布客户端,内部消息与代理自发布时为 null
352
+ */
353
+ _dispatchPublish(packet, client) {
354
+ if (this._isInternalTopic(packet.topic)) return;
355
+ if (!client && this._isSelfPublish(packet)) return;
356
+ this._handlePublish(client, packet);
357
+ }
358
+
359
+ /**
360
+ * 判断报文是否由代理自身发布(按报文上的自发布标记判断)
361
+ * @param {Object} packet aedes 发布报文
362
+ * @returns {boolean} 是否为代理自发布
363
+ */
364
+ _isSelfPublish(packet) {
365
+ return packet[SELF_PUBLISH] === true;
366
+ }
367
+
368
+ /**
369
+ * 判断是否为 broker 系统主题
370
+ * @param {string} topic 主题
371
+ * @returns {boolean} 是否为系统主题
372
+ */
373
+ _isInternalTopic(topic) {
374
+ return topic === '$SYS' || topic.startsWith('$SYS/');
375
+ }
376
+
377
+ /**
378
+ * 处理入站发布消息:交给事件管理器,返回发布描述时由代理重发
379
+ * @param {Object|null} client aedes client 实例,遗嘱等内部来源可能为 null
313
380
  * @param {Object} packet aedes 发布报文
314
381
  * @returns {Promise<void>} 无返回值
315
382
  */
316
383
  async _handlePublish(client, packet) {
317
- const params = {
318
- client_id: client.id,
384
+ const client_id = client ? client.id : null;
385
+ const params = this._buildParams(client_id, packet);
386
+ const ctx = this._buildContext(client_id, packet, client);
387
+ const options = client_id ? this._clientOptions(client_id) : {};
388
+ try {
389
+ const result = await this.#events.run(packet.topic, params, ctx, options);
390
+ const error = ctx.error || options.error;
391
+ if (error) this._emitError(error);
392
+ if (this._isRepublish(result)) {
393
+ await this.publish(result.topic, result.payload, result);
394
+ }
395
+ } catch (err) {
396
+ this._emitError(err);
397
+ }
398
+ }
399
+
400
+ /**
401
+ * 构建事件执行参数
402
+ * @param {string|null} client_id 客户端编号
403
+ * @param {Object} packet aedes 发布报文
404
+ * @returns {Object} 参数对象
405
+ */
406
+ _buildParams(client_id, packet) {
407
+ return {
408
+ client_id,
319
409
  topic: packet.topic,
320
410
  payload: packet.payload.toString('utf8'),
321
411
  qos: packet.qos,
322
412
  retain: packet.retain
323
413
  };
324
- const ctx = {
325
- client_id: client.id,
414
+ }
415
+
416
+ /**
417
+ * 构建事件执行上下文
418
+ * @param {string|null} client_id 客户端编号
419
+ * @param {Object} packet aedes 发布报文
420
+ * @param {Object|null} client aedes client 实例
421
+ * @returns {Object} 上下文对象
422
+ */
423
+ _buildContext(client_id, packet, client) {
424
+ return {
425
+ client_id,
326
426
  topic: packet.topic,
327
427
  qos: packet.qos,
328
428
  retain: packet.retain,
@@ -330,17 +430,6 @@ class MqttServer extends Mod {
330
430
  client,
331
431
  broker: this.#broker
332
432
  };
333
- const options = this._clientOptions(client.id);
334
- try {
335
- const result = await this.#events.run(packet.topic, params, ctx, options);
336
- const error = ctx.error || options.error;
337
- if (error) this._emitError(error);
338
- if (this._isRepublish(result)) {
339
- await this.publish(result.topic, result.payload, result);
340
- }
341
- } catch (err) {
342
- this._emitError(err);
343
- }
344
433
  }
345
434
 
346
435
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mm_mqtt_server",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "最薄的 MQTT 代理服务器入站适配器:消息直达 mm_event 事件管理器,不做路由与插件。",
5
5
  "keywords": [
6
6
  "超级美眉",