@lyrify/znl 0.5.2 → 0.6.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.
package/src/ZNL.js CHANGED
@@ -49,6 +49,7 @@ import {
49
49
  buildRegisterFrames,
50
50
  buildUnregisterFrames,
51
51
  buildHeartbeatFrames,
52
+ buildHeartbeatAckFrames,
52
53
  buildRequestFrames,
53
54
  buildResponseFrames,
54
55
  buildPublishFrames,
@@ -127,13 +128,19 @@ export class ZNL extends EventEmitter {
127
128
  /** @type {boolean} 是否启用 payload 摘要校验(安全模式用) */
128
129
  #enablePayloadDigest = DEFAULT_ENABLE_PAYLOAD_DIGEST;
129
130
 
131
+ /** @type {Map<string, string>} master 侧 authKey 配置表(按 slaveId) */
132
+ #authKeyMap = new Map();
133
+
134
+ /** @type {Map<string, { signKey: Buffer, encryptKey: Buffer }>} master 侧派生密钥缓存 */
135
+ #slaveKeyCache = new Map();
136
+
130
137
  // ─── PUB/SUB 状态 ────────────────────────────────────────────────────────
131
138
 
132
139
  /**
133
140
  * master 侧:已注册的在线 slave 表
134
141
  * key = slaveId(字符串)
135
- * value = { identity: Buffer, lastSeen: number }
136
- * @type {Map<string, { identity: Buffer, lastSeen: number }>}
142
+ * value = { identity: Buffer, lastSeen: number, signKey: Buffer|null, encryptKey: Buffer|null }
143
+ * @type {Map<string, { identity: Buffer, lastSeen: number, signKey: Buffer|null, encryptKey: Buffer|null }>}
137
144
  */
138
145
  #slaves = new Map();
139
146
 
@@ -148,9 +155,24 @@ export class ZNL extends EventEmitter {
148
155
  /** 心跳间隔(ms),0 表示禁用;slave/master 共用同一配置 */
149
156
  #heartbeatInterval;
150
157
 
151
- /** slave 侧发送心跳的定时器句柄 */
158
+ /** slave 侧单次心跳调度定时器句柄 */
152
159
  #heartbeatTimer = null;
153
160
 
161
+ /** slave 侧等待心跳应答的超时定时器句柄 */
162
+ #heartbeatAckTimer = null;
163
+
164
+ /** slave 侧当前是否存在未确认的心跳 */
165
+ #heartbeatWaitingAck = false;
166
+
167
+ /** slave 侧最近一次确认主节点在线的时间戳 */
168
+ #lastMasterSeenAt = 0;
169
+
170
+ /** slave 侧缓存的主节点在线状态 */
171
+ #masterOnline = false;
172
+
173
+ /** slave 侧正在进行的 Dealer 重连 Promise(防止并发重连) */
174
+ #dealerReconnectPromise = null;
175
+
154
176
  /** master 侧扫描死节点的定时器句柄 */
155
177
  #heartbeatCheckTimer = null;
156
178
 
@@ -185,6 +207,7 @@ export class ZNL extends EventEmitter {
185
207
  * endpoints? : { router?: string },
186
208
  * maxPending? : number,
187
209
  * authKey? : string,
210
+ * authKeyMap? : Record<string, string>,
188
211
  * heartbeatInterval? : number,
189
212
  * heartbeatTimeoutMs? : number,
190
213
  * encrypted? : boolean,
@@ -199,6 +222,7 @@ export class ZNL extends EventEmitter {
199
222
  endpoints = {},
200
223
  maxPending = DEFAULT_MAX_PENDING,
201
224
  authKey = "",
225
+ authKeyMap = null,
202
226
  heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL,
203
227
  heartbeatTimeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS,
204
228
  encrypted = false,
@@ -220,16 +244,35 @@ export class ZNL extends EventEmitter {
220
244
  this.endpoints = { ...DEFAULT_ENDPOINTS, ...endpoints };
221
245
  this.authKey = authKey == null ? "" : String(authKey);
222
246
 
247
+ if (this.role === "master") {
248
+ if (authKeyMap != null) {
249
+ if (typeof authKeyMap !== "object") {
250
+ throw new TypeError(
251
+ "`authKeyMap` 必须是对象(slaveId -> authKey)。",
252
+ );
253
+ }
254
+ for (const [slaveId, key] of Object.entries(authKeyMap)) {
255
+ if (key == null) continue;
256
+ this.#authKeyMap.set(String(slaveId), String(key));
257
+ }
258
+ }
259
+ }
260
+
223
261
  this.encrypted = Boolean(encrypted);
224
262
  this.#secureEnabled = this.encrypted;
225
263
 
226
264
  if (this.#secureEnabled) {
227
- if (!this.authKey) {
228
- throw new Error("启用 encrypted=true 时,必须提供非空 authKey。");
265
+ const hasAuthMap = this.role === "master" && authKeyMap != null;
266
+ if (!this.authKey && !hasAuthMap) {
267
+ throw new Error(
268
+ "启用 encrypted=true 时,必须提供非空 authKey 或 authKeyMap。",
269
+ );
270
+ }
271
+ if (this.authKey) {
272
+ const { signKey, encryptKey } = deriveKeys(this.authKey);
273
+ this.#signKey = signKey;
274
+ this.#encryptKey = encryptKey;
229
275
  }
230
- const { signKey, encryptKey } = deriveKeys(this.authKey);
231
- this.#signKey = signKey;
232
- this.#encryptKey = encryptKey;
233
276
  }
234
277
 
235
278
  this.#pending = new PendingManager(maxPending);
@@ -356,19 +399,34 @@ export class ZNL extends EventEmitter {
356
399
  if (this.#slaves.size === 0) return;
357
400
 
358
401
  const topicText = String(topic);
359
- const payloadFrames = this.#sealPayloadFrames(
360
- "publish",
361
- topicText,
362
- payload,
363
- );
364
- const authProof = this.#secureEnabled
365
- ? this.#createAuthProof("publish", topicText, payloadFrames)
366
- : "";
367
-
368
- const frames = buildPublishFrames(topicText, payloadFrames, authProof);
369
402
 
370
403
  for (const [slaveId, entry] of this.#slaves) {
404
+ const keys = this.#secureEnabled ? this.#resolveSlaveKeys(slaveId) : null;
405
+ if (this.#secureEnabled && !keys) {
406
+ if (this.#slaves.delete(slaveId)) {
407
+ this.emit("slave_disconnected", slaveId);
408
+ }
409
+ continue;
410
+ }
411
+
412
+ const payloadFrames = this.#sealPayloadFrames(
413
+ "publish",
414
+ topicText,
415
+ payload,
416
+ keys?.encryptKey ?? null,
417
+ );
418
+ const authProof = this.#secureEnabled
419
+ ? this.#createAuthProof(
420
+ "publish",
421
+ topicText,
422
+ payloadFrames,
423
+ keys.signKey,
424
+ )
425
+ : "";
426
+
427
+ const frames = buildPublishFrames(topicText, payloadFrames, authProof);
371
428
  const idFrame = identityToBuffer(entry.identity);
429
+
372
430
  this.#sendQueue
373
431
  .enqueue("router", () => socket.send([idFrame, ...frames]))
374
432
  .catch(() => {
@@ -424,6 +482,84 @@ export class ZNL extends EventEmitter {
424
482
  return [...this.#slaves.keys()];
425
483
  }
426
484
 
485
+ /**
486
+ * 【Slave 侧】当前已确认的主节点在线状态
487
+ * - 仅在 slave 角色下有意义
488
+ * - 收到合法 heartbeat_ack / request / response / publish 时置为 true
489
+ * - 心跳应答超时、重连、stop 时置为 false
490
+ */
491
+ get masterOnline() {
492
+ return this.role === "slave" ? this.#masterOnline : false;
493
+ }
494
+
495
+ /**
496
+ * 【Slave 侧】查询当前主节点是否在线
497
+ * 说明:
498
+ * - 这是对外公开的轻量 API,适合业务层主动轮询
499
+ * - 返回值基于最近一次链路确认结果,而非一次实时网络探测
500
+ * @returns {boolean}
501
+ */
502
+ isMasterOnline() {
503
+ return this.masterOnline;
504
+ }
505
+
506
+ /**
507
+ * 【Master 侧】动态添加/更新某个 slave 的 authKey(立即生效)
508
+ * @param {string} slaveId
509
+ * @param {string} authKey
510
+ * @returns {this}
511
+ */
512
+ addAuthKey(slaveId, authKey) {
513
+ if (this.role !== "master") {
514
+ throw new Error("addAuthKey() 只能在 master 侧调用。");
515
+ }
516
+ const id = String(slaveId ?? "");
517
+ if (!id) {
518
+ throw new Error("slaveId 不能为空。");
519
+ }
520
+ const key = authKey == null ? "" : String(authKey);
521
+ if (!key) {
522
+ throw new Error("authKey 不能为空。");
523
+ }
524
+
525
+ this.#authKeyMap.set(id, key);
526
+
527
+ if (this.#secureEnabled) {
528
+ const derived = deriveKeys(key);
529
+ this.#slaveKeyCache.set(id, derived);
530
+ const entry = this.#slaves.get(id);
531
+ if (entry) {
532
+ entry.signKey = derived.signKey;
533
+ entry.encryptKey = derived.encryptKey;
534
+ }
535
+ }
536
+ return this;
537
+ }
538
+
539
+ /**
540
+ * 【Master 侧】移除某个 slave 的 authKey(立即生效)
541
+ * @param {string} slaveId
542
+ * @returns {this}
543
+ */
544
+ removeAuthKey(slaveId) {
545
+ if (this.role !== "master") {
546
+ throw new Error("removeAuthKey() 只能在 master 侧调用。");
547
+ }
548
+ const id = String(slaveId ?? "");
549
+ if (!id) {
550
+ throw new Error("slaveId 不能为空。");
551
+ }
552
+
553
+ this.#authKeyMap.delete(id);
554
+ this.#slaveKeyCache.delete(id);
555
+
556
+ if (this.#slaves.has(id)) {
557
+ this.#slaves.delete(id);
558
+ this.emit("slave_disconnected", id);
559
+ }
560
+ return this;
561
+ }
562
+
427
563
  // ═══════════════════════════════════════════════════════════════════════════
428
564
  // 生命周期内部实现
429
565
  // ═══════════════════════════════════════════════════════════════════════════
@@ -450,8 +586,13 @@ export class ZNL extends EventEmitter {
450
586
  */
451
587
  async #doStop() {
452
588
  if (this.role === "slave" && this.#sockets.dealer) {
453
- clearInterval(this.#heartbeatTimer);
589
+ clearTimeout(this.#heartbeatTimer);
590
+ clearTimeout(this.#heartbeatAckTimer);
454
591
  this.#heartbeatTimer = null;
592
+ this.#heartbeatAckTimer = null;
593
+ this.#heartbeatWaitingAck = false;
594
+ this.#masterOnline = false;
595
+ this.#lastMasterSeenAt = 0;
455
596
 
456
597
  await this.#sendQueue
457
598
  .enqueue("dealer", () =>
@@ -470,9 +611,11 @@ export class ZNL extends EventEmitter {
470
611
  * 顺序:停止定时器 → 关闭 socket → 等待读循环退出 → 清空所有注册表
471
612
  */
472
613
  async #teardown() {
473
- clearInterval(this.#heartbeatTimer);
614
+ clearTimeout(this.#heartbeatTimer);
615
+ clearTimeout(this.#heartbeatAckTimer);
474
616
  clearInterval(this.#heartbeatCheckTimer);
475
617
  this.#heartbeatTimer = null;
618
+ this.#heartbeatAckTimer = null;
476
619
  this.#heartbeatCheckTimer = null;
477
620
 
478
621
  for (const socket of Object.values(this.#sockets)) {
@@ -486,7 +629,12 @@ export class ZNL extends EventEmitter {
486
629
  this.#sockets = {};
487
630
  this.#sendQueue.clear();
488
631
  this.#slaves.clear();
632
+ this.#slaveKeyCache.clear();
489
633
  this.#masterNodeId = null;
634
+ this.#masterOnline = false;
635
+ this.#lastMasterSeenAt = 0;
636
+ this.#heartbeatWaitingAck = false;
637
+ this.#dealerReconnectPromise = null;
490
638
  this.#replayGuard.clear();
491
639
  }
492
640
 
@@ -498,7 +646,7 @@ export class ZNL extends EventEmitter {
498
646
  * 初始化 Master 侧 ROUTER socket(bind 模式),并启动读循环和死节点扫描定时器
499
647
  */
500
648
  async #startMasterSockets() {
501
- const router = new zmq.Router();
649
+ const router = new zmq.Router({ handover: true });
502
650
  await router.bind(this.endpoints.router);
503
651
  this.#sockets.router = router;
504
652
 
@@ -511,23 +659,17 @@ export class ZNL extends EventEmitter {
511
659
  * 连接后立即发送注册帧,再启动心跳定时器
512
660
  */
513
661
  async #startSlaveSockets() {
514
- const dealer = new zmq.Dealer({ routingId: this.id });
515
- dealer.connect(this.endpoints.router);
516
- this.#sockets.dealer = dealer;
517
-
518
- this.#consume(dealer, (frames) => this.#handleDealerFrames(frames));
662
+ this.#masterOnline = false;
663
+ this.#lastMasterSeenAt = 0;
664
+ this.#heartbeatWaitingAck = false;
519
665
 
520
- // 注册帧:
521
- // - encrypted=true : 发送签名证明令牌(放在 authKey 字段位)
522
- // - encrypted=false : 不携带认证信息
523
- const registerToken = this.#secureEnabled
524
- ? this.#createAuthProof("register", "", [])
525
- : "";
526
-
527
- await this.#sendQueue.enqueue("dealer", () =>
528
- dealer.send(buildRegisterFrames(registerToken)),
529
- );
666
+ const dealer = this.#createDealerSocket();
667
+ this.#sockets.dealer = dealer;
530
668
 
669
+ // 注册帧改为“尽力发送”:
670
+ // - master 尚未上线时无需阻塞启动流程
671
+ // - 后续 heartbeat_ack 成功后仍可自然恢复在线状态
672
+ this.#trySendRegister();
531
673
  this.#startHeartbeat();
532
674
  }
533
675
 
@@ -554,42 +696,92 @@ export class ZNL extends EventEmitter {
554
696
 
555
697
  // ── 心跳 ────────────────────────────────────────────────────────────────
556
698
  if (parsed.kind === "heartbeat") {
699
+ let ackFrames = buildHeartbeatAckFrames();
700
+
557
701
  if (this.#secureEnabled) {
702
+ const keys = this.#resolveSlaveKeys(identityText);
703
+ if (!keys) {
704
+ this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
705
+ if (this.#slaves.delete(identityText)) {
706
+ this.emit("slave_disconnected", identityText);
707
+ }
708
+ return;
709
+ }
710
+
558
711
  const v = this.#verifyIncomingProof({
559
712
  kind: "heartbeat",
560
713
  proofToken: parsed.authProof,
561
714
  requestId: "",
562
715
  payloadFrames: [],
563
716
  expectedNodeId: identityText,
717
+ signKey: keys.signKey,
564
718
  });
565
719
  if (!v.ok) {
566
720
  this.#emitAuthFailed(event, v.error);
567
721
  return;
568
722
  }
723
+
724
+ this.#ensureSlaveOnline(identityText, identity, {
725
+ touch: true,
726
+ signKey: keys.signKey,
727
+ encryptKey: keys.encryptKey,
728
+ });
729
+
730
+ ackFrames = buildHeartbeatAckFrames(
731
+ this.#createAuthProof("heartbeat_ack", "", [], keys.signKey),
732
+ );
733
+ } else {
734
+ // 心跳视为在线确认:必要时自动补注册
735
+ this.#ensureSlaveOnline(identityText, identity, { touch: true });
569
736
  }
570
737
 
571
- // 心跳视为在线确认:必要时自动补注册
572
- this.#ensureSlaveOnline(identityText, identity, { touch: true });
738
+ await this.#sendQueue
739
+ .enqueue("router", () =>
740
+ this.#sockets.router.send([identityToBuffer(identity), ...ackFrames]),
741
+ )
742
+ .catch(() => {});
573
743
  return;
574
744
  }
575
745
 
576
746
  // ── 注册 ────────────────────────────────────────────────────────────────
577
747
  if (parsed.kind === "register") {
578
748
  if (this.#secureEnabled) {
749
+ const keys = this.#resolveSlaveKeys(identityText);
750
+ if (!keys) {
751
+ this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
752
+ if (this.#slaves.delete(identityText)) {
753
+ this.emit("slave_disconnected", identityText);
754
+ }
755
+ return;
756
+ }
757
+
579
758
  const v = this.#verifyIncomingProof({
580
759
  kind: "register",
581
760
  proofToken: parsed.authKey, // register 复用 authKey 字段承载 proof
582
761
  requestId: "",
583
762
  payloadFrames: [],
584
763
  expectedNodeId: identityText,
764
+ signKey: keys.signKey,
585
765
  });
586
766
  if (!v.ok) {
587
767
  this.#emitAuthFailed(event, v.error);
588
768
  return;
589
769
  }
770
+
771
+ this.#ensureSlaveOnline(identityText, identity, {
772
+ touch: true,
773
+ signKey: keys.signKey,
774
+ encryptKey: keys.encryptKey,
775
+ });
776
+ return;
590
777
  }
591
778
 
592
- this.#slaves.set(identityText, { identity, lastSeen: Date.now() });
779
+ this.#slaves.set(identityText, {
780
+ identity,
781
+ lastSeen: Date.now(),
782
+ signKey: null,
783
+ encryptKey: null,
784
+ });
593
785
  this.emit("slave_connected", identityText);
594
786
  return;
595
787
  }
@@ -607,12 +799,22 @@ export class ZNL extends EventEmitter {
607
799
  let finalFrames = parsed.payloadFrames;
608
800
 
609
801
  if (this.#secureEnabled) {
802
+ const keys = this.#resolveSlaveKeys(identityText);
803
+ if (!keys) {
804
+ this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
805
+ if (this.#slaves.delete(identityText)) {
806
+ this.emit("slave_disconnected", identityText);
807
+ }
808
+ return;
809
+ }
810
+
610
811
  const v = this.#verifyIncomingProof({
611
812
  kind: "request",
612
813
  proofToken: parsed.authKey, // request 复用 authKey 字段承载 proof
613
814
  requestId: parsed.requestId,
614
815
  payloadFrames: parsed.payloadFrames,
615
816
  expectedNodeId: identityText,
817
+ signKey: keys.signKey,
616
818
  });
617
819
  if (!v.ok) {
618
820
  this.#emitAuthFailed(event, v.error);
@@ -620,7 +822,11 @@ export class ZNL extends EventEmitter {
620
822
  }
621
823
 
622
824
  // 认证通过后允许补注册(避免 master 重启后丢失在线表)
623
- this.#ensureSlaveOnline(identityText, identity, { touch: true });
825
+ this.#ensureSlaveOnline(identityText, identity, {
826
+ touch: true,
827
+ signKey: keys.signKey,
828
+ encryptKey: keys.encryptKey,
829
+ });
624
830
 
625
831
  if (this.encrypted) {
626
832
  try {
@@ -629,6 +835,7 @@ export class ZNL extends EventEmitter {
629
835
  parsed.requestId,
630
836
  parsed.payloadFrames,
631
837
  identityText,
838
+ keys.encryptKey,
632
839
  );
633
840
  } catch (error) {
634
841
  this.#emitAuthFailed(
@@ -663,12 +870,22 @@ export class ZNL extends EventEmitter {
663
870
  let finalFrames = parsed.payloadFrames;
664
871
 
665
872
  if (this.#secureEnabled) {
873
+ const keys = this.#resolveSlaveKeys(identityText);
874
+ if (!keys) {
875
+ this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
876
+ if (this.#slaves.delete(identityText)) {
877
+ this.emit("slave_disconnected", identityText);
878
+ }
879
+ return;
880
+ }
881
+
666
882
  const v = this.#verifyIncomingProof({
667
883
  kind: "response",
668
884
  proofToken: parsed.authProof,
669
885
  requestId: parsed.requestId,
670
886
  payloadFrames: parsed.payloadFrames,
671
887
  expectedNodeId: identityText,
888
+ signKey: keys.signKey,
672
889
  });
673
890
  if (!v.ok) {
674
891
  this.#emitAuthFailed(event, v.error);
@@ -676,7 +893,11 @@ export class ZNL extends EventEmitter {
676
893
  }
677
894
 
678
895
  // 认证通过后允许补注册(避免 master 重启后丢失在线表)
679
- this.#ensureSlaveOnline(identityText, identity, { touch: true });
896
+ this.#ensureSlaveOnline(identityText, identity, {
897
+ touch: true,
898
+ signKey: keys.signKey,
899
+ encryptKey: keys.encryptKey,
900
+ });
680
901
 
681
902
  if (this.encrypted) {
682
903
  try {
@@ -685,6 +906,7 @@ export class ZNL extends EventEmitter {
685
906
  parsed.requestId,
686
907
  parsed.payloadFrames,
687
908
  identityText,
909
+ keys.encryptKey,
688
910
  );
689
911
  } catch (error) {
690
912
  this.#emitAuthFailed(
@@ -717,6 +939,28 @@ export class ZNL extends EventEmitter {
717
939
 
718
940
  const event = this.#buildAndEmit("dealer", frames, { payload, ...parsed });
719
941
 
942
+ // ── 心跳应答:master -> slave ──────────────────────────────────────────
943
+ if (parsed.kind === "heartbeat_ack") {
944
+ if (this.#secureEnabled) {
945
+ const v = this.#verifyIncomingProof({
946
+ kind: "heartbeat_ack",
947
+ proofToken: parsed.authProof,
948
+ requestId: "",
949
+ payloadFrames: [],
950
+ expectedNodeId: this.#masterNodeId,
951
+ });
952
+ if (!v.ok) {
953
+ this.#emitAuthFailed(event, v.error);
954
+ return;
955
+ }
956
+
957
+ if (!this.#masterNodeId) this.#masterNodeId = v.envelope.nodeId;
958
+ }
959
+
960
+ this.#confirmMasterReachable({ scheduleNextHeartbeat: true });
961
+ return;
962
+ }
963
+
720
964
  // ── PUB 广播:master -> slave ──────────────────────────────────────────
721
965
  if (parsed.kind === "publish") {
722
966
  let finalFrames = parsed.payloadFrames;
@@ -755,6 +999,8 @@ export class ZNL extends EventEmitter {
755
999
  }
756
1000
  }
757
1001
 
1002
+ this.#confirmMasterReachable({ scheduleNextHeartbeat: true });
1003
+
758
1004
  const finalPayload = payloadFromFrames(finalFrames);
759
1005
  const pubEvent = { topic: parsed.topic, payload: finalPayload };
760
1006
 
@@ -808,6 +1054,8 @@ export class ZNL extends EventEmitter {
808
1054
  }
809
1055
  }
810
1056
 
1057
+ this.#confirmMasterReachable({ scheduleNextHeartbeat: true });
1058
+
811
1059
  const finalPayload = payloadFromFrames(finalFrames);
812
1060
  const requestEvent = { ...event, payload: finalPayload };
813
1061
  this.emit("request", requestEvent);
@@ -860,6 +1108,8 @@ export class ZNL extends EventEmitter {
860
1108
  }
861
1109
  }
862
1110
 
1111
+ this.#confirmMasterReachable({ scheduleNextHeartbeat: true });
1112
+
863
1113
  const finalPayload = payloadFromFrames(finalFrames);
864
1114
  const responseEvent = { ...event, payload: finalPayload };
865
1115
  const key = this.#pending.key(parsed.requestId);
@@ -927,13 +1177,21 @@ export class ZNL extends EventEmitter {
927
1177
  identityText,
928
1178
  );
929
1179
 
1180
+ const keys = this.#secureEnabled
1181
+ ? this.#resolveSlaveKeys(identityText)
1182
+ : null;
1183
+ if (this.#secureEnabled && !keys) {
1184
+ throw new Error(`未配置该 slave 的 authKey:${identityText}`);
1185
+ }
1186
+
930
1187
  const payloadFrames = this.#sealPayloadFrames(
931
1188
  "request",
932
1189
  requestId,
933
1190
  payload,
1191
+ keys?.encryptKey ?? null,
934
1192
  );
935
1193
  const proofOrAuthKey = this.#secureEnabled
936
- ? this.#createAuthProof("request", requestId, payloadFrames)
1194
+ ? this.#createAuthProof("request", requestId, payloadFrames, keys.signKey)
937
1195
  : "";
938
1196
 
939
1197
  const frames = buildRequestFrames(requestId, payloadFrames, proofOrAuthKey);
@@ -981,15 +1239,29 @@ export class ZNL extends EventEmitter {
981
1239
  */
982
1240
  async #replyTo(identity, requestId, payload) {
983
1241
  const socket = this.#requireSocket("router", "ROUTER");
1242
+ const identityText = identityToString(identity);
984
1243
  const idFrame = identityToBuffer(identity);
985
1244
 
1245
+ const keys = this.#secureEnabled
1246
+ ? this.#resolveSlaveKeys(identityText)
1247
+ : null;
1248
+ if (this.#secureEnabled && !keys) {
1249
+ throw new Error(`未配置该 slave 的 authKey:${identityText}`);
1250
+ }
1251
+
986
1252
  const payloadFrames = this.#sealPayloadFrames(
987
1253
  "response",
988
1254
  requestId,
989
1255
  payload,
1256
+ keys?.encryptKey ?? null,
990
1257
  );
991
1258
  const authProof = this.#secureEnabled
992
- ? this.#createAuthProof("response", requestId, payloadFrames)
1259
+ ? this.#createAuthProof(
1260
+ "response",
1261
+ requestId,
1262
+ payloadFrames,
1263
+ keys.signKey,
1264
+ )
993
1265
  : "";
994
1266
 
995
1267
  const frames = buildResponseFrames(requestId, payloadFrames, authProof);
@@ -1003,27 +1275,235 @@ export class ZNL extends EventEmitter {
1003
1275
  // ═══════════════════════════════════════════════════════════════════════════
1004
1276
 
1005
1277
  /**
1006
- * 启动 slave 侧心跳发送定时器
1278
+ * 创建并初始化 slave 侧 Dealer socket
1279
+ * 设计要点:
1280
+ * - immediate=true:仅向已完成连接的 pipe 发送,避免离线期间无限积压旧消息
1281
+ * - linger=0 :关闭旧 socket 时直接丢弃残留发送队列,避免旧心跳回灌
1282
+ * - sendTimeout=0:尽力发送,当前不可发时立即失败,不阻塞重连/启动流程
1283
+ *
1284
+ * @returns {import("zeromq").Dealer}
1285
+ */
1286
+ #createDealerSocket() {
1287
+ const dealer = new zmq.Dealer({
1288
+ routingId: this.id,
1289
+ immediate: true,
1290
+ linger: 0,
1291
+ sendTimeout: 0,
1292
+ reconnectInterval: 200,
1293
+ reconnectMaxInterval: 1000,
1294
+ });
1295
+
1296
+ dealer.connect(this.endpoints.router);
1297
+ this.#consume(dealer, (frames) => this.#handleDealerFrames(frames));
1298
+ return dealer;
1299
+ }
1300
+
1301
+ /**
1302
+ * 尝试发送注册帧
1303
+ * 说明:
1304
+ * - 该操作是“尽力发送”,不阻塞启动流程
1305
+ * - master 未上线时允许静默失败,由后续 heartbeat_ack 驱动恢复
1306
+ */
1307
+ #trySendRegister() {
1308
+ if (this.role !== "slave" || !this.running || !this.#sockets.dealer) return;
1309
+
1310
+ const registerToken = this.#secureEnabled
1311
+ ? this.#createAuthProof("register", "", [])
1312
+ : "";
1313
+
1314
+ this.#sendQueue
1315
+ .enqueue("dealer", () =>
1316
+ this.#sockets.dealer.send(buildRegisterFrames(registerToken)),
1317
+ )
1318
+ .catch(() => {});
1319
+ }
1320
+
1321
+ /**
1322
+ * 标记主节点已确认在线
1323
+ */
1324
+ #markMasterOnline() {
1325
+ this.#masterOnline = true;
1326
+ this.#lastMasterSeenAt = Date.now();
1327
+ }
1328
+
1329
+ /**
1330
+ * 标记主节点离线,并清理当前等待中的心跳状态
1331
+ */
1332
+ #markMasterOffline() {
1333
+ this.#masterOnline = false;
1334
+ this.#lastMasterSeenAt = 0;
1335
+ this.#heartbeatWaitingAck = false;
1336
+ clearTimeout(this.#heartbeatAckTimer);
1337
+ this.#heartbeatAckTimer = null;
1338
+ }
1339
+
1340
+ /**
1341
+ * 确认链路已打通
1342
+ * - heartbeat_ack 是最直接的确认信号
1343
+ * - 其他来自 master 的合法业务帧同样证明链路可达
1344
+ *
1345
+ * @param {{ scheduleNextHeartbeat?: boolean }} [options]
1346
+ */
1347
+ #confirmMasterReachable({ scheduleNextHeartbeat = false } = {}) {
1348
+ this.#markMasterOnline();
1349
+
1350
+ if (!this.#heartbeatWaitingAck) return;
1351
+
1352
+ this.#heartbeatWaitingAck = false;
1353
+ clearTimeout(this.#heartbeatAckTimer);
1354
+ this.#heartbeatAckTimer = null;
1355
+
1356
+ if (scheduleNextHeartbeat && this.#heartbeatInterval > 0) {
1357
+ this.#scheduleNextHeartbeat();
1358
+ }
1359
+ }
1360
+
1361
+ /**
1362
+ * 计算心跳应答超时时间
1363
+ * - 优先复用用户配置的 heartbeatTimeoutMs
1364
+ * - 未配置时回退到 interval × 2,且至少 1000ms
1365
+ */
1366
+ #resolveHeartbeatAckTimeoutMs() {
1367
+ if (this.#heartbeatTimeoutMs > 0) return this.#heartbeatTimeoutMs;
1368
+ return Math.max(this.#heartbeatInterval * 2, 1000);
1369
+ }
1370
+
1371
+ /**
1372
+ * 调度下一次心跳发送(单飞模式)
1373
+ * - 任意时刻最多只允许一个未确认心跳在飞
1374
+ * - 只有收到 heartbeat_ack 或其他合法回流后,才会进入下一轮
1375
+ *
1376
+ * @param {number} [delayMs=this.#heartbeatInterval]
1377
+ */
1378
+ #scheduleNextHeartbeat(delayMs = this.#heartbeatInterval) {
1379
+ clearTimeout(this.#heartbeatTimer);
1380
+ this.#heartbeatTimer = setTimeout(
1381
+ () => {
1382
+ this.#sendHeartbeatOnce().catch((error) => this.emit("error", error));
1383
+ },
1384
+ Math.max(0, Number(delayMs) || 0),
1385
+ );
1386
+ }
1387
+
1388
+ /**
1389
+ * 启动 slave 侧心跳发送流程
1390
+ * 实现方式:
1391
+ * - 启动时立即发送第一帧心跳
1392
+ * - 后续改为“发一条 → 等应答 → 再调度下一条”
1007
1393
  */
1008
1394
  #startHeartbeat() {
1009
- if (this.#heartbeatInterval <= 0) return;
1395
+ if (this.role !== "slave" || this.#heartbeatInterval <= 0) return;
1010
1396
 
1011
- this.#heartbeatTimer = setInterval(() => {
1012
- if (!this.running || !this.#sockets.dealer) return;
1397
+ this.#heartbeatWaitingAck = false;
1398
+ clearTimeout(this.#heartbeatAckTimer);
1399
+ this.#heartbeatAckTimer = null;
1400
+ this.#scheduleNextHeartbeat(0);
1401
+ }
1013
1402
 
1014
- const proof = this.#secureEnabled
1015
- ? this.#createAuthProof("heartbeat", "", [])
1016
- : "";
1403
+ /**
1404
+ * 发送单次心跳,并进入等待应答状态
1405
+ */
1406
+ async #sendHeartbeatOnce() {
1407
+ if (
1408
+ !this.running ||
1409
+ this.role !== "slave" ||
1410
+ this.#heartbeatInterval <= 0 ||
1411
+ !this.#sockets.dealer ||
1412
+ this.#heartbeatWaitingAck
1413
+ ) {
1414
+ return;
1415
+ }
1017
1416
 
1018
- // plain 模式仍保持历史帧结构 [CONTROL_PREFIX, CONTROL_HEARTBEAT]
1019
- const frames = this.#secureEnabled
1020
- ? buildHeartbeatFrames(proof)
1021
- : [CONTROL_PREFIX, CONTROL_HEARTBEAT];
1417
+ const proof = this.#secureEnabled
1418
+ ? this.#createAuthProof("heartbeat", "", [])
1419
+ : "";
1022
1420
 
1023
- this.#sendQueue
1024
- .enqueue("dealer", () => this.#sockets.dealer.send(frames))
1025
- .catch(() => {});
1026
- }, this.#heartbeatInterval);
1421
+ // plain 模式仍保持历史帧结构 [CONTROL_PREFIX, CONTROL_HEARTBEAT]
1422
+ const frames = this.#secureEnabled
1423
+ ? buildHeartbeatFrames(proof)
1424
+ : [CONTROL_PREFIX, CONTROL_HEARTBEAT];
1425
+
1426
+ this.#heartbeatWaitingAck = true;
1427
+ clearTimeout(this.#heartbeatAckTimer);
1428
+ this.#heartbeatAckTimer = setTimeout(() => {
1429
+ this.#onHeartbeatAckTimeout();
1430
+ }, this.#resolveHeartbeatAckTimeoutMs());
1431
+
1432
+ try {
1433
+ await this.#sendQueue.enqueue("dealer", () =>
1434
+ this.#sockets.dealer.send(frames),
1435
+ );
1436
+ } catch {
1437
+ this.#onHeartbeatAckTimeout();
1438
+ }
1439
+ }
1440
+
1441
+ /**
1442
+ * 心跳应答超时处理:
1443
+ * - 将主节点状态置为离线
1444
+ * - 主动重建 Dealer,丢弃旧连接残留消息
1445
+ */
1446
+ #onHeartbeatAckTimeout() {
1447
+ if (!this.running || this.role !== "slave" || !this.#heartbeatWaitingAck) {
1448
+ return;
1449
+ }
1450
+
1451
+ this.#markMasterOffline();
1452
+ this.#restartDealer("heartbeat_ack_timeout").catch((error) =>
1453
+ this.emit("error", error),
1454
+ );
1455
+ }
1456
+
1457
+ /**
1458
+ * 重建 slave 侧 Dealer 连接
1459
+ * 设计目标:
1460
+ * - 避免 master 重启后旧 socket 中残留的心跳/请求继续回灌
1461
+ * - 将重连控制为单次串行过程,避免并发 close/connect 造成状态抖动
1462
+ *
1463
+ * @param {string} reason
1464
+ * @returns {Promise<void>}
1465
+ */
1466
+ async #restartDealer(reason = "reconnect") {
1467
+ if (this.role !== "slave" || !this.running) return;
1468
+ if (this.#dealerReconnectPromise) return this.#dealerReconnectPromise;
1469
+
1470
+ this.#dealerReconnectPromise = (async () => {
1471
+ clearTimeout(this.#heartbeatTimer);
1472
+ clearTimeout(this.#heartbeatAckTimer);
1473
+ this.#heartbeatTimer = null;
1474
+ this.#heartbeatAckTimer = null;
1475
+ this.#heartbeatWaitingAck = false;
1476
+ this.#masterOnline = false;
1477
+ this.#lastMasterSeenAt = 0;
1478
+
1479
+ this.#pending.rejectAll(
1480
+ new Error(
1481
+ `与主节点的连接已重建(reason=${String(reason)}),待处理请求已取消。`,
1482
+ ),
1483
+ );
1484
+
1485
+ const oldDealer = this.#sockets.dealer;
1486
+ delete this.#sockets.dealer;
1487
+
1488
+ // slave 侧发送队列只服务 dealer,重建时直接清空可避免旧任务继续写入旧 socket
1489
+ this.#sendQueue.clear();
1490
+
1491
+ if (oldDealer) {
1492
+ try {
1493
+ oldDealer.close();
1494
+ } catch {}
1495
+ }
1496
+
1497
+ const dealer = this.#createDealerSocket();
1498
+ this.#sockets.dealer = dealer;
1499
+
1500
+ this.#trySendRegister();
1501
+ this.#startHeartbeat();
1502
+ })().finally(() => {
1503
+ this.#dealerReconnectPromise = null;
1504
+ });
1505
+
1506
+ return this.#dealerReconnectPromise;
1027
1507
  }
1028
1508
 
1029
1509
  /**
@@ -1056,13 +1536,14 @@ export class ZNL extends EventEmitter {
1056
1536
  /**
1057
1537
  * 生成签名证明令牌
1058
1538
  *
1059
- * @param {"register"|"heartbeat"|"request"|"response"|"publish"} kind
1539
+ * @param {"register"|"heartbeat"|"heartbeat_ack"|"request"|"response"|"publish"} kind
1060
1540
  * @param {string} requestId
1061
1541
  * @param {Buffer[]} payloadFrames
1062
1542
  * @returns {string}
1063
1543
  */
1064
- #createAuthProof(kind, requestId, payloadFrames) {
1065
- if (!this.#secureEnabled || !this.#signKey) return "";
1544
+ #createAuthProof(kind, requestId, payloadFrames, signKeyOverride = null) {
1545
+ const signKey = signKeyOverride ?? this.#signKey;
1546
+ if (!this.#secureEnabled || !signKey) return "";
1066
1547
 
1067
1548
  const envelope = {
1068
1549
  kind: String(kind),
@@ -1079,14 +1560,14 @@ export class ZNL extends EventEmitter {
1079
1560
  // 保留 canonical 文本,便于后续排障(签名实际在 token 内完成)
1080
1561
  canonicalSignInput(envelope);
1081
1562
 
1082
- return encodeAuthProofToken(this.#signKey, envelope);
1563
+ return encodeAuthProofToken(signKey, envelope);
1083
1564
  }
1084
1565
 
1085
1566
  /**
1086
1567
  * 校验入站签名证明 + 防重放 + 摘要一致性
1087
1568
  *
1088
1569
  * @param {{
1089
- * kind: "register"|"heartbeat"|"request"|"response"|"publish",
1570
+ * kind: "register"|"heartbeat"|"heartbeat_ack"|"request"|"response"|"publish",
1090
1571
  * proofToken: string|null,
1091
1572
  * requestId: string|null,
1092
1573
  * payloadFrames: Buffer[],
@@ -1100,9 +1581,12 @@ export class ZNL extends EventEmitter {
1100
1581
  requestId,
1101
1582
  payloadFrames,
1102
1583
  expectedNodeId = null,
1584
+ signKey = null,
1103
1585
  }) {
1104
1586
  if (!this.#secureEnabled) return { ok: true, envelope: null };
1105
- if (!this.#signKey) {
1587
+
1588
+ const verifyKey = signKey ?? this.#signKey;
1589
+ if (!verifyKey) {
1106
1590
  return { ok: false, error: "签名密钥未初始化。" };
1107
1591
  }
1108
1592
 
@@ -1110,7 +1594,7 @@ export class ZNL extends EventEmitter {
1110
1594
  return { ok: false, error: "缺少认证证明(proofToken)。" };
1111
1595
  }
1112
1596
 
1113
- const decoded = decodeAuthProofToken(this.#signKey, proofToken, {
1597
+ const decoded = decodeAuthProofToken(verifyKey, proofToken, {
1114
1598
  maxSkewMs: this.#maxTimeSkewMs,
1115
1599
  now: Date.now(),
1116
1600
  });
@@ -1167,13 +1651,14 @@ export class ZNL extends EventEmitter {
1167
1651
  * @param {*} payload
1168
1652
  * @returns {Buffer[]}
1169
1653
  */
1170
- #sealPayloadFrames(kind, requestId, payload) {
1654
+ #sealPayloadFrames(kind, requestId, payload, encryptKeyOverride = null) {
1171
1655
  if (!this.encrypted) {
1172
1656
  // 非加密模式保持历史行为:沿用协议层的原始帧类型(string/Buffer)
1173
1657
  return normalizeFrames(payload);
1174
1658
  }
1175
1659
 
1176
- if (!this.#encryptKey) {
1660
+ const encryptKey = encryptKeyOverride ?? this.#encryptKey;
1661
+ if (!encryptKey) {
1177
1662
  throw new Error("加密密钥未初始化。");
1178
1663
  }
1179
1664
 
@@ -1183,11 +1668,7 @@ export class ZNL extends EventEmitter {
1183
1668
  "utf8",
1184
1669
  );
1185
1670
 
1186
- const { iv, ciphertext, tag } = encryptFrames(
1187
- this.#encryptKey,
1188
- rawFrames,
1189
- aad,
1190
- );
1671
+ const { iv, ciphertext, tag } = encryptFrames(encryptKey, rawFrames, aad);
1191
1672
 
1192
1673
  // 用统一信封包装:version + iv + tag + ciphertext
1193
1674
  return [Buffer.from(SECURITY_ENVELOPE_VERSION), iv, tag, ciphertext];
@@ -1204,9 +1685,16 @@ export class ZNL extends EventEmitter {
1204
1685
  * @param {string} senderNodeId
1205
1686
  * @returns {Buffer[]}
1206
1687
  */
1207
- #openPayloadFrames(kind, requestId, payloadFrames, senderNodeId) {
1688
+ #openPayloadFrames(
1689
+ kind,
1690
+ requestId,
1691
+ payloadFrames,
1692
+ senderNodeId,
1693
+ encryptKeyOverride = null,
1694
+ ) {
1208
1695
  if (!this.encrypted) return payloadFrames;
1209
- if (!this.#encryptKey) throw new Error("加密密钥未初始化。");
1696
+ const encryptKey = encryptKeyOverride ?? this.#encryptKey;
1697
+ if (!encryptKey) throw new Error("加密密钥未初始化。");
1210
1698
 
1211
1699
  if (!Array.isArray(payloadFrames) || payloadFrames.length !== 4) {
1212
1700
  throw new Error("加密信封格式非法:期望 4 帧。");
@@ -1222,7 +1710,7 @@ export class ZNL extends EventEmitter {
1222
1710
  "utf8",
1223
1711
  );
1224
1712
 
1225
- return decryptFrames(this.#encryptKey, iv, ciphertext, tag, aad);
1713
+ return decryptFrames(encryptKey, iv, ciphertext, tag, aad);
1226
1714
  }
1227
1715
 
1228
1716
  /**
@@ -1247,7 +1735,11 @@ export class ZNL extends EventEmitter {
1247
1735
  * @param {Buffer|string|Uint8Array} identity
1248
1736
  * @param {{ touch?: boolean }} [options]
1249
1737
  */
1250
- #ensureSlaveOnline(identityText, identity, { touch = true } = {}) {
1738
+ #ensureSlaveOnline(
1739
+ identityText,
1740
+ identity,
1741
+ { touch = true, signKey = null, encryptKey = null } = {},
1742
+ ) {
1251
1743
  const id = String(identityText ?? "");
1252
1744
  if (!id) return;
1253
1745
 
@@ -1255,16 +1747,55 @@ export class ZNL extends EventEmitter {
1255
1747
  const entry = this.#slaves.get(id);
1256
1748
  if (entry) {
1257
1749
  if (touch) entry.lastSeen = now;
1750
+ if (signKey) entry.signKey = signKey;
1751
+ if (encryptKey) entry.encryptKey = encryptKey;
1258
1752
  return;
1259
1753
  }
1260
1754
 
1261
1755
  this.#slaves.set(id, {
1262
1756
  identity: identityToBuffer(identity),
1263
1757
  lastSeen: now,
1758
+ signKey: signKey ?? null,
1759
+ encryptKey: encryptKey ?? null,
1264
1760
  });
1265
1761
  this.emit("slave_connected", id);
1266
1762
  }
1267
1763
 
1764
+ /**
1765
+ * master 侧解析某个 slave 的签名/加密密钥
1766
+ * - 优先匹配 authKeyMap
1767
+ * - 未命中时回退到 this.authKey(若提供)
1768
+ *
1769
+ * @param {string} slaveId
1770
+ * @returns {{ signKey: Buffer, encryptKey: Buffer }|null}
1771
+ */
1772
+ #resolveSlaveKeys(slaveId) {
1773
+ if (!this.#secureEnabled) return null;
1774
+
1775
+ const id = String(slaveId ?? "");
1776
+ if (!id) return null;
1777
+
1778
+ const cached = this.#slaveKeyCache.get(id);
1779
+ if (cached) return cached;
1780
+
1781
+ let key = this.#authKeyMap.get(id);
1782
+ if (!key && this.authKey) {
1783
+ key = this.authKey;
1784
+ }
1785
+ if (!key) return null;
1786
+
1787
+ const derived = deriveKeys(key);
1788
+ this.#slaveKeyCache.set(id, derived);
1789
+
1790
+ const entry = this.#slaves.get(id);
1791
+ if (entry) {
1792
+ entry.signKey = derived.signKey;
1793
+ entry.encryptKey = derived.encryptKey;
1794
+ }
1795
+
1796
+ return derived;
1797
+ }
1798
+
1268
1799
  // ═══════════════════════════════════════════════════════════════════════════
1269
1800
  // 工具方法
1270
1801
  // ═══════════════════════════════════════════════════════════════════════════