mm_os 4.3.6 → 5.0.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.
Files changed (234) hide show
  1. package/README.md +101 -504
  2. package/index.js +26 -19
  3. package/lib/loader.js +787 -0
  4. package/lib/os.js +369 -0
  5. package/package.json +55 -80
  6. package/LICENSE +0 -21
  7. package/README_EN.md +0 -505
  8. package/adapter/adapter.js +0 -484
  9. package/adapter/custom_persistence.js +0 -661
  10. package/adapter/mqtt.js +0 -254
  11. package/adapter/socket.js +0 -244
  12. package/adapter/web.js +0 -92
  13. package/adapter/websocket.js +0 -43
  14. package/ai.js +0 -130
  15. package/com/api/README.md +0 -3
  16. package/com/api/com.json +0 -5
  17. package/com/api/config.tpl.json +0 -41
  18. package/com/api/drive.js +0 -732
  19. package/com/api/index.js +0 -201
  20. package/com/api/oauth.js +0 -200
  21. package/com/api/script.tpl.js +0 -32
  22. package/com/cmd/README.md +0 -11
  23. package/com/cmd/com.json +0 -5
  24. package/com/cmd/config.tpl.json +0 -48
  25. package/com/cmd/drive.js +0 -176
  26. package/com/cmd/index.js +0 -287
  27. package/com/cmd/job/config.json +0 -66
  28. package/com/cmd/job/index.js +0 -241
  29. package/com/cmd/job/test.js +0 -188
  30. package/com/cmd/msg.json +0 -48
  31. package/com/cmd/script.tpl.js +0 -12
  32. package/com/db/README.md +0 -3
  33. package/com/db/com.json +0 -5
  34. package/com/db/drive.js +0 -2206
  35. package/com/db/index.js +0 -241
  36. package/com/event/README.md +0 -4
  37. package/com/event/com.json +0 -5
  38. package/com/event/config.tpl.json +0 -20
  39. package/com/event/drive.js +0 -61
  40. package/com/event/index.js +0 -438
  41. package/com/event/script.tpl.js +0 -23
  42. package/com/mqtt/README.md +0 -47
  43. package/com/mqtt/com.json +0 -5
  44. package/com/mqtt/config.tpl.json +0 -42
  45. package/com/mqtt/drive.js +0 -696
  46. package/com/mqtt/index.js +0 -1017
  47. package/com/mqtt/index.test.js +0 -503
  48. package/com/mqtt/mm_mqtt.js +0 -446
  49. package/com/mqtt/script.tpl.js +0 -719
  50. package/com/nav/README.md +0 -2
  51. package/com/nav/com.json +0 -5
  52. package/com/nav/config.tpl.json +0 -84
  53. package/com/nav/drive.js +0 -702
  54. package/com/nav/index.js +0 -228
  55. package/com/nav/tpl/admin_pc/page_channel.vue +0 -305
  56. package/com/nav/tpl/admin_pc/page_config.vue +0 -280
  57. package/com/nav/tpl/admin_pc/page_config_form.vue +0 -194
  58. package/com/nav/tpl/admin_pc/page_default.vue +0 -247
  59. package/com/nav/tpl/admin_pc/page_form.vue +0 -180
  60. package/com/nav/tpl/admin_pc/page_lang.vue +0 -264
  61. package/com/nav/tpl/admin_pc/page_nav.vue +0 -321
  62. package/com/nav/tpl/admin_pc/page_table.vue +0 -296
  63. package/com/nav/tpl/admin_pc/page_type.vue +0 -321
  64. package/com/nav/tpl/admin_pc/page_view.vue +0 -124
  65. package/com/nav/tpl/dev_pc/page_channel.vue +0 -253
  66. package/com/nav/tpl/dev_pc/page_config.vue +0 -253
  67. package/com/nav/tpl/dev_pc/page_default.vue +0 -247
  68. package/com/nav/tpl/dev_pc/page_form.vue +0 -162
  69. package/com/nav/tpl/dev_pc/page_nav.vue +0 -253
  70. package/com/nav/tpl/dev_pc/page_table.vue +0 -247
  71. package/com/nav/tpl/dev_pc/page_type.vue +0 -313
  72. package/com/nav/tpl/home_pc/page_channel.vue +0 -234
  73. package/com/nav/tpl/home_pc/page_default.vue +0 -234
  74. package/com/nav/tpl/home_pc/page_form.vue +0 -137
  75. package/com/nav/tpl/home_pc/page_list.vue +0 -234
  76. package/com/nav/tpl/home_pc/page_nav.vue +0 -221
  77. package/com/nav/tpl/home_pc/page_type.vue +0 -234
  78. package/com/nav/tpl/home_pc/page_view.vue +0 -125
  79. package/com/nav/tpl/home_phone/page_channel.vue +0 -234
  80. package/com/nav/tpl/home_phone/page_default.vue +0 -234
  81. package/com/nav/tpl/home_phone/page_form.vue +0 -137
  82. package/com/nav/tpl/home_phone/page_list.vue +0 -234
  83. package/com/nav/tpl/home_phone/page_nav.vue +0 -237
  84. package/com/nav/tpl/home_phone/page_type.vue +0 -234
  85. package/com/nav/tpl/home_phone/page_view.vue +0 -125
  86. package/com/nav/viewmodel.js +0 -446
  87. package/com/param/README.md +0 -2
  88. package/com/param/com.json +0 -5
  89. package/com/param/config.tpl.json +0 -136
  90. package/com/param/drive.js +0 -502
  91. package/com/param/index.js +0 -158
  92. package/com/param/script.tpl.js +0 -12
  93. package/com/pendant/README.md +0 -3
  94. package/com/pendant/com.json +0 -5
  95. package/com/pendant/config.tpl.json +0 -32
  96. package/com/pendant/drive.js +0 -204
  97. package/com/pendant/index.js +0 -467
  98. package/com/pendant/pendant.html +0 -16
  99. package/com/pendant/script.tpl.js +0 -18
  100. package/com/socket/README.md +0 -47
  101. package/com/socket/com.json +0 -5
  102. package/com/socket/config.tpl.json +0 -12
  103. package/com/socket/drive.js +0 -679
  104. package/com/socket/index.js +0 -402
  105. package/com/socket/script.tpl.js +0 -41
  106. package/com/sql/README.md +0 -3
  107. package/com/sql/com.json +0 -5
  108. package/com/sql/config.tpl.json +0 -74
  109. package/com/sql/drive.js +0 -1283
  110. package/com/sql/index.js +0 -152
  111. package/com/sql/script.tpl.js +0 -47
  112. package/com/static/README.md +0 -2
  113. package/com/static/com.json +0 -5
  114. package/com/static/config.tpl.json +0 -24
  115. package/com/static/drive.js +0 -194
  116. package/com/static/index.js +0 -237
  117. package/com/static/script.tpl.js +0 -28
  118. package/com/task/README.md +0 -3
  119. package/com/task/com.json +0 -5
  120. package/com/task/config.tpl.json +0 -24
  121. package/com/task/drive.js +0 -428
  122. package/com/task/index.js +0 -159
  123. package/com/task/script.tpl.js +0 -40
  124. package/com/template/com.json +0 -5
  125. package/com/template/config.tpl.json +0 -16
  126. package/com/template/drive.js +0 -81
  127. package/com/template/index.js +0 -141
  128. package/com.js +0 -237
  129. package/common/README.md +0 -2
  130. package/common/middleware/web_after/index.js +0 -26
  131. package/common/middleware/web_after/middleware.json +0 -9
  132. package/common/middleware/web_base/index.js +0 -113
  133. package/common/middleware/web_base/middleware.json +0 -19
  134. package/common/middleware/web_before/index.js +0 -33
  135. package/common/middleware/web_before/middleware.json +0 -9
  136. package/common/middleware/web_cors/index.js +0 -87
  137. package/common/middleware/web_cors/middleware.json +0 -24
  138. package/common/middleware/web_error/index.js +0 -131
  139. package/common/middleware/web_error/middleware.json +0 -19
  140. package/common/middleware/web_ip/index.js +0 -15
  141. package/common/middleware/web_ip/middleware.json +0 -14
  142. package/common/middleware/web_logger/index.js +0 -156
  143. package/common/middleware/web_logger/middleware.json +0 -14
  144. package/common/middleware/web_main/index.js +0 -26
  145. package/common/middleware/web_main/middleware.json +0 -9
  146. package/common/middleware/web_static/index.js +0 -73
  147. package/common/middleware/web_static/middleware.json +0 -54
  148. package/common/middleware/web_waf/index.js +0 -385
  149. package/common/middleware/web_waf/middleware.json +0 -13
  150. package/core/app/config.tpl.json +0 -67
  151. package/core/app/index.js +0 -710
  152. package/core/app/script.tpl.js +0 -52
  153. package/core/channel/index.js +0 -899
  154. package/core/channel/matcher.js +0 -585
  155. package/core/com/config.tpl.json +0 -16
  156. package/core/com/index.js +0 -77
  157. package/core/com/script.tpl.js +0 -5
  158. package/core/component/component.js +0 -42
  159. package/core/component/config.tpl.json +0 -63
  160. package/core/component/index.js +0 -273
  161. package/core/component/script.tpl.js +0 -19
  162. package/core/controller/config.tpl.json +0 -14
  163. package/core/controller/index.js +0 -371
  164. package/core/controller/script.tpl.js +0 -27
  165. package/core/factory/config.tpl.json +0 -14
  166. package/core/factory/entity.js +0 -275
  167. package/core/factory/index.js +0 -241
  168. package/core/factory/script.tpl.js +0 -16
  169. package/core/game/bat/index.js +0 -137
  170. package/core/game/bat/world.js +0 -622
  171. package/core/game/config.tpl.json +0 -16
  172. package/core/game/entity_admin.js +0 -230
  173. package/core/game/index.js +0 -186
  174. package/core/handler/config.tpl.json +0 -22
  175. package/core/handler/index.js +0 -181
  176. package/core/handler/script.tpl.js +0 -23
  177. package/core/logic/config.tpl.json +0 -14
  178. package/core/logic/index.js +0 -59
  179. package/core/logic/script.tpl.js +0 -19
  180. package/core/middleware/config.tpl.json +0 -16
  181. package/core/middleware/index.js +0 -117
  182. package/core/middleware/script.tpl.js +0 -37
  183. package/core/mod/config.tpl.json +0 -22
  184. package/core/mod/index.js +0 -136
  185. package/core/mod/script.tpl.js +0 -34
  186. package/core/model/config.tpl.json +0 -219
  187. package/core/model/index.js +0 -272
  188. package/core/model/model.js +0 -27
  189. package/core/model/script.tpl.js +0 -20
  190. package/core/notifier/config.tpl.json +0 -14
  191. package/core/notifier/index.js +0 -77
  192. package/core/notifier/script.tpl.js +0 -20
  193. package/core/plugin/config.tpl.json +0 -36
  194. package/core/plugin/index.js +0 -297
  195. package/core/plugin/script.tpl.js +0 -51
  196. package/core/pusher/config.tpl.json +0 -14
  197. package/core/pusher/index.js +0 -158
  198. package/core/pusher/script.tpl.js +0 -20
  199. package/core/room/bat/index.js +0 -170
  200. package/core/room/bat/room.js +0 -524
  201. package/core/room/config.tpl.json +0 -20
  202. package/core/room/index.js +0 -249
  203. package/core/room/room.js +0 -61
  204. package/core/scene/config.tpl.json +0 -14
  205. package/core/scene/index.js +0 -467
  206. package/core/scene/loop.js +0 -1306
  207. package/core/scene/map.js +0 -28
  208. package/core/scene/script.tpl.js +0 -22
  209. package/core/sender/config.tpl.json +0 -14
  210. package/core/sender/index.js +0 -79
  211. package/core/sender/script.tpl.js +0 -20
  212. package/core/service/config.tpl.json +0 -14
  213. package/core/service/index.js +0 -100
  214. package/core/service/script.tpl.js +0 -25
  215. package/core/store/config.tpl.json +0 -26
  216. package/core/store/index.js +0 -1794
  217. package/core/store/script.tpl.js +0 -22
  218. package/core/store/sql.js +0 -1488
  219. package/core/system/config.tpl.json +0 -18
  220. package/core/system/index.js +0 -312
  221. package/core/system/script.tpl.js +0 -77
  222. package/core/view/config.tpl.json +0 -14
  223. package/core/view/index.js +0 -91
  224. package/core/view/script.tpl.js +0 -20
  225. package/core/zone/bat/index.js +0 -744
  226. package/core/zone/config.tpl.json +0 -54
  227. package/core/zone/index.js +0 -655
  228. package/core/zone/script.tpl.js +0 -10
  229. package/core/zone/zone_bat.js +0 -136
  230. package/core//345/237/272/347/261/273/346/250/241/345/235/227/346/270/205/345/215/225.md +0 -24
  231. package/os.js +0 -53
  232. package/server.js +0 -595
  233. package/ulits/id_gen.js +0 -134
  234. package/ulits/word.js +0 -206
