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/CLAUDE.md +79 -0
- package/dist/handler.js +2 -1
- package/dist/index.d.ts +34 -8
- package/dist/index.js +314 -198
- package/dist/router.js +9 -9
- package/package.json +1 -1
- package/src/handler.ts +1 -1
- package/src/index.ts +369 -206
- 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.on('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++) {
|
|
@@ -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(
|
|
492
|
+
this.removeConnection(peerId, conn?.connectionId);
|
|
422
493
|
return;
|
|
423
494
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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',
|
|
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',
|
|
502
|
-
clearTimeout(timer);
|
|
584
|
+
conn.on('open', () => {
|
|
503
585
|
done(conn);
|
|
504
586
|
});
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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:
|
|
640
|
+
error: { message: 'build connection failed', code: 'CONN_FAILED' },
|
|
522
641
|
} as any;
|
|
523
642
|
}
|
|
524
643
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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(
|
|
665
|
+
resolve({
|
|
666
|
+
...payload,
|
|
667
|
+
body: {},
|
|
668
|
+
error: { message: 'request timeout', code: 'TIMEOUT' },
|
|
669
|
+
} as any);
|
|
534
670
|
}, payload.timeout || REQUEST_TIMEOUT);
|
|
535
|
-
|
|
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
|
-
|
|
545
|
-
|
|
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
|
-
|
|
551
|
-
|
|
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
|
-
|
|
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
|
-
|
|
663
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|