egos-transfer 0.2.3 → 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,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.on('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++) {
@@ -384,6 +449,7 @@ export abstract class PeerTransfer {
384
449
 
385
450
  const cur = this.connectPool.get(peerId);
386
451
  if (this.isValidConnection(cur)) {
452
+ clearTimeout(guardTimer);
387
453
  return resolve(cur);
388
454
  }
389
455
 
@@ -396,9 +462,11 @@ export abstract class PeerTransfer {
396
462
  }
397
463
  const connection = await this.onConnect(conn as DataConnection).catch(() => {});
398
464
  if (connection) {
465
+ clearTimeout(guardTimer);
399
466
  return resolve(connection);
400
467
  }
401
468
  }
469
+ clearTimeout(guardTimer);
402
470
  resolve(undefined);
403
471
  return undefined;
404
472
  } catch (err) {
@@ -412,26 +480,47 @@ export abstract class PeerTransfer {
412
480
  return conn;
413
481
  }
414
482
 
483
+ /**
484
+ * 心跳检测 - 直接 send,不走 request 管线,避免重试放大
485
+ */
415
486
  async ping(peerId: string) {
416
487
  if (this.deviceId === peerId) {
417
488
  return;
418
489
  }
419
490
  const conn = this.connectPool.get(peerId);
420
491
  if (!this.isValidConnection(conn)) {
421
- this.removeConnection(conn?.peer, conn?.connectionId);
492
+ this.removeConnection(peerId, conn?.connectionId);
422
493
  return;
423
494
  }
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);
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 {
432
522
  return false;
433
523
  }
434
- return true;
435
524
  }
436
525
 