@@ -1,503 +0,0 @@
1
- /**
2
- * MQTT模块单元测试
3
- * 测试修复的线程阻塞点:match缓存、dict_msg O(1)查找、并行处理
4
- */
5
-
6
- // Mock 框架扩展方法
7
- String.prototype.fullname = function (base) {
8
- return base ? base + '/' + this : this;
9
- };
10
- String.prototype.hasDir = function () {
11
- return false;
12
- };
13
- Array.prototype.getObj = function () {
14
- return null;
15
- };
16
- Array.prototype.to2D = function (size) {
17
- var result = [];
18
- for (var i = 0; i < this.length; i += size) {
19
- result.push(this.slice(i, i + size));
20
- }
21
- return result;
22
- };
23
-
24
- // Mock global $ 对象
25
- global.$ = {
26
- sleep: jest.fn(() => Promise.resolve()),
27
- pool: {},
28
- val: { scope: 'test' },
29
- admin: {
30
- mqtt: null,
31
- sql: jest.fn(() => ({
32
- db: jest.fn(() => ({
33
- get: jest.fn(() => Promise.resolve([])),
34
- set: jest.fn(() => Promise.resolve()),
35
- })),
36
- })),
37
- },
38
- };
39
-
40
- // Mock mm_machine Manager
41
- const mockGetMods = jest.fn(() => ({}));
42
- const mockGetInfos = jest.fn(() => []);
43
- const mockGetMod = jest.fn(() => null);
44
- const mockLog = jest.fn();
45
-
46
- jest.mock('mm_machine', () => {
47
- class MockManager {
48
- constructor(config, parent) {
49
- this.config = config;
50
- this.parent = parent;
51
- }
52
- }
53
- MockManager.prototype.getMods = mockGetMods;
54
- MockManager.prototype.getInfos = mockGetInfos;
55
- MockManager.prototype.getMod = mockGetMod;
56
- MockManager.prototype.log = mockLog;
57
- MockManager.prototype.getEventer = jest.fn(() => ({}));
58
- MockManager.prototype.getParentKey = jest.fn(() => 'Mqtt_test');
59
- MockManager.prototype.getBaseDir = jest.fn(() => __dirname);
60
- MockManager.prototype.getTplDir = jest.fn(() => __dirname);
61
- MockManager.prototype.getDirName = jest.fn(() => 'mqtt_test');
62
- return { Manager: MockManager, Drive: class MockDrive {}, MODE: { HOT_RELOAD: 3 } };
63
- });
64
-
65
- jest.mock('mqtt', () => ({
66
- connect: jest.fn(() => ({
67
- on: jest.fn(),
68
- subscribe: jest.fn(),
69
- unsubscribe: jest.fn(),
70
- publish: jest.fn(),
71
- end: jest.fn(),
72
- removeAllListeners: jest.fn(),
73
- options: { clientId: 'test_client' },
74
- })),
75
- }));
76
-
77
- const { Mqtt } = require('./index');
78
-
79
- describe('Mqtt 类', () => {
80
- let mqtt;
81
-
82
- beforeEach(() => {
83
- jest.clearAllMocks();
84
- mqtt = new Mqtt({ name: 'test' });
85
- mqtt._preset();
86
- });
87
-
88
- describe('match() 正则缓存', () => {
89
- test('完全匹配应返回true', () => {
90
- expect(mqtt.match('face/device1/status', 'face/device1/status')).toBe(true);
91
- });
92
-
93
- test('+通配符匹配', () => {
94
- expect(mqtt.match('face/device1/status', 'face/+/status')).toBe(true);
95
- });
96
-
97
- test('#通配符匹配', () => {
98
- expect(mqtt.match('face/device1/status/extra', 'face/device1/#')).toBe(true);
99
- });
100
-
101
- test('不匹配应返回false', () => {
102
- expect(mqtt.match('other/topic', 'face/+/status')).toBe(false);
103
- });
104
-
105
- test('正则缓存生效:相同模式复用RegExp', () => {
106
- mqtt.match('face/device1/status', 'face/+/status');
107
- mqtt.match('face/device2/status', 'face/+/status');
108
- // 缓存中应有该模式
109
- expect(mqtt._regex_cache['face/+/status']).toBeDefined();
110
- expect(mqtt._regex_cache['face/+/status'] instanceof RegExp).toBe(true);
111
- });
112
- });
113
-
114
- describe('dict_msg O(1)查找', () => {
115
- test('req() 应同步维护 dict_msg', () => {
116
- mqtt.client = {
117
- options: { clientId: 'test' },
118
- publish: jest.fn(),
119
- on: jest.fn(),
120
- };
121
- mqtt.send = jest.fn();
122
- var callback = jest.fn();
123
- mqtt.req('test/topic', 'testMethod', { key: 'val' }, callback);
124
-
125
- expect(mqtt.list_msg.length).toBe(1);
126
- expect(mqtt.dict_msg[mqtt.list_msg[0].id]).toBeDefined();
127
- expect(mqtt.dict_msg[mqtt.list_msg[0].id].func).toBe(callback);
128
- });
129
-
130
- test('_handleResponse() O(1) 查找并执行回调', () => {
131
- var callback = jest.fn();
132
- var msg_id = 'test_id_123';
133
- mqtt.dict_msg[msg_id] = {
134
- id: msg_id,
135
- func: callback,
136
- timer: null,
137
- };
138
- mqtt.list_msg.push({ id: msg_id, func: callback });
139
-
140
- mqtt._handleResponse({ id: msg_id, result: 'ok' });
141
-
142
- expect(callback).toHaveBeenCalledWith({ id: msg_id, result: 'ok' });
143
- // 消息应从dict_msg中删除
144
- expect(mqtt.dict_msg[msg_id]).toBeUndefined();
145
- });
146
-
147
- test('_handleResponse() 无id时应安全返回', () => {
148
- expect(() => mqtt._handleResponse(null)).not.toThrow();
149
- expect(() => mqtt._handleResponse({})).not.toThrow();
150
- });
151
-
152
- test('_delMsgById() 同步清理 dict_msg 和 list_msg', () => {
153
- var msg_id = 'del_test_123';
154
- mqtt.dict_msg[msg_id] = { id: msg_id, timer: null };
155
- mqtt.list_msg.push({ id: msg_id });
156
-
157
- mqtt._delMsgById(msg_id);
158
-
159
- expect(mqtt.dict_msg[msg_id]).toBeUndefined();
160
- expect(mqtt.list_msg.length).toBe(0);
161
- });
162
-
163
- test('_delMsgById() 清除关联定时器', () => {
164
- var clear_fn = jest.spyOn(global, 'clearTimeout');
165
- var msg_id = 'timer_test_123';
166
- var mock_timer = { _id: 999 };
167
- mqtt.dict_msg[msg_id] = { id: msg_id, timer: mock_timer };
168
- mqtt.list_msg.push({ id: msg_id });
169
-
170
- mqtt._delMsgById(msg_id);
171
-
172
- expect(clear_fn).toHaveBeenCalledWith(mock_timer);
173
- clear_fn.mockRestore();
174
- });
175
- });
176
-
177
- describe('_collectTopicPromises() 收集匹配主题', () => {
178
- test('应收集所有匹配主题的handler Promise', () => {
179
- var mockHandle = jest.fn(() => Promise.resolve('handled'));
180
- var mockMod = {
181
- config: { state: 1, topic: ['face/+/status', 'face/+/event'] },
182
- call: jest.fn((method, ...args) => {
183
- if (method === 'handle') {
184
- return mockHandle(...args);
185
- }
186
- return Promise.resolve();
187
- }),
188
- };
189
- mockGetMods.mockReturnValue({ mod1: mockMod });
190
-
191
- var result = mqtt._collectTopicPromises('face/device1/status', { data: 'test' });
192
-
193
- expect(result.promises.length).toBe(1);
194
- expect(result.topic).toBe('face/+/event');
195
- });
196
-
197
- test('应跳过 state !== 1 的mod', () => {
198
- var mockMod = {
199
- config: { state: 0, topic: ['face/+/status'] },
200
- call: jest.fn(),
201
- };
202
- mockGetMods.mockReturnValue({ mod1: mockMod });
203
-
204
- var result = mqtt._collectTopicPromises('face/device1/status', {});
205
-
206
- expect(result.promises.length).toBe(0);
207
- });
208
-
209
- test('应跳过无topic的mod', () => {
210
- var mockMod = {
211
- config: { state: 1 },
212
- call: jest.fn(),
213
- };
214
- mockGetMods.mockReturnValue({ mod1: mockMod });
215
-
216
- var result = mqtt._collectTopicPromises('face/device1/status', {});
217
-
218
- expect(result.promises.length).toBe(0);
219
- });
220
- });
221
-
222
- describe('_processTopics() 并行处理', () => {
223
- test('应并行执行多个handler', async () => {
224
- var call_order = [];
225
- var mockMod1 = {
226
- config: { state: 1, topic: ['face/+/status'] },
227
- call: jest.fn((method) => {
228
- if (method === 'handle') {
229
- return new Promise((resolve) => {
230
- setTimeout(() => {
231
- call_order.push('mod1');
232
- resolve('result1');
233
- }, 50);
234
- });
235
- }
236
- return Promise.resolve();
237
- }),
238
- };
239
- var mockMod2 = {
240
- config: { state: 1, topic: ['face/+/status'] },
241
- call: jest.fn((method) => {
242
- if (method === 'handle') {
243
- return new Promise((resolve) => {
244
- setTimeout(() => {
245
- call_order.push('mod2');
246
- resolve('result2');
247
- }, 10);
248
- });
249
- }
250
- return Promise.resolve();
251
- }),
252
- };
253
- mockGetMods.mockReturnValue({ mod1: mockMod1, mod2: mockMod2 });
254
-
255
- var result = await mqtt._processTopics('face/device1/status', {});
256
-
257
- // mod2(10ms)应在mod1(50ms)之前完成,证明是并行执行的
258
- expect(call_order[0]).toBe('mod2');
259
- expect(call_order[1]).toBe('mod1');
260
- // Promise.allSettled 保持输入顺序,从后往前取最后一个非空结果
261
- // 输入顺序: [mod1, mod2],所以 result = 'result2'
262
- expect(result.result).toBe('result2');
263
- expect(result.topic).toBe('face/+/status');
264
- });
265
-
266
- test('一个handler失败不应影响其他', async () => {
267
- var mod2_called = false;
268
- var mockMod1 = {
269
- config: { state: 1, topic: ['face/+/status'] },
270
- call: jest.fn((method) => {
271
- if (method === 'handle') {
272
- return Promise.reject(new Error('mod1 failed'));
273
- }
274
- return Promise.resolve();
275
- }),
276
- };
277
- var mockMod2 = {
278
- config: { state: 1, topic: ['face/+/status'] },
279
- call: jest.fn((method) => {
280
- if (method === 'handle') {
281
- mod2_called = true;
282
- return Promise.resolve('result2');
283
- }
284
- return Promise.resolve();
285
- }),
286
- };
287
- mockGetMods.mockReturnValue({ mod1: mockMod1, mod2: mockMod2 });
288
-
289
- var result = await mqtt._processTopics('face/device1/status', {});
290
-
291
- expect(mod2_called).toBe(true);
292
- expect(result.result).toBe('result2');
293
- });
294
- });
295
-
296
- describe('updateAfter() 并行初始化', () => {
297
- test('应并行执行所有mod的init', async () => {
298
- var init_order = [];
299
- var createMod = (name, delay) => ({
300
- config: { name },
301
- do: jest.fn((method) => {
302
- if (method === 'init') {
303
- return new Promise((resolve) => {
304
- setTimeout(() => {
305
- init_order.push(name);
306
- resolve();
307
- }, delay);
308
- });
309
- }
310
- return Promise.resolve();
311
- }),
312
- });
313
-
314
- mockGetInfos.mockReturnValue([
315
- { name: 'mod1', state: 1 },
316
- { name: 'mod2', state: 1 },
317
- ]);
318
- mockGetMod.mockImplementation((name) => {
319
- if (name === 'mod1') return createMod('mod1', 50);
320
- if (name === 'mod2') return createMod('mod2', 10);
321
- return null;
322
- });
323
-
324
- await mqtt.updateAfter();
325
-
326
- // mod2(10ms)应在mod1(50ms)之前完成
327
- expect(init_order[0]).toBe('mod2');
328
- expect(init_order[1]).toBe('mod1');
329
- });
330
- });
331
-
332
- describe('_updateDeviceStatus() 并行DB写入', () => {
333
- test('应并行执行所有saveOnline调用', async () => {
334
- mqtt.saveOnline = jest.fn(() => Promise.resolve());
335
-
336
- var list_offline = ['dev1', 'dev2', 'dev3', 'dev4'];
337
- var list_online = ['dev5', 'dev6'];
338
-
339
- await mqtt._updateDeviceStatus(list_offline, list_online);
340
-
341
- // saveOnline 应被调用(批次不固定,取决于to2D分组)
342
- expect(mqtt.saveOnline).toHaveBeenCalled();
343
- var calls = mqtt.saveOnline.mock.calls;
344
- // 验证离线设备传入online=0
345
- var off_calls = calls.filter((c) => c[1] === 0);
346
- expect(off_calls.length).toBeGreaterThan(0);
347
- // 验证在线设备传入online=1
348
- var on_calls = calls.filter((c) => c[1] === 1);
349
- expect(on_calls.length).toBeGreaterThan(0);
350
- });
351
- });
352
-
353
- describe('start() 定时器启动', () => {
354
- test('start() 不应包含setTimeout延迟', () => {
355
- var setTimeout_spy = jest.spyOn(global, 'setTimeout');
356
- mockGetInfos.mockReturnValue([]);
357
-
358
- // start() 内部会调用_createClient,但connect会失败
359
- // 我们只验证没有setTimeout调用
360
- mqtt._createClient = jest.fn(() => {
361
- mqtt.client = { on: true };
362
- });
363
- mqtt._setupClientEvents = jest.fn(() => Promise.resolve('connected'));
364
-
365
- mqtt.start();
366
-
367
- // 验证start()内部没有调用setTimeout
368
- var start_calls = setTimeout_spy.mock.calls.filter(
369
- (c) => typeof c[1] === 'number' && c[1] === 2000,
370
- );
371
- expect(start_calls.length).toBe(0);
372
-
373
- setTimeout_spy.mockRestore();
374
- });
375
- });
376
-
377
- describe('多厂商并行处理场景 (模拟 example/app/face/mqtt/)', () => {
378
- /**
379
- * 模拟真实场景:多个设备协议mod同时订阅同一主题
380
- * 例如 face/+/status 主题被 DA、TS、EF 三个厂商mod同时匹配
381
- * 每个mod的handler可能执行DB查询、HTTP请求、日志记录等耗时操作
382
- */
383
- test('多个厂商mod应并行处理同一条消息', async () => {
384
- var call_order = [];
385
-
386
- // 创建模拟mod的工厂函数
387
- var createMod = (name, delay) => ({
388
- config: {
389
- name,
390
- state: 1,
391
- topic: ['face/+/status', 'face/+/event/recognition'],
392
- },
393
- call: jest.fn((method, push_topic, msg, topic, n) => {
394
- if (method === 'handle') {
395
- return new Promise((resolve) => {
396
- setTimeout(() => {
397
- call_order.push(name);
398
- // 模拟真实场景:handler返回处理结果
399
- resolve({ result: 'ok', mod: name });
400
- }, delay);
401
- });
402
- }
403
- return Promise.resolve();
404
- }),
405
- });
406
-
407
- // 模拟5个厂商mod,各自有不同的处理耗时
408
- mockGetMods.mockReturnValue({
409
- da: createMod('DA', 80), // RV1109云对接协议 - 较慢
410
- ts: createMod('TS', 30), // 自定设备 - 中等
411
- ef: createMod('EF', 60), // 另一个厂商
412
- df: createMod('DF', 10), // 快速
413
- zs: createMod('ZS', 110), // 最慢(可能涉及HTTP请求)
414
- });
415
-
416
- var result = await mqtt._processTopics('face/device1/status', {
417
- cmd: 'reportVisitRecord',
418
- param: { CustomID: 'test_user', Name: '测试用户' },
419
- });
420
-
421
- // 验证并行执行:最快的DF(10ms)先完成,ZS(110ms)最后完成
422
- expect(call_order[0]).toBe('DF');
423
- expect(call_order[call_order.length - 1]).toBe('ZS');
424
- // 所有5个mod都被调用了
425
- expect(call_order.length).toBe(5);
426
- // 有返回结果
427
- expect(result.result).toBeDefined();
428
- expect(result.result.mod).toBeDefined();
429
- });
430
-
431
- test('一个厂商handler失败不应影响其他厂商', async () => {
432
- var called = [];
433
-
434
- var createMod = (name, should_fail) => ({
435
- config: {
436
- name,
437
- state: 1,
438
- topic: ['face/+/event/recognition'],
439
- },
440
- call: jest.fn((method) => {
441
- if (method === 'handle') {
442
- if (should_fail) {
443
- return Promise.reject(new Error(name + ' handler failed'));
444
- }
445
- called.push(name);
446
- return Promise.resolve({ result: 'ok', mod: name });
447
- }
448
- return Promise.resolve();
449
- }),
450
- });
451
-
452
- mockGetMods.mockReturnValue({
453
- da: createMod('DA', false),
454
- ts: createMod('TS', true), // TS 厂商handler失败
455
- ef: createMod('EF', false),
456
- df: createMod('DF', false),
457
- zs: createMod('ZS', false),
458
- });
459
-
460
- var result = await mqtt._processTopics('face/device1/event/recognition', {
461
- cmd: 'reportVisitRecord',
462
- param: { CustomID: 'test_user' },
463
- });
464
-
465
- // TS失败但其他4个厂商都被调用了
466
- expect(called.length).toBe(4);
467
- expect(called).toContain('DA');
468
- expect(called).toContain('EF');
469
- expect(called).toContain('DF');
470
- expect(called).toContain('ZS');
471
- expect(called).not.toContain('TS');
472
- // 有返回结果
473
- expect(result.result).toBeDefined();
474
- });
475
-
476
- test('全部厂商handler失败应返回空结果', async () => {
477
- var createMod = (name) => ({
478
- config: {
479
- name,
480
- state: 1,
481
- topic: ['face/+/status'],
482
- },
483
- call: jest.fn((method) => {
484
- if (method === 'handle') {
485
- return Promise.reject(new Error(name + ' failed'));
486
- }
487
- return Promise.resolve();
488
- }),
489
- });
490
-
491
- mockGetMods.mockReturnValue({
492
- da: createMod('DA'),
493
- ts: createMod('TS'),
494
- });
495
-
496
- var result = await mqtt._processTopics('face/device1/status', {});
497
-
498
- // 所有mod都失败,result应为undefined
499
- expect(result.result).toBeUndefined();
500
- expect(result.topic).toBeDefined();
501
- });
502
- });
503
- });