egos-transfer 0.2.14 → 0.2.16

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.
@@ -4,8 +4,6 @@ export interface ConnectionManagerDeps {
4
4
  getPeer: () => Peer | undefined;
5
5
  peerId: string;
6
6
  ping: (peerId: string) => Promise<boolean>;
7
- /** 连接关闭时回调,由 PeerTransfer 处理未完成的请求结算 */
8
- onConnClosed: (conn: DataConnection) => void;
9
7
  }
10
8
  /**
11
9
  * DataConnection 连接池管理器
@@ -30,13 +28,9 @@ export declare class ConnectionManager {
30
28
  * @param conn - 已打开的 DataConnection
31
29
  * @param onData - 数据事件回调(由 PeerTransfer 提供,路由消息)
32
30
  */
33
- setup(conn: DataConnection & {
34
- timestamp?: number;
35
- }, onData: (message: any) => void): DataConnection;
31
+ setup(conn: DataConnection, onData: (message: any) => void): DataConnection;
36
32
  remove(peerId: string, connectionId?: string): void;
37
- isValid(conn?: DataConnection & {
38
- timestamp?: number;
39
- }): boolean;
33
+ isValid(conn?: DataConnection): boolean;
40
34
  private handleConnClosed;
41
35
  startHealthCheck(): void;
42
36
  stopHealthCheck(): void;
@@ -13,6 +13,8 @@ exports.ConnectionManager = void 0;
13
13
  const HEALTH_CHECK_INTERVAL = 15000;
14
14
  /** 连接最大存活时间:超过 15 分钟的连接在 get 时自动丢弃,强制重建 */
15
15
  const MAX_CONNECTION_AGE = 15 * 60 * 1000;
16
+ /** 建立中连接的最大等待时间:仍未 open 视为僵死 pending,由健康检查清理 */
17
+ const PENDING_CONNECTION_TIMEOUT = 30000;
16
18
  /**
17
19
  * DataConnection 连接池管理器
18
20
  * - 管理 DataConnection 的生命周期(注册/移除/有效性检查)
@@ -33,6 +35,11 @@ class ConnectionManager {
33
35
  if (!conn) {
34
36
  return;
35
37
  }
38
+ // 建立中(未 open):等待其完成,不当场移除——remove 会主动 close,
39
+ // 相当于掐死一次进行中的 WebRTC 握手
40
+ if (!conn.open) {
41
+ return;
42
+ }
36
43
  if (this.isValid(conn)) {
37
44
  return conn;
38
45
  }
@@ -62,7 +69,14 @@ class ConnectionManager {
62
69
  * @param onData - 数据事件回调(由 PeerTransfer 提供,路由消息)
63
70
  */
64
71
  setup(conn, onData) {
65
- conn.timestamp = Date.now();
72
+ var _a, _b, _c;
73
+ const cur = this.get(conn.peer);
74
+ if (this.isValid(cur)) {
75
+ return cur;
76
+ }
77
+ // 复用 metadata.time 作为连接时间戳(发起方 connect 时已写入),
78
+ // 入站连接未携带时补上,供 isValid 的过期判定与 pending 清理使用
79
+ conn.metadata = Object.assign(Object.assign({}, ((_a = conn.metadata) !== null && _a !== void 0 ? _a : {})), { time: (_c = (_b = conn.metadata) === null || _b === void 0 ? void 0 : _b.time) !== null && _c !== void 0 ? _c : Date.now() });
66
80
  this.pool.set(conn.peer, conn);
67
81
  conn.on('data', onData);
68
82
  conn.on('close', () => this.handleConnClosed(conn));
@@ -76,53 +90,45 @@ class ConnectionManager {
76
90
  }
77
91
  };
78
92
  pc.addEventListener('connectionstatechange', onStateChange);
79
- // 将清理函数挂到 conn 上,remove 时一并移除
80
- conn.__cm_stateChangeCleanup = () => {
81
- pc.removeEventListener('connectionstatechange', onStateChange);
82
- };
83
93
  }
84
94
  return conn;
85
95
  }
86
96
  remove(peerId, connectionId) {
87
- var _a, _b, _c, _d, _e;
88
97
  const conn = this.pool.get(peerId);
89
98
  if (conn) {
90
99
  if (connectionId && conn.connectionId !== connectionId)
91
100
  return;
92
101
  this.pool.delete(peerId);
93
102
  // 先 close 触发 close 事件(handleConnClosed 会结算 in-flight 请求),
94
- // 再清理监听器与 PeerJS 内部引用,避免僵尸连接堆积导致无法建立新连接
95
103
  conn.close();
96
- (_b = (_a = conn).__cm_stateChangeCleanup) === null || _b === void 0 ? void 0 : _b.call(_a);
97
- conn.removeAllListeners();
98
- (_c = this.deps.getPeer()) === null || _c === void 0 ? void 0 : _c._removeConnection(conn);
99
- }
100
- else {
101
- // 清理 Peer 内部残留的 connection 引用
102
- const connections = ((_d = this.deps.getPeer()) === null || _d === void 0 ? void 0 : _d.connections) || {};
103
- for (const c of connections[peerId] || []) {
104
- c.removeAllListeners();
105
- c.close();
106
- (_e = this.deps.getPeer()) === null || _e === void 0 ? void 0 : _e._removeConnection(c);
107
- }
104
+ // 再清理监听器与 PeerJS 内部引用,避免僵尸连接堆积导致无法建立新连接
105
+ // (conn as any).__cm_stateChangeCleanup?.();
106
+ // conn.removeAllListeners();
107
+ // this.deps.getPeer()?._removeConnection(conn);
108
108
  }
109
109
  }
