egos-transfer 0.2.2 → 0.2.4

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,80 +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.stop();
154
- this.onConnectLimit();
155
- return done(null);
156
- }
157
- if (err.message == UNAUTHORIZED) {
158
- done(null);
159
- return this.requireAuth();
160
- }
161
- if (err.type === 'network') {
162
- done(null);
163
- return this.reconnect();
164
- }
165
- if (err.type === 'server-error') {
166
- done(null);
167
- return this.reconnect();
168
- }
169
- done(null);
170
- return this.createPeer(true);
171
- });
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.on('open', (id) => {
144
+ console.log('peer@open');
145
+ this.reconnectAttempt = 0; // 连接成功,重置退避计数
146
+ done(peer);
172
147
  });
173
- peer.on('connection', (conn: DataConnection) => {
174
- conn.label = 'incoming';
175
- 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);
176
159
  });
177
- const socket = peer.socket;
178
- socket?.on('message', (message: IMessage) => {
179
- 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);
180
190
  });
181
- 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 {
182
202
  await this.booted;
183
- } catch (err) {
184
- this.booted = undefined;
203
+ } catch {
204
+ // booted rejected via timer timeout
185
205
  } finally {
186
206
  this.booted = undefined;
187
207
  }
@@ -199,10 +219,7 @@ export abstract class PeerTransfer {
199
219
  }
200
220
 
201
221
  onHeartbeat(message: IMessage) {
202
- if (message.type !== 'HEARTBEAT') {
203
- return;
204
- }
205
- if (this.isPaused) {
222
+ if (message.type !== 'HEARTBEAT' || this.isPaused) {
206
223
  return;
207
224
  }
208
225
  }
@@ -221,7 +238,6 @@ export abstract class PeerTransfer {
221
238
  message: 'peer conflict',
222
239
  },
223
240
  });
224
- return;
225
241
  }
226
242
  }
227
243
 
@@ -235,33 +251,25 @@ export abstract class PeerTransfer {
235
251
  if (message.type !== 'ONLINE') {
236
252
  return;
237
253
  }
238
- const deviceId = message.payload.deviceId;
239
- this.online(deviceId);
254
+ this.online(message.payload.deviceId);
240
255
  }
241
256
 
242
257
  onDeviceOffline(message: IMessage) {
243
258
  if (message.type !== 'OFFLINE') {
244
259
  return;
245
260
  }
246
- const deviceId = message.payload.deviceId;
247
- this.ping(deviceId);
261
+ this.ping(message.payload.deviceId);
248
262
  }
249
263
 
