sx-peerjs-http-util 1.0.6 → 1.2.0
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/README.md +27 -282
- package/dist/CallSession.d.ts +99 -0
- package/dist/CallSession.d.ts.map +1 -0
- package/dist/MessageHandler.d.ts +110 -0
- package/dist/MessageHandler.d.ts.map +1 -0
- package/dist/PeerJsWrapper.d.ts +172 -0
- package/dist/PeerJsWrapper.d.ts.map +1 -0
- package/dist/Router.d.ts +186 -0
- package/dist/Router.d.ts.map +1 -0
- package/dist/RoutingDB.d.ts +75 -0
- package/dist/RoutingDB.d.ts.map +1 -0
- package/dist/constants.d.ts +23 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/index.d.ts +3 -184
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +1308 -181
- package/dist/index.esm.js.map +3 -3
- package/dist/index.umd.js +1306 -181
- package/dist/index.umd.js.map +3 -3
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1,22 +1,38 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/PeerJsWrapper.ts
|
|
2
2
|
import { Peer } from "peerjs";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function generateUUID() {
|
|
6
|
-
return crypto.randomUUID();
|
|
7
|
-
}
|
|
3
|
+
|
|
4
|
+
// src/CallSession.ts
|
|
8
5
|
var CallSessionImpl = class {
|
|
6
|
+
/** 对端的 Peer ID */
|
|
9
7
|
peerId;
|
|
8
|
+
/** 是否包含视频 */
|
|
10
9
|
hasVideo;
|
|
10
|
+
/** PeerJS MediaConnection 实例 */
|
|
11
11
|
mediaConnection;
|
|
12
|
+
/** 本地媒体流(麦克风/摄像头) */
|
|
12
13
|
localStream = null;
|
|
14
|
+
/** 远程媒体流(对方的音频/视频) */
|
|
13
15
|
remoteStream = null;
|
|
16
|
+
/** 通话状态监听器集合 */
|
|
14
17
|
stateListeners = /* @__PURE__ */ new Set();
|
|
18
|
+
/** 调试日志函数 */
|
|
15
19
|
debugLogFn;
|
|
20
|
+
/** 清理回调(通话结束时调用) */
|
|
16
21
|
onCleanup;
|
|
22
|
+
/** 当前通话状态 */
|
|
17
23
|
_state = "connecting";
|
|
24
|
+
/** 是否已静音 */
|
|
18
25
|
isMuted = false;
|
|
26
|
+
/** 视频是否开启 */
|
|
19
27
|
isVideoEnabled = true;
|
|
28
|
+
/**
|
|
29
|
+
* 创建通话会话实例
|
|
30
|
+
* @param peerId 对端 Peer ID
|
|
31
|
+
* @param mediaConnection PeerJS MediaConnection 实例
|
|
32
|
+
* @param hasVideo 是否包含视频
|
|
33
|
+
* @param debugLog 调试日志函数
|
|
34
|
+
* @param onCleanup 通话结束时的清理回调
|
|
35
|
+
*/
|
|
20
36
|
constructor(peerId, mediaConnection, hasVideo, debugLog, onCleanup) {
|
|
21
37
|
this.peerId = peerId;
|
|
22
38
|
this.mediaConnection = mediaConnection;
|
|
@@ -24,28 +40,43 @@ var CallSessionImpl = class {
|
|
|
24
40
|
this.debugLogFn = debugLog;
|
|
25
41
|
this.onCleanup = onCleanup;
|
|
26
42
|
}
|
|
43
|
+
/** 是否已连接 */
|
|
27
44
|
get isConnected() {
|
|
28
45
|
return this._state === "connected";
|
|
29
46
|
}
|
|
47
|
+
/** 当前通话状态 */
|
|
30
48
|
get state() {
|
|
31
49
|
return this._state;
|
|
32
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* 设置通话状态
|
|
53
|
+
* @param state 新的通话状态
|
|
54
|
+
* @param reason 状态变化原因(可选)
|
|
55
|
+
*/
|
|
33
56
|
setState(state, reason) {
|
|
34
57
|
this._state = state;
|
|
35
58
|
this.notifyStateChange(state, reason);
|
|
36
59
|
}
|
|
60
|
+
/** 设置本地媒体流 */
|
|
37
61
|
setLocalStream(stream) {
|
|
38
62
|
this.localStream = stream;
|
|
39
63
|
}
|
|
64
|
+
/** 设置远程媒体流 */
|
|
40
65
|
setRemoteStream(stream) {
|
|
41
66
|
this.remoteStream = stream;
|
|
42
67
|
}
|
|
68
|
+
/** 获取本地媒体流 */
|
|
43
69
|
getLocalStream() {
|
|
44
70
|
return this.localStream;
|
|
45
71
|
}
|
|
72
|
+
/** 获取远程媒体流 */
|
|
46
73
|
getRemoteStream() {
|
|
47
74
|
return this.remoteStream;
|
|
48
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* 切换静音状态
|
|
78
|
+
* @returns 切换后的静音状态(true = 已静音)
|
|
79
|
+
*/
|
|
49
80
|
toggleMute() {
|
|
50
81
|
if (!this.localStream) return this.isMuted;
|
|
51
82
|
const audioTracks = this.localStream.getAudioTracks();
|
|
@@ -56,6 +87,10 @@ var CallSessionImpl = class {
|
|
|
56
87
|
this.debugLogFn("CallSession", "toggleMute", this.isMuted);
|
|
57
88
|
return this.isMuted;
|
|
58
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* 切换视频开关(仅视频通话有效)
|
|
92
|
+
* @returns 切换后的视频状态(true = 视频开启)
|
|
93
|
+
*/
|
|
59
94
|
toggleVideo() {
|
|
60
95
|
if (!this.hasVideo || !this.localStream) return this.isVideoEnabled;
|
|
61
96
|
const videoTracks = this.localStream.getVideoTracks();
|
|
@@ -66,10 +101,15 @@ var CallSessionImpl = class {
|
|
|
66
101
|
this.debugLogFn("CallSession", "toggleVideo", this.isVideoEnabled);
|
|
67
102
|
return this.isVideoEnabled;
|
|
68
103
|
}
|
|
104
|
+
/** 挂断通话 */
|
|
69
105
|
hangUp() {
|
|
70
106
|
this.debugLogFn("CallSession", "hangUp", this.peerId);
|
|
71
107
|
this.mediaConnection.close();
|
|
72
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* 关闭通话会话
|
|
111
|
+
* 停止本地流,清理资源
|
|
112
|
+
*/
|
|
73
113
|
close() {
|
|
74
114
|
if (this.localStream) {
|
|
75
115
|
this.localStream.getTracks().forEach((track) => track.stop());
|
|
@@ -78,12 +118,19 @@ var CallSessionImpl = class {
|
|
|
78
118
|
this.remoteStream = null;
|
|
79
119
|
this._state = "ended";
|
|
80
120
|
}
|
|
121
|
+
/** 注册通话状态变化监听器 */
|
|
81
122
|
onStateChange(listener) {
|
|
82
123
|
this.stateListeners.add(listener);
|
|
83
124
|
}
|
|
125
|
+
/** 移除通话状态变化监听器 */
|
|
84
126
|
offStateChange(listener) {
|
|
85
127
|
this.stateListeners.delete(listener);
|
|
86
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* 通知状态变化
|
|
131
|
+
* @param state 新状态
|
|
132
|
+
* @param reason 变化原因(可选)
|
|
133
|
+
*/
|
|
87
134
|
notifyStateChange(state, reason) {
|
|
88
135
|
this.debugLogFn("CallSession", "stateChange", { peer: this.peerId, state, reason });
|
|
89
136
|
this.stateListeners.forEach((listener) => {
|
|
@@ -98,86 +145,916 @@ var CallSessionImpl = class {
|
|
|
98
145
|
}
|
|
99
146
|
}
|
|
100
147
|
};
|
|
101
|
-
|
|
148
|
+
|
|
149
|
+
// src/constants.ts
|
|
150
|
+
var CONNECTION_TIMEOUT_MS = 3e4;
|
|
151
|
+
var SEND_TIMEOUT_MS = 1e4;
|
|
152
|
+
var RECONNECT_DELAY_MS = 1e3;
|
|
153
|
+
var ROUTE_EXPIRE_AGE_MS = 5 * 60 * 1e3;
|
|
154
|
+
var MAX_DIRECT_NODES = 5;
|
|
155
|
+
var ROUTE_CLEANUP_INTERVAL_MS = 60 * 1e3;
|
|
156
|
+
var ROUTE_BROADCAST_INTERVAL_MS = 30 * 1e3;
|
|
157
|
+
var DEFAULT_TTL = 128;
|
|
158
|
+
|
|
159
|
+
// src/RoutingDB.ts
|
|
160
|
+
var DB_NAME = "peerjs-routing-db";
|
|
161
|
+
var DB_VERSION = 1;
|
|
162
|
+
var ROUTING_TABLE_STORE = "routing-table";
|
|
163
|
+
var DIRECT_NODES_STORE = "direct-nodes";
|
|
164
|
+
var db = null;
|
|
165
|
+
function withStore(storeName, mode, operation) {
|
|
166
|
+
return openDB().then((database) => {
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const tx = database.transaction(storeName, mode);
|
|
169
|
+
const store = tx.objectStore(storeName);
|
|
170
|
+
const request = operation(store);
|
|
171
|
+
if (request) {
|
|
172
|
+
request.onerror = () => reject(request.error);
|
|
173
|
+
request.onsuccess = () => resolve(request.result);
|
|
174
|
+
} else {
|
|
175
|
+
tx.oncomplete = () => resolve(void 0);
|
|
176
|
+
tx.onerror = () => reject(tx.error);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function openDB() {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
if (db) {
|
|
184
|
+
resolve(db);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
188
|
+
request.onerror = () => reject(request.error);
|
|
189
|
+
request.onsuccess = () => {
|
|
190
|
+
db = request.result;
|
|
191
|
+
resolve(db);
|
|
192
|
+
};
|
|
193
|
+
request.onupgradeneeded = (e) => {
|
|
194
|
+
const req = e.target;
|
|
195
|
+
const database = req.result;
|
|
196
|
+
if (!database.objectStoreNames.contains(ROUTING_TABLE_STORE)) {
|
|
197
|
+
const routingStore = database.createObjectStore(ROUTING_TABLE_STORE, {
|
|
198
|
+
keyPath: "target"
|
|
199
|
+
});
|
|
200
|
+
routingStore.createIndex("timestamp", "timestamp", { unique: false });
|
|
201
|
+
}
|
|
202
|
+
if (!database.objectStoreNames.contains(DIRECT_NODES_STORE)) {
|
|
203
|
+
const nodesStore = database.createObjectStore(DIRECT_NODES_STORE, {
|
|
204
|
+
keyPath: "nodeId"
|
|
205
|
+
});
|
|
206
|
+
nodesStore.createIndex("timestamp", "timestamp", { unique: false });
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async function initRoutingDB() {
|
|
212
|
+
await openDB();
|
|
213
|
+
}
|
|
214
|
+
async function saveRouteEntry(entry) {
|
|
215
|
+
await withStore(ROUTING_TABLE_STORE, "readwrite", (store) => store.put(entry));
|
|
216
|
+
}
|
|
217
|
+
async function saveRouteEntries(entries) {
|
|
218
|
+
await openDB().then((database) => {
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
const tx = database.transaction(ROUTING_TABLE_STORE, "readwrite");
|
|
221
|
+
const store = tx.objectStore(ROUTING_TABLE_STORE);
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
store.put(entry);
|
|
224
|
+
}
|
|
225
|
+
tx.oncomplete = () => resolve();
|
|
226
|
+
tx.onerror = () => reject(tx.error);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async function deleteRouteEntry(target) {
|
|
231
|
+
await withStore(ROUTING_TABLE_STORE, "readwrite", (store) => store.delete(target));
|
|
232
|
+
}
|
|
233
|
+
async function loadRoutingTable() {
|
|
234
|
+
return withStore(ROUTING_TABLE_STORE, "readonly", (store) => store.getAll());
|
|
235
|
+
}
|
|
236
|
+
async function saveDirectNode(node) {
|
|
237
|
+
await withStore(DIRECT_NODES_STORE, "readwrite", (store) => store.put(node));
|
|
238
|
+
}
|
|
239
|
+
async function saveDirectNodes(nodes) {
|
|
240
|
+
await openDB().then((database) => {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
const tx = database.transaction(DIRECT_NODES_STORE, "readwrite");
|
|
243
|
+
const store = tx.objectStore(DIRECT_NODES_STORE);
|
|
244
|
+
for (const node of nodes) {
|
|
245
|
+
store.put(node);
|
|
246
|
+
}
|
|
247
|
+
tx.oncomplete = () => resolve();
|
|
248
|
+
tx.onerror = () => reject(tx.error);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
async function loadDirectNodes() {
|
|
253
|
+
return withStore(DIRECT_NODES_STORE, "readonly", (store) => store.getAll());
|
|
254
|
+
}
|
|
255
|
+
async function clearAllRoutingData() {
|
|
256
|
+
const database = await openDB();
|
|
257
|
+
await new Promise((resolve, reject) => {
|
|
258
|
+
const tx = database.transaction(ROUTING_TABLE_STORE, "readwrite");
|
|
259
|
+
const store = tx.objectStore(ROUTING_TABLE_STORE);
|
|
260
|
+
store.clear();
|
|
261
|
+
tx.oncomplete = () => resolve();
|
|
262
|
+
tx.onerror = () => reject(tx.error);
|
|
263
|
+
});
|
|
264
|
+
await new Promise((resolve, reject) => {
|
|
265
|
+
const tx = database.transaction(DIRECT_NODES_STORE, "readwrite");
|
|
266
|
+
const store = tx.objectStore(DIRECT_NODES_STORE);
|
|
267
|
+
store.clear();
|
|
268
|
+
tx.oncomplete = () => resolve();
|
|
269
|
+
tx.onerror = () => reject(tx.error);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/Router.ts
|
|
274
|
+
var Router = class {
|
|
275
|
+
/** 路由表:target -> RouteEntry */
|
|
276
|
+
routingTable = /* @__PURE__ */ new Map();
|
|
277
|
+
/** 直连节点及延迟列表 */
|
|
278
|
+
directNodes = [];
|
|
279
|
+
/** 中继配置 */
|
|
280
|
+
relayConfig;
|
|
281
|
+
/** 回调函数集合 */
|
|
282
|
+
callbacks;
|
|
283
|
+
/** 等待路由发现响应的 pending 队列 */
|
|
284
|
+
pendingRouteQueries = /* @__PURE__ */ new Map();
|
|
285
|
+
/** 定时清理定时器 */
|
|
286
|
+
cleanupTimer = null;
|
|
287
|
+
/** 周期广播定时器 */
|
|
288
|
+
broadcastTimer = null;
|
|
102
289
|
/**
|
|
103
|
-
*
|
|
290
|
+
* 创建路由管理器
|
|
291
|
+
* @param callbacks 回调函数集合
|
|
292
|
+
* @param relayConfig 中继配置(可选)
|
|
104
293
|
*/
|
|
105
|
-
|
|
294
|
+
constructor(callbacks, relayConfig) {
|
|
295
|
+
this.callbacks = callbacks;
|
|
296
|
+
this.relayConfig = relayConfig ?? {};
|
|
297
|
+
}
|
|
106
298
|
/**
|
|
107
|
-
*
|
|
299
|
+
* 初始化路由管理器(从 IndexedDB 加载数据并启动定时任务)
|
|
108
300
|
*/
|
|
109
|
-
|
|
301
|
+
async init() {
|
|
302
|
+
await initRoutingDB();
|
|
303
|
+
await this.loadFromDB();
|
|
304
|
+
this.startMaintenanceTasks();
|
|
305
|
+
}
|
|
110
306
|
/**
|
|
111
|
-
*
|
|
307
|
+
* 从 IndexedDB 加载路由数据
|
|
112
308
|
*/
|
|
113
|
-
|
|
309
|
+
async loadFromDB() {
|
|
310
|
+
try {
|
|
311
|
+
const routes = await loadRoutingTable();
|
|
312
|
+
for (const entry of routes) {
|
|
313
|
+
this.routingTable.set(entry.target, entry);
|
|
314
|
+
}
|
|
315
|
+
this.callbacks.debugLog("Routing", "loadedRoutes", routes.length);
|
|
316
|
+
} catch (e) {
|
|
317
|
+
this.callbacks.debugLog("Routing", "loadError", e);
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
const nodes = await loadDirectNodes();
|
|
321
|
+
this.directNodes = nodes;
|
|
322
|
+
this.callbacks.debugLog("Routing", "loadedNodes", nodes.length);
|
|
323
|
+
} catch (e) {
|
|
324
|
+
this.callbacks.debugLog("Routing", "loadNodesError", e);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
114
327
|
/**
|
|
115
|
-
*
|
|
328
|
+
* 启动定时维护任务
|
|
116
329
|
*/
|
|
117
|
-
|
|
330
|
+
startMaintenanceTasks() {
|
|
331
|
+
this.cleanupTimer = setInterval(() => {
|
|
332
|
+
this.cleanupExpiredEntries();
|
|
333
|
+
}, ROUTE_CLEANUP_INTERVAL_MS);
|
|
334
|
+
this.broadcastTimer = setInterval(() => {
|
|
335
|
+
this.broadcastRouteUpdate();
|
|
336
|
+
}, ROUTE_BROADCAST_INTERVAL_MS);
|
|
337
|
+
}
|
|
118
338
|
/**
|
|
119
|
-
*
|
|
339
|
+
* 清理过期路由条目
|
|
120
340
|
*/
|
|
121
|
-
|
|
341
|
+
async cleanupExpiredEntries() {
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
for (const [target, entry] of this.routingTable) {
|
|
344
|
+
if (now - entry.timestamp > ROUTE_EXPIRE_AGE_MS) {
|
|
345
|
+
this.routingTable.delete(target);
|
|
346
|
+
deleteRouteEntry(target).catch(() => {
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
this.directNodes = this.directNodes.filter((node) => {
|
|
351
|
+
const isExpired = now - node.timestamp > ROUTE_EXPIRE_AGE_MS;
|
|
352
|
+
if (isExpired) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
return true;
|
|
356
|
+
});
|
|
357
|
+
this.callbacks.debugLog("Routing", "cleanup", {
|
|
358
|
+
routes: this.routingTable.size,
|
|
359
|
+
nodes: this.directNodes.length
|
|
360
|
+
});
|
|
361
|
+
}
|
|
122
362
|
/**
|
|
123
|
-
*
|
|
363
|
+
* 持久化路由表到 IndexedDB
|
|
124
364
|
*/
|
|
125
|
-
|
|
365
|
+
async persist() {
|
|
366
|
+
try {
|
|
367
|
+
const entries = Array.from(this.routingTable.values());
|
|
368
|
+
await saveRouteEntries(entries);
|
|
369
|
+
} catch (e) {
|
|
370
|
+
this.callbacks.debugLog("Routing", "persistError", e);
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
await saveDirectNodes(this.directNodes);
|
|
374
|
+
} catch (e) {
|
|
375
|
+
this.callbacks.debugLog("Routing", "persistNodesError", e);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
126
378
|
/**
|
|
127
|
-
*
|
|
379
|
+
* 销毁路由管理器(清理定时器)
|
|
128
380
|
*/
|
|
129
|
-
|
|
381
|
+
destroy() {
|
|
382
|
+
if (this.cleanupTimer) {
|
|
383
|
+
clearInterval(this.cleanupTimer);
|
|
384
|
+
this.cleanupTimer = null;
|
|
385
|
+
}
|
|
386
|
+
if (this.broadcastTimer) {
|
|
387
|
+
clearInterval(this.broadcastTimer);
|
|
388
|
+
this.broadcastTimer = null;
|
|
389
|
+
}
|
|
390
|
+
this.pendingRouteQueries.forEach((pending) => {
|
|
391
|
+
clearTimeout(pending.timer);
|
|
392
|
+
});
|
|
393
|
+
this.pendingRouteQueries.clear();
|
|
394
|
+
}
|
|
130
395
|
/**
|
|
131
|
-
*
|
|
396
|
+
* 记录成功的直连通信
|
|
397
|
+
* @param nodeId 节点 ID
|
|
398
|
+
* @param latency 延迟(毫秒)
|
|
132
399
|
*/
|
|
133
|
-
|
|
400
|
+
recordDirectNode(nodeId, latency) {
|
|
401
|
+
const myPeerId = this.callbacks.getMyPeerId();
|
|
402
|
+
if (nodeId === myPeerId) return;
|
|
403
|
+
const existing = this.directNodes.find((n) => n.nodeId === nodeId);
|
|
404
|
+
const timestamp = Date.now();
|
|
405
|
+
if (existing) {
|
|
406
|
+
existing.latency = latency;
|
|
407
|
+
existing.timestamp = timestamp;
|
|
408
|
+
} else {
|
|
409
|
+
const maxRelayNodes = this.relayConfig.maxRelayNodes ?? MAX_DIRECT_NODES;
|
|
410
|
+
this.directNodes.push({ nodeId, latency, timestamp });
|
|
411
|
+
if (this.directNodes.length > maxRelayNodes) {
|
|
412
|
+
this.directNodes.sort((a, b) => a.latency - b.latency);
|
|
413
|
+
this.directNodes.shift();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
this.callbacks.debugLog("Routing", "directNode", { nodeId, latency });
|
|
417
|
+
}
|
|
134
418
|
/**
|
|
135
|
-
*
|
|
419
|
+
* 获取直连节点列表(按延迟升序)
|
|
420
|
+
* @returns 直连节点列表
|
|
136
421
|
*/
|
|
137
|
-
|
|
422
|
+
getDirectNodes() {
|
|
423
|
+
return [...this.directNodes].sort((a, b) => a.latency - b.latency);
|
|
424
|
+
}
|
|
138
425
|
/**
|
|
139
|
-
*
|
|
426
|
+
* 检查是否可以直连目标节点
|
|
427
|
+
* @param targetId 目标节点 ID
|
|
428
|
+
* @returns 是否可以直连
|
|
140
429
|
*/
|
|
141
|
-
|
|
430
|
+
canReachDirectly(targetId) {
|
|
431
|
+
return this.directNodes.some((n) => n.nodeId === targetId);
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* 获取到直连节点的延迟
|
|
435
|
+
* @param nodeId 节点 ID
|
|
436
|
+
* @returns 延迟(毫秒),如果不存在返回 null
|
|
437
|
+
*/
|
|
438
|
+
getDirectLatency(nodeId) {
|
|
439
|
+
const node = this.directNodes.find((n) => n.nodeId === nodeId);
|
|
440
|
+
return node ? node.latency : null;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* 移除失效的路由(通信失败时调用)
|
|
444
|
+
* @param nodeId 失效的节点 ID
|
|
445
|
+
*/
|
|
446
|
+
removeRoute(nodeId) {
|
|
447
|
+
let removed = false;
|
|
448
|
+
for (const [target, entry] of this.routingTable) {
|
|
449
|
+
const originalLength = entry.nextHops.length;
|
|
450
|
+
entry.nextHops = entry.nextHops.filter((h) => h.nodeId !== nodeId);
|
|
451
|
+
if (entry.nextHops.length === 0) {
|
|
452
|
+
this.routingTable.delete(target);
|
|
453
|
+
deleteRouteEntry(target).catch(() => {
|
|
454
|
+
});
|
|
455
|
+
removed = true;
|
|
456
|
+
} else if (entry.nextHops.length < originalLength) {
|
|
457
|
+
entry.timestamp = Date.now();
|
|
458
|
+
saveRouteEntry(entry).catch(() => {
|
|
459
|
+
});
|
|
460
|
+
removed = true;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const nodeIndex = this.directNodes.findIndex((n) => n.nodeId === nodeId);
|
|
464
|
+
if (nodeIndex !== -1) {
|
|
465
|
+
this.directNodes.splice(nodeIndex, 1);
|
|
466
|
+
removed = true;
|
|
467
|
+
}
|
|
468
|
+
if (removed) {
|
|
469
|
+
this.callbacks.debugLog("Routing", "routeRemoved", nodeId);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* 检查路由表是否为空
|
|
474
|
+
* @returns 是否为空
|
|
475
|
+
*/
|
|
476
|
+
isRoutingTableEmpty() {
|
|
477
|
+
return this.routingTable.size === 0;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* 记录成功通信的节点(兼容旧接口)
|
|
481
|
+
* @param nodeId 节点 ID
|
|
482
|
+
*/
|
|
483
|
+
recordSuccessfulNode(nodeId) {
|
|
484
|
+
const myPeerId = this.callbacks.getMyPeerId();
|
|
485
|
+
if (nodeId === myPeerId) return;
|
|
486
|
+
const existing = this.directNodes.find((n) => n.nodeId === nodeId);
|
|
487
|
+
if (!existing) {
|
|
488
|
+
const maxRelayNodes = this.relayConfig.maxRelayNodes ?? 5;
|
|
489
|
+
this.directNodes.push({ nodeId, latency: 100, timestamp: Date.now() });
|
|
490
|
+
if (this.directNodes.length > maxRelayNodes) {
|
|
491
|
+
this.directNodes.sort((a, b) => a.latency - b.latency);
|
|
492
|
+
this.directNodes.shift();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* 广播路由更新
|
|
498
|
+
* 向所有直连节点发送路由更新消息,告知它们本节点可达的节点列表
|
|
499
|
+
*/
|
|
500
|
+
async broadcastRouteUpdate() {
|
|
501
|
+
const myPeerId = this.callbacks.getMyPeerId();
|
|
502
|
+
const reachableNodes = this.getReachableNodes();
|
|
503
|
+
for (const node of this.directNodes) {
|
|
504
|
+
try {
|
|
505
|
+
await this.sendRouteUpdate(node.nodeId, reachableNodes);
|
|
506
|
+
} catch {
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* 获取本节点可达的节点列表
|
|
512
|
+
* @returns 可达节点数组(直连节点 + 自己)
|
|
513
|
+
*/
|
|
514
|
+
getReachableNodes() {
|
|
515
|
+
const myPeerId = this.callbacks.getMyPeerId();
|
|
516
|
+
const directNodeIds = this.directNodes.map((n) => n.nodeId);
|
|
517
|
+
return [.../* @__PURE__ */ new Set([...directNodeIds, myPeerId])];
|
|
518
|
+
}
|
|
142
519
|
/**
|
|
143
|
-
*
|
|
520
|
+
* 发送路由更新到指定节点
|
|
521
|
+
* @param targetId 目标节点 ID
|
|
522
|
+
* @param reachableNodes 可达的节点列表
|
|
144
523
|
*/
|
|
524
|
+
async sendRouteUpdate(targetId, reachableNodes) {
|
|
525
|
+
const myPeerId = this.callbacks.getMyPeerId();
|
|
526
|
+
const message = {
|
|
527
|
+
type: "route-update",
|
|
528
|
+
id: `${myPeerId}-route-${Date.now()}`,
|
|
529
|
+
originalTarget: targetId,
|
|
530
|
+
relayPath: [],
|
|
531
|
+
forwardPath: [],
|
|
532
|
+
ttl: DEFAULT_TTL,
|
|
533
|
+
routeUpdate: { reachableNodes }
|
|
534
|
+
};
|
|
535
|
+
await this.callbacks.sendRelayMessage(targetId, message);
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* 处理收到的路由更新
|
|
539
|
+
* 合并对端发来的可达节点信息到本地路由表
|
|
540
|
+
* @param fromPeerId 发送路由更新的节点 ID
|
|
541
|
+
* @param message 路由更新消息
|
|
542
|
+
*/
|
|
543
|
+
handleRouteUpdate(fromPeerId, message) {
|
|
544
|
+
if (!message.routeUpdate) return;
|
|
545
|
+
const myPeerId = this.callbacks.getMyPeerId();
|
|
546
|
+
const { reachableNodes } = message.routeUpdate;
|
|
547
|
+
const timestamp = Date.now();
|
|
548
|
+
const viaLatency = this.getDirectLatency(fromPeerId) ?? 100;
|
|
549
|
+
for (const target of reachableNodes) {
|
|
550
|
+
if (target === myPeerId) continue;
|
|
551
|
+
let entry = this.routingTable.get(target);
|
|
552
|
+
const totalLatency = viaLatency + 100;
|
|
553
|
+
if (!entry) {
|
|
554
|
+
entry = {
|
|
555
|
+
target,
|
|
556
|
+
nextHops: [],
|
|
557
|
+
hops: 1,
|
|
558
|
+
timestamp
|
|
559
|
+
};
|
|
560
|
+
this.routingTable.set(target, entry);
|
|
561
|
+
}
|
|
562
|
+
const existingHop = entry.nextHops.find((h) => h.nodeId === fromPeerId);
|
|
563
|
+
if (existingHop) {
|
|
564
|
+
existingHop.latency = totalLatency;
|
|
565
|
+
} else {
|
|
566
|
+
entry.nextHops.push({ nodeId: fromPeerId, latency: totalLatency });
|
|
567
|
+
}
|
|
568
|
+
entry.nextHops.sort((a, b) => a.latency - b.latency);
|
|
569
|
+
entry.hops = Math.min(entry.hops, 1);
|
|
570
|
+
entry.timestamp = timestamp;
|
|
571
|
+
this.callbacks.debugLog("Routing", "update", { target, nextHop: fromPeerId, latency: totalLatency });
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* 执行路由发现广播
|
|
576
|
+
* 当直连和路由表都失败时,向所有直连节点广播询问谁能连通目标
|
|
577
|
+
* @param targetId 目标节点 ID
|
|
578
|
+
* @returns 路由条目(如果发现)
|
|
579
|
+
*/
|
|
580
|
+
async discoverRoute(targetId) {
|
|
581
|
+
const myPeerId = this.callbacks.getMyPeerId();
|
|
582
|
+
const directNodes = this.getDirectNodes();
|
|
583
|
+
if (directNodes.length === 0) {
|
|
584
|
+
this.callbacks.debugLog("Routing", "discoverRoute", "no direct nodes");
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
this.callbacks.debugLog("Routing", "discoverRoute", { targetId, directNodes: directNodes.length });
|
|
588
|
+
const queryId = `${myPeerId}-query-${Date.now()}`;
|
|
589
|
+
const routeEntry = await new Promise((resolve, reject) => {
|
|
590
|
+
const timer = setTimeout(() => {
|
|
591
|
+
this.pendingRouteQueries.delete(queryId);
|
|
592
|
+
resolve(null);
|
|
593
|
+
}, CONNECTION_TIMEOUT_MS);
|
|
594
|
+
this.pendingRouteQueries.set(queryId, { resolve, reject, timer });
|
|
595
|
+
const message = {
|
|
596
|
+
type: "route-query",
|
|
597
|
+
id: queryId,
|
|
598
|
+
originalTarget: targetId,
|
|
599
|
+
relayPath: [myPeerId],
|
|
600
|
+
forwardPath: [],
|
|
601
|
+
ttl: DEFAULT_TTL,
|
|
602
|
+
routeQuery: {
|
|
603
|
+
queryOrigin: myPeerId,
|
|
604
|
+
targetNode: targetId,
|
|
605
|
+
queryPath: [myPeerId]
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
for (const node of directNodes) {
|
|
609
|
+
this.callbacks.sendRelayMessage(node.nodeId, message).catch(() => {
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
return routeEntry;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* 处理路由查询消息
|
|
617
|
+
* @param fromPeerId 发送查询的节点
|
|
618
|
+
* @param message 路由查询消息
|
|
619
|
+
*/
|
|
620
|
+
handleRouteQuery(fromPeerId, message) {
|
|
621
|
+
if (!message.routeQuery) return;
|
|
622
|
+
const myPeerId = this.callbacks.getMyPeerId();
|
|
623
|
+
const { queryOrigin, targetNode, queryPath } = message.routeQuery;
|
|
624
|
+
if (targetNode === myPeerId) {
|
|
625
|
+
const latency = this.getDirectLatency(fromPeerId) ?? 100;
|
|
626
|
+
const response = {
|
|
627
|
+
type: "route-response",
|
|
628
|
+
id: `${myPeerId}-resp-${Date.now()}`,
|
|
629
|
+
originalTarget: queryOrigin,
|
|
630
|
+
relayPath: [],
|
|
631
|
+
forwardPath: [],
|
|
632
|
+
ttl: DEFAULT_TTL,
|
|
633
|
+
routeResponse: {
|
|
634
|
+
queryOrigin,
|
|
635
|
+
responder: myPeerId,
|
|
636
|
+
targetNode,
|
|
637
|
+
latency
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
this.callbacks.sendRelayMessage(fromPeerId, response);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (queryPath.includes(myPeerId)) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const currentTTL = message.ttl ?? DEFAULT_TTL;
|
|
647
|
+
if (currentTTL <= 0) {
|
|
648
|
+
this.callbacks.debugLog("Routing", "ttlExpired", { type: "route-query", id: message.id });
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const nextHop = this.findNextHopToTarget(targetNode);
|
|
652
|
+
if (nextHop) {
|
|
653
|
+
const latency = (this.getDirectLatency(fromPeerId) ?? 100) + nextHop.latency;
|
|
654
|
+
const response = {
|
|
655
|
+
type: "route-response",
|
|
656
|
+
id: `${myPeerId}-resp-${Date.now()}`,
|
|
657
|
+
originalTarget: queryOrigin,
|
|
658
|
+
relayPath: [],
|
|
659
|
+
forwardPath: [],
|
|
660
|
+
ttl: DEFAULT_TTL,
|
|
661
|
+
routeResponse: {
|
|
662
|
+
queryOrigin,
|
|
663
|
+
responder: myPeerId,
|
|
664
|
+
targetNode,
|
|
665
|
+
latency
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
this.callbacks.sendRelayMessage(fromPeerId, response);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const newPath = [...queryPath, myPeerId];
|
|
672
|
+
const forwardMessage = {
|
|
673
|
+
...message,
|
|
674
|
+
relayPath: newPath,
|
|
675
|
+
ttl: currentTTL - 1,
|
|
676
|
+
routeQuery: {
|
|
677
|
+
...message.routeQuery,
|
|
678
|
+
queryPath: newPath
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
for (const node of this.directNodes) {
|
|
682
|
+
if (node.nodeId !== fromPeerId) {
|
|
683
|
+
this.callbacks.sendRelayMessage(node.nodeId, forwardMessage).catch(() => {
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* 处理路由查询响应
|
|
690
|
+
* @param fromPeerId 响应者节点
|
|
691
|
+
* @param message 路由响应消息
|
|
692
|
+
*/
|
|
693
|
+
handleRouteResponse(fromPeerId, message) {
|
|
694
|
+
if (!message.routeResponse) return;
|
|
695
|
+
const { queryOrigin, targetNode, latency } = message.routeResponse;
|
|
696
|
+
const myPeerId = this.callbacks.getMyPeerId();
|
|
697
|
+
if (queryOrigin !== myPeerId) return;
|
|
698
|
+
const pending = Array.from(this.pendingRouteQueries.values())[0];
|
|
699
|
+
if (!pending) return;
|
|
700
|
+
let entry = this.routingTable.get(targetNode);
|
|
701
|
+
const timestamp = Date.now();
|
|
702
|
+
if (!entry) {
|
|
703
|
+
entry = {
|
|
704
|
+
target: targetNode,
|
|
705
|
+
nextHops: [],
|
|
706
|
+
hops: 1,
|
|
707
|
+
timestamp
|
|
708
|
+
};
|
|
709
|
+
this.routingTable.set(targetNode, entry);
|
|
710
|
+
}
|
|
711
|
+
const existingHop = entry.nextHops.find((h) => h.nodeId === fromPeerId);
|
|
712
|
+
if (existingHop) {
|
|
713
|
+
existingHop.latency = latency;
|
|
714
|
+
} else {
|
|
715
|
+
entry.nextHops.push({ nodeId: fromPeerId, latency });
|
|
716
|
+
}
|
|
717
|
+
entry.nextHops.sort((a, b) => a.latency - b.latency);
|
|
718
|
+
entry.timestamp = timestamp;
|
|
719
|
+
this.callbacks.debugLog("Routing", "discovered", { targetNode, nextHop: fromPeerId, latency });
|
|
720
|
+
clearTimeout(pending.timer);
|
|
721
|
+
pending.resolve(entry);
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* 查找到目标节点的下一跳
|
|
725
|
+
* @param targetId 目标节点 ID
|
|
726
|
+
* @returns 下一跳信息,如果没有则返回 null
|
|
727
|
+
*/
|
|
728
|
+
findNextHopToTarget(targetId) {
|
|
729
|
+
const entry = this.routingTable.get(targetId);
|
|
730
|
+
if (!entry || entry.nextHops.length === 0) return null;
|
|
731
|
+
return entry.nextHops[0];
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* 获取到目标节点的所有下一跳(按延迟升序)
|
|
735
|
+
* @param targetId 目标节点 ID
|
|
736
|
+
* @returns 下一跳列表
|
|
737
|
+
*/
|
|
738
|
+
getNextHopsToTarget(targetId) {
|
|
739
|
+
const entry = this.routingTable.get(targetId);
|
|
740
|
+
return entry ? [...entry.nextHops] : [];
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* 获取路由表
|
|
744
|
+
* @returns 路由表对象
|
|
745
|
+
*/
|
|
746
|
+
getRoutingTable() {
|
|
747
|
+
const result = {};
|
|
748
|
+
this.routingTable.forEach((entry, target) => {
|
|
749
|
+
result[target] = { ...entry, nextHops: [...entry.nextHops] };
|
|
750
|
+
});
|
|
751
|
+
return result;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* 获取已知节点列表(兼容旧接口)
|
|
755
|
+
* @returns 节点 ID 数组
|
|
756
|
+
*/
|
|
757
|
+
getKnownNodes() {
|
|
758
|
+
return this.directNodes.map((n) => n.nodeId);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
// src/MessageHandler.ts
|
|
763
|
+
var MessageHandler = class {
|
|
764
|
+
/** 回调函数集合 */
|
|
765
|
+
callbacks;
|
|
766
|
+
/**
|
|
767
|
+
* 创建消息处理器
|
|
768
|
+
* @param callbacks 回调函数集合
|
|
769
|
+
*/
|
|
770
|
+
constructor(callbacks) {
|
|
771
|
+
this.callbacks = callbacks;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* 处理收到的请求
|
|
775
|
+
* 根据是否有中继消息上下文判断是直连请求还是中继请求
|
|
776
|
+
* @param from 发送者 Peer ID
|
|
777
|
+
* @param request 请求数据
|
|
778
|
+
* @param relayMessage 中继消息上下文(可选,有则为中继请求)
|
|
779
|
+
* @returns 响应数据
|
|
780
|
+
*/
|
|
781
|
+
async handleRequest(from, request, relayMessage) {
|
|
782
|
+
if (relayMessage) {
|
|
783
|
+
return this.handleRelayRequest(from, request, relayMessage);
|
|
784
|
+
}
|
|
785
|
+
return this.handleDirectRequest(from, request);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* 处理直连请求
|
|
789
|
+
* @param from 发送者 Peer ID
|
|
790
|
+
* @param request 请求数据
|
|
791
|
+
* @returns 响应数据
|
|
792
|
+
*/
|
|
793
|
+
async handleDirectRequest(from, request) {
|
|
794
|
+
const result = await this.processHandler(request.path, from, request.data);
|
|
795
|
+
if (this.isErrorResponse(result)) {
|
|
796
|
+
return result;
|
|
797
|
+
}
|
|
798
|
+
return { status: 200, data: result };
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* 处理中继请求
|
|
802
|
+
* @param from 发送者 Peer ID
|
|
803
|
+
* @param request 请求数据
|
|
804
|
+
* @param relayMessage 中继消息上下文
|
|
805
|
+
* @returns 响应数据
|
|
806
|
+
*/
|
|
807
|
+
async handleRelayRequest(from, request, relayMessage) {
|
|
808
|
+
const myPeerId = this.callbacks.getMyPeerId();
|
|
809
|
+
const { originalTarget, relayPath, forwardPath } = relayMessage;
|
|
810
|
+
if (myPeerId === originalTarget) {
|
|
811
|
+
const result = await this.processHandler(request.path, from, request.data);
|
|
812
|
+
if (this.isErrorResponse(result)) {
|
|
813
|
+
return result;
|
|
814
|
+
}
|
|
815
|
+
return { status: 200, data: result };
|
|
816
|
+
}
|
|
817
|
+
const currentTTL = relayMessage.ttl ?? DEFAULT_TTL;
|
|
818
|
+
if (currentTTL <= 0) {
|
|
819
|
+
this.callbacks.debugLog("MessageHandler", "ttlExpired", { type: "relay-request", id: relayMessage.id });
|
|
820
|
+
return {
|
|
821
|
+
status: 502,
|
|
822
|
+
data: { error: "TTL expired - message dropped to prevent routing loop" }
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
if (forwardPath.length > 0) {
|
|
826
|
+
const nextHop = forwardPath[0];
|
|
827
|
+
const remainingPath = forwardPath.slice(1);
|
|
828
|
+
try {
|
|
829
|
+
const response = await this.forwardRelay(nextHop, {
|
|
830
|
+
type: "relay-request",
|
|
831
|
+
id: relayMessage.id,
|
|
832
|
+
originalTarget,
|
|
833
|
+
relayPath: [...relayPath, myPeerId],
|
|
834
|
+
forwardPath: remainingPath,
|
|
835
|
+
ttl: currentTTL - 1,
|
|
836
|
+
request
|
|
837
|
+
});
|
|
838
|
+
return response;
|
|
839
|
+
} catch (err) {
|
|
840
|
+
return {
|
|
841
|
+
status: 500,
|
|
842
|
+
data: { error: `Forward failed: ${err instanceof Error ? err.message : "Unknown error"}` }
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
const data = await this.forwardToTarget(originalTarget, request, relayMessage);
|
|
848
|
+
return { status: 200, data };
|
|
849
|
+
} catch (err) {
|
|
850
|
+
return {
|
|
851
|
+
status: 500,
|
|
852
|
+
data: { error: `Forward to target failed: ${err instanceof Error ? err.message : "Unknown error"}` }
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* 调用注册的处理器
|
|
858
|
+
* @param path 请求路径
|
|
859
|
+
* @param from 发送者 Peer ID
|
|
860
|
+
* @param data 请求数据
|
|
861
|
+
* @returns 处理器返回的数据,或 404 错误
|
|
862
|
+
*/
|
|
863
|
+
async processHandler(path, from, data) {
|
|
864
|
+
const handlers = this.callbacks.getSimpleHandlers();
|
|
865
|
+
const simpleHandler = handlers.get(path);
|
|
866
|
+
if (simpleHandler) {
|
|
867
|
+
return await simpleHandler(from, data);
|
|
868
|
+
}
|
|
869
|
+
return { status: 404, data: { error: `Path not found: ${path}` } };
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* 判断结果是否为错误响应
|
|
873
|
+
*/
|
|
874
|
+
isErrorResponse(result) {
|
|
875
|
+
return typeof result === "object" && result !== null && "status" in result && "data" in result;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* 创建连接并发送中继消息的通用方法
|
|
879
|
+
* @param targetId 目标节点 ID
|
|
880
|
+
* @param message 要发送的消息
|
|
881
|
+
* @param extractResponse 从响应消息中提取数据的函数
|
|
882
|
+
* @returns Promise<Response>
|
|
883
|
+
*/
|
|
884
|
+
createConnectionAndSend(targetId, message, extractResponse) {
|
|
885
|
+
return new Promise((resolve, reject) => {
|
|
886
|
+
this.callbacks.debugLog("MessageHandler", "createConnectionAndSend", { targetId });
|
|
887
|
+
this.callbacks.waitForReady().then(() => {
|
|
888
|
+
const peerInstance = this.callbacks.getPeerInstance();
|
|
889
|
+
if (!peerInstance) {
|
|
890
|
+
reject(new Error("Peer instance not available"));
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const conn = peerInstance.connect(targetId, { reliable: true });
|
|
894
|
+
const timeout = setTimeout(() => {
|
|
895
|
+
conn.close();
|
|
896
|
+
reject(new Error(`Connection timeout: ${targetId}`));
|
|
897
|
+
}, CONNECTION_TIMEOUT_MS);
|
|
898
|
+
conn.on("open", () => {
|
|
899
|
+
this.callbacks.debugLog("Conn", "open", targetId);
|
|
900
|
+
conn.send(message);
|
|
901
|
+
});
|
|
902
|
+
conn.on("data", (responseData) => {
|
|
903
|
+
const response = responseData;
|
|
904
|
+
const extracted = extractResponse(response);
|
|
905
|
+
if (extracted) {
|
|
906
|
+
clearTimeout(timeout);
|
|
907
|
+
conn.close();
|
|
908
|
+
resolve(extracted);
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
conn.on("error", (err) => {
|
|
912
|
+
this.callbacks.debugLog("Conn", "error", { peer: targetId, error: err });
|
|
913
|
+
clearTimeout(timeout);
|
|
914
|
+
reject(err);
|
|
915
|
+
});
|
|
916
|
+
conn.on("close", () => {
|
|
917
|
+
this.callbacks.debugLog("Conn", "close", targetId);
|
|
918
|
+
clearTimeout(timeout);
|
|
919
|
+
reject(new Error("Connection closed"));
|
|
920
|
+
});
|
|
921
|
+
}).catch(reject);
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* 转发中继请求到下一个节点
|
|
926
|
+
* @param nextHop 下一跳节点 ID
|
|
927
|
+
* @param message 要转发的消息
|
|
928
|
+
* @returns 响应数据
|
|
929
|
+
*/
|
|
930
|
+
async forwardRelay(nextHop, message) {
|
|
931
|
+
return this.createConnectionAndSend(
|
|
932
|
+
nextHop,
|
|
933
|
+
message,
|
|
934
|
+
(response) => {
|
|
935
|
+
if (response.type === "relay-response" && response.response) {
|
|
936
|
+
return response.response;
|
|
937
|
+
}
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* 转发到最终目标节点(当没有更多中继节点时使用)
|
|
944
|
+
* @param targetId 目标节点 ID
|
|
945
|
+
* @param request 请求数据
|
|
946
|
+
* @param originalMessage 原始中继消息
|
|
947
|
+
* @returns 响应数据
|
|
948
|
+
*/
|
|
949
|
+
async forwardToTarget(targetId, request, originalMessage) {
|
|
950
|
+
const myPeerId = this.callbacks.getMyPeerId();
|
|
951
|
+
const currentTTL = originalMessage.ttl ?? DEFAULT_TTL;
|
|
952
|
+
const message = {
|
|
953
|
+
type: "relay-request",
|
|
954
|
+
id: originalMessage.id,
|
|
955
|
+
originalTarget: originalMessage.originalTarget,
|
|
956
|
+
relayPath: [...originalMessage.relayPath, myPeerId],
|
|
957
|
+
forwardPath: [],
|
|
958
|
+
ttl: currentTTL - 1,
|
|
959
|
+
request
|
|
960
|
+
};
|
|
961
|
+
const response = await this.createConnectionAndSend(
|
|
962
|
+
targetId,
|
|
963
|
+
message,
|
|
964
|
+
(resp) => {
|
|
965
|
+
if (resp.type === "relay-response" && resp.response) {
|
|
966
|
+
return resp.response;
|
|
967
|
+
}
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
);
|
|
971
|
+
return response.data;
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
// src/PeerJsWrapper.ts
|
|
976
|
+
var VERSION = "1.2.0";
|
|
977
|
+
console.log(`[sx-peerjs-http-util] v${VERSION}`);
|
|
978
|
+
function generateUUID() {
|
|
979
|
+
return crypto.randomUUID();
|
|
980
|
+
}
|
|
981
|
+
var PeerJsWrapper = class _PeerJsWrapper {
|
|
982
|
+
/** 本地 Peer ID */
|
|
983
|
+
myPeerId;
|
|
984
|
+
/** PeerJS 实例 */
|
|
985
|
+
peerInstance = null;
|
|
986
|
+
/** 当前活跃的传入连接集合 */
|
|
987
|
+
connections = /* @__PURE__ */ new Set();
|
|
988
|
+
/** 待处理的请求映射表(用于请求-响应匹配) */
|
|
989
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
990
|
+
/** 路径处理器映射表 */
|
|
991
|
+
simpleHandlers = /* @__PURE__ */ new Map();
|
|
992
|
+
/** 重连定时器 */
|
|
993
|
+
reconnectTimer = null;
|
|
994
|
+
/** 是否已销毁 */
|
|
995
|
+
isDestroyed = false;
|
|
996
|
+
/** 是否开启调试模式 */
|
|
997
|
+
isDebug;
|
|
998
|
+
/** 服务器配置 */
|
|
999
|
+
serverConfig;
|
|
1000
|
+
/** 当前活跃的通话 */
|
|
1001
|
+
activeCall = null;
|
|
1002
|
+
/** 来电监听器集合 */
|
|
145
1003
|
incomingCallListeners = /* @__PURE__ */ new Set();
|
|
1004
|
+
/** 路由管理器 */
|
|
1005
|
+
router;
|
|
1006
|
+
/** 消息处理器 */
|
|
1007
|
+
messageHandler;
|
|
146
1008
|
/**
|
|
147
1009
|
* 创建 PeerJsWrapper 实例
|
|
148
1010
|
* @param peerId 可选的 Peer ID,如果不提供则自动生成 UUID
|
|
149
1011
|
* @param isDebug 是否开启调试模式,开启后会打印事件日志
|
|
150
1012
|
* @param server 可选的信令服务器配置,不提供则使用 PeerJS 公共服务器
|
|
1013
|
+
* @param relayConfig 可选的中继配置
|
|
151
1014
|
*/
|
|
152
|
-
constructor(peerId, isDebug, server) {
|
|
1015
|
+
constructor(peerId, isDebug, server, relayConfig) {
|
|
153
1016
|
this.myPeerId = peerId || generateUUID();
|
|
154
1017
|
this.isDebug = isDebug ?? false;
|
|
155
1018
|
this.serverConfig = server;
|
|
1019
|
+
const callbacks = {
|
|
1020
|
+
getMyPeerId: () => this.myPeerId,
|
|
1021
|
+
getPeerInstance: () => this.peerInstance,
|
|
1022
|
+
debugLog: this.debugLog.bind(this),
|
|
1023
|
+
sendRelayMessage: (targetId, message) => this.sendRelayMessage(targetId, message)
|
|
1024
|
+
};
|
|
1025
|
+
this.router = new Router(callbacks, relayConfig);
|
|
1026
|
+
this.router.init();
|
|
1027
|
+
this.messageHandler = new MessageHandler({
|
|
1028
|
+
...callbacks,
|
|
1029
|
+
waitForReady: () => this.waitForReady(),
|
|
1030
|
+
getSimpleHandlers: () => this.simpleHandlers,
|
|
1031
|
+
onRouteUpdate: (fromPeerId, message) => this.router.handleRouteUpdate(fromPeerId, message)
|
|
1032
|
+
});
|
|
156
1033
|
this.connect();
|
|
157
1034
|
}
|
|
158
1035
|
/**
|
|
159
|
-
*
|
|
160
|
-
* @param
|
|
161
|
-
* @param
|
|
162
|
-
* @param
|
|
1036
|
+
* 创建实例并等待就绪(语法糖)
|
|
1037
|
+
* @param peerId 可选的 Peer ID
|
|
1038
|
+
* @param isDebug 是否开启调试模式
|
|
1039
|
+
* @param server 可选的信令服务器配置
|
|
1040
|
+
* @param relayConfig 可选的中继配置
|
|
1041
|
+
* @returns Promise<PeerJsWrapper>
|
|
163
1042
|
*/
|
|
1043
|
+
static async create(peerId, isDebug, server, relayConfig) {
|
|
1044
|
+
const wrapper = new _PeerJsWrapper(peerId, isDebug, server, relayConfig);
|
|
1045
|
+
await wrapper.whenReady();
|
|
1046
|
+
return wrapper;
|
|
1047
|
+
}
|
|
164
1048
|
debugLog(obj, event, data) {
|
|
165
1049
|
if (this.isDebug) {
|
|
166
|
-
|
|
167
|
-
console.log(`${obj} ${event} ${dataStr}`);
|
|
1050
|
+
console.log(obj, event, data);
|
|
168
1051
|
}
|
|
169
1052
|
}
|
|
170
|
-
/**
|
|
171
|
-
* 连接到 PeerJS 服务器
|
|
172
|
-
*/
|
|
173
1053
|
connect() {
|
|
174
1054
|
if (this.isDestroyed) return;
|
|
175
1055
|
this.peerInstance = this.serverConfig ? new Peer(this.myPeerId, { ...this.serverConfig }) : new Peer(this.myPeerId);
|
|
176
1056
|
this.setupPeerEventHandlers();
|
|
177
1057
|
}
|
|
178
|
-
/**
|
|
179
|
-
* 设置 Peer 实例的事件处理器
|
|
180
|
-
*/
|
|
181
1058
|
setupPeerEventHandlers() {
|
|
182
1059
|
if (!this.peerInstance) return;
|
|
183
1060
|
this.peerInstance.on("open", (id) => {
|
|
@@ -205,20 +1082,14 @@ var PeerJsWrapper = class {
|
|
|
205
1082
|
});
|
|
206
1083
|
this.setupIncomingConnectionHandler();
|
|
207
1084
|
}
|
|
208
|
-
/**
|
|
209
|
-
* 安排重连
|
|
210
|
-
*/
|
|
211
1085
|
scheduleReconnect() {
|
|
212
1086
|
if (this.isDestroyed) return;
|
|
213
1087
|
if (this.reconnectTimer) return;
|
|
214
1088
|
this.reconnectTimer = setTimeout(() => {
|
|
215
1089
|
this.reconnectTimer = null;
|
|
216
1090
|
this.reconnect();
|
|
217
|
-
},
|
|
1091
|
+
}, RECONNECT_DELAY_MS);
|
|
218
1092
|
}
|
|
219
|
-
/**
|
|
220
|
-
* 执行重连
|
|
221
|
-
*/
|
|
222
1093
|
reconnect() {
|
|
223
1094
|
if (this.isDestroyed) return;
|
|
224
1095
|
this.debugLog("PeerJsWrapper", "reconnect");
|
|
@@ -231,23 +1102,12 @@ var PeerJsWrapper = class {
|
|
|
231
1102
|
}
|
|
232
1103
|
this.connect();
|
|
233
1104
|
}
|
|
234
|
-
/**
|
|
235
|
-
* 获取当前 Peer ID
|
|
236
|
-
* @returns string 当前 Peer ID
|
|
237
|
-
*/
|
|
238
1105
|
getPeerId() {
|
|
239
1106
|
return this.myPeerId;
|
|
240
1107
|
}
|
|
241
|
-
/**
|
|
242
|
-
* 等待 Peer 连接就绪(连接到信令服务器)
|
|
243
|
-
* @returns Promise<void> 当连接成功时 resolve
|
|
244
|
-
*/
|
|
245
1108
|
whenReady() {
|
|
246
1109
|
return this.waitForReady();
|
|
247
1110
|
}
|
|
248
|
-
/**
|
|
249
|
-
* 等待 Peer 连接就绪
|
|
250
|
-
*/
|
|
251
1111
|
waitForReady() {
|
|
252
1112
|
return new Promise((resolve, reject) => {
|
|
253
1113
|
if (!this.peerInstance) {
|
|
@@ -272,87 +1132,363 @@ var PeerJsWrapper = class {
|
|
|
272
1132
|
this.peerInstance.on("error", onError);
|
|
273
1133
|
});
|
|
274
1134
|
}
|
|
1135
|
+
getRoutingTable() {
|
|
1136
|
+
return this.router.getRoutingTable();
|
|
1137
|
+
}
|
|
1138
|
+
getKnownNodes() {
|
|
1139
|
+
return this.router.getKnownNodes();
|
|
1140
|
+
}
|
|
275
1141
|
/**
|
|
276
|
-
*
|
|
277
|
-
* @param
|
|
1142
|
+
* 发送中继消息的辅助方法
|
|
1143
|
+
* @param targetId 目标节点 ID
|
|
1144
|
+
* @param message 中继消息
|
|
1145
|
+
*/
|
|
1146
|
+
sendRelayMessage(targetId, message) {
|
|
1147
|
+
return new Promise((resolve, reject) => {
|
|
1148
|
+
if (!this.peerInstance) {
|
|
1149
|
+
reject(new Error("Peer instance not available"));
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const conn = this.peerInstance.connect(targetId, { reliable: true });
|
|
1153
|
+
const timeout = setTimeout(() => {
|
|
1154
|
+
conn.close();
|
|
1155
|
+
reject(new Error(`Send to ${targetId} timeout`));
|
|
1156
|
+
}, SEND_TIMEOUT_MS);
|
|
1157
|
+
conn.on("open", () => {
|
|
1158
|
+
conn.send(message);
|
|
1159
|
+
clearTimeout(timeout);
|
|
1160
|
+
conn.close();
|
|
1161
|
+
resolve();
|
|
1162
|
+
});
|
|
1163
|
+
conn.on("error", () => {
|
|
1164
|
+
clearTimeout(timeout);
|
|
1165
|
+
reject(new Error(`Send to ${targetId} failed`));
|
|
1166
|
+
});
|
|
1167
|
+
conn.on("close", () => {
|
|
1168
|
+
clearTimeout(timeout);
|
|
1169
|
+
resolve();
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* 尝试直连目标节点
|
|
1175
|
+
* @param targetId 目标节点 ID
|
|
278
1176
|
* @param path 请求路径
|
|
279
1177
|
* @param data 请求数据
|
|
280
|
-
* @
|
|
1178
|
+
* @param requestId 请求 ID
|
|
1179
|
+
* @returns Promise<unknown> - 响应数据
|
|
281
1180
|
*/
|
|
282
|
-
|
|
1181
|
+
tryDirectConnect(targetId, path, data, requestId) {
|
|
283
1182
|
return new Promise((resolve, reject) => {
|
|
284
|
-
this.
|
|
1183
|
+
if (!this.peerInstance) {
|
|
1184
|
+
reject(new Error("Peer instance not available"));
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const startTime = Date.now();
|
|
1188
|
+
const conn = this.peerInstance.connect(targetId, { reliable: true });
|
|
1189
|
+
let resolved = false;
|
|
1190
|
+
const timeout = setTimeout(() => {
|
|
1191
|
+
if (!resolved) {
|
|
1192
|
+
resolved = true;
|
|
1193
|
+
conn.close();
|
|
1194
|
+
reject(new Error(`Request timeout: ${targetId}${path}`));
|
|
1195
|
+
}
|
|
1196
|
+
}, CONNECTION_TIMEOUT_MS);
|
|
1197
|
+
conn.on("open", () => {
|
|
1198
|
+
const latency = Date.now() - startTime;
|
|
1199
|
+
this.debugLog("Conn", "open", { peer: targetId, latency });
|
|
1200
|
+
const request = { path, data };
|
|
1201
|
+
const message = {
|
|
1202
|
+
type: "request",
|
|
1203
|
+
id: requestId,
|
|
1204
|
+
request
|
|
1205
|
+
};
|
|
1206
|
+
conn.send(message);
|
|
1207
|
+
});
|
|
1208
|
+
conn.on("data", (responseData) => {
|
|
1209
|
+
this.debugLog("Conn", "data", { peer: targetId, data: responseData });
|
|
1210
|
+
const message = responseData;
|
|
1211
|
+
if (message.type === "response" && message.id === requestId) {
|
|
1212
|
+
if (!resolved) {
|
|
1213
|
+
resolved = true;
|
|
1214
|
+
clearTimeout(timeout);
|
|
1215
|
+
const response = message.response;
|
|
1216
|
+
if (response.status < 200 || response.status >= 300) {
|
|
1217
|
+
conn.close();
|
|
1218
|
+
reject(new Error(`Request failed: ${response.status} ${JSON.stringify(response.data)}`));
|
|
1219
|
+
} else {
|
|
1220
|
+
const latency = Date.now() - startTime;
|
|
1221
|
+
this.router.recordDirectNode(targetId, latency);
|
|
1222
|
+
this.router.broadcastRouteUpdate();
|
|
1223
|
+
resolve(response.data);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
conn.on("error", (err) => {
|
|
1229
|
+
if (!resolved) {
|
|
1230
|
+
resolved = true;
|
|
1231
|
+
clearTimeout(timeout);
|
|
1232
|
+
this.debugLog("Conn", "error", { peer: targetId, error: err });
|
|
1233
|
+
reject(err);
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
conn.on("close", () => {
|
|
1237
|
+
if (!resolved) {
|
|
1238
|
+
resolved = true;
|
|
1239
|
+
clearTimeout(timeout);
|
|
1240
|
+
this.debugLog("Conn", "close", targetId);
|
|
1241
|
+
reject(new Error("Connection closed"));
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* 通过中继节点转发请求
|
|
1248
|
+
* @param nextHopId 下一跳节点 ID
|
|
1249
|
+
* @param targetId 原始目标节点 ID
|
|
1250
|
+
* @param path 请求路径
|
|
1251
|
+
* @param data 请求数据
|
|
1252
|
+
* @returns Promise<unknown> - 响应数据
|
|
1253
|
+
*/
|
|
1254
|
+
relayVia(nextHopId, targetId, path, data) {
|
|
1255
|
+
return new Promise((resolve, reject) => {
|
|
1256
|
+
this.debugLog("PeerJsWrapper", "relayVia", { targetId, nextHop: nextHopId });
|
|
285
1257
|
this.waitForReady().then(() => {
|
|
286
1258
|
if (!this.peerInstance) {
|
|
287
1259
|
reject(new Error("Peer instance not available"));
|
|
288
1260
|
return;
|
|
289
1261
|
}
|
|
290
|
-
const conn = this.peerInstance.connect(
|
|
291
|
-
|
|
292
|
-
});
|
|
1262
|
+
const conn = this.peerInstance.connect(nextHopId, { reliable: true });
|
|
1263
|
+
const startTime = Date.now();
|
|
293
1264
|
const timeout = setTimeout(() => {
|
|
294
1265
|
conn.close();
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}, 3e4);
|
|
298
|
-
const requestId = `${this.myPeerId}-${Date.now()}-${Math.random()}`;
|
|
299
|
-
this.pendingRequests.set(requestId, { resolve, reject, timeout });
|
|
1266
|
+
reject(new Error(`Relay timeout: ${nextHopId}${path}`));
|
|
1267
|
+
}, CONNECTION_TIMEOUT_MS);
|
|
300
1268
|
conn.on("open", () => {
|
|
301
|
-
this.debugLog("Conn", "open",
|
|
1269
|
+
this.debugLog("Conn", "open", nextHopId);
|
|
302
1270
|
const request = { path, data };
|
|
303
1271
|
const message = {
|
|
304
|
-
type: "request",
|
|
305
|
-
id:
|
|
1272
|
+
type: "relay-request",
|
|
1273
|
+
id: `${this.myPeerId}-${Date.now()}-${Math.random()}`,
|
|
1274
|
+
originalTarget: targetId,
|
|
1275
|
+
relayPath: [this.myPeerId],
|
|
1276
|
+
forwardPath: [],
|
|
1277
|
+
ttl: DEFAULT_TTL,
|
|
306
1278
|
request
|
|
307
1279
|
};
|
|
308
1280
|
conn.send(message);
|
|
309
1281
|
});
|
|
310
1282
|
conn.on("data", (responseData) => {
|
|
311
|
-
this.debugLog("Conn", "data", { peer: peerId, data: responseData });
|
|
312
1283
|
const message = responseData;
|
|
313
|
-
if (message.type === "response"
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const response = message.response;
|
|
1284
|
+
if (message.type === "relay-response") {
|
|
1285
|
+
clearTimeout(timeout);
|
|
1286
|
+
conn.close();
|
|
1287
|
+
const response = message.response;
|
|
1288
|
+
if (response) {
|
|
319
1289
|
if (response.status < 200 || response.status >= 300) {
|
|
320
|
-
|
|
321
|
-
new Error(`Request failed: ${response.status} ${JSON.stringify(response.data)}`)
|
|
322
|
-
);
|
|
1290
|
+
reject(new Error(`Relay failed: ${response.status} ${JSON.stringify(response.data)}`));
|
|
323
1291
|
} else {
|
|
324
|
-
|
|
1292
|
+
const latency = Date.now() - startTime;
|
|
1293
|
+
this.router.recordDirectNode(nextHopId, latency);
|
|
1294
|
+
this.router.broadcastRouteUpdate();
|
|
1295
|
+
resolve(response.data);
|
|
325
1296
|
}
|
|
326
1297
|
}
|
|
327
|
-
|
|
1298
|
+
} else if (message.type === "route-update") {
|
|
1299
|
+
this.router.handleRouteUpdate(nextHopId, message);
|
|
1300
|
+
} else if (message.type === "route-query") {
|
|
1301
|
+
this.router.handleRouteQuery(nextHopId, message);
|
|
1302
|
+
} else if (message.type === "route-response") {
|
|
1303
|
+
this.router.handleRouteResponse(nextHopId, message);
|
|
328
1304
|
}
|
|
329
1305
|
});
|
|
330
1306
|
conn.on("error", (err) => {
|
|
331
|
-
this.debugLog("Conn", "error", { peer:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
clearTimeout(pending.timeout);
|
|
335
|
-
this.pendingRequests.delete(requestId);
|
|
336
|
-
pending.reject(err);
|
|
337
|
-
}
|
|
1307
|
+
this.debugLog("Conn", "error", { peer: nextHopId, error: err });
|
|
1308
|
+
clearTimeout(timeout);
|
|
1309
|
+
reject(err);
|
|
338
1310
|
});
|
|
339
1311
|
conn.on("close", () => {
|
|
340
|
-
this.debugLog("Conn", "close",
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
clearTimeout(pending.timeout);
|
|
344
|
-
this.pendingRequests.delete(requestId);
|
|
345
|
-
pending.reject(new Error("Connection closed"));
|
|
346
|
-
}
|
|
1312
|
+
this.debugLog("Conn", "close", nextHopId);
|
|
1313
|
+
clearTimeout(timeout);
|
|
1314
|
+
reject(new Error("Relay connection closed"));
|
|
347
1315
|
});
|
|
348
|
-
}).catch(
|
|
349
|
-
|
|
350
|
-
|
|
1316
|
+
}).catch(reject);
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* 自动路由发送
|
|
1321
|
+
*
|
|
1322
|
+
* 1. 查路由表 → 有路由 → 尝试中继 → 全部失败 → 降级直连 → 失败 → 结束
|
|
1323
|
+
* 2. 路由表无目标 → 直连 → 失败 → 结束
|
|
1324
|
+
* @param peerId 目标节点 ID
|
|
1325
|
+
* @param path 请求路径
|
|
1326
|
+
* @param data 请求数据
|
|
1327
|
+
* @returns Promise<unknown> - 响应数据
|
|
1328
|
+
*/
|
|
1329
|
+
send(peerId, path, data) {
|
|
1330
|
+
const requestId = `${this.myPeerId}-${Date.now()}-${Math.random()}`;
|
|
1331
|
+
return new Promise((resolve, reject) => {
|
|
1332
|
+
this.debugLog("PeerJsWrapper", "send", { peerId, path, data });
|
|
1333
|
+
const nextHops = this.router.getNextHopsToTarget(peerId);
|
|
1334
|
+
if (nextHops.length > 0) {
|
|
1335
|
+
this.tryRelayChain(peerId, path, data, nextHops, 0).then(resolve).catch((relayErr) => {
|
|
1336
|
+
this.debugLog("PeerJsWrapper", "relayFailed", { peerId, error: relayErr.message });
|
|
1337
|
+
this.fallbackToDirect(peerId, path, data, requestId).then(resolve).catch(reject);
|
|
1338
|
+
});
|
|
1339
|
+
} else {
|
|
1340
|
+
this.tryDirectConnect(peerId, path, data, requestId).then(resolve).catch((directErr) => {
|
|
1341
|
+
this.debugLog("PeerJsWrapper", "directFailed", { peerId, error: directErr.message });
|
|
1342
|
+
this.handleSendError(peerId, directErr).then(resolve).catch(reject);
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* 处理发送错误
|
|
1349
|
+
* @param peerId 目标节点 ID
|
|
1350
|
+
* @param error 错误对象
|
|
1351
|
+
* @returns Promise
|
|
1352
|
+
*/
|
|
1353
|
+
async handleSendError(peerId, error) {
|
|
1354
|
+
const errorMsg = error.message;
|
|
1355
|
+
const isHttpError = errorMsg.includes("Request failed:") || errorMsg.includes("404") || errorMsg.includes("500");
|
|
1356
|
+
const isConnectionError = errorMsg.includes("timeout") || errorMsg.includes("Connection closed") || errorMsg.includes("Peer instance not available");
|
|
1357
|
+
if (isHttpError) {
|
|
1358
|
+
throw error;
|
|
1359
|
+
}
|
|
1360
|
+
if (!isConnectionError) {
|
|
1361
|
+
throw error;
|
|
1362
|
+
}
|
|
1363
|
+
this.router.removeRoute(peerId);
|
|
1364
|
+
this.debugLog("PeerJsWrapper", "routeRemoved", peerId);
|
|
1365
|
+
if (!this.router.isRoutingTableEmpty()) {
|
|
1366
|
+
return this.performRouteDiscovery(peerId, "", void 0, "");
|
|
1367
|
+
}
|
|
1368
|
+
throw new Error(`Cannot reach ${peerId}: no route found and routing table is empty`);
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* 降级到直连尝试
|
|
1372
|
+
* @param peerId 目标节点 ID
|
|
1373
|
+
* @param path 请求路径
|
|
1374
|
+
* @param data 请求数据
|
|
1375
|
+
* @param requestId 请求 ID
|
|
1376
|
+
* @returns Promise
|
|
1377
|
+
*/
|
|
1378
|
+
async fallbackToDirect(peerId, path, data, requestId) {
|
|
1379
|
+
this.debugLog("PeerJsWrapper", "fallbackToDirect", peerId);
|
|
1380
|
+
return this.tryDirectConnect(peerId, path, data, requestId).catch((directErr) => {
|
|
1381
|
+
this.debugLog("PeerJsWrapper", "directFailed", { peerId, error: directErr.message });
|
|
1382
|
+
this.handleSendError(peerId, directErr);
|
|
351
1383
|
});
|
|
352
1384
|
}
|
|
353
1385
|
/**
|
|
354
|
-
*
|
|
1386
|
+
* 尝试通过中继链转发
|
|
1387
|
+
* @param targetId 目标节点 ID
|
|
1388
|
+
* @param path 请求路径
|
|
1389
|
+
* @param data 请求数据
|
|
1390
|
+
* @param nextHops 下一跳列表
|
|
1391
|
+
* @param index 当前尝试的下一跳索引
|
|
1392
|
+
* @returns Promise<unknown>
|
|
355
1393
|
*/
|
|
1394
|
+
tryRelayChain(targetId, path, data, nextHops, index) {
|
|
1395
|
+
if (index >= nextHops.length) {
|
|
1396
|
+
return Promise.reject(new Error("All relay nodes failed"));
|
|
1397
|
+
}
|
|
1398
|
+
const nextHop = nextHops[index];
|
|
1399
|
+
this.debugLog("PeerJsWrapper", "tryRelay", { targetId, nextHop: nextHop.nodeId, latency: nextHop.latency });
|
|
1400
|
+
return this.relayVia(nextHop.nodeId, targetId, path, data).catch(() => {
|
|
1401
|
+
return this.tryRelayChain(targetId, path, data, nextHops, index + 1);
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* 执行路由发现
|
|
1406
|
+
* @param targetId 目标节点 ID
|
|
1407
|
+
* @param path 请求路径
|
|
1408
|
+
* @param data 请求数据
|
|
1409
|
+
* @param requestId 请求 ID
|
|
1410
|
+
* @returns Promise<unknown>
|
|
1411
|
+
*/
|
|
1412
|
+
async performRouteDiscovery(targetId, path, data, requestId) {
|
|
1413
|
+
this.debugLog("PeerJsWrapper", "routeDiscovery", { targetId });
|
|
1414
|
+
const routeEntry = await this.router.discoverRoute(targetId);
|
|
1415
|
+
if (!routeEntry || routeEntry.nextHops.length === 0) {
|
|
1416
|
+
throw new Error(`Cannot reach ${targetId}: no route found`);
|
|
1417
|
+
}
|
|
1418
|
+
this.debugLog("PeerJsWrapper", "routeFound", { targetId, nextHops: routeEntry.nextHops });
|
|
1419
|
+
return this.tryRelayChain(targetId, path, data, routeEntry.nextHops, 0);
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* 中继发送(内部方法,不对外暴露)
|
|
1423
|
+
* @param targetId 目标节点 ID
|
|
1424
|
+
* @param path 请求路径
|
|
1425
|
+
* @param data 请求数据
|
|
1426
|
+
* @param relayNodes 手动指定的中继节点(可选,不指定则自动路由)
|
|
1427
|
+
* @returns Promise<unknown>
|
|
1428
|
+
*/
|
|
1429
|
+
relaySend(targetId, path, data, relayNodes) {
|
|
1430
|
+
if (!relayNodes || relayNodes.length === 0) {
|
|
1431
|
+
return this.send(targetId, path, data);
|
|
1432
|
+
}
|
|
1433
|
+
const [firstRelay, ...remainingRelays] = relayNodes;
|
|
1434
|
+
return new Promise((resolve, reject) => {
|
|
1435
|
+
this.debugLog("PeerJsWrapper", "relaySend", { targetId, firstRelay, remainingRelays });
|
|
1436
|
+
this.waitForReady().then(() => {
|
|
1437
|
+
if (!this.peerInstance) {
|
|
1438
|
+
reject(new Error("Peer instance not available"));
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
const conn = this.peerInstance.connect(firstRelay, { reliable: true });
|
|
1442
|
+
const timeout = setTimeout(() => {
|
|
1443
|
+
conn.close();
|
|
1444
|
+
reject(new Error(`Relay timeout: ${firstRelay}${path}`));
|
|
1445
|
+
}, CONNECTION_TIMEOUT_MS);
|
|
1446
|
+
conn.on("open", () => {
|
|
1447
|
+
this.debugLog("Conn", "open", firstRelay);
|
|
1448
|
+
const request = { path, data };
|
|
1449
|
+
const message = {
|
|
1450
|
+
type: "relay-request",
|
|
1451
|
+
id: `${this.myPeerId}-${Date.now()}-${Math.random()}`,
|
|
1452
|
+
originalTarget: targetId,
|
|
1453
|
+
relayPath: [],
|
|
1454
|
+
forwardPath: remainingRelays,
|
|
1455
|
+
ttl: DEFAULT_TTL,
|
|
1456
|
+
request
|
|
1457
|
+
};
|
|
1458
|
+
conn.send(message);
|
|
1459
|
+
});
|
|
1460
|
+
conn.on("data", (responseData) => {
|
|
1461
|
+
const message = responseData;
|
|
1462
|
+
if (message.type === "relay-response") {
|
|
1463
|
+
clearTimeout(timeout);
|
|
1464
|
+
conn.close();
|
|
1465
|
+
const response = message.response;
|
|
1466
|
+
if (response) {
|
|
1467
|
+
if (response.status < 200 || response.status >= 300) {
|
|
1468
|
+
reject(new Error(`Relay failed: ${response.status} ${JSON.stringify(response.data)}`));
|
|
1469
|
+
} else {
|
|
1470
|
+
this.router.recordSuccessfulNode(firstRelay);
|
|
1471
|
+
this.router.broadcastRouteUpdate();
|
|
1472
|
+
resolve(response.data);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
} else if (message.type === "route-update") {
|
|
1476
|
+
this.router.handleRouteUpdate(firstRelay, message);
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
conn.on("error", (err) => {
|
|
1480
|
+
this.debugLog("Conn", "error", { peer: firstRelay, error: err });
|
|
1481
|
+
clearTimeout(timeout);
|
|
1482
|
+
reject(err);
|
|
1483
|
+
});
|
|
1484
|
+
conn.on("close", () => {
|
|
1485
|
+
this.debugLog("Conn", "close", firstRelay);
|
|
1486
|
+
clearTimeout(timeout);
|
|
1487
|
+
reject(new Error("Relay connection closed"));
|
|
1488
|
+
});
|
|
1489
|
+
}).catch(reject);
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
356
1492
|
setupIncomingConnectionHandler() {
|
|
357
1493
|
if (!this.peerInstance) return;
|
|
358
1494
|
this.peerInstance.on("connection", (conn) => {
|
|
@@ -364,26 +1500,60 @@ var PeerJsWrapper = class {
|
|
|
364
1500
|
conn.on("data", async (data) => {
|
|
365
1501
|
this.debugLog("Conn", "data", { peer: conn.peer, data });
|
|
366
1502
|
const message = data;
|
|
367
|
-
if (message.type === "request"
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
1503
|
+
if (message.type === "request") {
|
|
1504
|
+
const internalMsg = message;
|
|
1505
|
+
if (internalMsg.request) {
|
|
1506
|
+
try {
|
|
1507
|
+
const response = await this.messageHandler.handleRequest(conn.peer, internalMsg.request);
|
|
1508
|
+
const responseMessage = {
|
|
1509
|
+
type: "response",
|
|
1510
|
+
id: internalMsg.id,
|
|
1511
|
+
response
|
|
1512
|
+
};
|
|
1513
|
+
conn.send(responseMessage);
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
const errorResponse = {
|
|
1516
|
+
type: "response",
|
|
1517
|
+
id: internalMsg.id,
|
|
1518
|
+
response: {
|
|
1519
|
+
status: 500,
|
|
1520
|
+
data: { error: error instanceof Error ? error.message : "Unknown error" }
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
conn.send(errorResponse);
|
|
1524
|
+
}
|
|
386
1525
|
}
|
|
1526
|
+
} else if (message.type === "relay-request") {
|
|
1527
|
+
const relayMsg = message;
|
|
1528
|
+
if (relayMsg.request) {
|
|
1529
|
+
try {
|
|
1530
|
+
const response = await this.messageHandler.handleRequest(conn.peer, relayMsg.request, relayMsg);
|
|
1531
|
+
const responseMessage = {
|
|
1532
|
+
type: "relay-response",
|
|
1533
|
+
id: relayMsg.id,
|
|
1534
|
+
originalTarget: relayMsg.originalTarget,
|
|
1535
|
+
relayPath: relayMsg.relayPath,
|
|
1536
|
+
forwardPath: [],
|
|
1537
|
+
response
|
|
1538
|
+
};
|
|
1539
|
+
conn.send(responseMessage);
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
const errorResponse = {
|
|
1542
|
+
type: "relay-response",
|
|
1543
|
+
id: relayMsg.id,
|
|
1544
|
+
originalTarget: relayMsg.originalTarget,
|
|
1545
|
+
relayPath: relayMsg.relayPath,
|
|
1546
|
+
forwardPath: [],
|
|
1547
|
+
response: {
|
|
1548
|
+
status: 500,
|
|
1549
|
+
data: { error: error instanceof Error ? error.message : "Unknown error" }
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
conn.send(errorResponse);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
} else if (message.type === "route-update") {
|
|
1556
|
+
this.router.handleRouteUpdate(conn.peer, message);
|
|
387
1557
|
}
|
|
388
1558
|
});
|
|
389
1559
|
conn.on("close", () => {
|
|
@@ -396,13 +1566,6 @@ var PeerJsWrapper = class {
|
|
|
396
1566
|
});
|
|
397
1567
|
});
|
|
398
1568
|
}
|
|
399
|
-
// ============== 语音/视频通话相关方法 ==============
|
|
400
|
-
/**
|
|
401
|
-
* 发起语音/视频通话
|
|
402
|
-
* @param peerId 对端设备 ID
|
|
403
|
-
* @param options 通话选项
|
|
404
|
-
* @returns Promise<CallSession> 通话会话对象
|
|
405
|
-
*/
|
|
406
1569
|
call(peerId, options) {
|
|
407
1570
|
return new Promise((resolve, reject) => {
|
|
408
1571
|
this.debugLog("PeerJsWrapper", "call", { peerId, options });
|
|
@@ -467,30 +1630,15 @@ var PeerJsWrapper = class {
|
|
|
467
1630
|
}).catch(reject);
|
|
468
1631
|
});
|
|
469
1632
|
}
|
|
470
|
-
/**
|
|
471
|
-
* 注册来电监听器
|
|
472
|
-
* @param listener 来电回调函数
|
|
473
|
-
*/
|
|
474
1633
|
onIncomingCall(listener) {
|
|
475
1634
|
this.incomingCallListeners.add(listener);
|
|
476
1635
|
}
|
|
477
|
-
/**
|
|
478
|
-
* 移除来电监听器
|
|
479
|
-
* @param listener 来电回调函数
|
|
480
|
-
*/
|
|
481
1636
|
offIncomingCall(listener) {
|
|
482
1637
|
this.incomingCallListeners.delete(listener);
|
|
483
1638
|
}
|
|
484
|
-
/**
|
|
485
|
-
* 获取当前活跃的通话
|
|
486
|
-
* @returns CallSession | null 当前通话会话,无通话时返回 null
|
|
487
|
-
*/
|
|
488
1639
|
getActiveCall() {
|
|
489
1640
|
return this.activeCall;
|
|
490
1641
|
}
|
|
491
|
-
/**
|
|
492
|
-
* 设置 MediaConnection 事件处理器
|
|
493
|
-
*/
|
|
494
1642
|
setupMediaConnectionHandlers(session, mediaConnection) {
|
|
495
1643
|
mediaConnection.on("stream", (remoteStream) => {
|
|
496
1644
|
this.debugLog("MediaConnection", "stream", { peer: mediaConnection.peer });
|
|
@@ -508,17 +1656,11 @@ var PeerJsWrapper = class {
|
|
|
508
1656
|
session.setState("ended", err.message || "Media error");
|
|
509
1657
|
});
|
|
510
1658
|
}
|
|
511
|
-
/**
|
|
512
|
-
* 清理通话资源
|
|
513
|
-
*/
|
|
514
1659
|
cleanupCall(session) {
|
|
515
1660
|
if (this.activeCall === session) {
|
|
516
1661
|
this.activeCall = null;
|
|
517
1662
|
}
|
|
518
1663
|
}
|
|
519
|
-
/**
|
|
520
|
-
* 处理来电
|
|
521
|
-
*/
|
|
522
1664
|
handleIncomingCall(mediaConnection) {
|
|
523
1665
|
this.debugLog("Peer", "call", { from: mediaConnection.peer, metadata: mediaConnection.metadata });
|
|
524
1666
|
const metadata = mediaConnection.metadata;
|
|
@@ -568,40 +1710,12 @@ var PeerJsWrapper = class {
|
|
|
568
1710
|
}
|
|
569
1711
|
});
|
|
570
1712
|
}
|
|
571
|
-
/**
|
|
572
|
-
* 注册简化处理器(直接返回数据,自动装箱)
|
|
573
|
-
* @param path 请求路径
|
|
574
|
-
* @param handler 处理器函数,接收请求数据,直接返回响应数据
|
|
575
|
-
*/
|
|
576
1713
|
registerHandler(path, handler) {
|
|
577
1714
|
this.simpleHandlers.set(path, handler);
|
|
578
1715
|
}
|
|
579
|
-
/**
|
|
580
|
-
* 注销简化处理器
|
|
581
|
-
* @param path 请求路径
|
|
582
|
-
*/
|
|
583
1716
|
unregisterHandler(path) {
|
|
584
1717
|
this.simpleHandlers.delete(path);
|
|
585
1718
|
}
|
|
586
|
-
/**
|
|
587
|
-
* 内部请求处理方法
|
|
588
|
-
* @param from 发送者的 Peer ID
|
|
589
|
-
* @param request 请求数据
|
|
590
|
-
*/
|
|
591
|
-
async handleRequest(from, request) {
|
|
592
|
-
const simpleHandler = this.simpleHandlers.get(request.path);
|
|
593
|
-
if (simpleHandler) {
|
|
594
|
-
const data = await simpleHandler(from, request.data);
|
|
595
|
-
return { status: 200, data };
|
|
596
|
-
}
|
|
597
|
-
return {
|
|
598
|
-
status: 404,
|
|
599
|
-
data: { error: `Path not found: ${request.path}` }
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
/**
|
|
603
|
-
* 关闭所有连接并销毁 Peer 实例
|
|
604
|
-
*/
|
|
605
1719
|
destroy() {
|
|
606
1720
|
this.isDestroyed = true;
|
|
607
1721
|
if (this.reconnectTimer) {
|
|
@@ -627,10 +1741,23 @@ var PeerJsWrapper = class {
|
|
|
627
1741
|
this.peerInstance.destroy();
|
|
628
1742
|
this.peerInstance = null;
|
|
629
1743
|
}
|
|
1744
|
+
if (this.router) {
|
|
1745
|
+
this.router.persist();
|
|
1746
|
+
this.router.destroy();
|
|
1747
|
+
}
|
|
630
1748
|
}
|
|
631
1749
|
};
|
|
632
1750
|
export {
|
|
633
1751
|
PeerJsWrapper,
|
|
634
|
-
VERSION
|
|
1752
|
+
VERSION,
|
|
1753
|
+
clearAllRoutingData,
|
|
1754
|
+
deleteRouteEntry,
|
|
1755
|
+
initRoutingDB,
|
|
1756
|
+
loadDirectNodes,
|
|
1757
|
+
loadRoutingTable,
|
|
1758
|
+
saveDirectNode,
|
|
1759
|
+
saveDirectNodes,
|
|
1760
|
+
saveRouteEntries,
|
|
1761
|
+
saveRouteEntry
|
|
635
1762
|
};
|
|
636
1763
|
//# sourceMappingURL=index.esm.js.map
|