egos-transfer 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,16 +1,15 @@
1
1
  import {
2
+ CONNECTION_DUPLICATED,
2
3
  CONNECTION_LIMIT_EXCEED,
3
4
  CONNECTION_PEER_CONFLICT,
4
- CONNECTION_RETRY,
5
5
  CONNECTION_TIMEOUT,
6
6
  HEARTBEAT_INTERVAL,
7
7
  PEER_OPEN_TIMEOUT,
8
8
  PING_TIMEOUT,
9
9
  UNAUTHORIZED,
10
10
  } from './constant';
11
- import { DataConnection, MediaConnection, Peer, PeerOptions } from 'peerjs';
11
+ import { DataConnection, Peer, PeerOptions } from 'peerjs';
12
12
  import {
13
- DataMessage,
14
13
  IDeviceStatus,
15
14
  IMessage,
16
15
  PeerMessage,
@@ -34,6 +33,10 @@ type PeerTransferOptions = PeerOptions & {
34
33
  turnExpiredAt: number; // date time ms
35
34
  };
36
35
 
36
+ /** 重连退避相关常量 */
37
+ const MAX_RECONNECT_BACKOFF = 30000; // 最大退避 30s
38
+ const BASE_RECONNECT_BACKOFF = 1000; // 基础退避 1s
39
+
37
40
  export abstract class PeerTransfer {
38
41
  protected peer: Peer | undefined;
39
42
  protected connectPool: Map<string, DataConnection>;
@@ -46,34 +49,41 @@ export abstract class PeerTransfer {
46
49
  }
47
50
  > = new Map();
48
51
 
49
- protected retry = 0;
50
-
51
52
  protected peerConfig: PeerTransferOptions | undefined;
52
- public deviceId: string;
53
+ public readonly deviceId: string;
54
+ /** 接入 peerjs 服务端的 peerId,默认与 deviceId 相同 */
55
+ public readonly peerId: string;
53
56
  protected requests: Map<string, ResponseCb>;
57
+ /** 追踪 addRequest 的延时清理定时器,避免重试时误删新回调 */
58
+ protected requestTimers: Map<string, ReturnType<typeof setTimeout>>;
54
59
  protected timeout: number;
55
60
  protected router: PeerRouter;
56
- protected booted: Promise<Peer | undefined>;
61
+ protected booted: Promise<Peer | undefined> | undefined;
57
62
  protected reconnectingPromise: Promise<Peer | undefined> | undefined;
58
- protected checkerId!: ReturnType<typeof setInterval>;
63
+ protected checkerId: ReturnType<typeof setInterval> | null = null;
59
64
  public isPaused: boolean;
60
- public peerId: string;
65
+ /** 重连尝试次数,成功连接后归零 */
66
+ protected reconnectAttempt = 0;
67
+ /** 标记实例已销毁,防止异步操作继续 */
68
+ protected destroyed = false;
69
+ /** 按 requestId 追踪进行中的请求 */
61
70
  private inRequest: Map<
62
71
  string,
63
72
  { conn: DataConnection; message: Record<string, any>; feedback: () => void }
64
73
  > = new Map();
65
74
 
66
- constructor(deviceId: string, config: PeerTransferOptions, isRadomPeerId = false) {
75
+ constructor(deviceId: string, config: PeerTransferOptions, isRandomPeerId = false) {
67
76
  this.connectPool = new Map<string, DataConnection>();
68
77
  this.peerConfig = {
69
78
  ...config,
70
79
  key: String(Date.now()),
71
80
  };
72
81
  this.requests = new Map();
82
+ this.requestTimers = new Map();
73
83
  this.timeout = REQUEST_TIMEOUT;
74
84
  this.router = new PeerRouter();
75
85
  this.isPaused = false;
76
- this.peerId = isRadomPeerId ? deviceId + '_' + Date.now() : deviceId;
86
+ this.peerId = isRandomPeerId ? deviceId + '_' + Date.now() : deviceId;
77
87
  this.deviceId = deviceId;
78
88
  }
79
89
 
@@ -81,7 +91,7 @@ export abstract class PeerTransfer {
81
91
  abstract online(deviceId: string): void;
82
92
  abstract offline(deviceId: string): void;
83
93
  abstract genRequestId(): Promise<string>;
84
- abstract getApi(deviceId: string): Promise<InternalApi | undefined>;
94
+ abstract getApi(peerId: string): Promise<InternalApi | undefined>;
85
95
  abstract discoverDeviceById(deviceId: string): Promise<void>;
86
96
  abstract onDestroy(): void;
87
97
  abstract requireAuth(): Promise<void>;
@@ -89,6 +99,7 @@ export abstract class PeerTransfer {
89
99
  abstract reloadConfig(config?: Record<string, any>): Promise<void>;
90
100
 
91
101
  async createPeer(force = false): Promise<Peer | undefined> {
102
+ console.log('createPeer', this.isPaused, force);
92
103
  if (this.isPaused && !force) {
93
104
  return;
94
105
  }
@@ -108,79 +119,89 @@ export abstract class PeerTransfer {
108
119
  this.peer = peer;
109
120
  await this.initialize(this.peer);
110
121
  this.checkConnection();
122
+ this.destroyed = false;
111
123
  return this.peer;
112
124
  }
125
+
113
126
  async initialize(peer: Peer) {
114
- try {
115
- if (this.checkerId) {
116
- clearInterval(this.checkerId);
117
- }
127
+ if (this.checkerId) {
128
+ clearInterval(this.checkerId);
129
+ this.checkerId = null;
130
+ }
118
131
 
119
- if (this.booted) {
120
- return this.booted;
121
- }
122
- this.booted = new Promise((resolve) => {
123
- const timer = setTimeout(() => {
124
- resolve(null);
125
- }, PEER_OPEN_TIMEOUT);
126
- const done = (peer?: Peer) => {
127
- clearTimeout(timer);
128
- resolve(peer);
129
- };
130
- peer.on('open', (id) => {
131
- console.log('peer@open', Date.now());
132
- clearTimeout(timer);
133
- done(peer);
134
- });
135
- peer.on('disconnected', () => {
136
- if (!this.isPaused) {
137
- setTimeout(() => {
138
- if (!this.isPaused) {
139
- this.reconnect();
140
- }
141
- }, CONNECTION_RETRY);
142
- }
143
- });
144
- // duplicated with close event
145
- peer.on('close', (...args) => {});
146
- peer.on('error', async (err) => {
147
- if (err.message === CONNECTION_PEER_CONFLICT) {
148
- this.stop();
149
- done(null);
150
- return;
151
- }
152
- if (err.message === CONNECTION_LIMIT_EXCEED) {
153
- this.onConnectLimit();
154
- return done(null);
155
- }
156
- if (err.message == UNAUTHORIZED) {
157
- done(null);
158
- return this.requireAuth();
159
- }
160
- if (err.type === 'network') {
161
- done(null);
162
- return this.reconnect();
163
- }
164
- if (err.type === 'server-error') {
165
- done(null);
166
- return this.reconnect();
167
- }
168
- done(null);
169
- return this.createPeer(true);
170
- });
132
+ if (this.booted) {
133
+ return this.booted;
134
+ }
135
+ this.booted = new Promise((resolve) => {
136
+ const timer = setTimeout(() => {
137
+ resolve(null);
138
+ }, PEER_OPEN_TIMEOUT);
139
+ const done = (p?: Peer) => {
140
+ clearTimeout(timer);
141
+ resolve(p);
142
+ };
143
+ peer.once('open', (id) => {
144
+ console.log('peer@open');
145
+ this.reconnectAttempt = 0; // 连接成功,重置退避计数
146
+ done(peer);
171
147
  });
172
- peer.on('connection', (conn: DataConnection) => {
173
- conn.label = 'incoming';
174
- this.onConnect(conn);
148
+ // peer.on('disconnected', () => {
149
+ // if (!this.isPaused) {
150
+ // this.reconnect();
151
+ // }
152
+ // });
153
+ peer.on('close', () => {
154
+ setTimeout(() => {
155
+ if (!this.isPaused && !this.destroyed) {
156
+ this.reconnect();
157
+ }
158
+ }, CONNECTION_TIMEOUT);
175
159
  });
176
- const socket = peer.socket;
177
- socket?.on('message', (message: IMessage) => {
178
- this.handleSocketMessage(message);
160
+ peer.on('error', async (err) => {
161
+ if (err.message === CONNECTION_PEER_CONFLICT) {
162
+ this.stop();
163
+ done(null);
164
+ return;
165
+ }
166
+ if (err.message === CONNECTION_LIMIT_EXCEED) {
167
+ this.onConnectLimit();
168
+ return done(null);
169
+ }
170
+ if (err.message === UNAUTHORIZED) {
171
+ done(null);
172
+ return this.requireAuth();
173
+ }
174
+ if (err.message === CONNECTION_DUPLICATED) {
175
+ // 连接级重复错误,不应销毁重建 peer
176
+ // createConnection 重试循环会处理
177
+ console.warn('peer@error: connection duplicated, will retry');
178
+ return;
179
+ }
180
+ if (err.type === 'network') {
181
+ done(null);
182
+ return this.reconnect();
183
+ }
184
+ if (err.type === 'server-error') {
185
+ done(null);
186
+ return this.reconnect();
187
+ }
188
+ done(null);
189
+ return this.createPeer(true);
179
190
  });
180
- this.onHeartbeat({ type: 'HEARTBEAT' } as IMessage);
191
+ });
192
+ peer.on('connection', (conn: DataConnection) => {
193
+ conn.label = 'incoming';
194
+ this.onConnect(conn);
195
+ });
196
+ const socket = peer.socket;
197
+ socket?.on('message', (message: IMessage) => {
198
+ this.handleSocketMessage(message);
199
+ });
200
+ this.onHeartbeat({ type: 'HEARTBEAT' } as IMessage);
201
+ try {
181
202
  await this.booted;
182
- } catch (err) {
183
- this.booted = undefined;
203
+ } catch {
204
+ // booted rejected via timer timeout
184
205
  } finally {
185
206
  this.booted = undefined;
186
207
  }
@@ -198,10 +219,7 @@ export abstract class PeerTransfer {
198
219
  }
199
220
 
200
221
  onHeartbeat(message: IMessage) {
201
- if (message.type !== 'HEARTBEAT') {
202
- return;
203
- }
204
- if (this.isPaused) {
222
+ if (message.type !== 'HEARTBEAT' || this.isPaused) {
205
223
  return;
206
224
  }
207
225
  }
@@ -220,7 +238,6 @@ export abstract class PeerTransfer {
220
238
  message: 'peer conflict',
221
239
  },
222
240
  });
223
- return;
224
241
  }
225
242
  }
226
243
 
@@ -234,33 +251,25 @@ export abstract class PeerTransfer {
234
251
  if (message.type !== 'ONLINE') {
235
252
  return;
236
253
  }
237
- const deviceId = message.payload.deviceId;
238
- this.online(deviceId);
254
+ this.online(message.payload.deviceId);
239
255
  }
240
256
 
241
257
  onDeviceOffline(message: IMessage) {
242
258
  if (message.type !== 'OFFLINE') {
243
259
  return;
244
260
  }
245
- const deviceId = message.payload.deviceId;
246
- this.ping(deviceId);
261
+ this.ping(message.payload.deviceId);
247
262
  }
248
263
 
249
264
  removeConnection(peerId: string, connectionId?: string) {
250
- // console.trace('removeConnection@', peerId, connectionId);
251
265
  const conn = this.connectPool.get(peerId);
252
266
  if (conn) {
253
267
  if (connectionId && conn.connectionId !== connectionId) {
254
268
  return;
255
269
  }
256
- const sending = this.inRequest.get(conn.connectionId);
257
- if (sending) {
258
- // 提前结束request,避免关闭连接时触发反馈
259
- sending.feedback();
260
- this.inRequest.delete(conn.connectionId);
261
- }
262
-
263
270
  this.connectPool.delete(peerId);
271
+
272
+ this.handleConnectionClose(peerId, conn);
264
273
  conn.removeAllListeners();
265
274
  conn.close();
266
275
  this.peer?._removeConnection(conn);
@@ -268,17 +277,61 @@ export abstract class PeerTransfer {
268
277
  const connections = this.peer?.connections || {};
269
278
  const conns = connections[peerId] || [];
270
279
  if (conns.length > 0) {
271
- for (const conn of conns) {
272
- conn.removeAllListeners();
273
- conn.close();
274
- this.peer?._removeConnection(conn);
280
+ for (const c of conns) {
281
+ c.removeAllListeners();
282
+ c.close();
283
+ this.peer?._removeConnection(c);
275
284
  }
276
285
  }
277
286
  }
278
287
  }
279
288
 
289
+ /**
290
+ * 连接关闭时尝试快速重连(5s 超时):
291
+ * - 重连成功 → 保留 inRequest 条目,更新 conn 引用
292
+ * - 重连失败 → 触发 feedback 并清理 inRequest
293
+ */
294
+ private async handleConnectionClose(peerId: string, conn: DataConnection) {
295
+ const matchedEntries: Array<
296
+ [string, { conn: DataConnection; message: Record<string, any>; feedback: () => void }]
297
+ > = [];
298
+ for (const [reqId, entry] of this.inRequest) {
299
+ if (entry.conn.connectionId === conn.connectionId) {
300
+ matchedEntries.push([reqId, entry]);
301
+ }
302
+ }
303
+
304
+ if (matchedEntries.length === 0) {
305
+ return;
306
+ }
307
+
308
+ // 快速重连,超时 5s
309
+ const newConn = await Promise.race([
310
+ this.connectToPeer(peerId),
311
+ delay(5000).then(() => undefined),
312
+ ]);
313
+
314
+ if (!this.isValidConnection(newConn)) {
315
+ // 重连失败,触发 feedback 让请求走重试逻辑
316
+ for (const [reqId, entry] of matchedEntries) {
317
+ entry.feedback();
318
+ this.inRequest.delete(reqId);
319
+ }
320
+ return;
321
+ }
322
+
323
+ // 重连成功,更新 inRequest 中的 conn 引用
324
+ for (const [reqId, entry] of matchedEntries) {
325
+ if (this.inRequest.has(reqId)) {
326
+ this.inRequest.set(reqId, {
327
+ ...entry,
328
+ conn: newConn,
329
+ });
330
+ }
331
+ }
332
+ }
333
+
280
334
  addConnection(conn: DataConnection) {
281
- // console.log('addConnection@', conn.label);
282
335
  this.connectPool.set(conn.peer, conn);
283
336
  const connecting = PeerTransfer.connecting.get(conn.peer);
284
337
  if (connecting) {
@@ -296,20 +349,25 @@ export abstract class PeerTransfer {
296
349
  }
297
350
  // turn credential expires
298
351
  if (this.peerConfig.turnExpiredAt && this.peerConfig.turnExpiredAt - 30000 < Date.now()) {
299
- // this.connectPool.clear();
300
352
  await this.reloadConfig();
301
353
  await this.createPeer(true);
302
354
  return;
303
355
  }
304
- const deviceIds = this.connectPool.keys();
305
- for (const deviceId of deviceIds) {
356
+
357
+ const deviceIds = Array.from(this.connectPool.keys());
358
+ const pingTasks = deviceIds.map(async (deviceId) => {
306
359
  const conn = this.connectPool.get(deviceId);
307
360
  if (!this.isValidConnection(conn)) {
308
361
  this.removeConnection(conn.peer, conn.connectionId);
309
- } else {
310
- this.ping(conn.peer);
362
+ return;
311
363
  }
312
- }
364
+ const ok = await this.ping(conn.peer);
365
+ if (!ok) {
366
+ this.removeConnection(conn.peer, conn.connectionId);
367
+ }
368
+ });
369
+ // 并行执行,不阻塞下个心跳周期
370
+ await Promise.allSettled(pingTasks);
313
371
  }, HEARTBEAT_INTERVAL);
314
372
  }
315
373
 
@@ -319,25 +377,24 @@ export abstract class PeerTransfer {
319
377
  const port = this.peerConfig?.port || 9000;
320
378
  return `${protocol}://${host}:${port}`;
321
379
  }
380
+
322
381
  async getDeviceStatus(peerId: string): Promise<IDeviceStatus | undefined> {
323
382
  try {
324
383
  const apiHost = this.getApiHost();
325
384
  const res = await axios.get(`${apiHost}/api/v1/devices/${peerId}/status`);
326
385
  if (res.status < 400) {
327
- const data = res.data;
328
- return data.data as IDeviceStatus;
386
+ return (res.data as { data: IDeviceStatus }).data;
329
387
  }
330
388
  } catch (err) {
331
389
  console.log('getDeviceStatus@error', err);
332
390
  }
333
391
  }
334
392
 
335
- async connectToPeer(deviceId: string): Promise<DataConnection | undefined> {
336
- if (deviceId === this.deviceId) {
393
+ async connectToPeer(peerId: string): Promise<DataConnection | undefined> {
394
+ if (peerId === this.deviceId) {
337
395
  return;
338
396
  }
339
397
 
340
- const peerId = deviceId;
341
398
  const existingConn = this.connectPool.get(peerId);
342
399
  console.log('connectToPeer', this.isPaused, this.connectPool.size);
343
400
  if (existingConn) {
@@ -360,18 +417,26 @@ export abstract class PeerTransfer {
360
417
  }
361
418
 
362
419
  private async createConnection(peerId: string): Promise<DataConnection | undefined> {
363
- let resolve!: (value: DataConnection | undefined) => void;
364
- let reject!: (reason?: any) => void;
365
420
  const q = PeerTransfer.connecting.get(peerId);
366
421
  if (q) {
367
422
  return q.promise;
368
423
  }
424
+
425
+ let resolve!: (value: DataConnection | undefined) => void;
426
+ let reject!: (reason?: any) => void;
369
427
  const connectionPromise = new Promise<DataConnection | undefined>((res, rej) => {
370
428
  resolve = res;
371
429
  reject = rej;
372
430
  });
373
431
 
374
432
  PeerTransfer.connecting.set(peerId, { promise: connectionPromise, resolve, reject });
433
+
434
+ // 兜底超时,防止 connecting 条目永远不清理
435
+ const guardTimer = setTimeout(() => {
436
+ PeerTransfer.connecting.delete(peerId);
437
+ resolve(undefined);
438
+ }, CONNECTION_TIMEOUT * 3);
439
+
375
440
  const doConnect = async () => {
376
441
  try {
377
442
  for (let retry = 0; retry < 3; retry++) {
@@ -382,8 +447,10 @@ export abstract class PeerTransfer {
382
447
  await this.reconnect();
383
448
  }
384
449
 
450
+ // 重试前检查:对方可能已经建连成功(duplicate 场景)
385
451
  const cur = this.connectPool.get(peerId);
386
452
  if (this.isValidConnection(cur)) {
453
+ clearTimeout(guardTimer);
387
454
  return resolve(cur);
388
455
  }
389
456
 
@@ -396,9 +463,18 @@ export abstract class PeerTransfer {
396
463
  }
397
464
  const connection = await this.onConnect(conn as DataConnection).catch(() => {});
398
465
  if (connection) {
466
+ clearTimeout(guardTimer);
399
467
  return resolve(connection);
400
468
  }
469
+
470
+ // duplicate 场景:onConnect 返回 undefined 但连接池可能已有对方建的连接
471
+ const pooled = this.connectPool.get(peerId);
472
+ if (this.isValidConnection(pooled)) {
473
+ clearTimeout(guardTimer);
474
+ return resolve(pooled);
475
+ }
401
476
  }
477
+ clearTimeout(guardTimer);
402
478
  resolve(undefined);
403
479
  return undefined;
404
480
  } catch (err) {
@@ -412,26 +488,47 @@ export abstract class PeerTransfer {
412
488
  return conn;
413
489
  }
414
490
 
491
+ /**
492
+ * 心跳检测 - 直接 send,不走 request 管线,避免重试放大
493
+ */
415
494
  async ping(peerId: string) {
416
495
  if (this.deviceId === peerId) {
417
496
  return;
418
497
  }
419
498
  const conn = this.connectPool.get(peerId);
420
499
  if (!this.isValidConnection(conn)) {
421
- this.removeConnection(conn?.peer, conn?.connectionId);
500
+ this.removeConnection(peerId, conn?.connectionId);
422
501
  return;
423
502
  }
424
- const res = await this.request(conn.peer, {
425
- route: PeerRoutes.HEARTBEAT,
426
- method: 'get',
427
- body: {},
428
- timeout: PING_TIMEOUT,
429
- });
430
- if (!res || res.error) {
431
- this.removeConnection(conn.peer, conn.connectionId);
503
+ try {
504
+ const requestId = await this.genRequestId();
505
+ return new Promise<boolean>((resolve) => {
506
+ const timer = setTimeout(() => {
507
+ this.requests.delete(requestId);
508
+ resolve(false);
509
+ }, PING_TIMEOUT);
510
+
511
+ this.requests.set(requestId, () => {
512
+ clearTimeout(timer);
513
+ resolve(true);
514
+ });
515
+
516
+ conn.send({
517
+ route: PeerRoutes.HEARTBEAT,
518
+ method: 'get',
519
+ body: {},
520
+ requestId,
521
+ scope: PeerScope.REQUEST,
522
+ action: PeerAction.SEND,
523
+ src: this.peerId,
524
+ dest: peerId,
525
+ headers: {},
526
+ createdAt: Date.now(),
527
+ });
528
+ });
529
+ } catch {
432
530
  return false;
433
531
  }
434
- return true;
435
532
  }
436
533
 
437
534
  async onConnect(conn: DataConnection): Promise<DataConnection | undefined> {
@@ -439,9 +536,9 @@ export abstract class PeerTransfer {
439
536
  return;
440
537
  }
441
538
  const exists = this.connectPool.get(conn.peer);
442
- // console.log('onConnect', conn.label, exists);
443
539
  if (exists) {
444
- if (exists.metadata.time > conn.metadata.time) {
540
+ // 双方同时建连时,保留 peerId 字典序大的那条(确定性,不依赖时间戳)
541
+ if (exists.connectionId > conn.connectionId) {
445
542
  this.removeConnection(conn.peer, conn.connectionId);
446
543
  if (this.isValidConnection(exists)) {
447
544
  return exists;
@@ -450,19 +547,28 @@ export abstract class PeerTransfer {
450
547
  this.removeConnection(exists.peer, exists.connectionId);
451
548
  }
452
549
  }
453
- let timer = null;
550
+
551
+ if (conn.open) {
552
+ this.addConnection(conn);
553
+ this.bindConnectionData(conn);
554
+ return conn;
555
+ }
556
+
454
557
  return new Promise((resolve) => {
558
+ let timer: ReturnType<typeof setTimeout> | null = null;
559
+
455
560
  const done = (connection?: DataConnection) => {
456
- if (connection) {
457
- this.addConnection(connection);
458
- }
459
561
  if (timer) {
460
562
  clearTimeout(timer);
461
563
  timer = null;
462
564
  }
565
+ if (connection) {
566
+ this.addConnection(connection);
567
+ }
463
568
  resolve(connection);
464
569
  };
465
- timer = setTimeout(async () => {
570
+
571
+ timer = setTimeout(() => {
466
572
  if (this.isValidConnection(conn)) {
467
573
  this.addConnection(conn);
468
574
  done(conn);
@@ -470,94 +576,145 @@ export abstract class PeerTransfer {
470
576
  this.removeConnection(conn.peer, conn.connectionId);
471
577
  done(undefined);
472
578
  }
473
- timer = null;
474
579
  }, CONNECTION_TIMEOUT);
475
- conn.on('data', (message: any) => {
476
- switch (message.scope) {
477
- case PeerScope.REQUEST:
478
- return this.onRequest(conn, message);
479
- case PeerScope.RESPONSE:
480
- return this.onResponse(conn, message);
481
- default:
482
- return this.responseFail(conn, message, 'unknown scope');
580
+
581
+ this.bindConnectionData(conn);
582
+
583
+ conn.on('error', (err) => {
584
+ // duplicate 场景:连接池可能已有对方建的连接,不应删除
585
+ const pooled = this.connectPool.get(conn.peer);
586
+ if (this.isValidConnection(pooled)) {
587
+ done(pooled);
588
+ return;
483
589
  }
484
- });
485
- conn.on('error', async (err) => {
486
590
  this.removeConnection(conn.peer, conn.connectionId);
487
- // if (!this.peer?.open) {
488
- // await this.reconnect();
489
- // }
490
591
  done(null);
491
592
  });
492
- conn.on('close', async () => {
593
+ conn.on('close', () => {
493
594
  console.log('close------------>', conn.label);
494
- clearTimeout(timer);
495
595
  this.removeConnection(conn.peer, conn.connectionId);
496
- // if (!this.peer?.open) {
497
- // await this.reconnect();
498
- // }
499
596
  done(null);
500
597
  });
501
- conn.on('open', async () => {
502
- clearTimeout(timer);
598
+ conn.on('open', () => {
503
599
  done(conn);
504
600
  });
505
- if (this.isValidConnection(conn)) {
506
- if (timer) {
507
- clearTimeout(timer);
508
- }
509
- timer = null;
510
- done(conn);
601
+ });
602
+ }
603
+
604
+ /** 绑定 DataConnection 的 data 事件 */
605
+ private bindConnectionData(conn: DataConnection) {
606
+ conn.on('data', (message: any) => {
607
+ switch (message.scope) {
608
+ case PeerScope.REQUEST:
609
+ return this.onRequest(conn, message);
610
+ case PeerScope.RESPONSE:
611
+ return this.onResponse(conn, message);
612
+ default:
613
+ return this.responseFail(conn, message, 'unknown scope');
511
614
  }
512
615
  });
513
616
  }
514
617
 
515
618
  async request(peerId: string, payload: PeerRequestPayload): Promise<PeerMessage> {
619
+ const requestId = payload.requestId || (await this.genRequestId());
620
+ const maxRetry = 2;
621
+ let res: PeerMessage;
622
+ for (let attempt = 0; attempt <= maxRetry; attempt++) {
623
+ res = await this.doRequestOnce(peerId, payload, requestId);
624
+ console.log('request once', res);
625
+ const code = (res.error as Record<string, any>)?.code;
626
+ if (!res.error || (code !== 'CONN_CLOSED' && code !== 'CONN_FAILED')) {
627
+ return res;
628
+ }
629
+ }
630
+ return res!;
631
+ }
632
+
633
+ /**
634
+ * 执行一次请求,返回带错误码的结果供外层判断是否重试
635
+ * @param peerId - 目标 peerId
636
+ * @param payload - 请求负载
637
+ * @param requestId - 请求ID(重试时复用)
638
+ * @returns error.code: CONN_CLOSED/CONN_FAILED(可重试)、TIMEOUT(不重试)
639
+ */
640
+ private async doRequestOnce(
641
+ peerId: string,
642
+ payload: PeerRequestPayload,
643
+ requestId: string,
644
+ ): Promise<PeerMessage> {
645
+ console.log('doRequestOnce@@', peerId, this.destroyed, payload.route);
646
+ if (this.destroyed) {
647
+ return { ...payload, body: {}, error: { message: 'destroyed', code: 'CONN_CLOSED' } } as any;
648
+ }
516
649
  const conn = await this.connectToPeer(peerId);
517
650
  if (!this.isValidConnection(conn)) {
518
651
  return {
519
652
  ...payload,
520
653
  body: {},
521
- error: { message: 'build connection failed', code: -1 },
654
+ error: { message: 'build connection failed', code: 'CONN_FAILED' },
522
655
  } as any;
523
656
  }
524
657
 
525
- const requestId = payload.requestId || (await this.genRequestId());
526
- const q = new Promise<Record<string, any>>((resolve) => {
527
- const timeout = setTimeout(() => {
528
- const msg = {
529
- body: {},
530
- error: { message: 'request timeout' },
531
- };
658
+ return new Promise((resolve) => {
659
+ let timeout: ReturnType<typeof setTimeout> | null = null;
660
+ const cleanup = () => {
661
+ if (timeout) {
662
+ clearTimeout(timeout);
663
+ timeout = null;
664
+ }
665
+ this.inRequest.delete(requestId);
666
+ this.requests.delete(requestId);
667
+ // 清理 addRequest 可能创建的定时器
668
+ const oldTimer = this.requestTimers.get(requestId);
669
+ if (oldTimer) {
670
+ clearTimeout(oldTimer);
671
+ this.requestTimers.delete(requestId);
672
+ }
673
+ };
674
+
675
+ timeout = setTimeout(() => {
676
+ // 先清理,避免 removeConnection 触发 feedback 二次 resolve
677
+ cleanup();
532
678
  this.removeConnection(conn.peer, conn.connectionId);
533
- resolve(msg);
679
+ resolve({
680
+ ...payload,
681
+ body: {},
682
+ error: { message: 'request timeout', code: 'TIMEOUT' },
683
+ } as any);
534
684
  }, payload.timeout || REQUEST_TIMEOUT);
535
- this.inRequest.set(conn.connectionId, {
685
+
686
+ this.inRequest.set(requestId, {
536
687
  conn,
537
688
  message: payload,
538
- feedback: () =>
689
+ feedback: () => {
690
+ cleanup();
539
691
  resolve({
692
+ ...payload,
540
693
  body: {},
541
- error: { message: 'send failed' },
542
- }),
694
+ error: { message: 'send failed', code: 'CONN_CLOSED' },
695
+ } as any);
696
+ },
543
697
  });
544
- this.addRequest(requestId, (message: Record<string, any>) => {
545
- clearTimeout(timeout);
698
+
699
+ // 直接 set,避免 addRequest 内部 setTimeout 在重试时误删新回调
700
+ this.requests.set(requestId, (message: Record<string, any>) => {
701
+ cleanup();
546
702
  const now = Date.now();
547
703
  if (now - message.createdAt > 3000) {
548
704
  console.warn('[egos] slow peer request:', message.route, now - message.createdAt);
549
705
  }
550
- this.inRequest.delete(conn.connectionId);
551
- resolve(message);
706
+ resolve(message as any);
707
+ });
708
+
709
+ this.send(conn, { ...payload, requestId, headers: { ...payload.headers } }).catch(() => {
710
+ cleanup();
711
+ resolve({
712
+ ...payload,
713
+ body: {},
714
+ error: { message: 'send failed', code: 'CONN_CLOSED' },
715
+ } as any);
552
716
  });
553
717
  });
554
- await this.send(conn, {
555
- ...payload,
556
- requestId,
557
- headers: { ...payload.headers },
558
- });
559
- const res = await q;
560
- return res as any;
561
718
  }
562
719
 
563
720
  async reply(
@@ -580,14 +737,13 @@ export abstract class PeerTransfer {
580
737
  const connection = await this.connectToPeer(conn.peer);
581
738
  return connection?.send(data);
582
739
  }
583
-
584
740
  return conn.send(data);
585
741
  }
586
742
 
587
743
  async send(conn: DataConnection, message: PeerRequestPayload) {
588
744
  const internalApi = await this.getApi(conn.peer);
589
745
  let connection = conn;
590
- if (!this.isValidConnection) {
746
+ if (!this.isValidConnection(conn)) {
591
747
  const con = await this.connectToPeer(conn.peer);
592
748
  if (!con) {
593
749
  return this.responseFail(conn, message as PeerMessage, 'send with invalid connection');
@@ -595,7 +751,7 @@ export abstract class PeerTransfer {
595
751
  connection = con;
596
752
  }
597
753
  const requestId = message.requestId || (await this.genRequestId());
598
- const sent = await connection.send(
754
+ return connection.send(
599
755
  {
600
756
  ...message,
601
757
  requestId,
@@ -611,7 +767,6 @@ export abstract class PeerTransfer {
611
767
  },
612
768
  false,
613
769
  );
614
- return sent;
615
770
  }
616
771
 
617
772
  inheritMessage(receiveMsg: PeerMessage) {
@@ -626,6 +781,7 @@ export abstract class PeerTransfer {
626
781
  }
627
782
 
628
783
  async reconnect(): Promise<Peer | undefined> {
784
+ console.log('reconnect', this.isPaused);
629
785
  if (this.isPaused) {
630
786
  return;
631
787
  }
@@ -654,31 +810,55 @@ export abstract class PeerTransfer {
654
810
  if (this.isPaused) {
655
811
  return;
656
812
  }
813
+ // 已连接且 token 未变,无需重连
657
814
  if (this.peer?.open && this.peerConfig.token === this.peer?.options.token) {
658
815
  return this.peer;
659
816
  }
660
817
 
818
+ // 指数退避
819
+ this.reconnectAttempt++;
820
+ const backoff = Math.min(
821
+ BASE_RECONNECT_BACKOFF * Math.pow(2, this.reconnectAttempt - 1),
822
+ MAX_RECONNECT_BACKOFF,
823
+ );
824
+ await delay(backoff);
825
+
661
826
  await this.reloadConfig();
662
- return this.createPeer();
663
- // return this.createPeer();
827
+
828
+ // 优先尝试 peer.reconnect() 轻量重连
664
829
  if (this.peer?.disconnected && this.peer?.options.token === this.peerConfig.token) {
665
830
  this.peer.reconnect();
666
831
  return this.peer;
667
- } else {
668
- await this.reloadConfig();
669
- return this.createPeer();
670
832
  }
833
+
834
+ return this.createPeer();
671
835
  } catch (err) {
672
836
  return this.createPeer();
673
837
  }
674
838
  }
675
839
 
676
840
  clearInstance() {
841
+ console.info('clearInstance');
842
+ this.destroyed = true;
677
843
  PeerTransfer.connecting.clear();
678
- const peerIds = this.connectPool.keys();
844
+
845
+ // 先触发所有 inRequest 的 feedback,让它们 resolve
846
+ for (const [reqId, entry] of this.inRequest) {
847
+ entry.feedback();
848
+ }
849
+ this.inRequest.clear();
850
+
851
+ const peerIds = Array.from(this.connectPool.keys());
679
852
  for (const pId of peerIds) {
680
853
  this.removeConnection(pId);
681
854
  }
855
+
856
+ this.requests.clear();
857
+ for (const timer of this.requestTimers.values()) {
858
+ clearTimeout(timer);
859
+ }
860
+ this.requestTimers.clear();
861
+
682
862
  this.peer?.destroy();
683
863
  this.peer?.removeAllListeners();
684
864
  this.booted = undefined;
@@ -696,9 +876,7 @@ export abstract class PeerTransfer {
696
876
 
697
877
  async onRequest(conn: DataConnection, message: PeerMessage) {
698
878
  try {
699
- // @todo default folder
700
- // console.log('onRequest-->', message.route);
701
- // @todo default folder
879
+ console.log('onRequest@@', conn.peer?.substring(0, 4), message.route);
702
880
  await this.router.run(conn, message);
703
881
  } catch (err: any) {
704
882
  this.responseFail(conn, message, { message: err.message, code: err.code });
@@ -706,20 +884,15 @@ export abstract class PeerTransfer {
706
884
  }
707
885
 
708
886
  async onResponse(conn: DataConnection, message: PeerMessage) {
709
- // @todo default folder
710
887
  const cb = this.requests.get(message.requestId);
711
888
  if (cb) {
712
889
  cb.apply(this, [message]);
713
890
  this.requests.delete(message.requestId);
714
891
  }
715
- // @todo throw err;
716
892
  }
717
893
 
718
894
  async responseOk(conn: DataConnection, message: PeerMessage) {
719
- const msg = {
720
- body: { ok: true },
721
- };
722
- return this.reply(conn, message, msg);
895
+ return this.reply(conn, message, { body: { ok: true } });
723
896
  }
724
897
 
725
898
  async responseFail(
@@ -727,18 +900,21 @@ export abstract class PeerTransfer {
727
900
  message: PeerMessage,
728
901
  err: string | Record<string, any>,
729
902
  ) {
730
- const msg = {
731
- body: {},
732
- error: err || 'service error',
733
- };
734
- return this.reply(conn, message, msg);
903
+ return this.reply(conn, message, { body: {}, error: err || 'service error' });
735
904
  }
736
905
 
737
906
  addRequest(id: string, cb: ResponseCb) {
907
+ // 清除已有定时器,避免重复注册时旧定时器误删新回调
908
+ const oldTimer = this.requestTimers.get(id);
909
+ if (oldTimer) {
910
+ clearTimeout(oldTimer);
911
+ }
738
912
  this.requests.set(id, cb);
739
- setTimeout(() => {
913
+ const timer = setTimeout(() => {
740
914
  this.requests.delete(id);
915
+ this.requestTimers.delete(id);
741
916
  }, this.timeout);
917
+ this.requestTimers.set(id, timer);
742
918
  }
743
919
 
744
920
  use(handler: RequestCallbackFn, prefix = '') {
@@ -756,6 +932,7 @@ export abstract class PeerTransfer {
756
932
 
757
933
  resume() {
758
934
  this.isPaused = false;
935
+ this.destroyed = false;
759
936
  return this.createPeer(true);
760
937
  }
761
938
  }