@lyrify/znl 0.4.1 → 0.5.2

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/README.md CHANGED
@@ -5,11 +5,17 @@
5
5
  ## 特性
6
6
 
7
7
  - 基于 `ROUTER/DEALER` 同时实现 RPC 请求-响应与 PUB/SUB 广播,一套连接两种模式
8
- - 内置请求 ID 匹配、超时控制、最大并发限制
8
+ - 自动处理并发消息匹配、自动处理心跳包、超时控制、最大并发限制
9
9
  - 支持 Master → Slave 主动发起请求(双向 RPC)
10
10
  - 基于 ROUTER 实现 PUB/SUB 广播,无需额外 socket 或端口
11
11
  - Slave 自动注册/注销,Master 实时感知在线节点
12
- - 可选的认证 Key 校验(注册与 RPC 请求均校验)
12
+ - 支持可选加密认证(签名 + 防重放 + AES-256-GCM 透明加密)
13
+ - 加密开关 `encrypted`:
14
+ - `false`:明文模式(不签名/不加密)
15
+ - `true`:签名 + 防重放 + payload 透明加密(AES-256-GCM)
16
+ - 可选关闭 payload 摘要校验(`enablePayloadDigest=false`)以提升性能
17
+ - 建议 master/slave 两端保持一致配置,避免认证不一致
18
+ - `authKey` 仅在 `encrypted=true` 时必填
13
19
  - Payload 支持 `string`、`Buffer`、`Uint8Array` 及其数组(多帧)
14
20
 
15
21
  ## 安装
@@ -36,6 +42,7 @@ const master = new ZNL({
36
42
  id: "master-1",
37
43
  endpoints: { router: "tcp://127.0.0.1:6003" },
38
44
  authKey: "your-shared-key",
45
+ encrypted: true, // 推荐:透明加密 + 防重放
39
46
  });
40
47
 
41
48
  // RPC:自动回复 slave 的请求
@@ -65,6 +72,7 @@ const slave = new ZNL({
65
72
  id: "slave-001",
66
73
  endpoints: { router: "tcp://127.0.0.1:6003" },
67
74
  authKey: "your-shared-key",
75
+ encrypted: true, // 需与 master 一致
68
76
  });
69
77
 
70
78
  // PUB/SUB:精确订阅(可在 start 前调用)
@@ -93,8 +101,14 @@ new ZNL({
93
101
  endpoints: {
94
102
  router: "tcp://127.0.0.1:6003",
95
103
  },
96
- maxPending: 0,
104
+ maxPending: 1000,
97
105
  authKey: "",
106
+ heartbeatInterval: 3000,
107
+ heartbeatTimeoutMs: 0,
108
+ encrypted: false,
109
+ enablePayloadDigest: true,
110
+ maxTimeSkewMs: 30000,
111
+ replayWindowMs: 120000,
98
112
  });
99
113
  ```
100
114
 
@@ -103,8 +117,14 @@ new ZNL({
103
117
  | `role` | ✓ | 节点角色,`"master"` 或 `"slave"` |
104
118
  | `id` | ✓ | 节点唯一标识;slave 端同时作为 ZMQ `routingId` |
105
119
  | `endpoints.router` | | ROUTER 端点,默认 `tcp://127.0.0.1:6003` |
106
- | `maxPending` | | 最大并发 RPC 请求数,`0` 表示不限制 |
107
- | `authKey` | | 可选共享认证 Key;master 开启后校验注册帧与 RPC 请求 |
120
+ | `maxPending` | | 最大并发 RPC 请求数,默认 `1000`;`0` 表示不限制 |
121
+ | `authKey` | | 共享认证 Key;仅在 `encrypted=true` 时必填(用于签名/加密) |
122
+ | `heartbeatInterval` | | 心跳间隔(毫秒),默认 `3000`,`0` 表示禁用心跳 |
123
+ | `heartbeatTimeoutMs` | | 心跳超时时间(毫秒),默认 `0` 表示使用 `heartbeatInterval × 3` |
124
+ | `encrypted` | | 是否启用加密:`false`(默认,明文) / `true`(签名+防重放+透明加密) |
125
+ | `enablePayloadDigest` | | 是否启用 payload 摘要校验,默认 `true`(关闭可提升性能) |
126
+ | `maxTimeSkewMs` | | 时间戳最大允许偏移(毫秒),默认 `30000`,用于防重放校验 |
127
+ | `replayWindowMs` | | nonce 重放缓存窗口(毫秒),默认 `120000` |
108
128
 
