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/CLAUDE.md +79 -0
- package/dist/handler.js +2 -1
- package/dist/index.d.ts +34 -8
- package/dist/index.js +327 -197
- package/dist/router.js +9 -9
- package/package.json +1 -1
- package/src/handler.ts +1 -1
- package/src/index.ts +382 -205
- package/src/router.ts +9 -9
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,
|
|
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
|
|
63
|
+
protected checkerId: ReturnType<typeof setInterval> | null = null;
|
|
59
64
|
public isPaused: boolean;
|
|
60
|
-
|
|
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,
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
127
|
+
if (this.checkerId) {
|
|
128
|
+
clearInterval(this.checkerId);
|
|
129
|
+
this.checkerId = null;
|
|
130
|
+
}
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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('
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
this.peer?._removeConnection(
|
|
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
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
336
|
-
if (
|
|
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(
|
|
500
|
+
this.removeConnection(peerId, conn?.connectionId);
|
|
422
501
|
return;
|
|
423
502
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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',
|
|
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',
|
|
502
|
-
clearTimeout(timer);
|
|
598
|
+
conn.on('open', () => {
|
|
503
599
|
done(conn);
|
|
504
600
|
});
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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:
|
|
654
|
+
error: { message: 'build connection failed', code: 'CONN_FAILED' },
|
|
522
655
|
} as any;
|
|
523
656
|
}
|
|
524
657
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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(
|
|
679
|
+
resolve({
|
|
680
|
+
...payload,
|
|
681
|
+
body: {},
|
|
682
|
+
error: { message: 'request timeout', code: 'TIMEOUT' },
|
|
683
|
+
} as any);
|
|
534
684
|
}, payload.timeout || REQUEST_TIMEOUT);
|
|
535
|
-
|
|
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
|
-
|
|
545
|
-
|
|
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
|
-
|
|
551
|
-
|
|
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
|
-
|
|
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
|
-
|
|
663
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|