lz-nframe 1.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.
- package/lib/NApp.d.ts +13 -0
- package/lib/NApp.js +23 -0
- package/lib/NConfigsMgr.d.ts +50 -0
- package/lib/NConfigsMgr.js +162 -0
- package/lib/NHttpClient.d.ts +71 -0
- package/lib/NHttpClient.js +196 -0
- package/lib/NSimpleHttpServer.d.ts +75 -0
- package/lib/NSimpleHttpServer.js +222 -0
- package/lib/NType.d.ts +64 -0
- package/lib/NType.js +8 -0
- package/lib/NWSClient.d.ts +44 -0
- package/lib/NWSClient.js +134 -0
- package/lib/NWSServer.d.ts +46 -0
- package/lib/NWSServer.js +133 -0
- package/lib/NWSServiceHub.d.ts +301 -0
- package/lib/NWSServiceHub.js +807 -0
- package/lib/NWSSocket.d.ts +23 -0
- package/lib/NWSSocket.js +30 -0
- package/lib/NWSUserClientJ.d.ts +76 -0
- package/lib/NWSUserClientJ.js +232 -0
- package/lib/NWSUserMgrJ.d.ts +82 -0
- package/lib/NWSUserMgrJ.js +208 -0
- package/package.json +28 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
/*******************************************************************************
|
|
2
|
+
文件: NWSServiceHub.ts
|
|
3
|
+
创建: 2026年05月07日
|
|
4
|
+
作者: 老张
|
|
5
|
+
描述:
|
|
6
|
+
NWS 后台服务通讯模块(单文件实现)
|
|
7
|
+
- 同时支持 Client + Server 双角色
|
|
8
|
+
- A/B 节点互连时单连接复用(自动仲裁)
|
|
9
|
+
- 支持 request/response、push、心跳、自动重连
|
|
10
|
+
|
|
11
|
+
使用方法(示例):
|
|
12
|
+
const hub = new NWSServiceHub({ nodeId: 'nodeA', log: true });
|
|
13
|
+
// 1) 开启服务端能力,供其他节点连入
|
|
14
|
+
hub.startServer(9001);
|
|
15
|
+
// 2) 注册对端并发起连接
|
|
16
|
+
hub.registerService({ nodeId: 'nodeB', url: 'http://127.0.0.1:9002', token: 'demo-token' });
|
|
17
|
+
hub.connectAll();
|
|
18
|
+
// 3) 注册请求处理器
|
|
19
|
+
hub.onRequest('user', 'ping', () => ({ pong: true }));
|
|
20
|
+
// 4) 发起请求
|
|
21
|
+
const res = await hub.request('nodeB', 'user', 'ping', { t: Date.now() });
|
|
22
|
+
// 5) 订阅推送
|
|
23
|
+
const onTip = (payload) => { console.log(payload); };
|
|
24
|
+
hub.onPush('notice', 'broadcast', onTip);
|
|
25
|
+
// 6) 发送推送
|
|
26
|
+
hub.send('nodeB', 'notice', 'broadcast', { msg: 'hello' });
|
|
27
|
+
// 7) 注销/下架(可选, 长期运行场景常用):
|
|
28
|
+
hub.offRequest('user', 'ping'); // 移除单个 request 路由
|
|
29
|
+
hub.offPush('notice', 'broadcast', onTip); // 移除某个 push 订阅(不传 handler 则清空该路由)
|
|
30
|
+
hub.unregisterService('nodeB'); // 完整释放对端的所有资源(连接/重连/挂起请求)
|
|
31
|
+
// 8) 关闭整个 hub
|
|
32
|
+
hub.disconnect();
|
|
33
|
+
|
|
34
|
+
命名约定 nodeId vs peerId(两者经常指向同一字符串,但语义不同):
|
|
35
|
+
- nodeId: 协议层"节点身份", 表示某个节点自己的唯一 id。
|
|
36
|
+
使用场景:
|
|
37
|
+
* this.nodeId 当前 Hub 自身节点 id
|
|
38
|
+
* packet.nodeId 协议包里发包方声明的节点 id
|
|
39
|
+
* NWSServicePeerConfig.nodeId 对端配置中的节点 id
|
|
40
|
+
- peerId: 本地视角下的"对端 key", 表示"我正在和哪个对端通信", 通常等于对端的 nodeId。
|
|
41
|
+
使用场景:
|
|
42
|
+
* peerChannels / clients / reconnectTimers 等 Map 的 key
|
|
43
|
+
* pendingRequests 中记录的归属 peerId
|
|
44
|
+
* request(peerId, ...) / send(peerId, ...) 等 public API 的参数
|
|
45
|
+
举例(A 收到 B 的包):
|
|
46
|
+
this.nodeId = 'nodeA' // A 自己
|
|
47
|
+
packet.nodeId = 'nodeB' // 包里 B 自称
|
|
48
|
+
const peerId = packet.nodeId; // peerId='nodeB', 作为本地索引使用
|
|
49
|
+
简记: 协议层/配置层/自身用 nodeId, 本地索引/函数参数/Map key 用 peerId。
|
|
50
|
+
|
|
51
|
+
两端互联(A/B 双方都 registerService 了对方)行为说明:
|
|
52
|
+
1) 初始建连——会出现"瞬时双连接":
|
|
53
|
+
两端各自的 connectAll() 没有先后约束, 会同时各发起一条 outbound,
|
|
54
|
+
因此每端短时间都会持有 1 条 outbound + 1 条 inbound, 物理上有 2 条 TCP。
|
|
55
|
+
2) 仲裁收敛——installOrArbitrateChannel + shouldKeepOutbound:
|
|
56
|
+
判定规则: nodeId 字典序更大的节点保留 outbound, 更小的节点保留 inbound。
|
|
57
|
+
两端独立做出的决定一定一致(规则与输入都对称), 不会出现互相打架的情况。
|
|
58
|
+
多余的那条连接会被 close()掉, 最终只剩一条物理连接 = "B.outbound ↔ A.inbound" (设 A < B)。
|
|
59
|
+
3) 稳态后的断线重连:
|
|
60
|
+
- outbound 侧(字典序大的一方): onOutboundDisconnected → scheduleReconnect, 进入指数退避重连。
|
|
61
|
+
- inbound 侧(字典序小的一方): 仅清理资源, 不主动重连(服务端不追客户端)。
|
|
62
|
+
所以重连只由仲裁后保留 outbound 的那一方承担, 不会形成重连风暴。
|
|
63
|
+
4) 后续并发再次互连仍然安全:
|
|
64
|
+
无论谁再随时多发起一次连接, 都会再次进入 installOrArbitrateChannel 仲裁,
|
|
65
|
+
多余的物理连接会被关掉, 状态收敛回唯一连接。
|
|
66
|
+
5) 心跳判死路径:
|
|
67
|
+
心跳判死后由 handleChannelDead 走与 onOutboundDisconnected 等价的清理 + 重连流程,
|
|
68
|
+
即 outbound 侧重连, inbound 侧只清理。
|
|
69
|
+
边角注意:
|
|
70
|
+
- outbound 侧进程死了不重启时, 链路会一直断开, 直到 outbound 侧再次启动主动连过来。
|
|
71
|
+
- 由于仲裁直接以 nodeId 字符串比较, 改名可能让"活跃的连接方向"换边, 一般无感。
|
|
72
|
+
|
|
73
|
+
TODO:
|
|
74
|
+
1.路由
|
|
75
|
+
*******************************************************************************/
|
|
76
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
77
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
78
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
79
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
80
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
81
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
82
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
import { EventEmitter } from 'events';
|
|
86
|
+
import NWSClient from './NWSClient';
|
|
87
|
+
import NWSServer from './NWSServer';
|
|
88
|
+
class NWSServiceHub extends EventEmitter {
|
|
89
|
+
constructor(options) {
|
|
90
|
+
super();
|
|
91
|
+
this.server = null;
|
|
92
|
+
this.peerConfigs = new Map();
|
|
93
|
+
this.clients = new Map();
|
|
94
|
+
this.peerChannels = new Map();
|
|
95
|
+
this.inboundLinkPeerMap = new Map();
|
|
96
|
+
this.pendingRequests = new Map();
|
|
97
|
+
this.requestHandlers = new Map();
|
|
98
|
+
this.pushHandlers = new Map();
|
|
99
|
+
this.reconnectTimers = new Map();
|
|
100
|
+
/** 当前每个 peer 的下一次重连等待时长(毫秒),用于指数退避 */
|
|
101
|
+
this.reconnectDelays = new Map();
|
|
102
|
+
this.heartbeatTimer = null;
|
|
103
|
+
/** 请求追踪计数器 */
|
|
104
|
+
this.traceCounter = 0;
|
|
105
|
+
if (!(options === null || options === void 0 ? void 0 : options.nodeId)) {
|
|
106
|
+
throw new Error('NWSServiceHub 需要 nodeId');
|
|
107
|
+
}
|
|
108
|
+
this.nodeId = options.nodeId;
|
|
109
|
+
this.log = options.log || false;
|
|
110
|
+
this.requestTimeoutMs = options.requestTimeoutMs || 5000;
|
|
111
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs || 10000;
|
|
112
|
+
this.reconnectIntervalMs = options.reconnectIntervalMs || 3000;
|
|
113
|
+
this.reconnectMaxIntervalMs = options.reconnectMaxIntervalMs || 60000;
|
|
114
|
+
this.heartbeatDeadFactor = options.heartbeatDeadFactor || 3;
|
|
115
|
+
}
|
|
116
|
+
registerService(config) {
|
|
117
|
+
if (!(config === null || config === void 0 ? void 0 : config.nodeId)) {
|
|
118
|
+
throw new Error('registerService 需要 nodeId');
|
|
119
|
+
}
|
|
120
|
+
if (config.nodeId === this.nodeId) {
|
|
121
|
+
throw new Error('不能注册自己作为对端服务');
|
|
122
|
+
}
|
|
123
|
+
this.peerConfigs.set(config.nodeId, config);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 注销已注册的对端服务, 完成完整资源释放:
|
|
127
|
+
* - 删除静态配置 peerConfigs(避免后续被 connectAll 再次拉起)
|
|
128
|
+
* - 取消该对端的重连定时器并清空退避计数
|
|
129
|
+
* - 关闭并丢弃 NWSClient 实例(包括其 EventEmitter 监听)
|
|
130
|
+
* - 关闭当前 channel(无论 inbound/outbound), 并清理 inboundLinkPeerMap 反查项
|
|
131
|
+
* - 拒绝该 peer 所有挂起请求, 防止永远等不到响应
|
|
132
|
+
* - 触发一次 EventPeerDisconnected, 便于上层做联动清理
|
|
133
|
+
* @returns 是否真的存在并被清理过(全部不存在时返回 false)
|
|
134
|
+
*/
|
|
135
|
+
unregisterService(peerId) {
|
|
136
|
+
if (!peerId) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
let touched = false;
|
|
140
|
+
if (this.peerConfigs.delete(peerId)) {
|
|
141
|
+
touched = true;
|
|
142
|
+
}
|
|
143
|
+
const reconnectTimer = this.reconnectTimers.get(peerId);
|
|
144
|
+
if (reconnectTimer) {
|
|
145
|
+
clearTimeout(reconnectTimer);
|
|
146
|
+
this.reconnectTimers.delete(peerId);
|
|
147
|
+
touched = true;
|
|
148
|
+
}
|
|
149
|
+
this.reconnectDelays.delete(peerId);
|
|
150
|
+
const client = this.clients.get(peerId);
|
|
151
|
+
if (client) {
|
|
152
|
+
// 先摘事件再 disconnect, 避免遗留闭包导致的内存悬挂
|
|
153
|
+
client.removeAllListeners();
|
|
154
|
+
client.disconnect();
|
|
155
|
+
this.clients.delete(peerId);
|
|
156
|
+
touched = true;
|
|
157
|
+
}
|
|
158
|
+
const channel = this.peerChannels.get(peerId);
|
|
159
|
+
if (channel) {
|
|
160
|
+
channel.status = 'closed';
|
|
161
|
+
try {
|
|
162
|
+
channel.close();
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
this.emitError(new Error(`注销时关闭通道异常: ${(err === null || err === void 0 ? void 0 : err.message) || err}`));
|
|
166
|
+
}
|
|
167
|
+
this.peerChannels.delete(peerId);
|
|
168
|
+
if (channel.direction === 'inbound') {
|
|
169
|
+
for (let [linkId, mappedPeer] of this.inboundLinkPeerMap.entries()) {
|
|
170
|
+
if (mappedPeer === peerId) {
|
|
171
|
+
this.inboundLinkPeerMap.delete(linkId);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
this.emit(NWSServiceHub.EventPeerDisconnected, peerId);
|
|
177
|
+
touched = true;
|
|
178
|
+
}
|
|
179
|
+
// 拒绝所有指向该 peer 的挂起请求, 调用方可立即得到失败而非等到超时
|
|
180
|
+
for (let [traceId, pending] of this.pendingRequests.entries()) {
|
|
181
|
+
if (pending.peerId === peerId) {
|
|
182
|
+
clearTimeout(pending.timer);
|
|
183
|
+
this.pendingRequests.delete(traceId);
|
|
184
|
+
pending.reject(new Error(`peer ${peerId} 已注销`));
|
|
185
|
+
touched = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (touched) {
|
|
189
|
+
this.logInfo(`对端 ${peerId} 已注销并释放资源`);
|
|
190
|
+
}
|
|
191
|
+
return touched;
|
|
192
|
+
}
|
|
193
|
+
/** 启动当前节点的 WS 服务端能力,用于承接其他节点主动连入 */
|
|
194
|
+
startServer(port, host = '0.0.0.0') {
|
|
195
|
+
if (this.server) {
|
|
196
|
+
this.stopServer();
|
|
197
|
+
}
|
|
198
|
+
this.server = new NWSServer({ log: this.log });
|
|
199
|
+
this.server.on(NWSServer.EventError, (err) => this.emitError(err));
|
|
200
|
+
this.server.on(NWSServer.EventClientMessage, (linkId, data) => this.onInboundMessage(linkId, data));
|
|
201
|
+
this.server.on(NWSServer.EventClientDisconnected, (linkId) => this.onInboundDisconnected(linkId));
|
|
202
|
+
this.server.start(port, host);
|
|
203
|
+
this.logInfo(`服务端启动: ws://${host}:${port}`);
|
|
204
|
+
}
|
|
205
|
+
/** 停止当前节点的 WS 服务端能力(不影响已注册配置) */
|
|
206
|
+
stopServer() {
|
|
207
|
+
if (!this.server) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.server.stop();
|
|
211
|
+
this.server.removeAllListeners();
|
|
212
|
+
this.server = null;
|
|
213
|
+
}
|
|
214
|
+
/** 按静态配置连接所有对端节点,并启动心跳 */
|
|
215
|
+
connectAll() {
|
|
216
|
+
for (let [peerId] of this.peerConfigs.entries()) {
|
|
217
|
+
this.ensurePeerConnected(peerId);
|
|
218
|
+
}
|
|
219
|
+
this.startHeartbeat();
|
|
220
|
+
}
|
|
221
|
+
/** 关闭 hub 的所有能力(客户端连接、服务端监听、定时器、挂起请求) */
|
|
222
|
+
disconnect() {
|
|
223
|
+
for (let timer of this.reconnectTimers.values()) {
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
}
|
|
226
|
+
this.reconnectTimers.clear();
|
|
227
|
+
this.reconnectDelays.clear();
|
|
228
|
+
if (this.heartbeatTimer) {
|
|
229
|
+
clearInterval(this.heartbeatTimer);
|
|
230
|
+
this.heartbeatTimer = null;
|
|
231
|
+
}
|
|
232
|
+
for (let client of this.clients.values()) {
|
|
233
|
+
client.disconnect();
|
|
234
|
+
}
|
|
235
|
+
this.clients.clear();
|
|
236
|
+
for (let channel of this.peerChannels.values()) {
|
|
237
|
+
channel.close();
|
|
238
|
+
}
|
|
239
|
+
this.peerChannels.clear();
|
|
240
|
+
this.inboundLinkPeerMap.clear();
|
|
241
|
+
for (let pending of this.pendingRequests.values()) {
|
|
242
|
+
clearTimeout(pending.timer);
|
|
243
|
+
pending.reject(new Error('hub disconnected'));
|
|
244
|
+
}
|
|
245
|
+
this.pendingRequests.clear();
|
|
246
|
+
this.stopServer();
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* 向指定 peer 发起请求并等待响应。
|
|
250
|
+
* 泛型:
|
|
251
|
+
* - TRes 返回值类型, 默认 any。建议显式指定以获得调用处的类型推导。
|
|
252
|
+
* - TReq 请求体类型, 默认 any。一般可由 payload 自动推断, 必要时显式指定。
|
|
253
|
+
* 用法: const user = await hub.request<UserInfo>('nodeB', 'user', 'get', { id: 1 });
|
|
254
|
+
*/
|
|
255
|
+
request(peerId, service, action, payload, timeoutMs) {
|
|
256
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
257
|
+
const channel = this.peerChannels.get(peerId);
|
|
258
|
+
if (!channel || channel.status !== 'connected') {
|
|
259
|
+
throw new Error(`peer ${peerId} 未连接`);
|
|
260
|
+
}
|
|
261
|
+
const traceId = this.genTraceId();
|
|
262
|
+
const packet = {
|
|
263
|
+
type: 'request',
|
|
264
|
+
nodeId: this.nodeId,
|
|
265
|
+
ts: Date.now(),
|
|
266
|
+
traceId,
|
|
267
|
+
service,
|
|
268
|
+
action,
|
|
269
|
+
payload,
|
|
270
|
+
};
|
|
271
|
+
const requestTimeout = timeoutMs || this.requestTimeoutMs;
|
|
272
|
+
return new Promise((resolve, reject) => {
|
|
273
|
+
const timer = setTimeout(() => {
|
|
274
|
+
this.pendingRequests.delete(traceId);
|
|
275
|
+
reject(new Error(`请求超时: ${service}.${action}`));
|
|
276
|
+
}, requestTimeout);
|
|
277
|
+
this.pendingRequests.set(traceId, { peerId, resolve, reject, timer });
|
|
278
|
+
const ok = channel.send(packet);
|
|
279
|
+
if (!ok) {
|
|
280
|
+
clearTimeout(timer);
|
|
281
|
+
this.pendingRequests.delete(traceId);
|
|
282
|
+
reject(new Error(`发送失败: ${peerId}`));
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/** 向指定 peer 发送 push 消息(单向,不等待响应) */
|
|
288
|
+
send(peerId, service, action, payload) {
|
|
289
|
+
const channel = this.peerChannels.get(peerId);
|
|
290
|
+
if (!channel || channel.status !== 'connected') {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
return channel.send({
|
|
294
|
+
type: 'push',
|
|
295
|
+
nodeId: this.nodeId,
|
|
296
|
+
ts: Date.now(),
|
|
297
|
+
service,
|
|
298
|
+
action,
|
|
299
|
+
payload,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/** 发送底层协议包(高级接口) */
|
|
303
|
+
sendToPeer(peerId, packet) {
|
|
304
|
+
const channel = this.peerChannels.get(peerId);
|
|
305
|
+
if (!channel || channel.status !== 'connected') {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
return channel.send(packet);
|
|
309
|
+
}
|
|
310
|
+
/** 注册请求处理器,匹配键为 service:action */
|
|
311
|
+
onRequest(service, action, handler) {
|
|
312
|
+
this.requestHandlers.set(this.makeRouteKey(service, action), handler);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* 注销请求处理器(同 onRequest 一对)。
|
|
316
|
+
* @returns 之前是否注册过该路由(false 表示不存在, 不会报错)
|
|
317
|
+
*/
|
|
318
|
+
offRequest(service, action) {
|
|
319
|
+
return this.requestHandlers.delete(this.makeRouteKey(service, action));
|
|
320
|
+
}
|
|
321
|
+
/** 注册 push 处理器,支持同一路由多个订阅者 */
|
|
322
|
+
onPush(service, action, handler) {
|
|
323
|
+
const key = this.makeRouteKey(service, action);
|
|
324
|
+
const handlers = this.pushHandlers.get(key) || [];
|
|
325
|
+
handlers.push(handler);
|
|
326
|
+
this.pushHandlers.set(key, handlers);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* 注销 push 处理器:
|
|
330
|
+
* - 传入 handler: 仅移除该具体 handler(若注册过多次会全部移除, 与 EventEmitter.off 习惯一致)
|
|
331
|
+
* - 不传 handler: 移除该路由下所有 handler
|
|
332
|
+
* @returns 实际被移除的 handler 数量(0 表示无对应注册)
|
|
333
|
+
*/
|
|
334
|
+
offPush(service, action, handler) {
|
|
335
|
+
const key = this.makeRouteKey(service, action);
|
|
336
|
+
const handlers = this.pushHandlers.get(key);
|
|
337
|
+
if (!handlers || handlers.length === 0) {
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
if (!handler) {
|
|
341
|
+
const count = handlers.length;
|
|
342
|
+
this.pushHandlers.delete(key);
|
|
343
|
+
return count;
|
|
344
|
+
}
|
|
345
|
+
let removed = 0;
|
|
346
|
+
// 倒序遍历, splice 不会影响后续索引
|
|
347
|
+
for (let i = handlers.length - 1; i >= 0; i--) {
|
|
348
|
+
if (handlers[i] === handler) {
|
|
349
|
+
handlers.splice(i, 1);
|
|
350
|
+
removed++;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (handlers.length === 0) {
|
|
354
|
+
this.pushHandlers.delete(key);
|
|
355
|
+
}
|
|
356
|
+
return removed;
|
|
357
|
+
}
|
|
358
|
+
ensurePeerConnected(peerId) {
|
|
359
|
+
const config = this.peerConfigs.get(peerId);
|
|
360
|
+
if (!(config === null || config === void 0 ? void 0 : config.url)) {
|
|
361
|
+
// 没有对端 URL 就无法主动发起连接,由对端作为"客户端"侧负责连入即可
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const existed = this.peerChannels.get(peerId);
|
|
365
|
+
if ((existed === null || existed === void 0 ? void 0 : existed.status) === 'connected') {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
let client = this.clients.get(peerId);
|
|
369
|
+
if (!client) {
|
|
370
|
+
client = new NWSClient({ log: this.log });
|
|
371
|
+
this.clients.set(peerId, client);
|
|
372
|
+
client.on(NWSClient.EventConnected, () => this.onOutboundConnected(peerId));
|
|
373
|
+
client.on(NWSClient.EventDisconnected, () => this.onOutboundDisconnected(peerId));
|
|
374
|
+
client.on(NWSClient.EventError, (err) => {
|
|
375
|
+
this.emitError(new Error(`对端 ${peerId} 连接异常: ${(err === null || err === void 0 ? void 0 : err.message) || err}`));
|
|
376
|
+
});
|
|
377
|
+
client.on(NWSClient.EventMessage, (raw) => this.onOutboundMessage(peerId, raw));
|
|
378
|
+
}
|
|
379
|
+
client.connect(this.buildUrlWithToken(config.url, config.token));
|
|
380
|
+
}
|
|
381
|
+
/** 处理客户端连接成功 */
|
|
382
|
+
onOutboundConnected(peerId) {
|
|
383
|
+
var _a;
|
|
384
|
+
const client = this.clients.get(peerId);
|
|
385
|
+
if (!client) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
// 连接成功后重置退避计数,下次断线从初始间隔重新开始
|
|
389
|
+
this.reconnectDelays.delete(peerId);
|
|
390
|
+
const channel = {
|
|
391
|
+
peerId,
|
|
392
|
+
direction: 'outbound',
|
|
393
|
+
send: (packet) => this.sendByClient(packet, client),
|
|
394
|
+
close: () => client.disconnect(),
|
|
395
|
+
lastActiveTime: Date.now(),
|
|
396
|
+
status: 'connected',
|
|
397
|
+
};
|
|
398
|
+
this.installOrArbitrateChannel(channel);
|
|
399
|
+
channel.send({
|
|
400
|
+
type: 'hello',
|
|
401
|
+
nodeId: this.nodeId,
|
|
402
|
+
ts: Date.now(),
|
|
403
|
+
token: (_a = this.peerConfigs.get(peerId)) === null || _a === void 0 ? void 0 : _a.token,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
onOutboundDisconnected(peerId) {
|
|
407
|
+
const channel = this.peerChannels.get(peerId);
|
|
408
|
+
if ((channel === null || channel === void 0 ? void 0 : channel.direction) === 'outbound') {
|
|
409
|
+
channel.status = 'closed';
|
|
410
|
+
this.peerChannels.delete(peerId);
|
|
411
|
+
this.emit(NWSServiceHub.EventPeerDisconnected, peerId);
|
|
412
|
+
}
|
|
413
|
+
this.scheduleReconnect(peerId);
|
|
414
|
+
}
|
|
415
|
+
onOutboundMessage(peerId, raw) {
|
|
416
|
+
const packet = this.parsePacket(raw);
|
|
417
|
+
if (!packet) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const channel = this.peerChannels.get(peerId);
|
|
421
|
+
if (channel) {
|
|
422
|
+
channel.lastActiveTime = Date.now();
|
|
423
|
+
}
|
|
424
|
+
this.onPacket(peerId, packet);
|
|
425
|
+
}
|
|
426
|
+
/** 处理客户端消息 */
|
|
427
|
+
onInboundMessage(linkId, data) {
|
|
428
|
+
const packet = this.parsePacket(data);
|
|
429
|
+
if (!packet) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const peerId = packet.nodeId;
|
|
433
|
+
if (!peerId) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const sendFunc = (msg) => { var _a; return ((_a = this.server) === null || _a === void 0 ? void 0 : _a.sendToClient(linkId, msg)) || false; };
|
|
437
|
+
let channel = this.peerChannels.get(peerId);
|
|
438
|
+
if (!channel || channel.direction !== 'inbound') {
|
|
439
|
+
const newChannel = {
|
|
440
|
+
peerId,
|
|
441
|
+
direction: 'inbound',
|
|
442
|
+
send: (pack) => this.sendBySocket(pack, sendFunc),
|
|
443
|
+
close: () => { var _a; return (_a = this.server) === null || _a === void 0 ? void 0 : _a.disconnectClient(linkId); },
|
|
444
|
+
lastActiveTime: Date.now(),
|
|
445
|
+
status: 'connected',
|
|
446
|
+
};
|
|
447
|
+
this.inboundLinkPeerMap.set(linkId, peerId);
|
|
448
|
+
this.installOrArbitrateChannel(newChannel);
|
|
449
|
+
channel = this.peerChannels.get(peerId);
|
|
450
|
+
}
|
|
451
|
+
if (channel) {
|
|
452
|
+
channel.lastActiveTime = Date.now();
|
|
453
|
+
}
|
|
454
|
+
this.onPacket(peerId, packet);
|
|
455
|
+
}
|
|
456
|
+
/** 处理客户端断开连接 */
|
|
457
|
+
onInboundDisconnected(linkId) {
|
|
458
|
+
const peerId = this.inboundLinkPeerMap.get(linkId);
|
|
459
|
+
if (!peerId) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
this.inboundLinkPeerMap.delete(linkId);
|
|
463
|
+
const channel = this.peerChannels.get(peerId);
|
|
464
|
+
if ((channel === null || channel === void 0 ? void 0 : channel.direction) === 'inbound') {
|
|
465
|
+
channel.status = 'closed';
|
|
466
|
+
this.peerChannels.delete(peerId);
|
|
467
|
+
this.emit(NWSServiceHub.EventPeerDisconnected, peerId);
|
|
468
|
+
}
|
|
469
|
+
// 重连只由"发起方(客户端)"负责:服务端不追客户端,避免对端不可达时持续重试。
|
|
470
|
+
// 若本节点同时也持有对端 URL,对端那侧自身会按其退避策略重连过来。
|
|
471
|
+
}
|
|
472
|
+
/** 安装或仲裁客户端连接 */
|
|
473
|
+
installOrArbitrateChannel(channel) {
|
|
474
|
+
const existed = this.peerChannels.get(channel.peerId);
|
|
475
|
+
if (!existed) {
|
|
476
|
+
this.peerChannels.set(channel.peerId, channel);
|
|
477
|
+
this.emit(NWSServiceHub.EventPeerConnected, channel.peerId);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
// 已存在连接时进行仲裁,确保同一对端最终只保留一个物理连接
|
|
481
|
+
const keep = this.shouldKeepOutbound(channel.peerId) ? 'outbound' : 'inbound';
|
|
482
|
+
if (channel.direction === keep) {
|
|
483
|
+
existed.close();
|
|
484
|
+
existed.status = 'closed';
|
|
485
|
+
this.peerChannels.set(channel.peerId, channel);
|
|
486
|
+
this.emit(NWSServiceHub.EventPeerConnected, channel.peerId);
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
channel.close();
|
|
490
|
+
channel.status = 'closed';
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/** 是否保持 outbound 连接 */
|
|
494
|
+
shouldKeepOutbound(peerId) {
|
|
495
|
+
// 固定仲裁规则:nodeId 字典序更大的节点保持 outbound。
|
|
496
|
+
// 这样 A/B 同时互连时会自然收敛为一条链路,不会形成双连接。
|
|
497
|
+
return this.nodeId > peerId;
|
|
498
|
+
}
|
|
499
|
+
onPacket(peerId, packet) {
|
|
500
|
+
switch (packet.type) {
|
|
501
|
+
case 'hello':
|
|
502
|
+
this.handleHello(peerId, packet);
|
|
503
|
+
break;
|
|
504
|
+
case 'hello_ack':
|
|
505
|
+
break;
|
|
506
|
+
case 'request':
|
|
507
|
+
this.handleRequest(peerId, packet);
|
|
508
|
+
break;
|
|
509
|
+
case 'response':
|
|
510
|
+
this.handleResponse(peerId, packet);
|
|
511
|
+
break;
|
|
512
|
+
case 'push':
|
|
513
|
+
this.handlePush(peerId, packet);
|
|
514
|
+
break;
|
|
515
|
+
case 'ping':
|
|
516
|
+
this.replyPong(peerId);
|
|
517
|
+
break;
|
|
518
|
+
case 'pong':
|
|
519
|
+
break;
|
|
520
|
+
default:
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
handleHello(peerId, packet) {
|
|
525
|
+
const conf = this.peerConfigs.get(peerId);
|
|
526
|
+
if ((conf === null || conf === void 0 ? void 0 : conf.token) && conf.token !== packet.token) {
|
|
527
|
+
this.emitError(new Error(`对端 ${peerId} 鉴权失败`));
|
|
528
|
+
const channel = this.peerChannels.get(peerId);
|
|
529
|
+
channel === null || channel === void 0 ? void 0 : channel.close();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const channel = this.peerChannels.get(peerId);
|
|
533
|
+
if (!channel || channel.status !== 'connected') {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
channel.send({
|
|
537
|
+
type: 'hello_ack',
|
|
538
|
+
nodeId: this.nodeId,
|
|
539
|
+
ts: Date.now(),
|
|
540
|
+
payload: { ok: true, nodeId: this.nodeId },
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
handleRequest(peerId, packet) {
|
|
544
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
545
|
+
const traceId = packet.traceId || '';
|
|
546
|
+
if (!traceId || !packet.service || !packet.action) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const routeKey = this.makeRouteKey(packet.service, packet.action);
|
|
550
|
+
const handler = this.requestHandlers.get(routeKey);
|
|
551
|
+
if (!handler) {
|
|
552
|
+
this.sendResponse(peerId, traceId, 404, null, `未找到处理器: ${routeKey}`);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
const result = yield handler(packet.payload, {
|
|
557
|
+
peerId,
|
|
558
|
+
service: packet.service,
|
|
559
|
+
action: packet.action,
|
|
560
|
+
traceId,
|
|
561
|
+
});
|
|
562
|
+
this.sendResponse(peerId, traceId, 0, result, 'ok');
|
|
563
|
+
}
|
|
564
|
+
catch (err) {
|
|
565
|
+
// 抛出端打印完整堆栈, 便于在本地排查; 对端只拿到精简的 message
|
|
566
|
+
if (this.log) {
|
|
567
|
+
console.error(`[NWSServiceHub] 请求处理异常 ${routeKey} (peer=${peerId}, traceId=${traceId}):\n${(err === null || err === void 0 ? void 0 : err.stack) || err}`);
|
|
568
|
+
}
|
|
569
|
+
this.sendResponse(peerId, traceId, 500, null, (err === null || err === void 0 ? void 0 : err.message) || '请求处理失败');
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
handleResponse(peerId, packet) {
|
|
574
|
+
const traceId = packet.traceId || '';
|
|
575
|
+
const pending = this.pendingRequests.get(traceId);
|
|
576
|
+
if (!pending) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (pending.peerId !== peerId) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
// traceId 命中后立即清理挂起表,避免重复响应污染状态
|
|
583
|
+
clearTimeout(pending.timer);
|
|
584
|
+
this.pendingRequests.delete(traceId);
|
|
585
|
+
if ((packet.code || 0) !== 0) {
|
|
586
|
+
pending.reject(new Error(packet.message || '请求失败'));
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
pending.resolve(packet.payload);
|
|
590
|
+
}
|
|
591
|
+
handlePush(peerId, packet) {
|
|
592
|
+
if (!packet.service || !packet.action) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const routeKey = this.makeRouteKey(packet.service, packet.action);
|
|
596
|
+
const handlers = this.pushHandlers.get(routeKey) || [];
|
|
597
|
+
for (let handler of handlers) {
|
|
598
|
+
try {
|
|
599
|
+
handler(packet.payload, {
|
|
600
|
+
peerId,
|
|
601
|
+
service: packet.service,
|
|
602
|
+
action: packet.action,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
catch (err) {
|
|
606
|
+
this.emitError(new Error(`push 回调异常: ${(err === null || err === void 0 ? void 0 : err.message) || err}`));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
sendResponse(peerId, traceId, code, payload, message) {
|
|
611
|
+
const channel = this.peerChannels.get(peerId);
|
|
612
|
+
if (!channel || channel.status !== 'connected') {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
channel.send({
|
|
616
|
+
type: 'response',
|
|
617
|
+
nodeId: this.nodeId,
|
|
618
|
+
ts: Date.now(),
|
|
619
|
+
traceId,
|
|
620
|
+
code,
|
|
621
|
+
payload,
|
|
622
|
+
message,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
replyPong(peerId) {
|
|
626
|
+
const channel = this.peerChannels.get(peerId);
|
|
627
|
+
if (!channel || channel.status !== 'connected') {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
channel.send({
|
|
631
|
+
type: 'pong',
|
|
632
|
+
nodeId: this.nodeId,
|
|
633
|
+
ts: Date.now(),
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
startHeartbeat() {
|
|
637
|
+
if (this.heartbeatTimer) {
|
|
638
|
+
clearInterval(this.heartbeatTimer);
|
|
639
|
+
}
|
|
640
|
+
this.heartbeatTimer = setInterval(() => {
|
|
641
|
+
const now = Date.now();
|
|
642
|
+
const deadThresholdMs = this.heartbeatIntervalMs * this.heartbeatDeadFactor;
|
|
643
|
+
// 先快照一份待处理的死链,避免在遍历过程中修改 peerChannels 导致迭代异常
|
|
644
|
+
const deadPeers = [];
|
|
645
|
+
for (let [peerId, channel] of this.peerChannels.entries()) {
|
|
646
|
+
if (channel.status !== 'connected') {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const idleMs = now - channel.lastActiveTime;
|
|
650
|
+
if (idleMs >= deadThresholdMs) {
|
|
651
|
+
// 心跳判死:连续多个周期没有任何来自对端的数据(包括 pong),认为链路已死
|
|
652
|
+
deadPeers.push(peerId);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (idleMs >= this.heartbeatIntervalMs) {
|
|
656
|
+
// 仅在空闲时发心跳,减少高频业务消息下的额外开销
|
|
657
|
+
channel.send({
|
|
658
|
+
type: 'ping',
|
|
659
|
+
nodeId: this.nodeId,
|
|
660
|
+
ts: now,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
for (let peerId of deadPeers) {
|
|
665
|
+
this.logInfo(`对端 ${peerId} 心跳超时,主动断开链路`);
|
|
666
|
+
this.handleChannelDead(peerId);
|
|
667
|
+
}
|
|
668
|
+
}, this.heartbeatIntervalMs);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* 断线后调度重连。
|
|
672
|
+
* 规则:
|
|
673
|
+
* 1) 只有"持有对端 URL"的一侧才会重连(即作为客户端发起方),
|
|
674
|
+
* 服务端只是被动承接,不会去追客户端,避免对端不可达时一直占用资源。
|
|
675
|
+
* 2) 采用指数退避:reconnectIntervalMs * 2 * 2 * ...,封顶 reconnectMaxIntervalMs,
|
|
676
|
+
* 连接成功后由 onOutboundConnected 清空 reconnectDelays 计数恢复初始间隔。
|
|
677
|
+
*/
|
|
678
|
+
scheduleReconnect(peerId) {
|
|
679
|
+
const config = this.peerConfigs.get(peerId);
|
|
680
|
+
if (!(config === null || config === void 0 ? void 0 : config.url)) {
|
|
681
|
+
// 没有对端 URL 表示本节点是"被连入"的服务端侧,不主动重连
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (this.reconnectTimers.has(peerId)) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const currentDelay = this.reconnectDelays.get(peerId) || this.reconnectIntervalMs;
|
|
688
|
+
const timer = setTimeout(() => {
|
|
689
|
+
this.reconnectTimers.delete(peerId);
|
|
690
|
+
// 失败时下一次延时翻倍,封顶 reconnectMaxIntervalMs
|
|
691
|
+
const nextDelay = Math.min(currentDelay * 2, this.reconnectMaxIntervalMs);
|
|
692
|
+
this.reconnectDelays.set(peerId, nextDelay);
|
|
693
|
+
this.ensurePeerConnected(peerId);
|
|
694
|
+
}, currentDelay);
|
|
695
|
+
this.reconnectTimers.set(peerId, timer);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* 心跳判死后清理链路。
|
|
699
|
+
* 注意:channel.close() 内部走的是 client.disconnect()/server.disconnectClient(),
|
|
700
|
+
* 为防止重复进入正常断开回调流程,这里需要手动完成 peerChannels 清理与重连调度。
|
|
701
|
+
* - outbound 死链:client.disconnect() 已抹掉监听不会回调,需要主动 scheduleReconnect。
|
|
702
|
+
* - inbound 死链:server 的 close 事件可能仍会触发 onInboundDisconnected,
|
|
703
|
+
* 但由于已先一步删除 inboundLinkPeerMap 项,回调会因找不到 peerId 直接返回。
|
|
704
|
+
*/
|
|
705
|
+
handleChannelDead(peerId) {
|
|
706
|
+
const channel = this.peerChannels.get(peerId);
|
|
707
|
+
if (!channel) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const direction = channel.direction;
|
|
711
|
+
channel.status = 'closed';
|
|
712
|
+
try {
|
|
713
|
+
channel.close();
|
|
714
|
+
}
|
|
715
|
+
catch (err) {
|
|
716
|
+
this.emitError(new Error(`关闭死链异常: ${(err === null || err === void 0 ? void 0 : err.message) || err}`));
|
|
717
|
+
}
|
|
718
|
+
this.peerChannels.delete(peerId);
|
|
719
|
+
if (direction === 'inbound') {
|
|
720
|
+
// 反向清理 linkId → peerId 映射,避免悬挂条目
|
|
721
|
+
for (let [linkId, mappedPeer] of this.inboundLinkPeerMap.entries()) {
|
|
722
|
+
if (mappedPeer === peerId) {
|
|
723
|
+
this.inboundLinkPeerMap.delete(linkId);
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
this.emit(NWSServiceHub.EventPeerDisconnected, peerId);
|
|
729
|
+
if (direction === 'outbound') {
|
|
730
|
+
// 只有作为发起方时才重连,与 onOutboundDisconnected 行为保持一致
|
|
731
|
+
this.scheduleReconnect(peerId);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
parsePacket(raw) {
|
|
735
|
+
let str;
|
|
736
|
+
if (typeof raw === 'string') {
|
|
737
|
+
str = raw;
|
|
738
|
+
}
|
|
739
|
+
else if (Buffer.isBuffer(raw)) {
|
|
740
|
+
str = raw.toString();
|
|
741
|
+
}
|
|
742
|
+
else if (raw === null || raw === void 0 ? void 0 : raw.toString) {
|
|
743
|
+
str = raw.toString();
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
const packet = JSON.parse(str);
|
|
750
|
+
if (!packet.type || !packet.nodeId) {
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
return packet;
|
|
754
|
+
}
|
|
755
|
+
catch (_a) {
|
|
756
|
+
// 非协议消息直接忽略,避免单条脏数据影响主流程
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
sendBySocket(packet, send) {
|
|
761
|
+
try {
|
|
762
|
+
send(JSON.stringify(packet));
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
catch (err) {
|
|
766
|
+
this.emitError(new Error(`发送失败: ${(err === null || err === void 0 ? void 0 : err.message) || err}`));
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
sendByClient(packet, client) {
|
|
771
|
+
try {
|
|
772
|
+
return client.send(JSON.stringify(packet));
|
|
773
|
+
}
|
|
774
|
+
catch (err) {
|
|
775
|
+
this.emitError(new Error(`发送失败: ${(err === null || err === void 0 ? void 0 : err.message) || err}`));
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
makeRouteKey(service, action) {
|
|
780
|
+
return `${service}:${action}`;
|
|
781
|
+
}
|
|
782
|
+
buildUrlWithToken(url, token) {
|
|
783
|
+
if (!token) {
|
|
784
|
+
return url;
|
|
785
|
+
}
|
|
786
|
+
const hasQuery = url.includes('?');
|
|
787
|
+
const sep = hasQuery ? '&' : '?';
|
|
788
|
+
return `${url}${sep}token=${encodeURIComponent(token)}`;
|
|
789
|
+
}
|
|
790
|
+
genTraceId() {
|
|
791
|
+
this.traceCounter++;
|
|
792
|
+
return `${this.nodeId}-${Date.now()}-${this.traceCounter}`;
|
|
793
|
+
}
|
|
794
|
+
logInfo(msg) {
|
|
795
|
+
if (this.log) {
|
|
796
|
+
console.log(`[NWSServiceHub] ${msg}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
emitError(err) {
|
|
800
|
+
this.emit(NWSServiceHub.EventError, err);
|
|
801
|
+
this.logInfo(`错误: ${err.message}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
NWSServiceHub.EventPeerConnected = 'peer_connected';
|
|
805
|
+
NWSServiceHub.EventPeerDisconnected = 'peer_disconnected';
|
|
806
|
+
NWSServiceHub.EventError = 'error';
|
|
807
|
+
export default NWSServiceHub;
|