109
129
  ## API
110
130
 
@@ -199,7 +219,7 @@ console.log(master.slaves); // ["slave-001", "slave-002"]
199
219
  | `publish` | Slave | 收到 master 广播,携带 `{ topic, payload }` |
200
220
  | `slave_connected` | Master | slave 注册成功上线,携带 `slaveId` |
201
221
  | `slave_disconnected` | Master | slave 注销或发送失败下线,携带 `slaveId` |
202
- | `auth_failed` | Master | 认证失败(注册或 RPC),请求已被丢弃 |
222
+ | `auth_failed` | Master / Slave | 认证失败(签名校验失败、重放检测失败、解密失败等),请求已被丢弃 |
203
223
  | `error` | 两者 | 内部错误 |
204
224
 
205
225
  ## 本地示例
@@ -224,13 +244,23 @@ pnpm test
224
244
  ## 并发压测
225
245
 
226
246
  ```bash
227
- # 终端 1:启动 Echo 服务端
247
+ # 终端 1:启动 Echo 服务端(plain)
228
248
  pnpm test:echo
229
249
 
230
- # 终端 2:发起并发压测
250
+ # 终端 2:发起并发压测(plain)
231
251
  pnpm test:100 -- 100 10000 slave-001
232
252
  ```
233
253
 
254
+ 启用安全模式示例:
255
+
256
+ ```bash
257
+ # 终端 1:加密模式启动 Echo 服务端
258
+ ZNL_AUTH_KEY=my-secret ZNL_ENCRYPTED=true pnpm test:echo
259
+
260
+ # 终端 2:加密模式压测
261
+ pnpm test:100 -- 100 10000 slave-001 my-secret true
262
+ ```
263
+
234
264
  参数说明:
235
265
 
236
266
  - 总请求数
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyrify/znl",
3
- "version": "0.4.1",
3
+ "version": "0.5.2",
4
4
  "description": "ZNL - ZeroMQ Node Link",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -35,12 +35,24 @@
35
35
  "dependencies": {
36
36
  "zeromq": "^6.5.0"
37
37
  },
38
+ "devDependencies": {
39
+ "eslint": "^9.39.4",
40
+ "eslint-plugin-import": "^2.32.0",
41
+ "prettier": "^3.8.1"
42
+ },
38
43
  "scripts": {
39
44
  "example:master": "node test/master/index.js",
40
45
  "example:slave": "node test/slave/index.js",
41
46
  "test": "node test/run.js",
47
+ "test:unit": "node --test \"test/unit/**/*.test.js\"",
48
+ "test:all": "pnpm test && pnpm test:unit",
42
49
  "test:echo": "node test/master/test-echo-server.js",
43
50
  "test:100": "node test/slave/test-100-concurrent.js",
44
- "check": "node --check index.js && node --check src/constants.js && node --check src/protocol.js && node --check src/PendingManager.js && node --check src/SendQueue.js && node --check src/ZNL.js && node --check test/master/index.js && node --check test/slave/index.js && node --check test/master/test-echo-server.js && node --check test/slave/test-100-concurrent.js && node --check test/run.js"
51
+ "test:capture": "node test/capture-frames.js",
52
+ "check": "node --check index.js && node --check src/constants.js && node --check src/protocol.js && node --check src/security.js && node --check src/PendingManager.js && node --check src/SendQueue.js && node --check src/ZNL.js && node --check test/master/index.js && node --check test/slave/index.js && node --check test/master/test-echo-server.js && node --check test/slave/test-100-concurrent.js && node --check test/run.js",
53
+ "lint": "eslint .",
54
+ "lint:fix": "eslint . --fix",
55
+ "format": "prettier . --check",
56
+ "format:write": "prettier . --write"
45
57
  }