110
110
  isValid(conn) {
111
+ var _a;
111
112
  if (!(conn === null || conn === void 0 ? void 0 : conn.open) || !(conn === null || conn === void 0 ? void 0 : conn.peerConnection)) {
112
113
  return false;
113
114
  }
114
115
  // 检查连接是否过期
115
- if (conn.timestamp && Date.now() - conn.timestamp > MAX_CONNECTION_AGE) {
116
+ const createdAt = (_a = conn.metadata) === null || _a === void 0 ? void 0 : _a.time;
117
+ if (createdAt && Date.now() - createdAt > MAX_CONNECTION_AGE) {
116
118
  return false;
117
119
  }
118
- return ['connected', 'completed'].includes(conn.peerConnection.connectionState);
120
+ // DC open 即可收发。RTCPeerConnection.connectionState 的迁移可能滞后于
121
+ // DC open(新连接常仍为 connecting),若据此判无效会在 get() 中把健康连接
122
+ // remove 掉,导致 ping 失败、连接被误杀。PC 终态(failed/closed)由
123
+ // connectionstatechange 监听负责清理,无需在此重复判定
124
+ return true;
119
125
  }
120
126
  handleConnClosed(conn) {
121
- var _a;
122
- this.pool.delete(conn.peer);
123
- // 同步移除 PeerJS 内部引用,避免僵尸连接堆积
124
- (_a = this.deps.getPeer()) === null || _a === void 0 ? void 0 : _a._removeConnection(conn);
125
- this.deps.onConnClosed(conn);
127
+ // 仅当池中登记的仍是该连接时才删除:连接被新连接替换后,
128
+ // 旧连接迟到的 close/error 不得清掉新连接的池项
129
+ if (this.pool.get(conn.peer) === conn) {
130
+ this.pool.delete(conn.peer);
131
+ }
126
132
  }
127
133
  // ── Health check ─────────────────────────────────────────────────
128
134
  startHealthCheck() {
@@ -141,14 +147,25 @@ class ConnectionManager {
141
147
  }
142
148
  checkAllConnections() {
143
149
  return __awaiter(this, void 0, void 0, function* () {
150
+ var _a;
144
151
  const peers = this.getAllPeers();
145
152
  for (const peerId of peers) {
146
153
  const conn = this.pool.get(peerId);
147
154
  if (!conn)
148
155
  continue;
156
+ // 建立中(未 open):setup 在 DC open 前就入池,健康检查若此刻判定会误杀刚建立的连接;
157
+ // 留出宽限期,仅清理超过 PENDING_CONNECTION_TIMEOUT 仍未 open 的僵死 pending
158
+ if (!conn.open) {
159
+ const createdAt = (_a = conn.metadata) === null || _a === void 0 ? void 0 : _a.time;
160
+ if (createdAt && Date.now() - createdAt > PENDING_CONNECTION_TIMEOUT) {
161
+ this.remove(peerId, conn.connectionId);
162
+ }
163
+ continue;
164
+ }
149
165
  const alive = yield this.pingConnection(conn);
150
166
  if (!alive) {
151
- this.remove(peerId);
167
+ // 带上 connectionId:await ping 期间该 peerId 可能已被新连接替换,避免误关新连接
168
+ this.remove(peerId, conn.connectionId);
152
169
  }
153
170
  }
154
171
  });
@@ -1,7 +1,7 @@
1
1
  export declare const REQUEST_TIMEOUT = 18000;
2
2
  export declare const HEARTBEAT_INTERVAL = 12000;
3
3
  export declare const TRANSFER_TIMEOUT = 300000;
4
- export declare const PING_TIMEOUT = 5000;
4
+ export declare const PING_TIMEOUT = 15000;
5
5
  export declare const CONNECTION_TIMEOUT = 6000;
6
6
  export declare const LOCKER_TIMEOUT = 15000;
7
7
  export declare const RECONNECT_TIMEOUT = 5000;
package/dist/constant.js CHANGED
@@ -4,7 +4,7 @@ exports.UNAUTHORIZED = exports.CONNECTION_DUPLICATED = exports.CONNECTION_LIMIT_
4
4
  exports.REQUEST_TIMEOUT = 18000;
5
5
  exports.HEARTBEAT_INTERVAL = 12000;
6
6
  exports.TRANSFER_TIMEOUT = 300000;
7
- exports.PING_TIMEOUT = 5000;
7
+ exports.PING_TIMEOUT = 15000;
8
8
  exports.CONNECTION_TIMEOUT = 6000;
9
9
  exports.LOCKER_TIMEOUT = 15000;
10
10
  exports.RECONNECT_TIMEOUT = 5000;
package/dist/handler.js CHANGED
@@ -41,6 +41,7 @@ class Handler {
41
41
  }
42
42
  catch (err) {
43
43
  console.log('onMessage@error', err);
44
+ yield transfer.responseFail(conn, message, err.message);
44
45
  }
45
46
  }),
46
47
  });
