egos-transfer 0.2.14 → 0.2.15

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,8 @@ 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
+ console.trace('removeConnection', peerId, connectionId);
324
331
  this.connectionManager.remove(peerId, connectionId);
325
332
  }
326
333
  isValidConnection(conn) {
@@ -329,12 +336,15 @@ class PeerTransfer {
329
336
  // ── Request / Reply ─────────────────────────────────────────────
330
337
  request(peerId, payload) {
331
338
  return __awaiter(this, void 0, void 0, function* () {
332
- var _a;
339
+ var _a, _b;
333
340
  const requestId = payload.requestId || (yield this.genRequestId());
334
341
  let res;
335
342
  for (let attempt = 0; attempt <= 2; payload.route, attempt++) {
343
+ if (((_a = this.peerConfig) === null || _a === void 0 ? void 0 : _a.debug) >= 2) {
344
+ console.info('request===>00', payload.route, requestId);
345
+ }
336
346
  res = yield this.doRequestOnce(peerId, payload, requestId);
337
- const code = (_a = res.error) === null || _a === void 0 ? void 0 : _a.code;
347
+ const code = (_b = res.error) === null || _b === void 0 ? void 0 : _b.code;
338
348
  if (!res.error || (code !== 'CONN_CLOSED' && code !== 'CONN_FAILED')) {
339
349
  return res;
340
350
  }
@@ -359,14 +369,6 @@ class PeerTransfer {
359
369
  timeout = null;
360
370
  }
361
371
  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
372
  };
371
373
  timeout = setTimeout(() => {
372
374
  cleanup();
@@ -375,12 +377,6 @@ class PeerTransfer {
375
377
  }
376
378
  resolve(Object.assign(Object.assign({}, payload), { body: {}, error: { message: 'timeout', code: 'TIMEOUT' } }));
377
379
  }, 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
380
  this.requests.set(requestId, (message) => {
385
381
  cleanup();
386
382
  resolve(message);
@@ -395,11 +391,18 @@ class PeerTransfer {
395
391
  reply(conn, oldMessage, message) {
396
392
  return __awaiter(this, void 0, void 0, function* () {
397
393
  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);
394
+ // 以 DataChannel open 为准发送:新建连接的 RTCPeerConnection.connectionState
395
+ // 可能滞后(仍为 connecting),但 DC 已可正常收发;误判会导致响应被静默丢弃、
396
+ // 发送方 ping 超时进而误杀健康连接
397
+ if (conn.open) {
398
+ return conn.send(data);
401
399
  }
402
- return conn.send(data);
400
+ const c = yield this.connectToPeer(conn.peer);
401
+ if (this.isValidConnection(c)) {
402
+ return c.send(data);
403
+ }
404
+ console.warn('reply dropped: no usable connection to', conn.peer);
405
+ return undefined;
403
406
  });
404
407
  }
405
408
  send(conn, message) {
@@ -520,6 +523,7 @@ class PeerTransfer {
520
523
  yield this.router.run(conn, message);
521
524
  }
522
525
  catch (err) {
526
+ console.log('onRequest error', err);
523
527
  this.responseFail(conn, message, { message: err.message, code: err.code });
524
528
  }
525
529
  });
@@ -594,7 +598,6 @@ class PeerTransfer {
594
598
  this.clearIdleTimer();
595
599
  this.clearHeartbeatTimer();
596
600
  this.requests.clear();
597
- this.connRequests.clear();
598
601
  this.destroyPeer();
599
602
  }
600
603
  destroy() {
@@ -613,7 +616,6 @@ class PeerTransfer {
613
616
  this.connectionManager.stopHealthCheck();
614
617
  this.connectionManager.clean();
615
618
  this.requests.clear();
616
- this.connRequests.clear();
617
619
  this.destroyPeer();
618
620
  this.reconnectAttempt = 0;
619
621
  }
@@ -622,6 +624,7 @@ class PeerTransfer {
622
624
  this.createEpoch++; // 旧创建的 isStale() 变为 true,自行 destroy 并抛错
623
625
  this.creatingPeer = null;
624
626
  this.lastPeerCreatedAt = 0; // 重置节流,createPeer 可立即再次创建
627
+ this.connecting.clear(); // 连接随 peer 一并销毁,进行中的尝试不再有意义
625
628
  }
626
629
  destroyPeer() {
627
630
  var _a;
@@ -637,5 +640,11 @@ class PeerTransfer {
637
640
  dst: peerId,
638
641
  });
639
642
  }
643
+ getRouter() {
644
+ return this.router;
645
+ }
646
+ setRouter(router) {
647
+ this.router = router;
648
+ }
640
649
  }
641
650
  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.15",
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,8 @@ 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);
372
- }
373
- }
374
-
375
381
  removeConnection(peerId: string, connectionId?: string) {
382
+ console.trace('removeConnection', peerId, connectionId);
376
383
  this.connectionManager.remove(peerId, connectionId);
377
384
  }
378
385
 
@@ -386,6 +393,9 @@ export abstract class PeerTransfer {
386
393
  const requestId = payload.requestId || (await this.genRequestId());
387
394
  let res: PeerMessage;
388
395
  for (let attempt = 0; attempt <= 2; payload.route, attempt++) {
396
+ if (this.peerConfig?.debug >= 2) {
397
+ console.info('request===>00', payload.route, requestId);
398
+ }
389
399
  res = await this.doRequestOnce(peerId, payload, requestId);
390
400
  const code = (res.error as Record<string, any>)?.code;
391
401
  if (!res.error || (code !== 'CONN_CLOSED' && code !== 'CONN_FAILED')) {
@@ -421,14 +431,6 @@ export abstract class PeerTransfer {
421
431
  timeout = null;
422
432
  }
423
433
  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
434
  };
433
435
 
434
436
  timeout = setTimeout(() => {
@@ -440,13 +442,6 @@ export abstract class PeerTransfer {
440
442
  resolve({ ...payload, body: {}, error: { message: 'timeout', code: 'TIMEOUT' } } as any);
441
443
  }, payload.timeout || REQUEST_TIMEOUT);
442
444
 
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
445
  this.requests.set(requestId, (message: Record<string, any>) => {
451
446
  cleanup();
452
447
  resolve(message as any);
@@ -479,11 +474,18 @@ export abstract class PeerTransfer {
479
474
  scope: PeerScope.RESPONSE,
480
475
  updatedAt: Date.now(),
481
476
  };
482
- if (!this.isValidConnection(conn)) {
483
- const c = await this.connectToPeer(conn.peer);
484
- return c?.send(data);
477
+ // 以 DataChannel open 为准发送:新建连接的 RTCPeerConnection.connectionState
478
+ // 可能滞后(仍为 connecting),但 DC 已可正常收发;误判会导致响应被静默丢弃、
479
+ // 发送方 ping 超时进而误杀健康连接
480
+ if (conn.open) {
481
+ return conn.send(data);
485
482
  }
486
- return conn.send(data);
483
+ const c = await this.connectToPeer(conn.peer);
484
+ if (this.isValidConnection(c)) {
485
+ return c.send(data);
486
+ }
487
+ console.warn('reply dropped: no usable connection to', conn.peer);
488
+ return undefined;
487
489
  }
488
490
 
489
491
  async send(conn: DataConnection, message: PeerRequestPayload) {
@@ -613,6 +615,7 @@ export abstract class PeerTransfer {
613
615
  try {
614
616
  await this.router.run(conn, message);
615
617
  } catch (err: any) {
618
+ console.log('onRequest error', err);
616
619
  this.responseFail(conn, message, { message: err.message, code: err.code });
617
620
  }
618
621
  }
@@ -695,7 +698,6 @@ export abstract class PeerTransfer {
695
698
  this.clearIdleTimer();
696
699
  this.clearHeartbeatTimer();
697
700
  this.requests.clear();
698
- this.connRequests.clear();
699
701
  this.destroyPeer();
700
702
  }
701
703
 
@@ -716,7 +718,6 @@ export abstract class PeerTransfer {
716
718
  this.connectionManager.stopHealthCheck();
717
719
  this.connectionManager.clean();
718
720
  this.requests.clear();
719
- this.connRequests.clear();
720
721
  this.destroyPeer();
721
722
  this.reconnectAttempt = 0;
722
723
  }
@@ -726,6 +727,7 @@ export abstract class PeerTransfer {
726
727
  this.createEpoch++; // 旧创建的 isStale() 变为 true,自行 destroy 并抛错
727
728
  this.creatingPeer = null;
728
729
  this.lastPeerCreatedAt = 0; // 重置节流,createPeer 可立即再次创建
730
+ this.connecting.clear(); // 连接随 peer 一并销毁,进行中的尝试不再有意义
729
731
  }
730
732
 
731
733
  private destroyPeer() {
@@ -741,4 +743,12 @@ export abstract class PeerTransfer {
741
743
  dst: peerId,
742
744
  });
743
745
  }
746
+
747
+ getRouter() {
748
+ return this.router;
749
+ }
750
+
751
+ setRouter(router: PeerRouter) {
752
+ this.router = router;
753
+ }
744
754
  }
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) => {