250
264
  removeConnection(peerId: string, connectionId?: string) {
251
- // console.trace('removeConnection@', peerId, connectionId);
252
265
  const conn = this.connectPool.get(peerId);
253
266
  if (conn) {
254
267
  if (connectionId && conn.connectionId !== connectionId) {
255
268
  return;
256
269
  }
257
- const sending = this.inRequest.get(conn.connectionId);
258
- if (sending) {
259
- // 提前结束request,避免关闭连接时触发反馈
260
- sending.feedback();
261
- this.inRequest.delete(conn.connectionId);
262
- }
263
-
264
270
  this.connectPool.delete(peerId);
271
+
272
+ this.handleConnectionClose(peerId, conn);
265
273
  conn.removeAllListeners();
266
274
  conn.close();
267
275
  this.peer?._removeConnection(conn);
@@ -269,17 +277,61 @@ export abstract class PeerTransfer {
269
277
  const connections = this.peer?.connections || {};
270
278
  const conns = connections[peerId] || [];
271
279
  if (conns.length > 0) {
272
- for (const conn of conns) {
273
- conn.removeAllListeners();
274
- conn.close();
275
- this.peer?._removeConnection(conn);
280
+ for (const c of conns) {
281
+ c.removeAllListeners();
282
+ c.close();
283
+ this.peer?._removeConnection(c);
276
284
  }
277
285
  }
278
286
  }
279
287
  }
280
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
+
281
334
  addConnection(conn: DataConnection) {
282
- // console.log('addConnection@', conn.label);
283
335
  this.connectPool.set(conn.peer, conn);
284
336
  const connecting = PeerTransfer.connecting.get(conn.peer);
285
337
  if (connecting) {
@@ -297,20 +349,25 @@ export abstract class PeerTransfer {
297
349
  }
298
350
  // turn credential expires
299
351
  if (this.peerConfig.turnExpiredAt && this.peerConfig.turnExpiredAt - 30000 < Date.now()) {
300
- // this.connectPool.clear();
301
352
  await this.reloadConfig();
302
353
  await this.createPeer(true);
303
354
  return;
304
355
  }
305
- const deviceIds = this.connectPool.keys();
306
- for (const deviceId of deviceIds) {
356
+
357
+ const deviceIds = Array.from(this.connectPool.keys());
358
+ const pingTasks = deviceIds.map(async (deviceId) => {
307
359
  const conn = this.connectPool.get(deviceId);
308
360
  if (!this.isValidConnection(conn)) {
309
361
  this.removeConnection(conn.peer, conn.connectionId);
310
- } else {
311
- this.ping(conn.peer);
362
+ return;
312
363
  }
313
- }
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);
314
371
  }, HEARTBEAT_INTERVAL);
315
372
  }
316
373
 
@@ -320,25 +377,24 @@ export abstract class PeerTransfer {
320
377
  const port = this.peerConfig?.port || 9000;
321
378
  return `${protocol}://${host}:${port}`;
322
379
  }
380
+
323
381
  async getDeviceStatus(peerId: string): Promise<IDeviceStatus | undefined> {
324
382
  try {
325
383
  const apiHost = this.getApiHost();
326
384
  const res = await axios.get(`${apiHost}/api/v1/devices/${peerId}/status`);
327
385
  if (res.status < 400) {
328
- const data = res.data;
329
- return data.data as IDeviceStatus;
386
+ return (res.data as { data: IDeviceStatus }).data;
330
387
  }
331
388
  } catch (err) {
332
389
  console.log('getDeviceStatus@error', err);
333
390
  }
334
391
  }
335
392
 
336
- async connectToPeer(deviceId: string): Promise<DataConnection | undefined> {
337
- if (deviceId === this.deviceId) {
393
+ async connectToPeer(peerId: string): Promise<DataConnection | undefined> {
394
+ if (peerId === this.deviceId) {
338
395
  return;
339
396
  }
340
397
 
341
- const peerId = deviceId;
342
398
  const existingConn = this.connectPool.get(peerId);
343
399
  console.log('connectToPeer', this.isPaused, this.connectPool.size);
344
400
  if (existingConn) {
@@ -361,18 +417,26 @@ export abstract class PeerTransfer {
361
417
  }
362
418
 
363
419
  private async createConnection(peerId: string): Promise<DataConnection | undefined> {
364
- let resolve!: (value: DataConnection | undefined) => void;
365
- let reject!: (reason?: any) => void;
366
420
  const q = PeerTransfer.connecting.get(peerId);
367
421
  if (q) {
368
422
  return q.promise;
369
423
  }
424
+
425
+ let resolve!: (value: DataConnection | undefined) => void;
426
+ let reject!: (reason?: any) => void;
370
427
  const connectionPromise = new Promise<DataConnection | undefined>((res, rej) => {
371
428
  resolve = res;
372
429
  reject = rej;
373
430
  });
374
431
 
375
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
+
376
440
  const doConnect = async () => {
377
441
  try {
378
442
  for (let retry = 0; retry < 3; retry++) {
@@ -385,6 +449,7 @@ export abstract class PeerTransfer {
385
449
 
386
450
  const cur = this.connectPool.get(peerId);
387
451
  if (this.isValidConnection(cur)) {
452
+ clearTimeout(guardTimer);
388
453
  return resolve(cur);
389
454
  }
390
455
 
@@ -397,9 +462,11 @@ export abstract class PeerTransfer {
397
462
  }
398
463
  const connection = await this.onConnect(conn as DataConnection).catch(() => {});
399
464
  if (connection) {
465
+ clearTimeout(guardTimer);
400
466
  return resolve(connection);
401
467
  }
402
468
  }
469
+ clearTimeout(guardTimer);
403
470
  resolve(undefined);
404
471
  return undefined;
405
472
  } catch (err) {
@@ -413,26 +480,47 @@ export abstract class PeerTransfer {
413
480
  return conn;
414
481
  }
415
482
 
483
+ /**
484
+ * 心跳检测 - 直接 send,不走 request 管线,避免重试放大
485
+ */
416
486
  async ping(peerId: string) {
417
487
  if (this.deviceId === peerId) {
418
488
  return;
419
489
  }
420
490
  const conn = this.connectPool.get(peerId);
421
491
  if (!this.isValidConnection(conn)) {
422
- this.removeConnection(conn?.peer, conn?.connectionId);
492
+ this.removeConnection(peerId, conn?.connectionId);
423
493
  return;
424
494
  }
425
- const res = await this.request(conn.peer, {
426
- route: PeerRoutes.HEARTBEAT,
427
- method: 'get',
428
- body: {},
429
- timeout: PING_TIMEOUT,
430
- });
431
- if (!res || res.error) {
432
- this.removeConnection(conn.peer, conn.connectionId);
495
+ try {
496
+ const requestId = await this.genRequestId();
497
+ return new Promise<boolean>((resolve) => {
498
+ const timer = setTimeout(() => {
499
+ this.requests.delete(requestId);
500
+ resolve(false);
501
+ }, PING_TIMEOUT);
502
+
503
+ this.requests.set(requestId, () => {
504
+ clearTimeout(timer);
505
+ resolve(true);
506
+ });
507
+
508
+ conn.send({
509
+ route: PeerRoutes.HEARTBEAT,
510
+ method: 'get',
511
+ body: {},
512
+ requestId,
513
+ scope: PeerScope.REQUEST,
514
+ action: PeerAction.SEND,
515
+ src: this.peerId,
516
+ dest: peerId,
517
+ headers: {},
518
+ createdAt: Date.now(),
519
+ });
520
+ });
521
+ } catch {
433
522
  return false;
434
523
  }
435
- return true;
436
524
  }
437
525
 
438
526
  async onConnect(conn: DataConnection): Promise<DataConnection | undefined> {
@@ -440,9 +528,9 @@ export abstract class PeerTransfer {
440
528
  return;
441
529
  }
442
530
  const exists = this.connectPool.get(conn.peer);
443
- // console.log('onConnect', conn.label, exists);
444
531
  if (exists) {
445
- if (exists.metadata.time > conn.metadata.time) {
532
+ // 双方同时建连时,保留 peerId 字典序大的那条(确定性,不依赖时间戳)
533
+ if (exists.connectionId > conn.connectionId) {
446
534
  this.removeConnection(conn.peer, conn.connectionId);
447
535
  if (this.isValidConnection(exists)) {
448
536
  return exists;
@@ -451,19 +539,28 @@ export abstract class PeerTransfer {
451
539
  this.removeConnection(exists.peer, exists.connectionId);
452
540
  }
453
541
  }
454
- let timer = null;
542
+
543
+ if (conn.open) {
544
+ this.addConnection(conn);
545
+ this.bindConnectionData(conn);
546
+ return conn;
547
+ }
548
+
455
549
  return new Promise((resolve) => {
550
+ let timer: ReturnType<typeof setTimeout> | null = null;
551
+
456
552
  const done = (connection?: DataConnection) => {
457
- if (connection) {
458
- this.addConnection(connection);
459
- }
460
553
  if (timer) {
461
554
  clearTimeout(timer);
462
555
  timer = null;
463
556
  }
557
+ if (connection) {
558
+ this.addConnection(connection);
559
+ }
464
560
  resolve(connection);
465
561
  };
466
- timer = setTimeout(async () => {
562
+
563
+ timer = setTimeout(() => {
467
564
  if (this.isValidConnection(conn)) {
468
565
  this.addConnection(conn);
469
566
  done(conn);
@@ -471,94 +568,139 @@ export abstract class PeerTransfer {
471
568
  this.removeConnection(conn.peer, conn.connectionId);
472
569
  done(undefined);
473
570
  }
474
- timer = null;
475
571
  }, CONNECTION_TIMEOUT);
476
- conn.on('data', (message: any) => {
477
- switch (message.scope) {
478
- case PeerScope.REQUEST:
479
- return this.onRequest(conn, message);
480
- case PeerScope.RESPONSE:
481
- return this.onResponse(conn, message);
482
- default:
483
- return this.responseFail(conn, message, 'unknown scope');
484
- }
485
- });
486
- conn.on('error', async (err) => {
572
+
573
+ this.bindConnectionData(conn);
574
+
575
+ conn.on('error', () => {
487
576
  this.removeConnection(conn.peer, conn.connectionId);
488
- // if (!this.peer?.open) {
489
- // await this.reconnect();
490
- // }
491
577
  done(null);
492
578
  });
493
- conn.on('close', async () => {
579
+ conn.on('close', () => {
494
580
  console.log('close------------>', conn.label);
495
- clearTimeout(timer);
496
581
  this.removeConnection(conn.peer, conn.connectionId);
497
- // if (!this.peer?.open) {
498
- // await this.reconnect();
499
- // }
500
582
  done(null);
501
583
  });
502
- conn.on('open', async () => {
503
- clearTimeout(timer);
584
+ conn.on('open', () => {
504
585
  done(conn);
505
586
  });
506
- if (this.isValidConnection(conn)) {
507
- if (timer) {
508
- clearTimeout(timer);
509
- }
510
- timer = null;
511
- done(conn);
587
+ });
588
+ }
589
+
590
+ /** 绑定 DataConnection 的 data 事件 */
591
+ private bindConnectionData(conn: DataConnection) {
592
+ conn.on('data', (message: any) => {
593
+ switch (message.scope) {
594
+ case PeerScope.REQUEST:
595
+ return this.onRequest(conn, message);
596
+ case PeerScope.RESPONSE:
597
+ return this.onResponse(conn, message);
598
+ default:
599
+ return this.responseFail(conn, message, 'unknown scope');
512
600
  }
513
601
  });
514
602
  }
515
603
 
516
604
  async request(peerId: string, payload: PeerRequestPayload): Promise<PeerMessage> {
605
+ const requestId = payload.requestId || (await this.genRequestId());
606
+ const maxRetry = 2;
607
+ let res: PeerMessage;
608
+ for (let attempt = 0; attempt <= maxRetry; attempt++) {
609
+ res = await this.doRequestOnce(peerId, payload, requestId);
610
+ console.log('request once', res);
611
+ const code = (res.error as Record<string, any>)?.code;
612
+ if (!res.error || (code !== 'CONN_CLOSED' && code !== 'CONN_FAILED')) {
613
+ return res;
614
+ }
615
+ }
616
+ return res!;
617
+ }
618
+
619
+ /**
620
+ * 执行一次请求,返回带错误码的结果供外层判断是否重试
621
+ * @param peerId - 目标 peerId
622
+ * @param payload - 请求负载
623
+ * @param requestId - 请求ID(重试时复用)
624
+ * @returns error.code: CONN_CLOSED/CONN_FAILED(可重试)、TIMEOUT(不重试)
625
+ */
626
+ private async doRequestOnce(
627
+ peerId: string,
628
+ payload: PeerRequestPayload,
629
+ requestId: string,
630
+ ): Promise<PeerMessage> {
631
+ console.log('doRequestOnce@@', peerId, this.destroyed, payload.route);
632
+ if (this.destroyed) {
633
+ return { ...payload, body: {}, error: { message: 'destroyed', code: 'CONN_CLOSED' } } as any;
634
+ }
517
635
  const conn = await this.connectToPeer(peerId);
518
636
  if (!this.isValidConnection(conn)) {
519
637
  return {
520
638
  ...payload,
521
639
  body: {},
522
- error: { message: 'build connection failed', code: -1 },
640
+ error: { message: 'build connection failed', code: 'CONN_FAILED' },
523
641
  } as any;
524
642
  }
525
643
 
526
- const requestId = payload.requestId || (await this.genRequestId());
527
- const q = new Promise<Record<string, any>>((resolve) => {
528
- const timeout = setTimeout(() => {
529
- const msg = {
530
- body: {},
531
- error: { message: 'request timeout' },
532
- };
644
+ return new Promise((resolve) => {
645
+ let timeout: ReturnType<typeof setTimeout> | null = null;
646
+ const cleanup = () => {
647
+ if (timeout) {
648
+ clearTimeout(timeout);
649
+ timeout = null;
650
+ }
651
+ this.inRequest.delete(requestId);
652
+ this.requests.delete(requestId);
653
+ // 清理 addRequest 可能创建的定时器
654
+ const oldTimer = this.requestTimers.get(requestId);
655
+ if (oldTimer) {
656
+ clearTimeout(oldTimer);
657
+ this.requestTimers.delete(requestId);
658
+ }
659
+ };
660
+
661
+ timeout = setTimeout(() => {
662
+ // 先清理,避免 removeConnection 触发 feedback 二次 resolve
663
+ cleanup();
533
664
  this.removeConnection(conn.peer, conn.connectionId);
534
- resolve(msg);
665
+ resolve({
666
+ ...payload,
667
+ body: {},
668
+ error: { message: 'request timeout', code: 'TIMEOUT' },
669
+ } as any);
535
670
  }, payload.timeout || REQUEST_TIMEOUT);
536
- this.inRequest.set(conn.connectionId, {
671
+
672
+ this.inRequest.set(requestId, {
537
673
  conn,
538
674
  message: payload,
539
- feedback: () =>
675
+ feedback: () => {
676
+ cleanup();
540
677
  resolve({
678
+ ...payload,
541
679
  body: {},
542
- error: { message: 'send failed' },
543
- }),
680
+ error: { message: 'send failed', code: 'CONN_CLOSED' },
681
+ } as any);
682
+ },
544
683
  });
545
- this.addRequest(requestId, (message: Record<string, any>) => {
546
- clearTimeout(timeout);
684
+
685
+ // 直接 set,避免 addRequest 内部 setTimeout 在重试时误删新回调
686
+ this.requests.set(requestId, (message: Record<string, any>) => {
687
+ cleanup();
547
688
  const now = Date.now();
548
689
  if (now - message.createdAt > 3000) {
549
690
  console.warn('[egos] slow peer request:', message.route, now - message.createdAt);
550
691
  }
551
- this.inRequest.delete(conn.connectionId);
552
- resolve(message);
692
+ resolve(message as any);
693
+ });
694
+
695
+ this.send(conn, { ...payload, requestId, headers: { ...payload.headers } }).catch(() => {
696
+ cleanup();
697
+ resolve({
698
+ ...payload,
699
+ body: {},
700
+ error: { message: 'send failed', code: 'CONN_CLOSED' },
701
+ } as any);
553
702
  });
554
703
  });
555
- await this.send(conn, {
556
- ...payload,
557
- requestId,
558
- headers: { ...payload.headers },
559
- });
560
- const res = await q;
561
- return res as any;
562
704
  }
563
705
 
564
706
  async reply(
@@ -581,14 +723,13 @@ export abstract class PeerTransfer {
581
723
  const connection = await this.connectToPeer(conn.peer);
582
724
  return connection?.send(data);
583
725
  }
584
-
585
726
  return conn.send(data);
586
727
  }
587
728
 
588
729
  async send(conn: DataConnection, message: PeerRequestPayload) {
589
730
  const internalApi = await this.getApi(conn.peer);
590
731
  let connection = conn;
591
- if (!this.isValidConnection) {
732
+ if (!this.isValidConnection(conn)) {
592
733
  const con = await this.connectToPeer(conn.peer);
593
734
  if (!con) {
594
735
  return this.responseFail(conn, message as PeerMessage, 'send with invalid connection');
@@ -596,7 +737,7 @@ export abstract class PeerTransfer {
596
737
  connection = con;
597
738
  }
598
739
  const requestId = message.requestId || (await this.genRequestId());
599
- const sent = await connection.send(
740
+ return connection.send(
600
741
  {
601
742
  ...message,
602
743
  requestId,
@@ -612,7 +753,6 @@ export abstract class PeerTransfer {
612
753
  },
613
754
  false,
614
755
  );
615
- return sent;
616
756
  }
617
757
 
618
758
  inheritMessage(receiveMsg: PeerMessage) {
@@ -627,6 +767,7 @@ export abstract class PeerTransfer {
627
767
  }
628
768
 
629
769
  async reconnect(): Promise<Peer | undefined> {
770
+ console.log('reconnect', this.isPaused);
630
771
  if (this.isPaused) {
631
772
  return;
632
773
  }
@@ -655,31 +796,55 @@ export abstract class PeerTransfer {
655
796
  if (this.isPaused) {
656
797
  return;
657
798
  }
799
+ // 已连接且 token 未变,无需重连
658
800
  if (this.peer?.open && this.peerConfig.token === this.peer?.options.token) {
659
801
  return this.peer;
660
802
  }
661
803
 
804
+ // 指数退避
805
+ this.reconnectAttempt++;
806
+ const backoff = Math.min(
807
+ BASE_RECONNECT_BACKOFF * Math.pow(2, this.reconnectAttempt - 1),
808
+ MAX_RECONNECT_BACKOFF,
809
+ );
810
+ await delay(backoff);
811
+
662
812
  await this.reloadConfig();
663
- return this.createPeer();
664
- // return this.createPeer();
813
+
814
+ // 优先尝试 peer.reconnect() 轻量重连
665
815
  if (this.peer?.disconnected && this.peer?.options.token === this.peerConfig.token) {
666
816
  this.peer.reconnect();
667
817
  return this.peer;
668
- } else {
669
- await this.reloadConfig();
670
- return this.createPeer();
671
818
  }
819
+
820
+ return this.createPeer();
672
821
  } catch (err) {
673
822
  return this.createPeer();
674
823
  }
675
824
  }
676
825
 
677
826
  clearInstance() {
827
+ console.info('clearInstance');
828
+ this.destroyed = true;
678
829
  PeerTransfer.connecting.clear();
679
- const peerIds = this.connectPool.keys();
830
+
831
+ // 先触发所有 inRequest 的 feedback,让它们 resolve
832
+ for (const [reqId, entry] of this.inRequest) {
833
+ entry.feedback();
834
+ }
835
+ this.inRequest.clear();
836
+
837
+ const peerIds = Array.from(this.connectPool.keys());
680
838
  for (const pId of peerIds) {
681
839
  this.removeConnection(pId);
682
840
  }
841
+
842
+ this.requests.clear();
843
+ for (const timer of this.requestTimers.values()) {
844
+ clearTimeout(timer);
845
+ }
846
+ this.requestTimers.clear();
847
+
683
848
  this.peer?.destroy();
684
849
  this.peer?.removeAllListeners();
685
850
  this.booted = undefined;
@@ -697,9 +862,7 @@ export abstract class PeerTransfer {
697
862
 
698
863
  async onRequest(conn: DataConnection, message: PeerMessage) {
699
864
  try {
700
- // @todo default folder
701
- // console.log('onRequest-->', message.route);
702
- // @todo default folder
865
+ console.log('onRequest@@', conn.peer, message.route);
703
866
  await this.router.run(conn, message);
704
867
  } catch (err: any) {
705
868
  this.responseFail(conn, message, { message: err.message, code: err.code });
@@ -707,20 +870,15 @@ export abstract class PeerTransfer {
707
870
  }
708
871
 
709
872
  async onResponse(conn: DataConnection, message: PeerMessage) {
710
- // @todo default folder
711
873
  const cb = this.requests.get(message.requestId);
712
874
  if (cb) {
713
875
  cb.apply(this, [message]);
714
876
  this.requests.delete(message.requestId);
715
877
  }
716
- // @todo throw err;
717
878
  }
718
879
 
719
880
  async responseOk(conn: DataConnection, message: PeerMessage) {
720
- const msg = {
721
- body: { ok: true },
722
- };
723
- return this.reply(conn, message, msg);
881
+ return this.reply(conn, message, { body: { ok: true } });
724
882
  }
725
883
 
726
884
  async responseFail(
@@ -728,18 +886,21 @@ export abstract class PeerTransfer {
728
886
  message: PeerMessage,
729
887
  err: string | Record<string, any>,
730
888
  ) {
731
- const msg = {
732
- body: {},
733
- error: err || 'service error',
734
- };
735
- return this.reply(conn, message, msg);
889
+ return this.reply(conn, message, { body: {}, error: err || 'service error' });
736
890
  }
737
891
 
738
892
  addRequest(id: string, cb: ResponseCb) {
893
+ // 清除已有定时器,避免重复注册时旧定时器误删新回调
894
+ const oldTimer = this.requestTimers.get(id);
895
+ if (oldTimer) {
896
+ clearTimeout(oldTimer);
897
+ }
739
898
  this.requests.set(id, cb);
740
- setTimeout(() => {
899
+ const timer = setTimeout(() => {
741
900
  this.requests.delete(id);
901
+ this.requestTimers.delete(id);
742
902
  }, this.timeout);
903
+ this.requestTimers.set(id, timer);
743
904
  }
744
905
 
745
906
  use(handler: RequestCallbackFn, prefix = '') {
@@ -757,6 +918,7 @@ export abstract class PeerTransfer {
757
918
 
758
919
  resume() {
759
920
  this.isPaused = false;
921
+ this.destroyed = false;
760
922
  return this.createPeer(true);
761
923
  }
762
924
  }