@@ -22,8 +22,6 @@ export declare abstract class PeerTransfer {
22
22
  protected router: PeerRouter;
23
23
  protected reconnectAttempt: number;
24
24
  protected destroyed: boolean;
25
- /** connectionId → Set<requestId> — settle in-flight requests on connection drop */
26
- private connRequests;
27
25
  private _reconnecting;
28
26
  /** 退出标志:为 true 时禁止自动重连,直到 createPeer 被调用 */
29
27
  private _exited;
@@ -33,6 +31,8 @@ export declare abstract class PeerTransfer {
33
31
  private heartbeatTimer;
34
32
  /** 进行中的 Peer 创建:并发调用复用同一个 Promise,防止交叉 destroy/新建 */
35
33
  private creatingPeer;
34
+ /** 进行中的连接尝试:peerId → Promise,防止并发 connect 产生重复 DataConnection */
35
+ private connecting;
36
36
  /** 上次成功创建 Peer 的时间戳:节流窗口内直接复用现有实例 */
37
37
  private lastPeerCreatedAt;
38
38
  /** 创建代际:stop/clearInstance/新一轮创建时递增,使旧的进行中创建作废 */
@@ -56,7 +56,6 @@ export declare abstract class PeerTransfer {
56
56
  private createConnection;
57
57
  onConnect(conn: DataConnection): Promise<DataConnection | undefined>;
58
58
  private setupConnection;
59
- private onConnClosed;
60
59
  removeConnection(peerId: string, connectionId?: string): void;
61
60
  isValidConnection(conn?: DataConnection): boolean;
62
61
  request(peerId: string, payload: PeerRequestPayload): Promise<PeerMessage>;
@@ -100,5 +99,7 @@ export declare abstract class PeerTransfer {
100
99
  private abortCreate;
101
100
  private destroyPeer;
102
101
  askReload(peerId: string): void;
102
+ getRouter(): PeerRouter;
103
+ setRouter(router: PeerRouter): void;
103
104
  }
104
105
  export {};
@@ -37,8 +37,6 @@ class PeerTransfer {
37
37
  this.router = new router_1.PeerRouter();
38
38
  this.reconnectAttempt = 0;
39
39
  this.destroyed = false;
40
- /** connectionId → Set<requestId> — settle in-flight requests on connection drop */
41
- this.connRequests = new Map();
42
40
  this._reconnecting = false;
43
41
  /** 退出标志:为 true 时禁止自动重连,直到 createPeer 被调用 */
44
42
  this._exited = false;
@@ -48,6 +46,8 @@ class PeerTransfer {
48
46
  this.heartbeatTimer = null;
49
47
  /** 进行中的 Peer 创建:并发调用复用同一个 Promise,防止交叉 destroy/新建 */
50
48
  this.creatingPeer = null;
49
+ /** 进行中的连接尝试:peerId → Promise,防止并发 connect 产生重复 DataConnection */
50
+ this.connecting = new Map();
51
51
  /** 上次成功创建 Peer 的时间戳:节流窗口内直接复用现有实例 */
52
52
  this.lastPeerCreatedAt = 0;
53
53
  /** 创建代际:stop/clearInstance/新一轮创建时递增,使旧的进行中创建作废 */
@@ -58,7 +58,6 @@ class PeerTransfer {
58
58
  this.connectionManager = new connection_manager_1.ConnectionManager({
59
59
  getPeer: () => this.peer,
60
60
  peerId: this.peerId,
61
- onConnClosed: (conn) => this.onConnClosed(conn),
62
61
  ping: (peerId) => this.ping(peerId),
63
62
  });
64
63
  }
@@ -223,7 +222,17 @@ class PeerTransfer {
223
222
  if (existing) {
224
223
  return existing;
225
224
  }
226
- return this.createConnection(peerId);
225
+ // 并发去重:同一 peer 的连接尝试只进行一次,后续调用复用进行中的 Promise,
226
+ // 防止并发 request 触发多次 peer.connect() 产生重复 DataConnection
227
+ const inFlight = this.connecting.get(peerId);
228
+ if (inFlight) {
229
+ return inFlight;
230
+ }
231
+ const attempt = this.createConnection(peerId).finally(() => {
232
+ this.connecting.delete(peerId);
233
+ });
234
+ this.connecting.set(peerId, attempt);
235
+ return attempt;
227
236
  });
228
237
  }
229
238
  createConnection(peerId) {
@@ -271,8 +280,10 @@ class PeerTransfer {
271
280
  return __awaiter(this, void 0, void 0, function* () {
272
281
  if (!conn)
273
282
  return;
283
+ // 立即入池并绑定 data 监听:DC open 前绑监听无副作用,
284
+ // 避免弱网下 DC 晚开时(超过 CONNECTION_TIMEOUT)成为无人监听的"聋子"连接
285
+ this.setupConnection(conn);
274
286
  if (conn.open) {
275
- this.setupConnection(conn);
276
287
  return conn;
277
288
  }
278
289
  return new Promise((resolve) => {
@@ -280,11 +291,12 @@ class PeerTransfer {
280
291
  conn.off('open', onOpen);
281
292
  conn.off('error', onError);
282
293
  conn.off('close', onClose);
283
- resolve(this.isValidConnection(conn) ? this.setupConnection(conn) : undefined);
294
+ // 超时但未关闭:连接可能仍在建立中,保留监听让其晚开后仍可处理消息
295
+ resolve(conn.open || this.isValidConnection(conn) ? conn : undefined);
284
296
  }, constant_1.CONNECTION_TIMEOUT);
285
297
  const onOpen = () => {
286
298
  clearTimeout(timer);
287
- resolve(this.setupConnection(conn));
299
+ resolve(conn);
288
300
  };
289
301
  const onError = () => {
290
302
  clearTimeout(timer);
@@ -314,13 +326,11 @@ class PeerTransfer {
314
326
  }
315
327
  });
316
328
  }
317
- onConnClosed(conn) {
318
- const reqIds = this.connRequests.get(conn.connectionId);
319
- if (reqIds) {
320
- this.connRequests.delete(conn.connectionId);
321
- }
322
- }
323
329
  removeConnection(peerId, connectionId) {
330
+ var _a;
331
+ if (((_a = this.peerConfig) === null || _a === void 0 ? void 0 : _a.debug) >= 2) {
332
+ console.log('removeConnection', peerId, connectionId);
333
+ }
324
334
  this.connectionManager.remove(peerId, connectionId);
325
335
  }
326
336
  isValidConnection(conn) {
@@ -329,12 +339,15 @@ class PeerTransfer {
329
339
  // ── Request / Reply ─────────────────────────────────────────────
330
340
  request(peerId, payload) {
331
341
  return __awaiter(this, void 0, void 0, function* () {
332
- var _a;
342
+ var _a, _b;
333
343
  const requestId = payload.requestId || (yield this.genRequestId());
334
344
  let res;
335
345
  for (let attempt = 0; attempt <= 2; payload.route, attempt++) {
346
+ if (((_a = this.peerConfig) === null || _a === void 0 ? void 0 : _a.debug) >= 2) {
347
+ console.info('request===>00', payload.route, requestId);
348
+ }
336
349
  res = yield this.doRequestOnce(peerId, payload, requestId);
337
- const code = (_a = res.error) === null || _a === void 0 ? void 0 : _a.code;
350
+ const code = (_b = res.error) === null || _b === void 0 ? void 0 : _b.code;
338
351
  if (!res.error || (code !== 'CONN_CLOSED' && code !== 'CONN_FAILED')) {
339
352
  return res;
340
353
  }
@@ -359,14 +372,6 @@ class PeerTransfer {
359
372
  timeout = null;
360
373
  }
361
374
  this.requests.delete(requestId);
362
- const reqs = this.connRequests.get(conn.connectionId);
363
- if (reqs) {
364
- reqs.delete(requestId);
365
- // connection pool cleanup
366
- if (reqs.size === 0) {
367
- this.connRequests.delete(conn.connectionId);
368
- }
369
- }
370
375
  };
371
376
  timeout = setTimeout(() => {
372
377
  cleanup();
@@ -375,12 +380,6 @@ class PeerTransfer {
375
380
  }
376
381
  resolve(Object.assign(Object.assign({}, payload), { body: {}, error: { message: 'timeout', code: 'TIMEOUT' } }));
377
382
  }, payload.timeout || entity_1.REQUEST_TIMEOUT);
378
- let reqs = this.connRequests.get(conn.connectionId);
379
- if (!reqs) {
380
- reqs = new Set();
381
- this.connRequests.set(conn.connectionId, reqs);
382
- }
383
- reqs.add(requestId);
384
383
  this.requests.set(requestId, (message) => {
385
384
  cleanup();
386
385
  resolve(message);
@@ -395,11 +394,18 @@ class PeerTransfer {
395
394
  reply(conn, oldMessage, message) {
396
395
  return __awaiter(this, void 0, void 0, function* () {
397
396
  const data = Object.assign(Object.assign(Object.assign({}, this.inheritMessage(oldMessage)), message), { dest: conn.peer, scope: entity_1.PeerScope.RESPONSE, updatedAt: Date.now() });
398
- if (!this.isValidConnection(conn)) {
399
- const c = yield this.connectToPeer(conn.peer);
400
- return c === null || c === void 0 ? void 0 : c.send(data);
397
+ // 以 DataChannel open 为准发送:新建连接的 RTCPeerConnection.connectionState
398
+ // 可能滞后(仍为 connecting),但 DC 已可正常收发;误判会导致响应被静默丢弃、
399
+ // 发送方 ping 超时进而误杀健康连接
400
+ if (conn.open) {
401
+ return conn.send(data);
401
402
  }
402
- return conn.send(data);
403
+ const c = yield this.connectToPeer(conn.peer);
404
+ if (this.isValidConnection(c)) {
405
+ return c.send(data);
406
+ }
407
+ console.warn('reply dropped: no usable connection to', conn.peer);
408
+ return undefined;
403
409
  });
404
410
  }
405
411
  send(conn, message) {
@@ -520,6 +526,7 @@ class PeerTransfer {
520
526
  yield this.router.run(conn, message);
521
527
  }
522
528
  catch (err) {
529
+ console.log('onRequest error', err);
523
530
  this.responseFail(conn, message, { message: err.message, code: err.code });
524
531
  }
525
532
  });
@@ -594,7 +601,6 @@ class PeerTransfer {
594
601
  this.clearIdleTimer();
595
602
  this.clearHeartbeatTimer();
596
603
  this.requests.clear();
597
- this.connRequests.clear();
598
604
  this.destroyPeer();
599
605
  }
600
606
  destroy() {
@@ -613,7 +619,6 @@ class PeerTransfer {
613
619
  this.connectionManager.stopHealthCheck();
614
620
  this.connectionManager.clean();
615
621
  this.requests.clear();
616
- this.connRequests.clear();
617
622
  this.destroyPeer();
618
623
  this.reconnectAttempt = 0;
619
624
  }
@@ -622,6 +627,7 @@ class PeerTransfer {
622
627
  this.createEpoch++; // 旧创建的 isStale() 变为 true,自行 destroy 并抛错
623
628
  this.creatingPeer = null;
624
629
  this.lastPeerCreatedAt = 0; // 重置节流,createPeer 可立即再次创建
630
+ this.connecting.clear(); // 连接随 peer 一并销毁,进行中的尝试不再有意义
625
631
  }
626
632
  destroyPeer() {
627
633
  var _a;
@@ -637,5 +643,11 @@ class PeerTransfer {
637
643
  dst: peerId,
638
644
  });
639
645
  }
646
+ getRouter() {
647
+ return this.router;
648
+ }
649
+ setRouter(router) {
650
+ this.router = router;
651
+ }
640
652
  }
641
653
  exports.PeerTransfer = PeerTransfer;
package/dist/router.js CHANGED
@@ -116,7 +116,7 @@ class PeerRouter {
116
116
  .slice(0, route.scope)
117
117
  .filter((item) => (0, path_to_regexp_1.match)(item.path)(path))
118
118
  .sort((a, b) => (b.priority || 0) - (a.priority || 0));
119
- const res = yield this.compose(stack)(conn, message);
119
+ yield this.compose(stack)(conn, message);
120
120
  // Execute route handler with timeout
121
121
  const routePromise = Promise.resolve(route.callback.apply(null, [conn, message]));
122
122
  const timeoutPromise = new Promise((_, reject) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "egos-transfer",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "1234",
5
5
  "homepage": "https://github.com/superbogy/peer-transfer#readme",
6
6
  "bugs": {
@@ -5,14 +5,14 @@ type ResponseCb = (args: any) => void;
5
5
  const HEALTH_CHECK_INTERVAL = 15000;
6
6
  /** 连接最大存活时间:超过 15 分钟的连接在 get 时自动丢弃,强制重建 */
7
7
  const MAX_CONNECTION_AGE = 15 * 60 * 1000;
8
+ /** 建立中连接的最大等待时间:仍未 open 视为僵死 pending,由健康检查清理 */
9
+ const PENDING_CONNECTION_TIMEOUT = 30000;
8
10
 
9
11
  export interface ConnectionManagerDeps {
10
12
  /** 获取当前 Peer 实例(PeerTransfer 可能异步重建) */
11
13
  getPeer: () => Peer | undefined;
12
14
  peerId: string;
13
15
  ping: (peerId: string) => Promise<boolean>;
14
- /** 连接关闭时回调,由 PeerTransfer 处理未完成的请求结算 */
15
- onConnClosed: (conn: DataConnection) => void;
16
16
  }
17
17
 
18
18
  /**
@@ -40,6 +40,11 @@ export class ConnectionManager {
40
40
  if (!conn) {
41
41
  return;
42
42
  }
43
+ // 建立中(未 open):等待其完成,不当场移除——remove 会主动 close,
44
+ // 相当于掐死一次进行中的 WebRTC 握手
45
+ if (!conn.open) {
46
+ return;
47
+ }
43
48
  if (this.isValid(conn)) {
44
49
  return conn;
45
50
  } else {
@@ -73,11 +78,17 @@ export class ConnectionManager {
73
78
  * @param conn - 已打开的 DataConnection
74
79
  * @param onData - 数据事件回调(由 PeerTransfer 提供,路由消息)
75
80
  */
76
- setup(
77
- conn: DataConnection & { timestamp?: number },
78
- onData: (message: any) => void,
79
- ): DataConnection {
80
- conn.timestamp = Date.now();
81
+ setup(conn: DataConnection, onData: (message: any) => void) {
82
+ const cur = this.get(conn.peer);
83
+ if (this.isValid(cur)) {
84
+ return cur;
85
+ }
86
+ // 复用 metadata.time 作为连接时间戳(发起方 connect 时已写入),
87
+ // 入站连接未携带时补上,供 isValid 的过期判定与 pending 清理使用
88
+ (conn as { metadata: any }).metadata = {
89
+ ...(conn.metadata ?? {}),
90
+ time: conn.metadata?.time ?? Date.now(),
91
+ };
81
92
  this.pool.set(conn.peer, conn);
82
93
  conn.on('data', onData);
83
94
  conn.on('close', () => this.handleConnClosed(conn));
@@ -92,10 +103,6 @@ export class ConnectionManager {
92
103
  }
93
104
  };
94
105
  pc.addEventListener('connectionstatechange', onStateChange);
95
- // 将清理函数挂到 conn 上,remove 时一并移除
96
- (conn as any).__cm_stateChangeCleanup = () => {
97
- pc.removeEventListener('connectionstatechange', onStateChange);
98
- };
99
106
  }
100
107
 
101
108
  return conn;
@@ -107,38 +114,36 @@ export class ConnectionManager {
107
114
  if (connectionId && conn.connectionId !== connectionId) return;
108
115
  this.pool.delete(peerId);
109
116
  // 先 close 触发 close 事件(handleConnClosed 会结算 in-flight 请求),
110
- // 再清理监听器与 PeerJS 内部引用,避免僵尸连接堆积导致无法建立新连接
111
117
  conn.close();
112
- (conn as any).__cm_stateChangeCleanup?.();
113
- conn.removeAllListeners();
114
- this.deps.getPeer()?._removeConnection(conn);
115
- } else {
116
- // 清理 Peer 内部残留的 connection 引用
117
- const connections = this.deps.getPeer()?.connections || {};
118
- for (const c of connections[peerId] || []) {
119
- c.removeAllListeners();
120
- c.close();
121
- this.deps.getPeer()?._removeConnection(c);
122
- }
118
+ // 再清理监听器与 PeerJS 内部引用,避免僵尸连接堆积导致无法建立新连接
119
+ // (conn as any).__cm_stateChangeCleanup?.();
120
+ // conn.removeAllListeners();
121
+ // this.deps.getPeer()?._removeConnection(conn);
123
122
  }
124
123
  }
125
124
 
126
- isValid(conn?: DataConnection & { timestamp?: number }): boolean {
125
+ isValid(conn?: DataConnection): boolean {
127
126
  if (!conn?.open || !conn?.peerConnection) {
128
127
  return false;
129
128
  }
130
129
  // 检查连接是否过期
131
- if (conn.timestamp && Date.now() - conn.timestamp > MAX_CONNECTION_AGE) {
130
+ const createdAt = conn.metadata?.time;
131
+ if (createdAt && Date.now() - createdAt > MAX_CONNECTION_AGE) {
132
132
  return false;
133
133
  }
134
- return ['connected', 'completed'].includes(conn.peerConnection.connectionState);
134
+ // DC open 即可收发。RTCPeerConnection.connectionState 的迁移可能滞后于
135
+ // DC open(新连接常仍为 connecting),若据此判无效会在 get() 中把健康连接
136
+ // remove 掉,导致 ping 失败、连接被误杀。PC 终态(failed/closed)由
137
+ // connectionstatechange 监听负责清理,无需在此重复判定
138
+ return true;
135
139
  }
136
140
 
137
141
  private handleConnClosed(conn: DataConnection): void {
138
- this.pool.delete(conn.peer);
139
- // 同步移除 PeerJS 内部引用,避免僵尸连接堆积
140
- this.deps.getPeer()?._removeConnection(conn);
141
- this.deps.onConnClosed(conn);
142
+ // 仅当池中登记的仍是该连接时才删除:连接被新连接替换后,
143
+ // 旧连接迟到的 close/error 不得清掉新连接的池项
144
+ if (this.pool.get(conn.peer) === conn) {
145
+ this.pool.delete(conn.peer);
146
+ }
142
147
  }
143
148
 
144
149
  // ── Health check ─────────────────────────────────────────────────
@@ -163,9 +168,19 @@ export class ConnectionManager {
163
168
  for (const peerId of peers) {
164
169
  const conn = this.pool.get(peerId);
165
170
  if (!conn) continue;
171
+ // 建立中(未 open):setup 在 DC open 前就入池,健康检查若此刻判定会误杀刚建立的连接;
172
+ // 留出宽限期,仅清理超过 PENDING_CONNECTION_TIMEOUT 仍未 open 的僵死 pending
173
+ if (!conn.open) {
174
+ const createdAt = conn.metadata?.time;
175
+ if (createdAt && Date.now() - createdAt > PENDING_CONNECTION_TIMEOUT) {
176
+ this.remove(peerId, conn.connectionId);
177
+ }
178
+ continue;
179
+ }
166
180
  const alive = await this.pingConnection(conn);
167
181
  if (!alive) {
168
- this.remove(peerId);
182
+ // 带上 connectionId:await ping 期间该 peerId 可能已被新连接替换,避免误关新连接
183
+ this.remove(peerId, conn.connectionId);
169
184
  }
170
185
  }
171
186
  }
package/src/constant.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export const REQUEST_TIMEOUT = 18000;
2
2
  export const HEARTBEAT_INTERVAL = 12000;
3
3
  export const TRANSFER_TIMEOUT = 300000;
4
- export const PING_TIMEOUT = 5000;
4
+ export const PING_TIMEOUT = 15000;
5
5
  export const CONNECTION_TIMEOUT = 6000;
6
6
  export const LOCKER_TIMEOUT = 15000;
7
7
  export const RECONNECT_TIMEOUT = 5000;
package/src/handler.ts CHANGED
@@ -68,6 +68,7 @@ export class Handler {
68
68
  await transfer.responseOk(conn, message);
69
69
  } catch (err) {
70
70
  console.log('onMessage@error', err);
71
+ await transfer.responseFail(conn, message, err.message);
71
72
  }
72
73
  },
73
74
  });
@@ -57,8 +57,6 @@ export abstract class PeerTransfer {
57
57
  protected reconnectAttempt = 0;
58
58
  protected destroyed = false;
59
59
 
60
- /** connectionId → Set<requestId> — settle in-flight requests on connection drop */
61
- private connRequests = new Map<string, Set<string>>();
62
60
  private _reconnecting = false;
63
61
  /** 退出标志:为 true 时禁止自动重连,直到 createPeer 被调用 */
64
62
  private _exited = false;
@@ -68,6 +66,8 @@ export abstract class PeerTransfer {
68
66
  private heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
69
67
  /** 进行中的 Peer 创建:并发调用复用同一个 Promise,防止交叉 destroy/新建 */
70
68
  private creatingPeer: Promise<Peer> | null = null;
69
+ /** 进行中的连接尝试:peerId → Promise,防止并发 connect 产生重复 DataConnection */
70
+ private connecting = new Map<string, Promise<DataConnection | undefined>>();
71
71
  /** 上次成功创建 Peer 的时间戳:节流窗口内直接复用现有实例 */
72
72
  private lastPeerCreatedAt = 0;
73
73
  /** 创建代际:stop/clearInstance/新一轮创建时递增,使旧的进行中创建作废 */
@@ -80,7 +80,6 @@ export abstract class PeerTransfer {
80
80
  this.connectionManager = new ConnectionManager({
81
81
  getPeer: () => this.peer,
82
82
  peerId: this.peerId,
83
- onConnClosed: (conn) => this.onConnClosed(conn),
84
83
  ping: (peerId: string) => this.ping(peerId),
85
84
  });
86
85
  }
@@ -270,7 +269,17 @@ export abstract class PeerTransfer {
270
269
  return existing;
271
270
  }
272
271
 
273
- return this.createConnection(peerId);
272
+ // 并发去重:同一 peer 的连接尝试只进行一次,后续调用复用进行中的 Promise,
273
+ // 防止并发 request 触发多次 peer.connect() 产生重复 DataConnection
274
+ const inFlight = this.connecting.get(peerId);
275
+ if (inFlight) {
276
+ return inFlight;
277
+ }
278
+ const attempt = this.createConnection(peerId).finally(() => {
279
+ this.connecting.delete(peerId);
280
+ });
281
+ this.connecting.set(peerId, attempt);
282
+ return attempt;
274
283
  }
275
284
 
276
285
  private async createConnection(peerId: string): Promise<DataConnection | undefined> {
@@ -316,8 +325,11 @@ export abstract class PeerTransfer {
316
325
  async onConnect(conn: DataConnection): Promise<DataConnection | undefined> {
317
326
  if (!conn) return;
318
327
 
328
+ // 立即入池并绑定 data 监听:DC open 前绑监听无副作用,
329
+ // 避免弱网下 DC 晚开时(超过 CONNECTION_TIMEOUT)成为无人监听的"聋子"连接
330
+ this.setupConnection(conn);
331
+
319
332
  if (conn.open) {
320
- this.setupConnection(conn);
321
333
  return conn;
322
334
  }
323
335
 
@@ -326,12 +338,13 @@ export abstract class PeerTransfer {
326
338
  conn.off('open', onOpen);
327
339
  conn.off('error', onError);
328
340
  conn.off('close', onClose);
329
- resolve(this.isValidConnection(conn) ? this.setupConnection(conn) : undefined);
341
+ // 超时但未关闭:连接可能仍在建立中,保留监听让其晚开后仍可处理消息
342
+ resolve(conn.open || this.isValidConnection(conn) ? conn : undefined);
330
343
  }, CONNECTION_TIMEOUT);
331
344
 
332
345
  const onOpen = () => {
333
346
  clearTimeout(timer);
334
- resolve(this.setupConnection(conn));
347
+ resolve(conn);
335
348
  };
336
349
 
337
350
  const onError = () => {
@@ -365,14 +378,11 @@ export abstract class PeerTransfer {
365
378
  });
366
379
  }
367
380
 
368
- private onConnClosed(conn: DataConnection) {
369
- const reqIds = this.connRequests.get(conn.connectionId);
370
- if (reqIds) {
371
- this.connRequests.delete(conn.connectionId);
381
+ removeConnection(peerId: string, connectionId?: string) {
382
+ if (this.peerConfig?.debug >= 2) {
383
+ console.log('removeConnection', peerId, connectionId);
372
384
  }
373
- }
374
385
 
375
- removeConnection(peerId: string, connectionId?: string) {
376
386
  this.connectionManager.remove(peerId, connectionId);
377
387
  }
378
388
 
@@ -386,6 +396,9 @@ export abstract class PeerTransfer {
386
396
  const requestId = payload.requestId || (await this.genRequestId());
387
397
  let res: PeerMessage;
388
398
  for (let attempt = 0; attempt <= 2; payload.route, attempt++) {
399
+ if (this.peerConfig?.debug >= 2) {
400
+ console.info('request===>00', payload.route, requestId);
401
+ }
389
402
  res = await this.doRequestOnce(peerId, payload, requestId);
390
403
  const code = (res.error as Record<string, any>)?.code;
391
404
  if (!res.error || (code !== 'CONN_CLOSED' && code !== 'CONN_FAILED')) {
@@ -421,14 +434,6 @@ export abstract class PeerTransfer {
421
434
  timeout = null;
422
435
  }
423
436
  this.requests.delete(requestId);
424
- const reqs = this.connRequests.get(conn.connectionId);
425
- if (reqs) {
426
- reqs.delete(requestId);
427
- // connection pool cleanup
428
- if (reqs.size === 0) {
429
- this.connRequests.delete(conn.connectionId);
430
- }
431
- }
432
437
  };
433
438
 
434
439
  timeout = setTimeout(() => {
@@ -440,13 +445,6 @@ export abstract class PeerTransfer {
440
445
  resolve({ ...payload, body: {}, error: { message: 'timeout', code: 'TIMEOUT' } } as any);
441
446
  }, payload.timeout || REQUEST_TIMEOUT);
442
447
 
443
- let reqs = this.connRequests.get(conn.connectionId);
444
- if (!reqs) {
445
- reqs = new Set();
446
- this.connRequests.set(conn.connectionId, reqs);
447
- }
448
- reqs.add(requestId);
449
-
450
448
  this.requests.set(requestId, (message: Record<string, any>) => {
451
449
  cleanup();
452
450
  resolve(message as any);
@@ -479,11 +477,18 @@ export abstract class PeerTransfer {
479
477
  scope: PeerScope.RESPONSE,
480
478
  updatedAt: Date.now(),
481
479
  };
482
- if (!this.isValidConnection(conn)) {
483
- const c = await this.connectToPeer(conn.peer);
484
- return c?.send(data);
480
+ // 以 DataChannel open 为准发送:新建连接的 RTCPeerConnection.connectionState
481
+ // 可能滞后(仍为 connecting),但 DC 已可正常收发;误判会导致响应被静默丢弃、
482
+ // 发送方 ping 超时进而误杀健康连接
483
+ if (conn.open) {
484
+ return conn.send(data);
485
+ }
486
+ const c = await this.connectToPeer(conn.peer);
487
+ if (this.isValidConnection(c)) {
488
+ return c.send(data);
485
489
  }
486
- return conn.send(data);
490
+ console.warn('reply dropped: no usable connection to', conn.peer);
491
+ return undefined;
487
492
  }
488
493
 
489
494
  async send(conn: DataConnection, message: PeerRequestPayload) {
@@ -613,6 +618,7 @@ export abstract class PeerTransfer {
613
618
  try {
614
619
  await this.router.run(conn, message);
615
620
  } catch (err: any) {
621
+ console.log('onRequest error', err);
616
622
  this.responseFail(conn, message, { message: err.message, code: err.code });
617
623
  }
618
624
  }
@@ -695,7 +701,6 @@ export abstract class PeerTransfer {
695
701
  this.clearIdleTimer();
696
702
  this.clearHeartbeatTimer();
697
703
  this.requests.clear();
698
- this.connRequests.clear();
699
704
  this.destroyPeer();
700
705
  }
701
706
 
@@ -716,7 +721,6 @@ export abstract class PeerTransfer {
716
721
  this.connectionManager.stopHealthCheck();
717
722
  this.connectionManager.clean();
718
723
  this.requests.clear();
719
- this.connRequests.clear();
720
724
  this.destroyPeer();
721
725
  this.reconnectAttempt = 0;
722
726
  }
@@ -726,6 +730,7 @@ export abstract class PeerTransfer {
726
730
  this.createEpoch++; // 旧创建的 isStale() 变为 true,自行 destroy 并抛错
727
731
  this.creatingPeer = null;
728
732
  this.lastPeerCreatedAt = 0; // 重置节流,createPeer 可立即再次创建
733
+ this.connecting.clear(); // 连接随 peer 一并销毁,进行中的尝试不再有意义
729
734
  }
730
735
 
731
736
  private destroyPeer() {
@@ -741,4 +746,12 @@ export abstract class PeerTransfer {
741
746
  dst: peerId,
742
747
  });
743
748
  }
749
+
750
+ getRouter() {
751
+ return this.router;
752
+ }
753
+
754
+ setRouter(router: PeerRouter) {
755
+ this.router = router;
756
+ }
744
757
  }
package/src/router.ts CHANGED
@@ -104,7 +104,6 @@ export class PeerRouter {
104
104
  async run(conn: DataConnection, message: PeerMessage) {
105
105
  const routeKey = `${message.method}:${message.route}`;
106
106
  this.performanceMonitor.startTimer(routeKey);
107
-
108
107
  try {
109
108
  if (!this.routes.length) {
110
109
  return;
@@ -147,8 +146,7 @@ export class PeerRouter {
147
146
  .slice(0, route.scope)
148
147
  .filter((item) => match(item.path)(path))
149
148
  .sort((a, b) => (b.priority || 0) - (a.priority || 0));
150
-
151
- const res = await this.compose(stack)(conn, message);
149
+ await this.compose(stack)(conn, message);
152
150
  // Execute route handler with timeout
153
151
  const routePromise = Promise.resolve(route.callback.apply(null, [conn, message]));
154
152
  const timeoutPromise = new Promise((_, reject) => {