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