437
526
  async onConnect(conn: DataConnection): Promise<DataConnection | undefined> {
@@ -439,9 +528,9 @@ export abstract class PeerTransfer {
439
528
  return;
440
529
  }
441
530
  const exists = this.connectPool.get(conn.peer);
442
- // console.log('onConnect', conn.label, exists);
443
531
  if (exists) {
444
- if (exists.metadata.time > conn.metadata.time) {
532
+ // 双方同时建连时,保留 peerId 字典序大的那条(确定性,不依赖时间戳)
533
+ if (exists.connectionId > conn.connectionId) {
445
534
  this.removeConnection(conn.peer, conn.connectionId);
446
535
  if (this.isValidConnection(exists)) {
447
536
  return exists;
@@ -450,19 +539,28 @@ export abstract class PeerTransfer {
450
539
  this.removeConnection(exists.peer, exists.connectionId);
451
540
  }
452
541
  }
453
- let timer = null;
542
+
543
+ if (conn.open) {
544
+ this.addConnection(conn);
545
+ this.bindConnectionData(conn);
546
+ return conn;
547
+ }
548
+
454
549
  return new Promise((resolve) => {
550
+ let timer: ReturnType<typeof setTimeout> | null = null;
551
+
455
552
  const done = (connection?: DataConnection) => {
456
- if (connection) {
457
- this.addConnection(connection);
458
- }
459
553
  if (timer) {
460
554
  clearTimeout(timer);
461
555
  timer = null;
462
556
  }
557
+ if (connection) {
558
+ this.addConnection(connection);
559
+ }
463
560
  resolve(connection);
464
561
  };
465
- timer = setTimeout(async () => {
562
+
563
+ timer = setTimeout(() => {
466
564
  if (this.isValidConnection(conn)) {
467
565
  this.addConnection(conn);
468
566
  done(conn);
@@ -470,94 +568,139 @@ export abstract class PeerTransfer {
470
568
  this.removeConnection(conn.peer, conn.connectionId);
471
569
  done(undefined);
472
570
  }
473
- timer = null;
474
571
  }, 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');
483
- }
484
- });
485
- conn.on('error', async (err) => {
572
+
573
+ this.bindConnectionData(conn);
574
+
575
+ conn.on('error', () => {
486
576
  this.removeConnection(conn.peer, conn.connectionId);
487
- // if (!this.peer?.open) {
488
- // await this.reconnect();
489
- // }
490
577
  done(null);
491
578
  });
492
- conn.on('close', async () => {
579
+ conn.on('close', () => {
493
580
  console.log('close------------>', conn.label);
494
- clearTimeout(timer);
495
581
  this.removeConnection(conn.peer, conn.connectionId);
496
- // if (!this.peer?.open) {
497
- // await this.reconnect();
498
- // }
499
582
  done(null);
500
583
  });
501
- conn.on('open', async () => {
502
- clearTimeout(timer);
584
+ conn.on('open', () => {
503
585
  done(conn);
504
586
  });
505
- if (this.isValidConnection(conn)) {
506
- if (timer) {
507
- clearTimeout(timer);
508
- }
509
- timer = null;
510
- 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');
511
600
  }
512
601
  });
513
602
  }
514
603
 
515
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
+ }
516
635
  const conn = await this.connectToPeer(peerId);
517
636
  if (!this.isValidConnection(conn)) {
518
637
  return {
519
638
  ...payload,
520
639
  body: {},
521
- error: { message: 'build connection failed', code: -1 },
640
+ error: { message: 'build connection failed', code: 'CONN_FAILED' },
522
641
  } as any;
523
642
  }
524
643
 
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
- };
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();
532
664
  this.removeConnection(conn.peer, conn.connectionId);
533
- resolve(msg);
665
+ resolve({
666
+ ...payload,
667
+ body: {},
668
+ error: { message: 'request timeout', code: 'TIMEOUT' },
669
+ } as any);
534
670
  }, payload.timeout || REQUEST_TIMEOUT);
535
- this.inRequest.set(conn.connectionId, {
671
+
672
+ this.inRequest.set(requestId, {
536
673
  conn,
537
674
  message: payload,
538
- feedback: () =>
675
+ feedback: () => {
676
+ cleanup();
539
677
  resolve({
678
+ ...payload,
540
679
  body: {},
541
- error: { message: 'send failed' },
542
- }),
680
+ error: { message: 'send failed', code: 'CONN_CLOSED' },
681
+ } as any);
682
+ },
543
683
  });
544
- this.addRequest(requestId, (message: Record<string, any>) => {
545
- clearTimeout(timeout);
684
+
685
+ // 直接 set,避免 addRequest 内部 setTimeout 在重试时误删新回调
686
+ this.requests.set(requestId, (message: Record<string, any>) => {
687
+ cleanup();
546
688
  const now = Date.now();
547
689
  if (now - message.createdAt > 3000) {
548
690
  console.warn('[egos] slow peer request:', message.route, now - message.createdAt);
549
691
  }
550
- this.inRequest.delete(conn.connectionId);
551
- 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);
552
702
  });
553
703
  });
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
704
  }
562
705
 
563
706
  async reply(
@@ -580,14 +723,13 @@ export abstract class PeerTransfer {
580
723
  const connection = await this.connectToPeer(conn.peer);
581
724
  return connection?.send(data);
582
725
  }
583
-
584
726
  return conn.send(data);
585
727
  }
586
728
 
587
729
  async send(conn: DataConnection, message: PeerRequestPayload) {
588
730
  const internalApi = await this.getApi(conn.peer);
589
731
  let connection = conn;
590
- if (!this.isValidConnection) {
732
+ if (!this.isValidConnection(conn)) {
591
733
  const con = await this.connectToPeer(conn.peer);
592
734
  if (!con) {
593
735
  return this.responseFail(conn, message as PeerMessage, 'send with invalid connection');
@@ -595,7 +737,7 @@ export abstract class PeerTransfer {
595
737
  connection = con;
596
738
  }
597
739
  const requestId = message.requestId || (await this.genRequestId());
598
- const sent = await connection.send(
740
+ return connection.send(
599
741
  {
600
742
  ...message,
601
743
  requestId,
@@ -611,7 +753,6 @@ export abstract class PeerTransfer {
611
753
  },
612
754
  false,
613
755
  );
614
- return sent;
615
756
  }
616
757
 
617
758
  inheritMessage(receiveMsg: PeerMessage) {
@@ -626,6 +767,7 @@ export abstract class PeerTransfer {
626
767
  }
627
768
 
628
769
  async reconnect(): Promise<Peer | undefined> {
770
+ console.log('reconnect', this.isPaused);
629
771
  if (this.isPaused) {
630
772
  return;
631
773
  }
@@ -654,31 +796,55 @@ export abstract class PeerTransfer {
654
796
  if (this.isPaused) {
655
797
  return;
656
798
  }
799
+ // 已连接且 token 未变,无需重连
657
800
  if (this.peer?.open && this.peerConfig.token === this.peer?.options.token) {
658
801
  return this.peer;
659
802
  }
660
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
+
661
812
  await this.reloadConfig();
662
- return this.createPeer();
663
- // return this.createPeer();
813
+
814
+ // 优先尝试 peer.reconnect() 轻量重连
664
815
  if (this.peer?.disconnected && this.peer?.options.token === this.peerConfig.token) {
665
816
  this.peer.reconnect();
666
817
  return this.peer;
667
- } else {
668
- await this.reloadConfig();
669
- return this.createPeer();
670
818
  }
819
+
820
+ return this.createPeer();
671
821
  } catch (err) {
672
822
  return this.createPeer();
673
823
  }
674
824
  }
675
825
 
676
826
  clearInstance() {
827
+ console.info('clearInstance');
828
+ this.destroyed = true;
677
829
  PeerTransfer.connecting.clear();
678
- 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());
679
838
  for (const pId of peerIds) {
680
839
  this.removeConnection(pId);
681
840
  }
841
+
842
+ this.requests.clear();
843
+ for (const timer of this.requestTimers.values()) {
844
+ clearTimeout(timer);
845
+ }
846
+ this.requestTimers.clear();
847
+
682
848
  this.peer?.destroy();
683
849
  this.peer?.removeAllListeners();
684
850
  this.booted = undefined;
@@ -696,9 +862,7 @@ export abstract class PeerTransfer {
696
862
 
697
863
  async onRequest(conn: DataConnection, message: PeerMessage) {
698
864
  try {
699
- // @todo default folder
700
- // console.log('onRequest-->', message.route);
701
- // @todo default folder
865
+ console.log('onRequest@@', conn.peer, message.route);
702
866
  await this.router.run(conn, message);
703
867
  } catch (err: any) {
704
868
  this.responseFail(conn, message, { message: err.message, code: err.code });
@@ -706,20 +870,15 @@ export abstract class PeerTransfer {
706
870
  }
707
871
 
708
872
  async onResponse(conn: DataConnection, message: PeerMessage) {
709
- // @todo default folder
710
873
  const cb = this.requests.get(message.requestId);
711
874
  if (cb) {
712
875
  cb.apply(this, [message]);
713
876
  this.requests.delete(message.requestId);
714
877
  }
715
- // @todo throw err;
716
878
  }
717
879
 
718
880
  async responseOk(conn: DataConnection, message: PeerMessage) {
719
- const msg = {
720
- body: { ok: true },
721
- };
722
- return this.reply(conn, message, msg);
881
+ return this.reply(conn, message, { body: { ok: true } });
723
882
  }
724
883
 
725
884
  async responseFail(
@@ -727,18 +886,21 @@ export abstract class PeerTransfer {
727
886
  message: PeerMessage,
728
887
  err: string | Record<string, any>,
729
888
  ) {
730
- const msg = {
731
- body: {},
732
- error: err || 'service error',
733
- };
734
- return this.reply(conn, message, msg);
889
+ return this.reply(conn, message, { body: {}, error: err || 'service error' });
735
890
  }
736
891
 
737
892
  addRequest(id: string, cb: ResponseCb) {
893
+ // 清除已有定时器,避免重复注册时旧定时器误删新回调
894
+ const oldTimer = this.requestTimers.get(id);
895
+ if (oldTimer) {
896
+ clearTimeout(oldTimer);
897
+ }
738
898
  this.requests.set(id, cb);
739
- setTimeout(() => {
899
+ const timer = setTimeout(() => {
740
900
  this.requests.delete(id);
901
+ this.requestTimers.delete(id);
741
902
  }, this.timeout);
903
+ this.requestTimers.set(id, timer);
742
904
  }
743
905
 
744
906
  use(handler: RequestCallbackFn, prefix = '') {
@@ -756,6 +918,7 @@ export abstract class PeerTransfer {
756
918
 
757
919
  resume() {
758
920
  this.isPaused = false;
921
+ this.destroyed = false;
759
922
  return this.createPeer(true);
760
923
  }
761
924
  }