sx-peerjs-http-util 1.1.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # sx-peerjs-http-util
2
2
 
3
- 一个浏览器端库,将 PeerJS 封装成简单易用的类似 HTTP 的 API,并支持语音/视频通话。
3
+ PeerJS 封装成类似 HTTP 的 API,支持语音/视频通话和自动路由。
4
4
 
5
5
  ## 在线 Demo
6
6
 
@@ -11,235 +11,54 @@
11
11
  | [语音通话](https://anarckk.github.io/sx-peerjs-http-util/demos/voice-call/index.html) | 一对一语音通话 |
12
12
  | [视频通话](https://anarckk.github.io/sx-peerjs-http-util/demos/video-call/index.html) | 一对一视频通话 |
13
13
 
14
- > **提示**:打开两个浏览器窗口,分别选择"身份1"和"身份2"即可开始通信,无需手动复制 Peer ID。
15
-
16
- ## 特性
17
-
18
- - 简单的请求-响应 API,类似 HTTP
19
- - 基于 PeerJS (WebRTC) 实现 P2P 通信
20
- - **语音/视频通话**:支持一对一语音和视频通话
21
- - TypeScript 支持
22
- - 完整的 E2E 测试
23
- - **支持 NPM 和 CDN 两种引入方式**
24
- - **自动断线重连**
25
- - **可指定或自动生成 Peer ID**
26
-
27
14
  ## 安装
28
15
 
29
- ### NPM 方式
30
-
16
+ **NPM:**
31
17
  ```bash
32
18
  npm install sx-peerjs-http-util peerjs
33
19
  ```
34
20
 
35
- ### CDN 方式
36
-
21
+ **CDN:**
37
22
  ```html
38
- <!-- UMD 版本已内置 PeerJS,只需引入一个文件 -->
39
23
  <script src="https://unpkg.com/sx-peerjs-http-util/dist/index.umd.js"></script>
40
24
  ```
41
25
 
42
- ## 使用方式
43
-
44
- ### PeerJsWrapper 类
26
+ ## 快速开始
45
27
 
46
28
  ```typescript
47
29
  import { PeerJsWrapper } from 'sx-peerjs-http-util';
48
30
 
49
- // 创建实例(不指定 ID 则自动生成 UUID)
50
31
  const wrapper = new PeerJsWrapper();
51
-
52
- // 或指定 Peer ID
53
- // const wrapper = new PeerJsWrapper('my-custom-id');
54
-
55
- // 等待连接就绪
56
32
  await wrapper.whenReady();
57
33
 
58
- // 获取 Peer ID(同步方法)
59
- const peerId = wrapper.getPeerId();
60
- console.log('My Peer ID:', peerId);
61
- ```
62
-
63
- ### 发送请求 (send)
64
-
65
- ```typescript
66
- // 发送请求到对端设备
67
- const data = await wrapper.send('remote-peer-id', '/api/hello', { name: 'world' });
68
- console.log(data); // 直接输出响应数据(自动拆箱)
69
- ```
70
-
71
- ### 注册处理器 (registerHandler)
72
-
73
- ```typescript
74
- // 服务端注册处理器
34
+ // 注册处理器
75
35
  wrapper.registerHandler('/api/hello', (from, data) => {
76
- return { message: 'hello', received: data }; // 直接返回数据,自动装箱
36
+ return { message: 'hello', received: data };
77
37
  });
78
38
 
79
- // 注销处理器
80
- wrapper.unregisterHandler('/api/hello');
39
+ // 发送请求(自动路由)
40
+ const data = await wrapper.send('remote-peer-id', '/api/hello', { name: 'world' });
81
41
  ```
82
42
 
83
- ### 语音/视频通话
43
+ ## 语音/视频通话
84
44
 
85
45
  ```typescript
86
- // 发起语音通话
87
- const callSession = await wrapper.call('remote-peer-id', { video: false });
88
-
89
- // 发起视频通话
90
- // const callSession = await wrapper.call('remote-peer-id', { video: true });
46
+ // 发起通话
47
+ const call = await wrapper.call('remote-peer-id', { video: true });
91
48
 
92
49
  // 监听来电
93
- wrapper.onIncomingCall((event) => {
94
- console.log('来电:', event.from, event.hasVideo ? '视频' : '语音');
95
-
96
- // 接听
97
- const session = await event.answer();
98
-
99
- // 或拒绝
100
- // event.reject();
50
+ wrapper.onIncomingCall(async (event) => {
51
+ const session = await event.answer(); // 接听
52
+ // 或 event.reject(); // 拒绝
101
53
  });
102
-
103
- // 获取本地媒体流(立即可用)
104
- const localStream = callSession.getLocalStream();
105
-
106
- // 远程媒体流需要等待 connected 状态
107
- callSession.onStateChange((state) => {
108
- if (state === 'connected') {
109
- const remoteStream = callSession.getRemoteStream();
110
- // 将 remoteStream 设置到 <audio> 或 <video> 元素
111
- }
112
- });
113
-
114
- // 控制通话
115
- callSession.toggleMute(); // 切换静音
116
- callSession.toggleVideo(); // 切换视频开关
117
- callSession.hangUp(); // 挂断
118
- ```
119
-
120
- ### 销毁实例 (destroy)
121
-
122
- ```typescript
123
- wrapper.destroy();
124
- ```
125
-
126
- ## API 参考
127
-
128
- ### `new PeerJsWrapper(peerId?: string, isDebug?: boolean, server?: ServerConfig)`
129
-
130
- 创建 PeerJsWrapper 实例。
131
-
132
- - `peerId` (可选): 指定 Peer ID,不提供则自动生成 UUID
133
- - `isDebug` (可选): 是否开启调试模式,开启后会打印事件日志,格式为 `{对象} {事件名} {事件变量}`
134
- - `server` (可选): 自定义信令服务器配置,不提供则使用 PeerJS 公共服务器
135
- - `host`: 服务器地址
136
- - `port`: 端口号
137
- - `path`: 路径(如 `/peerjs`)
138
- - `secure`: 是否使用 HTTPS/WSS
139
-
140
- ### `getPeerId(): string`
141
-
142
- 获取当前 Peer ID(同步方法,立即返回)。
143
-
144
- ### `whenReady(): Promise<void>`
145
-
146
- 等待 Peer 连接到信令服务器。
147
-
148
- ### `send(peerId: string, path: string, data?: unknown): Promise<unknown>`
149
-
150
- 发送请求到指定 Peer。
151
-
152
- - `peerId`: 对端设备 ID
153
- - `path`: 请求路径
154
- - `data`: 请求数据 (可选)
155
- - 返回: 响应数据(自动拆箱,只返回 data 部分)
156
-
157
- ### `registerHandler(path: string, handler: SimpleHandler): void`
158
-
159
- 注册路径处理器。
160
-
161
- - `path`: 请求路径
162
- - `handler`: 处理器函数,签名 `(from: string, data?: unknown) => Promise<unknown> | unknown`
163
-
164
- ### `unregisterHandler(path: string): void`
165
-
166
- 注销路径处理器。
167
-
168
- ### `call(peerId: string, options?: CallOptions): Promise<CallSession>`
169
-
170
- 发起语音/视频通话。
171
-
172
- - `peerId`: 对端设备 ID
173
- - `options`: 通话选项
174
- - `video`: 是否启用视频(默认 false)
175
- - `metadata`: 自定义元数据
176
- - 返回: `CallSession` 通话会话对象
177
-
178
- ### `onIncomingCall(listener: IncomingCallListener): void`
179
-
180
- 注册来电监听器。
181
-
182
- - `listener`: 监听器函数,接收 `IncomingCallEvent` 对象
183
-
184
- ### `offIncomingCall(listener: IncomingCallListener): void`
185
-
186
- 移除来电监听器。
187
-
188
- ### `getActiveCall(): CallSession | null`
189
-
190
- 获取当前活跃的通话会话。
191
-
192
- ### `destroy(): void`
193
-
194
- 关闭所有连接并销毁实例(会自动挂断活跃通话)。
195
-
196
- ## CallSession 接口
197
-
198
- 通话会话对象,用于控制通话。
199
-
200
- | 属性/方法 | 说明 |
201
- |-----------|------|
202
- | `peerId` | 对端的 Peer ID |
203
- | `hasVideo` | 是否包含视频 |
204
- | `isConnected` | 是否已连接 |
205
- | `getLocalStream()` | 获取本地媒体流 |
206
- | `getRemoteStream()` | 获取远程媒体流 |
207
- | `toggleMute()` | 切换静音状态,返回新的静音状态 |
208
- | `toggleVideo()` | 切换视频开关,返回新的视频状态 |
209
- | `hangUp()` | 挂断通话 |
210
- | `onStateChange(listener)` | 注册状态变化监听器 |
211
- | `offStateChange(listener)` | 移除状态变化监听器 |
212
-
213
- ## IncomingCallEvent 接口
214
-
215
- 来电事件对象。
216
-
217
- | 属性/方法 | 说明 |
218
- |-----------|------|
219
- | `from` | 呼叫者的 Peer ID |
220
- | `hasVideo` | 是否包含视频 |
221
- | `metadata` | 呼叫者传递的元数据 |
222
- | `answer()` | 接听来电,返回 `Promise<CallSession>` |
223
- | `reject()` | 拒绝来电 |
224
-
225
- ## E2E 测试
226
-
227
- ```bash
228
- npm run test:e2e
229
54
  ```
230
55
 
231
- ## 注意事项
56
+ ## API 文档
232
57
 
233
- - 每次请求都会创建新的 Peer 连接,请求完成后会自动清理
234
- - 请求超时时间为 30 秒
235
- - 此库仅用于浏览器环境
236
- - 需要使用 PeerJS 信令服务器(默认使用公共服务器)
237
- - CDN 版本 (UMD) 已内置 PeerJS,无需额外引入
238
- - NPM 版本需要单独安装 peerjs 依赖
239
- - **自动断线重连**:网络断开时会自动尝试重连(每秒重试一次)
240
- - **语音/视频通话**:同一时间只能有一个活跃通话,通话超时 30 秒无应答自动挂断
58
+ 完整 API 文档见 [docs/api.md](docs/api.md)
241
59
 
242
- ## 发布
60
+ ## 注意
243
61
 
244
- - NPM: https://www.npmjs.com/package/sx-peerjs-http-util
245
- - CDN: https://unpkg.com/sx-peerjs-http-util/dist/index.umd.js
62
+ - 仅限浏览器环境
63
+ - 请求超时 30 秒
64
+ - 每次请求创建新连接
@@ -83,6 +83,14 @@ export declare class MessageHandler {
83
83
  * 判断结果是否为错误响应
84
84
  */
85
85
  private isErrorResponse;
86
+ /**
87
+ * 创建连接并发送中继消息的通用方法
88
+ * @param targetId 目标节点 ID
89
+ * @param message 要发送的消息
90
+ * @param extractResponse 从响应消息中提取数据的函数
91
+ * @returns Promise<Response>
92
+ */
93
+ private createConnectionAndSend;
86
94
  /**
87
95
  * 转发中继请求到下一个节点
88
96
  * @param nextHop 下一跳节点 ID
@@ -1 +1 @@
1
- {"version":3,"file":"MessageHandler.d.ts","sourceRoot":"","sources":["../src/MessageHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE9E;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,mBAAmB;IACnB,WAAW,IAAI,MAAM,CAAC;IACtB,mBAAmB;IACnB,eAAe,IAAI,IAAI,GAAG,IAAI,CAAC;IAC/B,aAAa;IACb,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,eAAe;IACf,iBAAiB,IAAI,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAChD,aAAa;IACb,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/D,iBAAiB;IACjB,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;CACrE;AAED;;;GAGG;AACH,qBAAa,cAAc;IACzB,aAAa;IACb,OAAO,CAAC,SAAS,CAA0B;IAE3C;;;OAGG;gBACS,SAAS,EAAE,uBAAuB;IAI9C;;;;;;;OAOG;IACG,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC;IAOnG;;;;;OAKG;YACW,mBAAmB;IAQjC;;;;;;OAMG;YACW,kBAAkB;IAgDhC;;;;;;OAMG;IACG,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAUlF;;OAEG;IACH,OAAO,CAAC,eAAe;IAIvB;;;;;OAKG;YACW,YAAY;IAsD1B;;;;;;OAMG;YACW,eAAe;CAgE9B"}
1
+ {"version":3,"file":"MessageHandler.d.ts","sourceRoot":"","sources":["../src/MessageHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG9E;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,mBAAmB;IACnB,WAAW,IAAI,MAAM,CAAC;IACtB,mBAAmB;IACnB,eAAe,IAAI,IAAI,GAAG,IAAI,CAAC;IAC/B,aAAa;IACb,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,eAAe;IACf,iBAAiB,IAAI,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAChD,aAAa;IACb,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/D,iBAAiB;IACjB,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;CACrE;AAED;;;GAGG;AACH,qBAAa,cAAc;IACzB,aAAa;IACb,OAAO,CAAC,SAAS,CAA0B;IAE3C;;;OAGG;gBACS,SAAS,EAAE,uBAAuB;IAI9C;;;;;;;OAOG;IACG,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC;IAOnG;;;;;OAKG;YACW,mBAAmB;IAQjC;;;;;;OAMG;YACW,kBAAkB;IAuDhC;;;;;;OAMG;IACG,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAUlF;;OAEG;IACH,OAAO,CAAC,eAAe;IAIvB;;;;;;OAMG;IACH,OAAO,CAAC,uBAAuB;IAsD/B;;;;;OAKG;YACW,YAAY;IAa1B;;;;;;OAMG;YACW,eAAe;CA2B9B"}
@@ -47,7 +47,7 @@ export declare class PeerJsWrapper {
47
47
  /** 来电监听器集合 */
48
48
  private incomingCallListeners;
49
49
  /** 路由管理器 */
50
- private routingManager;
50
+ private router;
51
51
  /** 消息处理器 */
52
52
  private messageHandler;
53
53
  /**
@@ -58,18 +58,104 @@ export declare class PeerJsWrapper {
58
58
  * @param relayConfig 可选的中继配置
59
59
  */
60
60
  constructor(peerId?: string, isDebug?: boolean, server?: ServerConfig, relayConfig?: RelayConfig);
61
+ /**
62
+ * 创建实例并等待就绪(语法糖)
63
+ * @param peerId 可选的 Peer ID
64
+ * @param isDebug 是否开启调试模式
65
+ * @param server 可选的信令服务器配置
66
+ * @param relayConfig 可选的中继配置
67
+ * @returns Promise<PeerJsWrapper>
68
+ */
69
+ static create(peerId?: string, isDebug?: boolean, server?: ServerConfig, relayConfig?: RelayConfig): Promise<PeerJsWrapper>;
61
70
  private debugLog;
62
71
  private connect;
63
72
  private setupPeerEventHandlers;
64
73
  private scheduleReconnect;
65
74
  private reconnect;
66
75
  getPeerId(): string;
67
- whenReady(): Promise<void>;
76
+ private whenReady;
68
77
  private waitForReady;
69
- relaySend(targetId: string, path: string, data: unknown, relayNodes: string[]): Promise<unknown>;
70
78
  getRoutingTable(): Record<string, RouteEntry>;
71
79
  getKnownNodes(): string[];
80
+ /**
81
+ * 发送中继消息的辅助方法
82
+ * @param targetId 目标节点 ID
83
+ * @param message 中继消息
84
+ */
85
+ private sendRelayMessage;
86
+ /**
87
+ * 尝试直连目标节点
88
+ * @param targetId 目标节点 ID
89
+ * @param path 请求路径
90
+ * @param data 请求数据
91
+ * @param requestId 请求 ID
92
+ * @returns Promise<unknown> - 响应数据
93
+ */
94
+ private tryDirectConnect;
95
+ /**
96
+ * 通过中继节点转发请求
97
+ * @param nextHopId 下一跳节点 ID
98
+ * @param targetId 原始目标节点 ID
99
+ * @param path 请求路径
100
+ * @param data 请求数据
101
+ * @returns Promise<unknown> - 响应数据
102
+ */
103
+ private relayVia;
104
+ /**
105
+ * 自动路由发送
106
+ *
107
+ * 1. 查路由表 → 有路由 → 尝试中继 → 全部失败 → 降级直连 → 失败 → 结束
108
+ * 2. 路由表无目标 → 直连 → 失败 → 结束
109
+ * @param peerId 目标节点 ID
110
+ * @param path 请求路径
111
+ * @param data 请求数据
112
+ * @returns Promise<unknown> - 响应数据
113
+ */
72
114
  send(peerId: string, path: string, data?: unknown): Promise<unknown>;
115
+ /**
116
+ * 处理发送错误
117
+ * @param peerId 目标节点 ID
118
+ * @param error 错误对象
119
+ * @returns Promise
120
+ */
121
+ private handleSendError;
122
+ /**
123
+ * 降级到直连尝试
124
+ * @param peerId 目标节点 ID
125
+ * @param path 请求路径
126
+ * @param data 请求数据
127
+ * @param requestId 请求 ID
128
+ * @returns Promise
129
+ */
130
+ private fallbackToDirect;
131
+ /**
132
+ * 尝试通过中继链转发
133
+ * @param targetId 目标节点 ID
134
+ * @param path 请求路径
135
+ * @param data 请求数据
136
+ * @param nextHops 下一跳列表
137
+ * @param index 当前尝试的下一跳索引
138
+ * @returns Promise<unknown>
139
+ */
140
+ private tryRelayChain;
141
+ /**
142
+ * 执行路由发现
143
+ * @param targetId 目标节点 ID
144
+ * @param path 请求路径
145
+ * @param data 请求数据
146
+ * @param requestId 请求 ID
147
+ * @returns Promise<unknown>
148
+ */
149
+ private performRouteDiscovery;
150
+ /**
151
+ * 中继发送(内部方法,不对外暴露)
152
+ * @param targetId 目标节点 ID
153
+ * @param path 请求路径
154
+ * @param data 请求数据
155
+ * @param relayNodes 手动指定的中继节点(可选,不指定则自动路由)
156
+ * @returns Promise<unknown>
157
+ */
158
+ private relaySend;
73
159
  private setupIncomingConnectionHandler;
74
160
  call(peerId: string, options?: CallOptions): Promise<CallSession>;
75
161
  onIncomingCall(listener: IncomingCallListener): void;
@@ -1 +1 @@
1
- {"version":3,"file":"PeerJsWrapper.d.ts","sourceRoot":"","sources":["../src/PeerJsWrapper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,OAAO,KAAK,EACV,OAAO,EACP,QAAQ,EACR,aAAa,EACb,WAAW,EACX,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,UAAU,EACV,WAAW,EACX,YAAY,EACZ,YAAY,EACb,MAAM,SAAS,CAAC;AAIjB,eAAO,MAAM,OAAO,QAAc,CAAC;AAoCnC;;;GAGG;AACH,qBAAa,aAAa;IACxB,iBAAiB;IACjB,OAAO,CAAC,QAAQ,CAAS;IACzB,gBAAgB;IAChB,OAAO,CAAC,YAAY,CAAqB;IACzC,kBAAkB;IAClB,OAAO,CAAC,WAAW,CAA6B;IAChD,2BAA2B;IAC3B,OAAO,CAAC,eAAe,CAAqC;IAC5D,eAAe;IACf,OAAO,CAAC,cAAc,CAAoC;IAC1D,YAAY;IACZ,OAAO,CAAC,cAAc,CAA8C;IACpE,YAAY;IACZ,OAAO,CAAC,WAAW,CAAS;IAC5B,eAAe;IACf,OAAO,CAAC,OAAO,CAAU;IACzB,YAAY;IACZ,OAAO,CAAC,YAAY,CAAC,CAAe;IACpC,cAAc;IACd,OAAO,CAAC,UAAU,CAAgC;IAClD,cAAc;IACd,OAAO,CAAC,qBAAqB,CAAmC;IAEhE,YAAY;IACZ,OAAO,CAAC,cAAc,CAAiB;IACvC,YAAY;IACZ,OAAO,CAAC,cAAc,CAAiB;IAEvC;;;;;;OAMG;gBACS,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,YAAY,EAAE,WAAW,CAAC,EAAE,WAAW;IAsBhG,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,OAAO;IAUf,OAAO,CAAC,sBAAsB;IAuC9B,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,SAAS;IAgBjB,SAAS,IAAI,MAAM;IAInB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAI1B,OAAO,CAAC,YAAY;IA6BpB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IA6EhG,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC;IAI7C,aAAa,IAAI,MAAM,EAAE;IAIzB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAkFpE,OAAO,CAAC,8BAA8B;IA2FtC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IA+EjE,cAAc,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI;IAIpD,eAAe,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI;IAIrD,aAAa,IAAI,WAAW,GAAG,IAAI;IAInC,OAAO,CAAC,4BAA4B;IAoBpC,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,kBAAkB;IA6D1B,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI;IAI3D,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIrC,OAAO,IAAI,IAAI;CAgChB;AAED,YAAY,EACV,OAAO,EACP,QAAQ,EACR,aAAa,EACb,WAAW,EACX,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,UAAU,EACV,WAAW,EACX,YAAY,EACb,CAAC"}
1
+ {"version":3,"file":"PeerJsWrapper.d.ts","sourceRoot":"","sources":["../src/PeerJsWrapper.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Router - 路由管理器
3
+ *
4
+ * 负责中继通信的核心功能:
5
+ * - 维护直连节点列表及延迟(directNodes)
6
+ * - 维护路由表(routingTable):目标节点 -> 多个下一跳(含延迟)
7
+ * - 广播路由更新到邻居节点
8
+ * - 路由发现:当直连和路由表都失败时,广播询问谁能连通目标
9
+ * - 处理路由查询和响应
10
+ *
11
+ * 路由机制:
12
+ * 1. 每次成功通信后,记录对方节点为直连节点并测量延迟
13
+ * 2. 成功后广播路由更新,告知对方自己可达的节点
14
+ * 3. 收到路由更新后,合并到本地路由表
15
+ * 4. 直连失败且路由表为空时,执行路由发现广播
16
+ */
17
+ import type { Peer } from 'peerjs';
18
+ import type { RouteEntry, NextHop, DirectNodeLatency, RelayConfig, RelayMessage } from './types';
19
+ /**
20
+ * 路由管理器回调接口
21
+ */
22
+ export interface RoutingCallbacks {
23
+ /** 获取本地 Peer ID */
24
+ getMyPeerId(): string;
25
+ /** 获取 PeerJS 实例 */
26
+ getPeerInstance(): Peer | null;
27
+ /** 调试日志函数 */
28
+ debugLog: (obj: string, event: string, data?: unknown) => void;
29
+ /** 发送中继消息 */
30
+ sendRelayMessage(targetId: string, message: RelayMessage): Promise<void>;
31
+ /** 处理路由查询响应 */
32
+ onRouteDiscoveryResponse?: (targetId: string, latency: number) => void;
33
+ }
34
+ /**
35
+ * 路由器类
36
+ * 负责维护路由表、节点发现和自动路由选择
37
+ */
38
+ export declare class Router {
39
+ /** 路由表:target -> RouteEntry */
40
+ private routingTable;
41
+ /** 直连节点及延迟列表 */
42
+ private directNodes;
43
+ /** 中继配置 */
44
+ private relayConfig;
45
+ /** 回调函数集合 */
46
+ private callbacks;
47
+ /** 等待路由发现响应的 pending 队列 */
48
+ private pendingRouteQueries;
49
+ /** 定时清理定时器 */
50
+ private cleanupTimer;
51
+ /** 周期广播定时器 */
52
+ private broadcastTimer;
53
+ /**
54
+ * 创建路由管理器
55
+ * @param callbacks 回调函数集合
56
+ * @param relayConfig 中继配置(可选)
57
+ */
58
+ constructor(callbacks: RoutingCallbacks, relayConfig?: RelayConfig);
59
+ /**
60
+ * 初始化路由管理器(从 IndexedDB 加载数据并启动定时任务)
61
+ */
62
+ init(): Promise<void>;
63
+ /**
64
+ * 从 IndexedDB 加载路由数据
65
+ */
66
+ private loadFromDB;
67
+ /**
68
+ * 启动定时维护任务
69
+ */
70
+ private startMaintenanceTasks;
71
+ /**
72
+ * 清理过期路由条目
73
+ */
74
+ private cleanupExpiredEntries;
75
+ /**
76
+ * 持久化路由表到 IndexedDB
77
+ */
78
+ persist(): Promise<void>;
79
+ /**
80
+ * 销毁路由管理器(清理定时器)
81
+ */
82
+ destroy(): void;
83
+ /**
84
+ * 记录成功的直连通信
85
+ * @param nodeId 节点 ID
86
+ * @param latency 延迟(毫秒)
87
+ */
88
+ recordDirectNode(nodeId: string, latency: number): void;
89
+ /**
90
+ * 获取直连节点列表(按延迟升序)
91
+ * @returns 直连节点列表
92
+ */
93
+ getDirectNodes(): DirectNodeLatency[];
94
+ /**
95
+ * 检查是否可以直连目标节点
96
+ * @param targetId 目标节点 ID
97
+ * @returns 是否可以直连
98
+ */
99
+ canReachDirectly(targetId: string): boolean;
100
+ /**
101
+ * 获取到直连节点的延迟
102
+ * @param nodeId 节点 ID
103
+ * @returns 延迟(毫秒),如果不存在返回 null
104
+ */
105
+ getDirectLatency(nodeId: string): number | null;
106
+ /**
107
+ * 移除失效的路由(通信失败时调用)
108
+ * @param nodeId 失效的节点 ID
109
+ */
110
+ removeRoute(nodeId: string): void;
111
+ /**
112
+ * 检查路由表是否为空
113
+ * @returns 是否为空
114
+ */
115
+ isRoutingTableEmpty(): boolean;
116
+ /**
117
+ * 记录成功通信的节点(兼容旧接口)
118
+ * @param nodeId 节点 ID
119
+ */
120
+ recordSuccessfulNode(nodeId: string): void;
121
+ /**
122
+ * 广播路由更新
123
+ * 向所有直连节点发送路由更新消息,告知它们本节点可达的节点列表
124
+ */
125
+ broadcastRouteUpdate(): Promise<void>;
126
+ /**
127
+ * 获取本节点可达的节点列表
128
+ * @returns 可达节点数组(直连节点 + 自己)
129
+ */
130
+ private getReachableNodes;
131
+ /**
132
+ * 发送路由更新到指定节点
133
+ * @param targetId 目标节点 ID
134
+ * @param reachableNodes 可达的节点列表
135
+ */
136
+ private sendRouteUpdate;
137
+ /**
138
+ * 处理收到的路由更新
139
+ * 合并对端发来的可达节点信息到本地路由表
140
+ * @param fromPeerId 发送路由更新的节点 ID
141
+ * @param message 路由更新消息
142
+ */
143
+ handleRouteUpdate(fromPeerId: string, message: RelayMessage): void;
144
+ /**
145
+ * 执行路由发现广播
146
+ * 当直连和路由表都失败时,向所有直连节点广播询问谁能连通目标
147
+ * @param targetId 目标节点 ID
148
+ * @returns 路由条目(如果发现)
149
+ */
150
+ discoverRoute(targetId: string): Promise<RouteEntry | null>;
151
+ /**
152
+ * 处理路由查询消息
153
+ * @param fromPeerId 发送查询的节点
154
+ * @param message 路由查询消息
155
+ */
156
+ handleRouteQuery(fromPeerId: string, message: RelayMessage): void;
157
+ /**
158
+ * 处理路由查询响应
159
+ * @param fromPeerId 响应者节点
160
+ * @param message 路由响应消息
161
+ */
162
+ handleRouteResponse(fromPeerId: string, message: RelayMessage): void;
163
+ /**
164
+ * 查找到目标节点的下一跳
165
+ * @param targetId 目标节点 ID
166
+ * @returns 下一跳信息,如果没有则返回 null
167
+ */
168
+ findNextHopToTarget(targetId: string): NextHop | null;
169
+ /**
170
+ * 获取到目标节点的所有下一跳(按延迟升序)
171
+ * @param targetId 目标节点 ID
172
+ * @returns 下一跳列表
173
+ */
174
+ getNextHopsToTarget(targetId: string): NextHop[];
175
+ /**
176
+ * 获取路由表
177
+ * @returns 路由表对象
178
+ */
179
+ getRoutingTable(): Record<string, RouteEntry>;
180
+ /**
181
+ * 获取已知节点列表(兼容旧接口)
182
+ * @returns 节点 ID 数组
183
+ */
184
+ getKnownNodes(): string[];
185
+ }
186
+ //# sourceMappingURL=Router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Router.d.ts","sourceRoot":"","sources":["../src/Router.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * RoutingDB.ts - 路由表 IndexedDB 持久化模块
3
+ *
4
+ * 提供路由表的持久化存储功能,使用 IndexedDB 而非 localStorage
5
+ * 以支持大规模路由表存储
6
+ *
7
+ * 数据库结构:
8
+ * - peerjs-routing-db (版本 1)
9
+ * - routing-table: 路由表条目 (keyPath: target)
10
+ * - direct-nodes: 直连节点 (keyPath: nodeId)
11
+ */
12
+ import type { RouteEntry, DirectNodeLatency } from './types';
13
+ /**
14
+ * 初始化数据库
15
+ * @returns Promise<void>
16
+ */
17
+ export declare function initRoutingDB(): Promise<void>;
18
+ /**
19
+ * 保存路由表条目
20
+ * @param entry 路由表条目
21
+ */
22
+ export declare function saveRouteEntry(entry: RouteEntry): Promise<void>;
23
+ /**
24
+ * 批量保存路由表条目
25
+ * @param entries 路由表条目数组
26
+ */
27
+ export declare function saveRouteEntries(entries: RouteEntry[]): Promise<void>;
28
+ /**
29
+ * 删除路由表条目
30
+ * @param target 目标节点 ID
31
+ */
32
+ export declare function deleteRouteEntry(target: string): Promise<void>;
33
+ /**
34
+ * 加载全部路由表
35
+ * @returns Promise<RouteEntry[]>
36
+ */
37
+ export declare function loadRoutingTable(): Promise<RouteEntry[]>;
38
+ /**
39
+ * 清理过期路由表条目
40
+ * @param maxAgeMs 最大保留时间(毫秒),默认 5 分钟
41
+ * @returns Promise<number> 删除的条目数量
42
+ */
43
+ export declare function cleanupExpiredRoutes(maxAgeMs?: number): Promise<number>;
44
+ /**
45
+ * 保存直连节点
46
+ * @param node 直连节点
47
+ */
48
+ export declare function saveDirectNode(node: DirectNodeLatency): Promise<void>;
49
+ /**
50
+ * 批量保存直连节点
51
+ * @param nodes 直连节点数组
52
+ */
53
+ export declare function saveDirectNodes(nodes: DirectNodeLatency[]): Promise<void>;
54
+ /**
55
+ * 加载全部直连节点
56
+ * @returns Promise<DirectNodeLatency[]>
57
+ */
58
+ export declare function loadDirectNodes(): Promise<DirectNodeLatency[]>;
59
+ /**
60
+ * 删除直连节点
61
+ * @param nodeId 节点 ID
62
+ */
63
+ export declare function deleteDirectNode(nodeId: string): Promise<void>;
64
+ /**
65
+ * 清理过期直连节点
66
+ * @param maxAgeMs 最大保留时间(毫秒),默认 5 分钟
67
+ * @returns Promise<number> 删除的节点数量
68
+ */
69
+ export declare function cleanupExpiredNodes(maxAgeMs?: number): Promise<number>;
70
+ /**
71
+ * 清除全部路由数据
72
+ * @returns Promise<void>
73
+ */
74
+ export declare function clearAllRoutingData(): Promise<void>;
75
+ //# sourceMappingURL=RoutingDB.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RoutingDB.d.ts","sourceRoot":"","sources":["../src/RoutingDB.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AA+E7D;;;GAGG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAErE;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAc3E;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpE;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAE9D;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,QAAQ,GAAE,MAA4B,GAAG,OAAO,CAAC,MAAM,CAAC,CAwBlG;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAE3E;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAc/E;AAED;;;GAGG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAEpE;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpE;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,GAAE,MAA4B,GAAG,OAAO,CAAC,MAAM,CAAC,CAwBjG;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAkBzD"}