@lyrify/znl 0.4.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.
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/index.js +1 -0
- package/package.json +46 -0
- package/src/PendingManager.js +158 -0
- package/src/SendQueue.js +47 -0
- package/src/ZNL.js +773 -0
- package/src/constants.js +42 -0
- package/src/protocol.js +277 -0
package/src/ZNL.js
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZNL —— ZeroMQ 集群节点主类
|
|
3
|
+
*
|
|
4
|
+
* 封装 ROUTER/DEALER 双向通信,对外提供:
|
|
5
|
+
* - DEALER() slave 侧发 RPC 请求 / 注册自动回复处理器
|
|
6
|
+
* - ROUTER() master 侧发 RPC 请求 / 注册自动回复处理器
|
|
7
|
+
* - publish() master 侧向所有在线 slave 广播消息(模拟 PUB)
|
|
8
|
+
* - subscribe() slave 侧订阅指定 topic
|
|
9
|
+
* - unsubscribe() slave 侧取消订阅
|
|
10
|
+
* - start() / stop() 生命周期管理
|
|
11
|
+
* - EventEmitter 事件总线:
|
|
12
|
+
* router / dealer / request / response / message
|
|
13
|
+
* auth_failed / slave_connected / slave_disconnected
|
|
14
|
+
* publish / error
|
|
15
|
+
*
|
|
16
|
+
* 心跳机制:
|
|
17
|
+
* - slave 每隔 heartbeatInterval ms 向 master 发送一帧心跳
|
|
18
|
+
* - master 每隔 heartbeatInterval ms 扫描一次在线列表
|
|
19
|
+
* - 超过 heartbeatInterval × 3 ms 未收到心跳,判定 slave 已崩溃并移除
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { randomUUID } from "node:crypto";
|
|
23
|
+
import { EventEmitter } from "node:events";
|
|
24
|
+
import * as zmq from "zeromq";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_ENDPOINTS,
|
|
28
|
+
DEFAULT_HEARTBEAT_INTERVAL,
|
|
29
|
+
CONTROL_PREFIX,
|
|
30
|
+
CONTROL_HEARTBEAT,
|
|
31
|
+
} from "./constants.js";
|
|
32
|
+
import {
|
|
33
|
+
identityToString,
|
|
34
|
+
identityToBuffer,
|
|
35
|
+
normalizeFrames,
|
|
36
|
+
payloadFromFrames,
|
|
37
|
+
buildRegisterFrames,
|
|
38
|
+
buildUnregisterFrames,
|
|
39
|
+
buildRequestFrames,
|
|
40
|
+
buildResponseFrames,
|
|
41
|
+
buildPublishFrames,
|
|
42
|
+
parseControlFrames,
|
|
43
|
+
} from "./protocol.js";
|
|
44
|
+
import { PendingManager } from "./PendingManager.js";
|
|
45
|
+
import { SendQueue } from "./SendQueue.js";
|
|
46
|
+
|
|
47
|
+
export class ZNL extends EventEmitter {
|
|
48
|
+
// ─── 节点配置(构造后只读)────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/** @type {"master"|"slave"} */
|
|
51
|
+
role;
|
|
52
|
+
/** @type {string} 节点唯一标识,slave 侧同时作为 ZMQ routingId */
|
|
53
|
+
id;
|
|
54
|
+
/** @type {{ router: string }} */
|
|
55
|
+
endpoints;
|
|
56
|
+
/** @type {string} 认证 Key(空字符串表示不启用认证) */
|
|
57
|
+
authKey;
|
|
58
|
+
/** @type {boolean} master 侧是否强制校验认证 Key */
|
|
59
|
+
requireAuth;
|
|
60
|
+
|
|
61
|
+
// ─── 运行状态 ─────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/** @type {boolean} 当前节点是否处于运行状态 */
|
|
64
|
+
running = false;
|
|
65
|
+
|
|
66
|
+
// ─── 私有运行时字段 ───────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/** @type {{ router?: import("zeromq").Router, dealer?: import("zeromq").Dealer }} */
|
|
69
|
+
#sockets = {};
|
|
70
|
+
|
|
71
|
+
/** @type {Promise[]} 所有 socket 读取循环的 Promise 引用(用于 teardown 等待) */
|
|
72
|
+
#readLoops = [];
|
|
73
|
+
|
|
74
|
+
/** @type {Promise<void>|null} 正在进行的 start 操作(去重用) */
|
|
75
|
+
#startPromise = null;
|
|
76
|
+
|
|
77
|
+
/** @type {Promise<void>|null} 正在进行的 stop 操作(去重用) */
|
|
78
|
+
#stopPromise = null;
|
|
79
|
+
|
|
80
|
+
/** @type {PendingManager} in-flight RPC 请求管理器 */
|
|
81
|
+
#pending;
|
|
82
|
+
|
|
83
|
+
/** @type {SendQueue} 串行发送队列(防止 socket 并发写入) */
|
|
84
|
+
#sendQueue;
|
|
85
|
+
|
|
86
|
+
/** @type {Function|null} master 侧收到 slave 请求时的自动回复处理器 */
|
|
87
|
+
#routerAutoHandler = null;
|
|
88
|
+
|
|
89
|
+
/** @type {Function|null} slave 侧收到 master 请求时的自动回复处理器 */
|
|
90
|
+
#dealerAutoHandler = null;
|
|
91
|
+
|
|
92
|
+
// ─── PUB/SUB 状态 ────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* master 侧:已注册的在线 slave 表
|
|
96
|
+
* key = slaveId(字符串)
|
|
97
|
+
* value = { identity: Buffer(用于 ROUTER 发送), lastSeen: number(最后心跳时间戳)}
|
|
98
|
+
* @type {Map<string, { identity: Buffer, lastSeen: number }>}
|
|
99
|
+
*/
|
|
100
|
+
#slaves = new Map();
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* slave 侧:topic → handler 订阅表
|
|
104
|
+
* @type {Map<string, Function>}
|
|
105
|
+
*/
|
|
106
|
+
#subscriptions = new Map();
|
|
107
|
+
|
|
108
|
+
// ─── 心跳 ────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/** 心跳间隔(ms),0 表示禁用;slave/master 共用同一配置 */
|
|
111
|
+
#heartbeatInterval;
|
|
112
|
+
|
|
113
|
+
/** slave 侧发送心跳的定时器句柄 */
|
|
114
|
+
#heartbeatTimer = null;
|
|
115
|
+
|
|
116
|
+
/** master 侧扫描死节点的定时器句柄 */
|
|
117
|
+
#heartbeatCheckTimer = null;
|
|
118
|
+
|
|
119
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
120
|
+
// 构造函数
|
|
121
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {{
|
|
125
|
+
* role : "master"|"slave",
|
|
126
|
+
* id : string,
|
|
127
|
+
* endpoints? : { router?: string },
|
|
128
|
+
* maxPending? : number,
|
|
129
|
+
* authKey? : string,
|
|
130
|
+
* heartbeatInterval? : number,
|
|
131
|
+
* }} options
|
|
132
|
+
*/
|
|
133
|
+
constructor({
|
|
134
|
+
role,
|
|
135
|
+
id,
|
|
136
|
+
endpoints = {},
|
|
137
|
+
maxPending = 0,
|
|
138
|
+
authKey = "",
|
|
139
|
+
heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL,
|
|
140
|
+
} = {}) {
|
|
141
|
+
super();
|
|
142
|
+
|
|
143
|
+
if (role !== "master" && role !== "slave") {
|
|
144
|
+
throw new Error('`role` 必须为 "master" 或 "slave"。');
|
|
145
|
+
}
|
|
146
|
+
if (!id) {
|
|
147
|
+
throw new Error("`id` 为必填项。");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.role = role;
|
|
151
|
+
this.id = String(id);
|
|
152
|
+
this.endpoints = { ...DEFAULT_ENDPOINTS, ...endpoints };
|
|
153
|
+
this.authKey = authKey == null ? "" : String(authKey);
|
|
154
|
+
this.requireAuth = this.role === "master" && this.authKey.length > 0;
|
|
155
|
+
|
|
156
|
+
this.#pending = new PendingManager(maxPending);
|
|
157
|
+
this.#sendQueue = new SendQueue();
|
|
158
|
+
this.#heartbeatInterval =
|
|
159
|
+
this.#normalizeHeartbeatInterval(heartbeatInterval);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
163
|
+
// 公开 API —— 生命周期
|
|
164
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 启动节点
|
|
168
|
+
* - master:bind ROUTER socket,启动死节点扫描定时器
|
|
169
|
+
* - slave :connect DEALER socket,自动发送注册帧,启动心跳定时器
|
|
170
|
+
*
|
|
171
|
+
* 重复调用安全:若已在启动中,返回同一个 Promise;
|
|
172
|
+
* 若上次 stop 尚未完成,等待其结束后再启动。
|
|
173
|
+
*/
|
|
174
|
+
async start() {
|
|
175
|
+
if (this.running) return;
|
|
176
|
+
if (this.#startPromise) return this.#startPromise;
|
|
177
|
+
if (this.#stopPromise) await this.#stopPromise;
|
|
178
|
+
|
|
179
|
+
this.#startPromise = this.#doStart().finally(() => {
|
|
180
|
+
this.#startPromise = null;
|
|
181
|
+
});
|
|
182
|
+
await this.#startPromise;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 停止节点
|
|
187
|
+
* - slave:停止心跳定时器,先发注销帧,再关闭 socket
|
|
188
|
+
* - master:停止扫描定时器,清空在线列表,关闭 socket
|
|
189
|
+
* 重复调用安全。
|
|
190
|
+
*/
|
|
191
|
+
async stop() {
|
|
192
|
+
if (this.#stopPromise) return this.#stopPromise;
|
|
193
|
+
if (this.#startPromise) await this.#startPromise.catch(() => {});
|
|
194
|
+
|
|
195
|
+
if (!this.running && Object.keys(this.#sockets).length === 0) return;
|
|
196
|
+
|
|
197
|
+
this.#stopPromise = this.#doStop().finally(() => {
|
|
198
|
+
this.#stopPromise = null;
|
|
199
|
+
});
|
|
200
|
+
await this.#stopPromise;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
204
|
+
// 公开 API —— RPC
|
|
205
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Slave 侧:向 Master 发送 RPC 请求并等待响应。
|
|
209
|
+
* 传入函数时:注册 slave 侧自动回复处理器(Master 主动发来 RPC 请求时触发)。
|
|
210
|
+
*
|
|
211
|
+
* @param {string|Buffer|Uint8Array|Array|Function} payloadOrHandler
|
|
212
|
+
* @param {{ timeoutMs?: number }} [options]
|
|
213
|
+
* @returns {Promise<Buffer|Array>|void}
|
|
214
|
+
*/
|
|
215
|
+
async DEALER(payloadOrHandler, options = {}) {
|
|
216
|
+
if (typeof payloadOrHandler === "function") {
|
|
217
|
+
this.#dealerAutoHandler = payloadOrHandler;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const response = await this.#request(payloadOrHandler, options);
|
|
221
|
+
return response.payload;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Master 侧:向指定 Slave 发送 RPC 请求并等待响应。
|
|
226
|
+
* 传入函数时:注册 master 侧自动回复处理器(Slave 发来 RPC 请求时触发)。
|
|
227
|
+
*
|
|
228
|
+
* @param {string|Buffer|Function} identityOrHandler - Slave ID 或处理器函数
|
|
229
|
+
* @param {string|Buffer|Uint8Array|Array} [payload]
|
|
230
|
+
* @param {{ timeoutMs?: number }} [options]
|
|
231
|
+
* @returns {Promise<Buffer|Array>|void}
|
|
232
|
+
*/
|
|
233
|
+
async ROUTER(identityOrHandler, payload, options = {}) {
|
|
234
|
+
if (typeof identityOrHandler === "function") {
|
|
235
|
+
this.#routerAutoHandler = identityOrHandler;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const response = await this.#requestTo(identityOrHandler, payload, options);
|
|
239
|
+
return response.payload;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
243
|
+
// 公开 API —— PUB/SUB
|
|
244
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 【Master 侧】向所有已注册的在线 slave 广播消息(fire-and-forget)
|
|
248
|
+
*
|
|
249
|
+
* 发送异步入队后立即返回,无需 await。
|
|
250
|
+
* 若某个 slave 发送失败,自动将其从在线列表移除并触发 slave_disconnected 事件。
|
|
251
|
+
*
|
|
252
|
+
* @param {string} topic - 消息主题,slave 侧可按 topic 精确过滤
|
|
253
|
+
* @param {string|Buffer|Uint8Array|Array} payload
|
|
254
|
+
*/
|
|
255
|
+
publish(topic, payload) {
|
|
256
|
+
if (this.role !== "master") {
|
|
257
|
+
throw new Error("publish() 只能在 master 侧调用。");
|
|
258
|
+
}
|
|
259
|
+
const socket = this.#requireSocket("router", "ROUTER");
|
|
260
|
+
if (this.#slaves.size === 0) return;
|
|
261
|
+
|
|
262
|
+
const frames = buildPublishFrames(String(topic), normalizeFrames(payload));
|
|
263
|
+
|
|
264
|
+
// 遍历所有在线 slave,逐一入队发送
|
|
265
|
+
// 使用 #sendQueue 保证 socket 写入安全,不与 RPC 帧交叉
|
|
266
|
+
for (const [slaveId, entry] of this.#slaves) {
|
|
267
|
+
const idFrame = identityToBuffer(entry.identity);
|
|
268
|
+
this.#sendQueue
|
|
269
|
+
.enqueue("router", () => socket.send([idFrame, ...frames]))
|
|
270
|
+
.catch(() => {
|
|
271
|
+
// 发送失败说明 slave 已断线,静默移除并通知外部
|
|
272
|
+
if (this.#slaves.delete(slaveId)) {
|
|
273
|
+
this.emit("slave_disconnected", slaveId);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 【Slave 侧】订阅指定 topic,当 master 广播该 topic 时触发 handler
|
|
281
|
+
*
|
|
282
|
+
* 可在 start() 前后任意时刻调用,订阅信息跨 stop/start 周期保留。
|
|
283
|
+
* 同一 topic 重复订阅会覆盖旧的 handler。
|
|
284
|
+
*
|
|
285
|
+
* 同时也可通过 `slave.on("publish", ({ topic, payload }) => ...)` 监听所有 topic。
|
|
286
|
+
*
|
|
287
|
+
* @param {string} topic
|
|
288
|
+
* @param {(event: { topic: string, payload: Buffer|Array }) => void|Promise<void>} handler
|
|
289
|
+
* @returns {this} 支持链式调用
|
|
290
|
+
*/
|
|
291
|
+
subscribe(topic, handler) {
|
|
292
|
+
if (this.role !== "slave") {
|
|
293
|
+
throw new Error("subscribe() 只能在 slave 侧调用。");
|
|
294
|
+
}
|
|
295
|
+
if (typeof handler !== "function") {
|
|
296
|
+
throw new TypeError("handler 必须是函数。");
|
|
297
|
+
}
|
|
298
|
+
this.#subscriptions.set(String(topic), handler);
|
|
299
|
+
return this;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 【Slave 侧】取消订阅指定 topic
|
|
304
|
+
*
|
|
305
|
+
* @param {string} topic
|
|
306
|
+
* @returns {this} 支持链式调用
|
|
307
|
+
*/
|
|
308
|
+
unsubscribe(topic) {
|
|
309
|
+
if (this.role !== "slave") {
|
|
310
|
+
throw new Error("unsubscribe() 只能在 slave 侧调用。");
|
|
311
|
+
}
|
|
312
|
+
this.#subscriptions.delete(String(topic));
|
|
313
|
+
return this;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 【Master 侧】获取当前所有在线 slave 的 ID 列表(只读快照)
|
|
318
|
+
* @returns {string[]}
|
|
319
|
+
*/
|
|
320
|
+
get slaves() {
|
|
321
|
+
return [...this.#slaves.keys()];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
325
|
+
// 生命周期内部实现
|
|
326
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
327
|
+
|
|
328
|
+
/** 启动流程:设置 running 标志,初始化对应角色的 socket */
|
|
329
|
+
async #doStart() {
|
|
330
|
+
this.running = true;
|
|
331
|
+
try {
|
|
332
|
+
await (this.role === "master"
|
|
333
|
+
? this.#startMasterSockets()
|
|
334
|
+
: this.#startSlaveSockets());
|
|
335
|
+
} catch (error) {
|
|
336
|
+
// 启动失败则回滚所有状态
|
|
337
|
+
this.running = false;
|
|
338
|
+
await this.#teardown();
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 停止流程
|
|
345
|
+
* 1. slave:停止心跳定时器 → 发注销帧(在 socket 关闭前)
|
|
346
|
+
* 2. 拒绝所有 in-flight pending 请求
|
|
347
|
+
* 3. 关闭 socket,等待读循环退出
|
|
348
|
+
*/
|
|
349
|
+
async #doStop() {
|
|
350
|
+
if (this.role === "slave" && this.#sockets.dealer) {
|
|
351
|
+
// 先停心跳,避免关闭期间继续发送
|
|
352
|
+
clearInterval(this.#heartbeatTimer);
|
|
353
|
+
this.#heartbeatTimer = null;
|
|
354
|
+
|
|
355
|
+
// 优雅下线:在关闭 socket 前先发送注销帧
|
|
356
|
+
await this.#sendQueue
|
|
357
|
+
.enqueue("dealer", () =>
|
|
358
|
+
this.#sockets.dealer.send(buildUnregisterFrames()),
|
|
359
|
+
)
|
|
360
|
+
.catch(() => {}); // master 可能已关闭,忽略发送失败
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.running = false;
|
|
364
|
+
this.#pending.rejectAll(new Error("节点已停止,所有待处理请求已取消。"));
|
|
365
|
+
await this.#teardown();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* 清理所有运行时资源
|
|
370
|
+
* 顺序:停止定时器 → 关闭 socket → 等待读循环退出 → 清空所有注册表
|
|
371
|
+
*/
|
|
372
|
+
async #teardown() {
|
|
373
|
+
// 停止心跳相关定时器
|
|
374
|
+
clearInterval(this.#heartbeatTimer);
|
|
375
|
+
clearInterval(this.#heartbeatCheckTimer);
|
|
376
|
+
this.#heartbeatTimer = null;
|
|
377
|
+
this.#heartbeatCheckTimer = null;
|
|
378
|
+
|
|
379
|
+
for (const socket of Object.values(this.#sockets)) {
|
|
380
|
+
try {
|
|
381
|
+
socket.close();
|
|
382
|
+
} catch {}
|
|
383
|
+
}
|
|
384
|
+
await Promise.allSettled(this.#readLoops);
|
|
385
|
+
|
|
386
|
+
this.#readLoops = [];
|
|
387
|
+
this.#sockets = {};
|
|
388
|
+
this.#sendQueue.clear();
|
|
389
|
+
this.#slaves.clear(); // 清空 slave 注册表,避免 restart 时残留旧条目
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
393
|
+
// Socket 初始化
|
|
394
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 初始化 Master 侧 ROUTER socket(bind 模式),并启动读循环和死节点扫描定时器
|
|
398
|
+
*/
|
|
399
|
+
async #startMasterSockets() {
|
|
400
|
+
const router = new zmq.Router();
|
|
401
|
+
await router.bind(this.endpoints.router);
|
|
402
|
+
this.#sockets.router = router;
|
|
403
|
+
|
|
404
|
+
this.#consume(router, (frames) => this.#handleRouterFrames(frames));
|
|
405
|
+
this.#startHeartbeatCheck();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 初始化 Slave 侧 DEALER socket(connect 模式),并启动读循环
|
|
410
|
+
* 连接后立即发送注册帧,再启动心跳定时器
|
|
411
|
+
*/
|
|
412
|
+
async #startSlaveSockets() {
|
|
413
|
+
// routingId 即节点 id,Master 侧通过此字段识别发送方身份
|
|
414
|
+
const dealer = new zmq.Dealer({ routingId: this.id });
|
|
415
|
+
dealer.connect(this.endpoints.router);
|
|
416
|
+
this.#sockets.dealer = dealer;
|
|
417
|
+
|
|
418
|
+
this.#consume(dealer, (frames) => this.#handleDealerFrames(frames));
|
|
419
|
+
|
|
420
|
+
// 自动发送注册帧(携带 authKey,供 master 进行认证)
|
|
421
|
+
await this.#sendQueue.enqueue("dealer", () =>
|
|
422
|
+
dealer.send(buildRegisterFrames(this.authKey)),
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// 注册成功后启动心跳
|
|
426
|
+
this.#startHeartbeat();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
430
|
+
// 帧处理(接收方向)
|
|
431
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 处理 Master 侧 ROUTER 收到的帧
|
|
435
|
+
* ZMQ ROUTER 帧结构:[identity, ...控制帧]
|
|
436
|
+
* identity 由 ZMQ 自动注入,表示发送方的 routingId
|
|
437
|
+
*/
|
|
438
|
+
async #handleRouterFrames(rawFrames) {
|
|
439
|
+
const [identity, ...bodyFrames] = rawFrames;
|
|
440
|
+
const identityText = identityToString(identity);
|
|
441
|
+
const parsed = parseControlFrames(bodyFrames);
|
|
442
|
+
const payload = payloadFromFrames(parsed.payloadFrames);
|
|
443
|
+
|
|
444
|
+
const event = this.#buildAndEmit("router", rawFrames, {
|
|
445
|
+
identity,
|
|
446
|
+
identityText,
|
|
447
|
+
payload,
|
|
448
|
+
...parsed,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// ── 心跳:更新 slave 的最后活跃时间 ───────────────────────────────────
|
|
452
|
+
if (parsed.kind === "heartbeat") {
|
|
453
|
+
const entry = this.#slaves.get(identityText);
|
|
454
|
+
if (entry) entry.lastSeen = Date.now();
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ── 注册:slave 上线 ────────────────────────────────────────────────────
|
|
459
|
+
if (parsed.kind === "register") {
|
|
460
|
+
// 若启用认证,注册时同样校验 authKey
|
|
461
|
+
if (this.requireAuth && parsed.authKey !== this.authKey) {
|
|
462
|
+
this.emit("auth_failed", { ...event, expectedAuthKey: this.authKey });
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.#slaves.set(identityText, { identity, lastSeen: Date.now() });
|
|
466
|
+
this.emit("slave_connected", identityText);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ── 注销:slave 主动下线 ────────────────────────────────────────────────
|
|
471
|
+
if (parsed.kind === "unregister") {
|
|
472
|
+
if (this.#slaves.delete(identityText)) {
|
|
473
|
+
this.emit("slave_disconnected", identityText);
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ── RPC 请求:slave 发来的请求 ──────────────────────────────────────────
|
|
479
|
+
if (parsed.kind === "request") {
|
|
480
|
+
// 认证校验:Key 不匹配则丢弃并触发 auth_failed 事件
|
|
481
|
+
if (this.requireAuth && parsed.authKey !== this.authKey) {
|
|
482
|
+
this.emit("auth_failed", { ...event, expectedAuthKey: this.authKey });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this.emit("request", event);
|
|
487
|
+
|
|
488
|
+
// 有自动回复处理器时,执行并回复
|
|
489
|
+
if (this.#routerAutoHandler) {
|
|
490
|
+
try {
|
|
491
|
+
const replyPayload = await this.#routerAutoHandler(event);
|
|
492
|
+
await this.#replyTo(identity, parsed.requestId, replyPayload);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
this.emit("error", error);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ── RPC 响应:slave 回复了 master 之前主动发出的请求 ───────────────────
|
|
501
|
+
if (parsed.kind === "response") {
|
|
502
|
+
const key = this.#pending.key(parsed.requestId, identityText);
|
|
503
|
+
this.#pending.resolve(key, event);
|
|
504
|
+
this.emit("response", event);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* 处理 Slave 侧 DEALER 收到的帧
|
|
510
|
+
* ZMQ DEALER 帧结构:[...控制帧](无 identity 帧)
|
|
511
|
+
*/
|
|
512
|
+
async #handleDealerFrames(frames) {
|
|
513
|
+
const parsed = parseControlFrames(frames);
|
|
514
|
+
const payload = payloadFromFrames(parsed.payloadFrames);
|
|
515
|
+
|
|
516
|
+
const event = this.#buildAndEmit("dealer", frames, { payload, ...parsed });
|
|
517
|
+
|
|
518
|
+
// ── PUB 广播:master 推送的消息 ────────────────────────────────────────
|
|
519
|
+
if (parsed.kind === "publish") {
|
|
520
|
+
const pubEvent = { topic: parsed.topic, payload };
|
|
521
|
+
|
|
522
|
+
// 触发统一广播事件(所有 topic 都会触发,方便兜底监听)
|
|
523
|
+
this.emit("publish", pubEvent);
|
|
524
|
+
|
|
525
|
+
// 触发精确 topic 订阅处理器
|
|
526
|
+
const handler = this.#subscriptions.get(parsed.topic);
|
|
527
|
+
if (handler) {
|
|
528
|
+
try {
|
|
529
|
+
await handler(pubEvent);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
this.emit("error", error);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ── RPC 请求:master 主动发来的请求 ────────────────────────────────────
|
|
538
|
+
if (parsed.kind === "request") {
|
|
539
|
+
this.emit("request", event);
|
|
540
|
+
|
|
541
|
+
if (this.#dealerAutoHandler) {
|
|
542
|
+
try {
|
|
543
|
+
const replyPayload = await this.#dealerAutoHandler(event);
|
|
544
|
+
await this.#reply(parsed.requestId, replyPayload);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
this.emit("error", error);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ── RPC 响应:master 回复了 slave 之前发出的请求 ───────────────────────
|
|
553
|
+
if (parsed.kind === "response") {
|
|
554
|
+
const key = this.#pending.key(parsed.requestId);
|
|
555
|
+
this.#pending.resolve(key, event);
|
|
556
|
+
this.emit("response", event);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
561
|
+
// RPC 请求发送(主动方向)
|
|
562
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Slave → Master:通过 DEALER socket 发送 RPC 请求,返回响应 Promise
|
|
566
|
+
* 流程:创建 pending → 入队等待发送 → 发送后启动超时计时 → 等待响应 resolve
|
|
567
|
+
*/
|
|
568
|
+
async #request(payload, { timeoutMs } = {}) {
|
|
569
|
+
const socket = this.#requireSocket("dealer", "DEALER");
|
|
570
|
+
this.#pending.ensureCapacity();
|
|
571
|
+
|
|
572
|
+
const requestId = randomUUID();
|
|
573
|
+
const key = this.#pending.key(requestId);
|
|
574
|
+
const { promise, startTimer } = this.#pending.create(
|
|
575
|
+
key,
|
|
576
|
+
timeoutMs,
|
|
577
|
+
requestId,
|
|
578
|
+
);
|
|
579
|
+
const frames = buildRequestFrames(
|
|
580
|
+
requestId,
|
|
581
|
+
normalizeFrames(payload),
|
|
582
|
+
this.authKey,
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
// 入队:等前一个发送完成后再发;发送完毕后立即启动超时计时
|
|
586
|
+
this.#sendQueue
|
|
587
|
+
.enqueue("dealer", async () => {
|
|
588
|
+
startTimer();
|
|
589
|
+
await socket.send(frames);
|
|
590
|
+
})
|
|
591
|
+
.catch((error) => this.#pending.reject(key, error));
|
|
592
|
+
|
|
593
|
+
return promise;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Master → Slave:通过 ROUTER socket 向指定 Slave 发送 RPC 请求,返回响应 Promise
|
|
598
|
+
*/
|
|
599
|
+
async #requestTo(identity, payload, { timeoutMs } = {}) {
|
|
600
|
+
const socket = this.#requireSocket("router", "ROUTER");
|
|
601
|
+
this.#pending.ensureCapacity();
|
|
602
|
+
|
|
603
|
+
const identityText = identityToString(identity);
|
|
604
|
+
const idFrame = identityToBuffer(identity);
|
|
605
|
+
const requestId = randomUUID();
|
|
606
|
+
const key = this.#pending.key(requestId, identityText);
|
|
607
|
+
const { promise, startTimer } = this.#pending.create(
|
|
608
|
+
key,
|
|
609
|
+
timeoutMs,
|
|
610
|
+
requestId,
|
|
611
|
+
identityText,
|
|
612
|
+
);
|
|
613
|
+
const frames = buildRequestFrames(
|
|
614
|
+
requestId,
|
|
615
|
+
normalizeFrames(payload),
|
|
616
|
+
this.authKey,
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
this.#sendQueue
|
|
620
|
+
.enqueue("router", async () => {
|
|
621
|
+
startTimer();
|
|
622
|
+
// ROUTER socket 发送时必须在最前面附上 identity 帧
|
|
623
|
+
await socket.send([idFrame, ...frames]);
|
|
624
|
+
})
|
|
625
|
+
.catch((error) => this.#pending.reject(key, error));
|
|
626
|
+
|
|
627
|
+
return promise;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
631
|
+
// RPC 响应发送(被动方向)
|
|
632
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Slave 回复 Master 的 RPC 请求(使用 DEALER socket)
|
|
636
|
+
* @param {string} requestId
|
|
637
|
+
* @param {*} payload
|
|
638
|
+
*/
|
|
639
|
+
async #reply(requestId, payload) {
|
|
640
|
+
const socket = this.#requireSocket("dealer", "DEALER");
|
|
641
|
+
const frames = buildResponseFrames(requestId, normalizeFrames(payload));
|
|
642
|
+
await this.#sendQueue.enqueue("dealer", () => socket.send(frames));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Master 回复指定 Slave 的 RPC 请求(使用 ROUTER socket)
|
|
647
|
+
* @param {Buffer|string} identity
|
|
648
|
+
* @param {string} requestId
|
|
649
|
+
* @param {*} payload
|
|
650
|
+
*/
|
|
651
|
+
async #replyTo(identity, requestId, payload) {
|
|
652
|
+
const socket = this.#requireSocket("router", "ROUTER");
|
|
653
|
+
const idFrame = identityToBuffer(identity);
|
|
654
|
+
const frames = buildResponseFrames(requestId, normalizeFrames(payload));
|
|
655
|
+
await this.#sendQueue.enqueue("router", () =>
|
|
656
|
+
socket.send([idFrame, ...frames]),
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
661
|
+
// 心跳(内部实现)
|
|
662
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* 启动 slave 侧心跳发送定时器
|
|
666
|
+
* 每隔 #heartbeatInterval ms 向 master 发送一帧心跳,证明自己还活着
|
|
667
|
+
*/
|
|
668
|
+
#startHeartbeat() {
|
|
669
|
+
if (this.#heartbeatInterval <= 0) return;
|
|
670
|
+
|
|
671
|
+
this.#heartbeatTimer = setInterval(() => {
|
|
672
|
+
if (!this.running || !this.#sockets.dealer) return;
|
|
673
|
+
this.#sendQueue
|
|
674
|
+
.enqueue("dealer", () =>
|
|
675
|
+
this.#sockets.dealer.send([CONTROL_PREFIX, CONTROL_HEARTBEAT]),
|
|
676
|
+
)
|
|
677
|
+
.catch(() => {}); // 发送失败静默处理,不影响业务
|
|
678
|
+
}, this.#heartbeatInterval);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* 启动 master 侧死节点扫描定时器
|
|
683
|
+
* 每隔 #heartbeatInterval ms 扫描一次,将超过 3 个周期未活跃的 slave 视为崩溃并移除
|
|
684
|
+
*/
|
|
685
|
+
#startHeartbeatCheck() {
|
|
686
|
+
if (this.#heartbeatInterval <= 0) return;
|
|
687
|
+
|
|
688
|
+
// 超时阈值 = 3 个心跳周期,给网络抖动留出充分余量
|
|
689
|
+
const timeout = this.#heartbeatInterval * 3;
|
|
690
|
+
|
|
691
|
+
this.#heartbeatCheckTimer = setInterval(() => {
|
|
692
|
+
const now = Date.now();
|
|
693
|
+
for (const [slaveId, entry] of this.#slaves) {
|
|
694
|
+
if (now - entry.lastSeen > timeout) {
|
|
695
|
+
this.#slaves.delete(slaveId);
|
|
696
|
+
this.emit("slave_disconnected", slaveId);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}, this.#heartbeatInterval);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
703
|
+
// 工具方法
|
|
704
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* 启动 socket 的异步读取循环(for-await-of)
|
|
708
|
+
* 循环在 socket 被关闭后自然退出,Promise 保存到 #readLoops 供 teardown 等待
|
|
709
|
+
*
|
|
710
|
+
* @param {import("zeromq").Socket} socket
|
|
711
|
+
* @param {(frames: Array) => Promise<void>} handler
|
|
712
|
+
*/
|
|
713
|
+
#consume(socket, handler) {
|
|
714
|
+
const loop = (async () => {
|
|
715
|
+
try {
|
|
716
|
+
for await (const rawFrames of socket) {
|
|
717
|
+
if (!this.running) return;
|
|
718
|
+
// zeromq v6 单帧时返回 Buffer,多帧时返回数组,统一转为数组处理
|
|
719
|
+
const frames = Array.isArray(rawFrames) ? rawFrames : [rawFrames];
|
|
720
|
+
try {
|
|
721
|
+
await handler(frames);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
this.emit("error", error);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
} catch (error) {
|
|
727
|
+
// socket 关闭时 for-await 会抛出,仅在运行中时才视为错误
|
|
728
|
+
if (this.running) this.emit("error", error);
|
|
729
|
+
}
|
|
730
|
+
})();
|
|
731
|
+
|
|
732
|
+
this.#readLoops.push(loop);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* 构建事件对象,并同时触发 channel 专项事件和 message 统一事件
|
|
737
|
+
* @param {string} channel
|
|
738
|
+
* @param {Array} frames
|
|
739
|
+
* @param {object} extra
|
|
740
|
+
* @returns {object} 事件对象
|
|
741
|
+
*/
|
|
742
|
+
#buildAndEmit(channel, frames, extra = {}) {
|
|
743
|
+
const event = { channel, frames, ...extra };
|
|
744
|
+
this.emit(channel, event);
|
|
745
|
+
this.emit("message", event);
|
|
746
|
+
return event;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* 获取指定 socket,不存在时抛出明确的错误提示
|
|
751
|
+
* @param {string} name
|
|
752
|
+
* @param {string} displayName
|
|
753
|
+
* @returns {import("zeromq").Socket}
|
|
754
|
+
*/
|
|
755
|
+
#requireSocket(name, displayName) {
|
|
756
|
+
const socket = this.#sockets[name];
|
|
757
|
+
if (!socket) {
|
|
758
|
+
throw new Error(`${displayName} socket 尚未就绪,请先调用 start()。`);
|
|
759
|
+
}
|
|
760
|
+
return socket;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* 规范化心跳间隔:必须是非负整数,否则使用默认值
|
|
765
|
+
* @param {*} n
|
|
766
|
+
* @returns {number}
|
|
767
|
+
*/
|
|
768
|
+
#normalizeHeartbeatInterval(n) {
|
|
769
|
+
const v = Number(n);
|
|
770
|
+
if (Number.isFinite(v) && v >= 0) return Math.floor(v);
|
|
771
|
+
return DEFAULT_HEARTBEAT_INTERVAL;
|
|
772
|
+
}
|
|
773
|
+
}
|