46
58
  }
package/src/SendQueue.js CHANGED
@@ -6,17 +6,15 @@
6
6
  * 虽然 zeromq.js v6 在 JS 层做了一定保护,但显式串行化更安全可靠。
7
7
  *
8
8
  * 实现原理:
9
- * 为每个 socket 通道维护一条 Promise 链(队列尾指针)。
10
- * 每次入队时,新任务追加在当前链尾,前一个任务完成后自动触发。
11
- * 无论前一个任务成功或失败,队列都会继续执行下一个任务。
9
+ * 为每个 socket 通道维护一个任务队列与独立消费循环。
10
+ * 通过单线程 async loop 串行执行,避免长 Promise 链增长。
12
11
  */
13
-
14
12
  export class SendQueue {
15
13
  /**
16
- * 各通道的队列尾指针
17
- * @type {Map<string, Promise<void>>}
14
+ * 通道级队列与运行状态
15
+ * @type {Map<string, { queue: Array<{ task: Function, resolve: Function, reject: Function }>, running: boolean }>}
18
16
  */
19
- #tails = new Map();
17
+ #channels = new Map();
20
18
 
21
19
  /**
22
20
  * 将发送任务追加到指定通道的队列尾部
@@ -26,22 +24,58 @@ export class SendQueue {
26
24
  * @returns {Promise<void>} 本次任务的 Promise(可用于错误捕获)
27
25
  */
28
26
  enqueue(channel, task) {
29
- const tail = this.#tails.get(channel) ?? Promise.resolve();
30
-
31
- // 无论前一个任务成功或失败,都继续执行当前任务
32
- const run = tail.then(task, task);
27
+ const name = String(channel);
28
+ const entry = this.#ensureChannel(name);
33
29
 
34
- // 更新队尾(吞掉错误,防止产生 UnhandledRejection)
35
- this.#tails.set(channel, run.catch(() => {}));
36
-
37
- return run;
30
+ return new Promise((resolve, reject) => {
31
+ entry.queue.push({ task, resolve, reject });
32
+ if (!entry.running) this.#drain(name, entry);
33
+ });
38
34
  }
39
35
 
40
36
  /**
41
37
  * 清空所有通道的队列引用(节点停止时调用)
42
- * 注意:已入队但未执行的任务不会被取消,仅释放尾指针引用
38
+ * 注意:已入队但未执行的任务不会被取消,仅释放引用
43
39
  */
44
40
  clear() {
45
- this.#tails.clear();
41
+ this.#channels.clear();
42
+ }
43
+
44
+ /**
45
+ * 确保通道数据结构存在
46
+ * @param {string} name
47
+ * @returns {{ queue: Array, running: boolean }}
48
+ */
49
+ #ensureChannel(name) {
50
+ let entry = this.#channels.get(name);
51
+ if (!entry) {
52
+ entry = { queue: [], running: false };
53
+ this.#channels.set(name, entry);
54
+ }
55
+ return entry;
56
+ }
57
+
58
+ /**
59
+ * 串行消费队列
60
+ * @param {string} name
61
+ * @param {{ queue: Array, running: boolean }} entry
62
+ */
63
+ async #drain(name, entry) {
64
+ entry.running = true;
65
+
66
+ while (entry.queue.length > 0) {
67
+ const { task, resolve, reject } = entry.queue.shift();
68
+ try {
69
+ await task();
70
+ resolve();
71
+ } catch (error) {
72
+ reject(error);
73
+ }
74
+ }
75
+
76
+ entry.running = false;
77
+
78
+ // 清理空通道,避免 Map 无限增长
79
+ if (entry.queue.length === 0) this.#channels.delete(name);
46
80
  }
47
81
  }