@threejs-shared/protobuf 0.1.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/package.json +25 -0
- package/src/ProtoBufManager.ts +311 -0
- package/src/ProtobufPlaybackClient.ts +238 -0
- package/src/ProtobufWebSocketClient.ts +125 -0
- package/src/TimerManager.ts +103 -0
- package/src/WebSocketManager.ts +352 -0
- package/src/index.ts +13 -0
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@threejs-shared/protobuf",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Protobuf manager for loading and parsing proto files",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"import": "./src/index.ts",
|
|
12
|
+
"default": "./src/index.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"protobufjs": "^7.0.0",
|
|
20
|
+
"@threejs-shared/xodr": "0.1.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc --noEmit"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// import protobuf from 'protobufjs'
|
|
2
|
+
|
|
3
|
+
// /**
|
|
4
|
+
// * Protobuf 管理器,用于加载和缓存 proto 文件
|
|
5
|
+
// */
|
|
6
|
+
// export class ProtoBufManager {
|
|
7
|
+
// private protoCache: Map<string, protobuf.Type> = new Map()
|
|
8
|
+
|
|
9
|
+
// /**
|
|
10
|
+
// * 加载 proto 文件并获取指定的消息类型
|
|
11
|
+
// * @param protoFilePath proto 文件的路径(URL)
|
|
12
|
+
// * @param messageType 消息类型名称(例如:'protobuf.WsFrameData')
|
|
13
|
+
// * @returns Promise<protobuf.Type> 解析后的消息类型
|
|
14
|
+
// */
|
|
15
|
+
// async loadProto(protoFilePath: string, messageType: string): Promise<protobuf.Type> {
|
|
16
|
+
// const cacheKey = `${protoFilePath}-${messageType}`
|
|
17
|
+
// if (this.protoCache.has(cacheKey)) {
|
|
18
|
+
// // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
19
|
+
// return this.protoCache.get(cacheKey)!
|
|
20
|
+
// }
|
|
21
|
+
// try {
|
|
22
|
+
// const response = await fetch(protoFilePath)
|
|
23
|
+
// if (!response.ok) {
|
|
24
|
+
// throw new Error(`Failed to fetch proto file: ${response.statusText}`)
|
|
25
|
+
// }
|
|
26
|
+
// const content = await response.text()
|
|
27
|
+
// const root = protobuf.parse(content).root
|
|
28
|
+
// const messageTypeDefinition = root.lookupType(messageType)
|
|
29
|
+
// if (!messageTypeDefinition) {
|
|
30
|
+
// throw new Error(`Message type '${messageType}' not found in proto file`)
|
|
31
|
+
// }
|
|
32
|
+
// this.protoCache.set(cacheKey, messageTypeDefinition)
|
|
33
|
+
// return messageTypeDefinition
|
|
34
|
+
// } catch (error) {
|
|
35
|
+
// console.error('Error loading proto file:', error)
|
|
36
|
+
// throw error
|
|
37
|
+
// }
|
|
38
|
+
// }
|
|
39
|
+
|
|
40
|
+
// /**
|
|
41
|
+
// * 清除缓存
|
|
42
|
+
// * @param protoFilePath 可选的 proto 文件路径,如果提供则只清除该文件的缓存,否则清除所有缓存
|
|
43
|
+
// */
|
|
44
|
+
// clearCache(protoFilePath?: string): void {
|
|
45
|
+
// if (protoFilePath) {
|
|
46
|
+
// const keysToDelete: string[] = []
|
|
47
|
+
// this.protoCache.forEach((_, key) => {
|
|
48
|
+
// if (key.startsWith(protoFilePath)) {
|
|
49
|
+
// keysToDelete.push(key)
|
|
50
|
+
// }
|
|
51
|
+
// })
|
|
52
|
+
// keysToDelete.forEach(key => this.protoCache.delete(key))
|
|
53
|
+
// } else {
|
|
54
|
+
// this.protoCache.clear()
|
|
55
|
+
// }
|
|
56
|
+
// }
|
|
57
|
+
|
|
58
|
+
// /**
|
|
59
|
+
// * 获取缓存的大小
|
|
60
|
+
// */
|
|
61
|
+
// getCacheSize(): number {
|
|
62
|
+
// return this.protoCache.size
|
|
63
|
+
// }
|
|
64
|
+
// }
|
|
65
|
+
|
|
66
|
+
import protobuf from 'protobufjs'
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* ProtoBufManager 配置选项
|
|
70
|
+
*/
|
|
71
|
+
export interface ProtoBufManagerOptions {
|
|
72
|
+
/** 是否启用日志 */
|
|
73
|
+
enableLog?: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Protobuf 管理器,用于加载和缓存 proto 文件
|
|
78
|
+
*
|
|
79
|
+
* 特性:
|
|
80
|
+
* - 自动缓存已加载的 proto 类型,避免重复加载
|
|
81
|
+
* - 并发请求控制,同一 proto 文件的多次请求会共享同一个 Promise
|
|
82
|
+
* - 完善的错误处理和参数验证
|
|
83
|
+
* - 可配置的日志输出
|
|
84
|
+
*/
|
|
85
|
+
export class ProtoBufManager {
|
|
86
|
+
/** 已加载的 proto 类型缓存 */
|
|
87
|
+
private protoCache: Map<string, protobuf.Type> = new Map()
|
|
88
|
+
/** 正在加载的 Promise 缓存,用于并发控制 */
|
|
89
|
+
private loadingPromises: Map<string, Promise<protobuf.Type>> = new Map()
|
|
90
|
+
/** 配置选项 */
|
|
91
|
+
private options: Required<ProtoBufManagerOptions>
|
|
92
|
+
|
|
93
|
+
constructor(options?: ProtoBufManagerOptions) {
|
|
94
|
+
this.options = {
|
|
95
|
+
enableLog: options?.enableLog ?? true,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 加载 proto 文件并获取指定的消息类型
|
|
101
|
+
*
|
|
102
|
+
* @param protoFilePath proto 文件的路径(URL)
|
|
103
|
+
* @param messageType 消息类型名称(例如:'protobuf.WsFrameData' 或 'SimulationMonitor.SimulationMonitorMsg')
|
|
104
|
+
* @returns Promise<protobuf.Type> 解析后的消息类型
|
|
105
|
+
* @throws {Error} 如果参数无效、文件加载失败、解析失败或消息类型不存在
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* const manager = new ProtoBufManager()
|
|
110
|
+
* const messageType = await manager.loadProto('/proto/SimulationMonitor.proto', 'SimulationMonitor.SimulationMonitorMsg')
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
async loadProto(protoFilePath: string, messageType: string): Promise<protobuf.Type> {
|
|
114
|
+
// 参数验证
|
|
115
|
+
if (!protoFilePath || typeof protoFilePath !== 'string') {
|
|
116
|
+
throw new Error('protoFilePath must be a non-empty string')
|
|
117
|
+
}
|
|
118
|
+
if (!messageType || typeof messageType !== 'string') {
|
|
119
|
+
throw new Error('messageType must be a non-empty string')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cacheKey = `${protoFilePath}-${messageType}`
|
|
123
|
+
|
|
124
|
+
// 如果已缓存,直接返回
|
|
125
|
+
if (this.protoCache.has(cacheKey)) {
|
|
126
|
+
this.log(`Using cached proto type: ${messageType}`)
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
128
|
+
return this.protoCache.get(cacheKey)!
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 如果正在加载,返回同一个 Promise(并发控制)
|
|
132
|
+
if (this.loadingPromises.has(cacheKey)) {
|
|
133
|
+
this.log(`Proto file is already loading: ${protoFilePath}, waiting...`)
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
135
|
+
return this.loadingPromises.get(cacheKey)!
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 创建加载 Promise
|
|
139
|
+
const loadPromise = this.loadProtoInternal(protoFilePath, messageType, cacheKey)
|
|
140
|
+
this.loadingPromises.set(cacheKey, loadPromise)
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const result = await loadPromise
|
|
144
|
+
return result
|
|
145
|
+
} finally {
|
|
146
|
+
// 加载完成后移除 Promise 缓存
|
|
147
|
+
this.loadingPromises.delete(cacheKey)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 内部加载方法
|
|
153
|
+
*/
|
|
154
|
+
private async loadProtoInternal(
|
|
155
|
+
protoFilePath: string,
|
|
156
|
+
messageType: string,
|
|
157
|
+
cacheKey: string
|
|
158
|
+
): Promise<protobuf.Type> {
|
|
159
|
+
try {
|
|
160
|
+
this.log(`Loading proto file: ${protoFilePath}`)
|
|
161
|
+
|
|
162
|
+
// 1. 获取 proto 文件内容
|
|
163
|
+
let response: Response
|
|
164
|
+
try {
|
|
165
|
+
response = await fetch(protoFilePath)
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Failed to fetch proto file from "${protoFilePath}": ${error instanceof Error ? error.message : String(error)}`
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Failed to fetch proto file: HTTP ${response.status} ${response.statusText}`
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 2. 读取文件内容
|
|
179
|
+
let content: string
|
|
180
|
+
try {
|
|
181
|
+
content = await response.text()
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Failed to read proto file content: ${error instanceof Error ? error.message : String(error)}`
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!content || content.trim().length === 0) {
|
|
189
|
+
throw new Error('Proto file is empty')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 3. 解析 proto 文件
|
|
193
|
+
let root: protobuf.Root
|
|
194
|
+
try {
|
|
195
|
+
root = protobuf.parse(content).root
|
|
196
|
+
} catch (error) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Failed to parse proto file: ${error instanceof Error ? error.message : String(error)}`
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 4. 查找消息类型
|
|
203
|
+
const messageTypeDefinition = root.lookupType(messageType)
|
|
204
|
+
if (!messageTypeDefinition) {
|
|
205
|
+
// 尝试列出可用的类型,帮助调试
|
|
206
|
+
const availableTypes: string[] = []
|
|
207
|
+
root.nestedArray.forEach((nested) => {
|
|
208
|
+
if (nested instanceof protobuf.Type) {
|
|
209
|
+
availableTypes.push(nested.fullName)
|
|
210
|
+
} else if (nested instanceof protobuf.Namespace) {
|
|
211
|
+
nested.nestedArray.forEach((nestedType) => {
|
|
212
|
+
if (nestedType instanceof protobuf.Type) {
|
|
213
|
+
availableTypes.push(nestedType.fullName)
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const availableTypesHint = availableTypes.length > 0
|
|
220
|
+
? ` Available types: ${availableTypes.slice(0, 10).join(', ')}${availableTypes.length > 10 ? '...' : ''}`
|
|
221
|
+
: ''
|
|
222
|
+
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Message type "${messageType}" not found in proto file.${availableTypesHint}`
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 5. 缓存结果
|
|
229
|
+
this.protoCache.set(cacheKey, messageTypeDefinition)
|
|
230
|
+
this.log(`Successfully loaded proto type: ${messageType}`)
|
|
231
|
+
|
|
232
|
+
return messageTypeDefinition
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
235
|
+
this.log(`Error loading proto file: ${errorMessage}`, 'error')
|
|
236
|
+
|
|
237
|
+
// 重新抛出错误,但使用更友好的错误信息
|
|
238
|
+
throw error instanceof Error ? error : new Error(errorMessage)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 清除缓存
|
|
244
|
+
* @param protoFilePath 可选的 proto 文件路径,如果提供则只清除该文件的缓存,否则清除所有缓存
|
|
245
|
+
*/
|
|
246
|
+
clearCache(protoFilePath?: string): void {
|
|
247
|
+
if (protoFilePath) {
|
|
248
|
+
const keysToDelete: string[] = []
|
|
249
|
+
this.protoCache.forEach((_, key) => {
|
|
250
|
+
if (key.startsWith(protoFilePath)) {
|
|
251
|
+
keysToDelete.push(key)
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
keysToDelete.forEach((key) => {
|
|
255
|
+
this.protoCache.delete(key)
|
|
256
|
+
this.log(`Cleared cache for: ${key}`)
|
|
257
|
+
})
|
|
258
|
+
this.log(`Cleared ${keysToDelete.length} cache entries for proto file: ${protoFilePath}`)
|
|
259
|
+
} else {
|
|
260
|
+
const size = this.protoCache.size
|
|
261
|
+
this.protoCache.clear()
|
|
262
|
+
this.log(`Cleared all ${size} cache entries`)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 获取缓存的大小
|
|
268
|
+
*/
|
|
269
|
+
getCacheSize(): number {
|
|
270
|
+
return this.protoCache.size
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 检查指定 proto 类型是否已缓存
|
|
275
|
+
* @param protoFilePath proto 文件路径
|
|
276
|
+
* @param messageType 消息类型名称
|
|
277
|
+
* @returns 是否已缓存
|
|
278
|
+
*/
|
|
279
|
+
isCached(protoFilePath: string, messageType: string): boolean {
|
|
280
|
+
const cacheKey = `${protoFilePath}-${messageType}`
|
|
281
|
+
return this.protoCache.has(cacheKey)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 获取所有已缓存的 proto 文件路径
|
|
286
|
+
* @returns 已缓存的 proto 文件路径数组
|
|
287
|
+
*/
|
|
288
|
+
getCachedProtoFiles(): string[] {
|
|
289
|
+
const protoFiles = new Set<string>()
|
|
290
|
+
this.protoCache.forEach((_, key) => {
|
|
291
|
+
const protoFilePath = key.split('-').slice(0, -1).join('-')
|
|
292
|
+
protoFiles.add(protoFilePath)
|
|
293
|
+
})
|
|
294
|
+
return Array.from(protoFiles)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 日志输出
|
|
299
|
+
*/
|
|
300
|
+
private log(message: string, level: 'log' | 'error' = 'log'): void {
|
|
301
|
+
if (!this.options.enableLog) return
|
|
302
|
+
|
|
303
|
+
const prefix = '[ProtoBufManager]'
|
|
304
|
+
if (level === 'error') {
|
|
305
|
+
console.error(prefix, message)
|
|
306
|
+
} else {
|
|
307
|
+
console.log(prefix, message)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { ProtoBufManager } from './ProtoBufManager'
|
|
2
|
+
import { TimerManager } from './TimerManager'
|
|
3
|
+
import type { ProtoBufManagerOptions } from './ProtoBufManager'
|
|
4
|
+
import type { LoadXodrOptions } from '@threejs-shared/xodr'
|
|
5
|
+
import { loadXodr } from '@threejs-shared/xodr'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Protobuf 回放客户端回调函数
|
|
9
|
+
*/
|
|
10
|
+
export interface ProtobufPlaybackCallbacks {
|
|
11
|
+
/** 每帧数据回调 */
|
|
12
|
+
onFrame?: (frameData: any) => void
|
|
13
|
+
/** 播放进度回调(当前秒数) */
|
|
14
|
+
onProcess?: (currentSecond: number) => void
|
|
15
|
+
/** 播放完成回调 */
|
|
16
|
+
onComplete?: () => void
|
|
17
|
+
/** 错误回调 */
|
|
18
|
+
onError?: (error: Error | any) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Protobuf 回放客户端配置
|
|
23
|
+
*/
|
|
24
|
+
export interface ProtobufPlaybackClientConfig {
|
|
25
|
+
/** Proto 文件路径 */
|
|
26
|
+
protoPath: string
|
|
27
|
+
/** 消息类型名称(例如:'SimulationMonitor.SimulationMonitorBag') */
|
|
28
|
+
messageType: string
|
|
29
|
+
/** 文件下载配置(URL、请求头等) */
|
|
30
|
+
fileOptions: LoadXodrOptions
|
|
31
|
+
/** 播放频率(帧/秒,FPS),默认 50。例如:50 表示每秒播放 50 帧,即每 20ms 触发一次帧回调 */
|
|
32
|
+
frequency?: number
|
|
33
|
+
/** 每帧返回的数据量,默认 1。例如:1 表示每次 onFrame 回调接收 1 个帧数据对象;2 表示每次接收 2 个帧数据对象 */
|
|
34
|
+
frameSize?: number
|
|
35
|
+
/** ProtoBuf 管理器选项 */
|
|
36
|
+
protoBufOptions?: ProtoBufManagerOptions
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Protobuf 回放客户端
|
|
41
|
+
*
|
|
42
|
+
* 这是一个高级封装类,将 Protobuf 文件下载、解码和 TimerManager 的使用流程封装起来,
|
|
43
|
+
* 简化业务代码中的使用。业务代码只需要配置参数和定义回调函数即可。
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const client = new ProtobufPlaybackClient({
|
|
48
|
+
* protoPath: '/proto/SimulationMonitor.proto',
|
|
49
|
+
* messageType: 'SimulationMonitor.SimulationMonitorBag',
|
|
50
|
+
* fileOptions: {
|
|
51
|
+
* url: '/api/simpro/simtask/pb/download/?task_id=14532&scene_id=1276197',
|
|
52
|
+
* responseType: 'arrayBuffer',
|
|
53
|
+
* useStreaming: false,
|
|
54
|
+
* header: {
|
|
55
|
+
* Authorization: `JWT ${token}`,
|
|
56
|
+
* 'X-Project-Id': projectId,
|
|
57
|
+
* }
|
|
58
|
+
* },
|
|
59
|
+
* frequency: 50,
|
|
60
|
+
* })
|
|
61
|
+
*
|
|
62
|
+
* await client.loadAndPlay({
|
|
63
|
+
* onFrame: (frameData) => {
|
|
64
|
+
* console.log('Frame data:', frameData)
|
|
65
|
+
* },
|
|
66
|
+
* onProcess: (currentSecond) => {
|
|
67
|
+
* console.log('Current second:', currentSecond)
|
|
68
|
+
* },
|
|
69
|
+
* onComplete: () => {
|
|
70
|
+
* console.log('Playback completed')
|
|
71
|
+
* },
|
|
72
|
+
* onError: (error) => {
|
|
73
|
+
* console.error('Error:', error)
|
|
74
|
+
* }
|
|
75
|
+
* })
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export class ProtobufPlaybackClient {
|
|
79
|
+
private protoBufManager: ProtoBufManager
|
|
80
|
+
private timerManager: TimerManager | null = null
|
|
81
|
+
private config: ProtobufPlaybackClientConfig
|
|
82
|
+
|
|
83
|
+
constructor(config: ProtobufPlaybackClientConfig) {
|
|
84
|
+
this.config = config
|
|
85
|
+
this.protoBufManager = new ProtoBufManager(config.protoBufOptions)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 加载 Protobuf 文件并开始播放
|
|
90
|
+
* @param callbacks 事件回调函数
|
|
91
|
+
* @returns Promise<void>
|
|
92
|
+
*/
|
|
93
|
+
async loadAndPlay(callbacks?: ProtobufPlaybackCallbacks): Promise<void> {
|
|
94
|
+
try {
|
|
95
|
+
// 1. 下载文件
|
|
96
|
+
const fileContent = await loadXodr(this.config.fileOptions) as ArrayBuffer | Uint8Array
|
|
97
|
+
|
|
98
|
+
// 2. 加载 Proto 文件
|
|
99
|
+
const protobufType = await this.protoBufManager.loadProto(
|
|
100
|
+
this.config.protoPath,
|
|
101
|
+
this.config.messageType
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
// 3. 解码文件内容
|
|
105
|
+
let decodedData: any
|
|
106
|
+
try {
|
|
107
|
+
decodedData = protobufType.decode(fileContent as Uint8Array)
|
|
108
|
+
} catch (decodeError) {
|
|
109
|
+
// 如果解码失败,尝试解析为 JSON 错误消息
|
|
110
|
+
try {
|
|
111
|
+
const decoder = new TextDecoder('utf-8')
|
|
112
|
+
const rawText = decoder.decode(fileContent as ArrayBuffer)
|
|
113
|
+
const errorData = JSON.parse(rawText)
|
|
114
|
+
const error = new Error(errorData?.err || 'Failed to decode protobuf file')
|
|
115
|
+
callbacks?.onError?.(error)
|
|
116
|
+
throw error
|
|
117
|
+
} catch (jsonError) {
|
|
118
|
+
const error = decodeError instanceof Error
|
|
119
|
+
? decodeError
|
|
120
|
+
: new Error('Failed to decode protobuf file')
|
|
121
|
+
callbacks?.onError?.(error)
|
|
122
|
+
throw error
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 4. 创建 TimerManager 并开始播放
|
|
127
|
+
// frequency: 播放频率(帧/秒,FPS),默认 50,表示每秒播放 50 帧数据
|
|
128
|
+
// 例如:frequency = 50 表示每 20ms(1000ms / 50)触发一次帧回调
|
|
129
|
+
const frequency = this.config.frequency || 50
|
|
130
|
+
// frameSize: 每帧返回的数据量,默认 1,表示每次回调返回 1 个帧数据对象
|
|
131
|
+
// 例如:frameSize = 1 表示每次 onFrame 回调接收 1 个帧数据;frameSize = 2 表示每次接收 2 个帧数据
|
|
132
|
+
const frameSize = this.config.frameSize || 1
|
|
133
|
+
|
|
134
|
+
if (!decodedData.frames || !Array.isArray(decodedData.frames)) {
|
|
135
|
+
throw new Error('Decoded data does not contain a valid "frames" array')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.timerManager = new TimerManager(decodedData.frames, frequency, frameSize)
|
|
139
|
+
|
|
140
|
+
this.timerManager.connect(
|
|
141
|
+
(frameData: any) => {
|
|
142
|
+
callbacks?.onFrame?.(frameData)
|
|
143
|
+
},
|
|
144
|
+
(currentSecond: number) => {
|
|
145
|
+
callbacks?.onProcess?.(currentSecond)
|
|
146
|
+
},
|
|
147
|
+
() => {
|
|
148
|
+
callbacks?.onComplete?.()
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const err = error instanceof Error ? error : new Error(String(error))
|
|
153
|
+
callbacks?.onError?.(err)
|
|
154
|
+
throw err
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 开始播放(如果已加载)
|
|
160
|
+
*/
|
|
161
|
+
start(): void {
|
|
162
|
+
if (!this.timerManager) {
|
|
163
|
+
throw new Error('TimerManager is not initialized. Please call loadAndPlay() first.')
|
|
164
|
+
}
|
|
165
|
+
this.timerManager.start()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 从指定秒数开始播放
|
|
170
|
+
* @param startSecond 起始秒数
|
|
171
|
+
*/
|
|
172
|
+
startFrom(startSecond: number): void {
|
|
173
|
+
if (!this.timerManager) {
|
|
174
|
+
throw new Error('TimerManager is not initialized. Please call loadAndPlay() first.')
|
|
175
|
+
}
|
|
176
|
+
this.timerManager.startFrom(startSecond)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 暂停播放
|
|
181
|
+
*/
|
|
182
|
+
pause(): void {
|
|
183
|
+
if (!this.timerManager) {
|
|
184
|
+
console.warn('TimerManager is not initialized. Cannot pause.')
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
this.timerManager.pause()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 继续播放
|
|
192
|
+
*/
|
|
193
|
+
resume(): void {
|
|
194
|
+
if (!this.timerManager) {
|
|
195
|
+
console.warn('TimerManager is not initialized. Cannot resume.')
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
this.timerManager.resume()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 停止播放
|
|
203
|
+
*/
|
|
204
|
+
stop(): void {
|
|
205
|
+
if (this.timerManager) {
|
|
206
|
+
this.timerManager.stop()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 重置播放器(可重新开始)
|
|
212
|
+
*/
|
|
213
|
+
reset(): void {
|
|
214
|
+
if (!this.timerManager) {
|
|
215
|
+
console.warn('TimerManager is not initialized. Cannot reset.')
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
this.timerManager.reset()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 销毁客户端,清理所有资源
|
|
223
|
+
*/
|
|
224
|
+
destroy(): void {
|
|
225
|
+
if (this.timerManager) {
|
|
226
|
+
this.timerManager.stop()
|
|
227
|
+
this.timerManager = null
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 获取 TimerManager 实例(用于高级操作)
|
|
233
|
+
*/
|
|
234
|
+
getTimerManager(): TimerManager | null {
|
|
235
|
+
return this.timerManager
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { ProtoBufManager } from './ProtoBufManager'
|
|
2
|
+
import { WebSocketManager, WebSocketStatus, MessageFormat } from './WebSocketManager'
|
|
3
|
+
import type { ProtobufType, WebSocketManagerOptions, WebSocketCallbacks, ProtoBufManagerOptions } from './index'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Protobuf WebSocket 客户端配置
|
|
7
|
+
*/
|
|
8
|
+
export interface ProtobufWebSocketClientConfig {
|
|
9
|
+
/** Proto 文件路径 */
|
|
10
|
+
protoPath: string
|
|
11
|
+
/** 消息类型名称(例如:'SimulationMonitor.SimulationMonitorMsg') */
|
|
12
|
+
messageType: string
|
|
13
|
+
/** WebSocket URL */
|
|
14
|
+
wsUrl: string
|
|
15
|
+
/** WebSocket 管理器选项 */
|
|
16
|
+
wsOptions?: WebSocketManagerOptions
|
|
17
|
+
/** ProtoBuf 管理器选项 */
|
|
18
|
+
protoBufOptions?: ProtoBufManagerOptions
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Protobuf WebSocket 客户端
|
|
23
|
+
*
|
|
24
|
+
* 这是一个高级封装类,将 ProtoBufManager 和 WebSocketManager 的使用流程封装起来,
|
|
25
|
+
* 简化业务代码中的使用。业务代码只需要配置参数和定义回调函数即可。
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const client = new ProtobufWebSocketClient({
|
|
30
|
+
* protoPath: '/proto/SimulationMonitor.proto',
|
|
31
|
+
* messageType: 'SimulationMonitor.SimulationMonitorMsg',
|
|
32
|
+
* wsUrl: 'ws://localhost:8080',
|
|
33
|
+
* wsOptions: {
|
|
34
|
+
* autoReconnect: true,
|
|
35
|
+
* reconnectDelay: 3000,
|
|
36
|
+
* },
|
|
37
|
+
* protoBufOptions: {
|
|
38
|
+
* enableLog: true, // 控制 ProtoBufManager 的日志
|
|
39
|
+
* }
|
|
40
|
+
* })
|
|
41
|
+
*
|
|
42
|
+
* await client.connect({
|
|
43
|
+
* onOpen: () => {
|
|
44
|
+
* // 连接成功后可以在这里发送初始消息
|
|
45
|
+
* client.send({ frameRate: { frameRate: 20 } })
|
|
46
|
+
* },
|
|
47
|
+
* onMessage: (data) => {
|
|
48
|
+
* console.log('Received:', data)
|
|
49
|
+
* }
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export class ProtobufWebSocketClient {
|
|
54
|
+
private protoBufManager: ProtoBufManager
|
|
55
|
+
public webSocketManager: WebSocketManager | null = null
|
|
56
|
+
private config: ProtobufWebSocketClientConfig
|
|
57
|
+
private protobufType: ProtobufType | null = null
|
|
58
|
+
|
|
59
|
+
constructor(config: ProtobufWebSocketClientConfig) {
|
|
60
|
+
this.config = config
|
|
61
|
+
this.protoBufManager = new ProtoBufManager(config.protoBufOptions)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 连接 WebSocket(会自动加载 Proto 文件)
|
|
66
|
+
* @param callbacks 事件回调函数
|
|
67
|
+
* @returns Promise<void>
|
|
68
|
+
*/
|
|
69
|
+
async connect(callbacks?: WebSocketCallbacks): Promise<void> {
|
|
70
|
+
try {
|
|
71
|
+
// 加载 Proto 文件
|
|
72
|
+
this.protobufType = await this.protoBufManager.loadProto(
|
|
73
|
+
this.config.protoPath,
|
|
74
|
+
this.config.messageType
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
// 创建 WebSocket 管理器
|
|
78
|
+
this.webSocketManager = new WebSocketManager(
|
|
79
|
+
this.config.wsUrl,
|
|
80
|
+
this.protobufType,
|
|
81
|
+
this.config.wsOptions
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// 连接 WebSocket
|
|
85
|
+
this.webSocketManager.connect(callbacks)
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('Protobuf WebSocket 连接失败:', error)
|
|
88
|
+
throw error
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 发送消息
|
|
93
|
+
* @param message 要发送的消息对象或 JSON 字符串
|
|
94
|
+
* @param format 消息格式,默认为 Protobuf 格式
|
|
95
|
+
* @throws 如果连接未建立或消息验证失败
|
|
96
|
+
*/
|
|
97
|
+
send(message: object | string, format: MessageFormat = MessageFormat.PROTOBUF): void {
|
|
98
|
+
if (!this.webSocketManager) {
|
|
99
|
+
throw new Error('WebSocket is not connected. Please call connect() first.')
|
|
100
|
+
}
|
|
101
|
+
this.webSocketManager.send(message, format)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 断开连接
|
|
106
|
+
* @param code 关闭代码
|
|
107
|
+
* @param reason 关闭原因
|
|
108
|
+
*/
|
|
109
|
+
disconnect(code?: number, reason?: string): void {
|
|
110
|
+
if (this.webSocketManager) {
|
|
111
|
+
this.webSocketManager.disconnect(code, reason)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 销毁客户端,清理所有资源
|
|
116
|
+
*/
|
|
117
|
+
destroy(): void {
|
|
118
|
+
if (this.webSocketManager) {
|
|
119
|
+
this.webSocketManager.destroy()
|
|
120
|
+
this.webSocketManager = null
|
|
121
|
+
}
|
|
122
|
+
this.protobufType = null
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
type FrameDataCallback = (frameData: Array<{ id: number; value: number }>) => void
|
|
2
|
+
type Callback = (args?: any) => void
|
|
3
|
+
|
|
4
|
+
export class TimerManager {
|
|
5
|
+
private dataArray: Array<{ id: number; value: number }> // 数据数组
|
|
6
|
+
private frameSize: number // 每帧返回的数据量
|
|
7
|
+
private interval: number // 每帧的时间间隔(毫秒)
|
|
8
|
+
private currentIndex: number // 当前帧的起始索引
|
|
9
|
+
private timer: ReturnType<typeof setInterval> | null // 定时器句柄
|
|
10
|
+
isPaused: Boolean // 是否暂停
|
|
11
|
+
private onFrame: (params: any) => void // 是否暂停
|
|
12
|
+
private onComplete: Callback // 播放完成时回调函数,是否暂停
|
|
13
|
+
private onProcess: Callback // 播放过程中回调函数
|
|
14
|
+
|
|
15
|
+
constructor(dataArray = [], interval = 50, frameSize = 1) {
|
|
16
|
+
this.dataArray = dataArray
|
|
17
|
+
this.frameSize = frameSize
|
|
18
|
+
this.interval = interval
|
|
19
|
+
this.currentIndex = 0
|
|
20
|
+
this.timer = null
|
|
21
|
+
this.isPaused = false
|
|
22
|
+
this.onFrame = () => {
|
|
23
|
+
console.log('Frame updated')
|
|
24
|
+
}
|
|
25
|
+
this.onComplete = () => {
|
|
26
|
+
console.log('Timer complete')
|
|
27
|
+
}
|
|
28
|
+
this.onProcess = (args?: any) => {
|
|
29
|
+
console.log('Timer process', args)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// 开始模拟 WebSocket 数据发送
|
|
33
|
+
public connect(onFrame: FrameDataCallback, onProcess?: Callback, onComplete?: Callback): void {
|
|
34
|
+
this.onFrame = onFrame || this.onFrame
|
|
35
|
+
this.onProcess = onProcess || this.onProcess
|
|
36
|
+
this.onComplete = onComplete || this.onComplete
|
|
37
|
+
if (this.timer) {
|
|
38
|
+
console.warn('Simulation is already running.')
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
this.start()
|
|
42
|
+
}
|
|
43
|
+
public start(): void {
|
|
44
|
+
const processFrame = () => {
|
|
45
|
+
if (this.isPaused) return // 如果暂停,跳过处理
|
|
46
|
+
if (this.currentIndex >= this.dataArray.length) {
|
|
47
|
+
this.stop() // 停止定时器
|
|
48
|
+
if (this.onComplete) this.onComplete() // 调用完成回调
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
const frameData: any = this.dataArray.slice(this.currentIndex, this.currentIndex + this.frameSize) // 获取当前帧数据
|
|
52
|
+
this.currentIndex += this.frameSize // 更新索引
|
|
53
|
+
this.onFrame(frameData[0]) // 调用帧数据处理回调
|
|
54
|
+
|
|
55
|
+
const currentSecond = Math.floor(this.currentIndex / this.interval)
|
|
56
|
+
this.onProcess?.(currentSecond)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.timer && clearInterval(this.timer)
|
|
60
|
+
this.timer = setInterval(processFrame, 1000 / this.interval)
|
|
61
|
+
}
|
|
62
|
+
public startFrom(startSecond: number = 0) {
|
|
63
|
+
this.currentIndex = startSecond * this.interval
|
|
64
|
+
// this.isPaused = false
|
|
65
|
+
this.start()
|
|
66
|
+
}
|
|
67
|
+
// 停止模拟
|
|
68
|
+
public stop(): void {
|
|
69
|
+
if (this.timer) {
|
|
70
|
+
clearInterval(this.timer)
|
|
71
|
+
this.timer = null
|
|
72
|
+
this.isPaused = true
|
|
73
|
+
console.log('Simulation stopped.')
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// 重置模拟器(可重新开始)
|
|
77
|
+
public reset(): void {
|
|
78
|
+
this.stop()
|
|
79
|
+
this.currentIndex = 0
|
|
80
|
+
this.isPaused = false
|
|
81
|
+
this.start()
|
|
82
|
+
console.log('Simulation reset.')
|
|
83
|
+
}
|
|
84
|
+
// 暂停模拟
|
|
85
|
+
public pause(): void {
|
|
86
|
+
if (this.timer) {
|
|
87
|
+
this.isPaused = true // 设置暂停状态
|
|
88
|
+
// console.log('Simulation paused.')
|
|
89
|
+
} else {
|
|
90
|
+
console.warn('Simulation is not running. Cannot pause.')
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 继续模拟
|
|
95
|
+
public resume(): void {
|
|
96
|
+
if (this.timer && this.isPaused) {
|
|
97
|
+
this.isPaused = false // 恢复非暂停状态
|
|
98
|
+
// console.log('Simulation resumed.')
|
|
99
|
+
} else {
|
|
100
|
+
console.warn('Simulation is not running or already resumed.')
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import type { Type as ProtobufType } from 'protobufjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WebSocket 连接状态
|
|
5
|
+
*/
|
|
6
|
+
export enum WebSocketStatus {
|
|
7
|
+
CONNECTING = 'CONNECTING',
|
|
8
|
+
CONNECTED = 'CONNECTED',
|
|
9
|
+
DISCONNECTING = 'DISCONNECTING',
|
|
10
|
+
DISCONNECTED = 'DISCONNECTED',
|
|
11
|
+
ERROR = 'ERROR'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 消息发送格式
|
|
16
|
+
*/
|
|
17
|
+
export enum MessageFormat {
|
|
18
|
+
/** JSON 格式 */
|
|
19
|
+
JSON = 'JSON',
|
|
20
|
+
/** Protobuf 二进制格式 */
|
|
21
|
+
PROTOBUF = 'PROTOBUF',
|
|
22
|
+
/** 原始字符串格式 */
|
|
23
|
+
STRING = 'STRING'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* WebSocket 管理器配置选项
|
|
28
|
+
*/
|
|
29
|
+
export interface WebSocketManagerOptions {
|
|
30
|
+
/** 是否启用自动重连 */
|
|
31
|
+
autoReconnect?: boolean
|
|
32
|
+
/** 重连延迟时间(毫秒) */
|
|
33
|
+
reconnectDelay?: number
|
|
34
|
+
/** 最大重连次数 */
|
|
35
|
+
maxReconnectAttempts?: number
|
|
36
|
+
/** 是否启用日志 */
|
|
37
|
+
enableLog?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* WebSocket 事件回调
|
|
42
|
+
*/
|
|
43
|
+
export interface WebSocketCallbacks {
|
|
44
|
+
/** 连接打开时的回调 */
|
|
45
|
+
onOpen?: () => void
|
|
46
|
+
/** 接收到消息时的回调 */
|
|
47
|
+
onMessage?: (data: any) => void
|
|
48
|
+
/** 连接错误时的回调 */
|
|
49
|
+
onError?: (error: Event) => void
|
|
50
|
+
/** 连接关闭时的回调 */
|
|
51
|
+
onClose?: (event: CloseEvent) => void
|
|
52
|
+
/** 重连时的回调 */
|
|
53
|
+
onReconnect?: (attempt: number) => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* WebSocket 管理器,用于管理基于 Protobuf 的 WebSocket 连接
|
|
58
|
+
*/
|
|
59
|
+
export class WebSocketManager {
|
|
60
|
+
private socket: WebSocket | null = null
|
|
61
|
+
private MessagePb: ProtobufType | null = null
|
|
62
|
+
private wsUrl: string
|
|
63
|
+
private status: WebSocketStatus = WebSocketStatus.DISCONNECTED
|
|
64
|
+
private reconnectAttempts = 0
|
|
65
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
66
|
+
private options: Required<WebSocketManagerOptions>
|
|
67
|
+
private callbacks: WebSocketCallbacks = {}
|
|
68
|
+
|
|
69
|
+
constructor(wsUrl: string, protobufType: ProtobufType, options?: WebSocketManagerOptions) {
|
|
70
|
+
this.wsUrl = wsUrl
|
|
71
|
+
this.MessagePb = protobufType
|
|
72
|
+
this.options = {
|
|
73
|
+
autoReconnect: options?.autoReconnect ?? false,
|
|
74
|
+
reconnectDelay: options?.reconnectDelay ?? 3000,
|
|
75
|
+
maxReconnectAttempts: options?.maxReconnectAttempts ?? 5,
|
|
76
|
+
enableLog: options?.enableLog ?? true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 连接 WebSocket
|
|
82
|
+
* @param callbacks 事件回调
|
|
83
|
+
*/
|
|
84
|
+
connect(callbacks?: WebSocketCallbacks): void {
|
|
85
|
+
if (this.socket && this.status === WebSocketStatus.CONNECTED) {
|
|
86
|
+
this.log('WebSocket is already connected')
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 保存回调
|
|
91
|
+
if (callbacks) {
|
|
92
|
+
this.callbacks = { ...this.callbacks, ...callbacks }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 清理之前的连接
|
|
96
|
+
this.cleanup()
|
|
97
|
+
try {
|
|
98
|
+
this.status = WebSocketStatus.CONNECTING
|
|
99
|
+
this.socket = new WebSocket(this.wsUrl)
|
|
100
|
+
this.setupEventListeners()
|
|
101
|
+
} catch (error) {
|
|
102
|
+
this.status = WebSocketStatus.ERROR
|
|
103
|
+
this.log('Failed to create WebSocket:', error)
|
|
104
|
+
this.callbacks.onError?.(error as Event)
|
|
105
|
+
throw error
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 设置事件监听器
|
|
111
|
+
*/
|
|
112
|
+
private setupEventListeners(): void {
|
|
113
|
+
if (!this.socket) return
|
|
114
|
+
|
|
115
|
+
this.socket.addEventListener('open', () => {
|
|
116
|
+
this.status = WebSocketStatus.CONNECTED
|
|
117
|
+
this.reconnectAttempts = 0
|
|
118
|
+
this.log('WebSocket connected')
|
|
119
|
+
this.callbacks.onOpen?.()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
this.socket.addEventListener('message', async (event: MessageEvent) => {
|
|
123
|
+
if (!this.MessagePb) {
|
|
124
|
+
this.log('Protobuf type is not set')
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const blob = event.data
|
|
130
|
+
const buffer = await blob.arrayBuffer()
|
|
131
|
+
const data = new Uint8Array(buffer)
|
|
132
|
+
const receivedMessage = this.MessagePb.decode(data)
|
|
133
|
+
this.callbacks.onMessage?.(receivedMessage)
|
|
134
|
+
} catch (error) {
|
|
135
|
+
this.log('Failed to decode message:', error)
|
|
136
|
+
this.callbacks.onError?.(error as Event)
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
this.socket.addEventListener('error', (error: Event) => {
|
|
141
|
+
this.status = WebSocketStatus.ERROR
|
|
142
|
+
this.log('WebSocket error:', error)
|
|
143
|
+
this.callbacks.onError?.(error)
|
|
144
|
+
|
|
145
|
+
// 如果启用了自动重连,尝试重连
|
|
146
|
+
if (this.options.autoReconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
|
|
147
|
+
this.scheduleReconnect()
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
this.socket.addEventListener('close', (event: CloseEvent) => {
|
|
152
|
+
this.status = WebSocketStatus.DISCONNECTED
|
|
153
|
+
this.log('WebSocket closed', { code: event.code, reason: event.reason })
|
|
154
|
+
this.callbacks.onClose?.(event)
|
|
155
|
+
|
|
156
|
+
// 如果启用了自动重连且不是主动关闭,尝试重连
|
|
157
|
+
if (
|
|
158
|
+
this.options.autoReconnect &&
|
|
159
|
+
this.reconnectAttempts < this.options.maxReconnectAttempts &&
|
|
160
|
+
event.code !== 1000 // 1000 表示正常关闭
|
|
161
|
+
) {
|
|
162
|
+
this.scheduleReconnect()
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 发送消息
|
|
169
|
+
* @param message 要发送的消息对象或字符串
|
|
170
|
+
* @param format 消息格式,默认为 Protobuf 格式
|
|
171
|
+
* @throws 如果连接未建立或消息验证失败
|
|
172
|
+
*/
|
|
173
|
+
send(message: object | string, format: MessageFormat = MessageFormat.PROTOBUF): void {
|
|
174
|
+
if (!this.socket || this.status !== WebSocketStatus.CONNECTED) {
|
|
175
|
+
throw new Error('WebSocket is not connected')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
if (format === MessageFormat.STRING) {
|
|
180
|
+
// 原始字符串格式发送,直接发送字符串
|
|
181
|
+
if (typeof message !== 'string') {
|
|
182
|
+
throw new Error('STRING format requires a string message')
|
|
183
|
+
}
|
|
184
|
+
this.socket.send(message)
|
|
185
|
+
this.log('Message sent (STRING):', message)
|
|
186
|
+
} else if (format === MessageFormat.JSON) {
|
|
187
|
+
// JSON 格式发送
|
|
188
|
+
if (typeof message === 'string') {
|
|
189
|
+
// 如果是字符串,直接发送(假设已经是 JSON 字符串)
|
|
190
|
+
this.socket.send(message)
|
|
191
|
+
this.log('Message sent (JSON):', message)
|
|
192
|
+
} else {
|
|
193
|
+
// 如果是对象,序列化为 JSON 字符串
|
|
194
|
+
const jsonString = JSON.stringify(message)
|
|
195
|
+
this.socket.send(jsonString)
|
|
196
|
+
this.log('Message sent (JSON):', message)
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
// Protobuf 格式发送
|
|
200
|
+
if (!this.MessagePb) {
|
|
201
|
+
throw new Error('Protobuf type is not set')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 如果传入的是字符串,解析为对象
|
|
205
|
+
let messageObj: object
|
|
206
|
+
if (typeof message === 'string') {
|
|
207
|
+
try {
|
|
208
|
+
messageObj = JSON.parse(message)
|
|
209
|
+
} catch (error) {
|
|
210
|
+
throw new Error(`Invalid JSON string for Protobuf: ${error}`)
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
messageObj = message
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 验证数据是否符合 Protobuf 定义
|
|
217
|
+
const errMsg = this.MessagePb.verify(messageObj)
|
|
218
|
+
if (errMsg) {
|
|
219
|
+
throw new Error(`Message verification failed: ${errMsg}`)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 创建 Protobuf 消息
|
|
223
|
+
const msg = this.MessagePb.create(messageObj)
|
|
224
|
+
|
|
225
|
+
// 序列化消息为二进制格式
|
|
226
|
+
const buffer = this.MessagePb.encode(msg).finish()
|
|
227
|
+
|
|
228
|
+
// 通过 WebSocket 发送二进制数据
|
|
229
|
+
this.socket.send(buffer)
|
|
230
|
+
this.log('Message sent (Protobuf):', messageObj)
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
this.log('Failed to send message:', error)
|
|
234
|
+
throw error
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 断开连接
|
|
240
|
+
* @param code 关闭代码
|
|
241
|
+
* @param reason 关闭原因
|
|
242
|
+
*/
|
|
243
|
+
disconnect(code?: number, reason?: string): void {
|
|
244
|
+
if (this.socket) {
|
|
245
|
+
this.status = WebSocketStatus.DISCONNECTING
|
|
246
|
+
this.socket.close(code ?? 1000, reason)
|
|
247
|
+
this.cleanup()
|
|
248
|
+
this.status = WebSocketStatus.DISCONNECTED
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 获取当前连接状态
|
|
254
|
+
*/
|
|
255
|
+
getStatus(): WebSocketStatus {
|
|
256
|
+
return this.status
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 检查是否已连接
|
|
261
|
+
*/
|
|
262
|
+
isConnected(): boolean {
|
|
263
|
+
return this.status === WebSocketStatus.CONNECTED &&
|
|
264
|
+
this.socket?.readyState === WebSocket.OPEN
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 设置 Protobuf 类型
|
|
269
|
+
*/
|
|
270
|
+
setProtobufType(protobufType: ProtobufType): void {
|
|
271
|
+
this.MessagePb = protobufType
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 更新回调函数
|
|
276
|
+
*/
|
|
277
|
+
updateCallbacks(callbacks: WebSocketCallbacks): void {
|
|
278
|
+
this.callbacks = { ...this.callbacks, ...callbacks }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 计划重连
|
|
283
|
+
*/
|
|
284
|
+
private scheduleReconnect(): void {
|
|
285
|
+
if (this.reconnectTimer) {
|
|
286
|
+
clearTimeout(this.reconnectTimer)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this.reconnectAttempts++
|
|
290
|
+
this.log(`Scheduling reconnect attempt ${this.reconnectAttempts}/${this.options.maxReconnectAttempts}`)
|
|
291
|
+
|
|
292
|
+
this.callbacks.onReconnect?.(this.reconnectAttempts)
|
|
293
|
+
|
|
294
|
+
this.reconnectTimer = setTimeout(() => {
|
|
295
|
+
this.log(`Reconnecting... (attempt ${this.reconnectAttempts})`)
|
|
296
|
+
try {
|
|
297
|
+
this.socket = new WebSocket(this.wsUrl)
|
|
298
|
+
this.setupEventListeners()
|
|
299
|
+
} catch (error) {
|
|
300
|
+
this.log('Reconnect failed:', error)
|
|
301
|
+
if (this.reconnectAttempts < this.options.maxReconnectAttempts) {
|
|
302
|
+
this.scheduleReconnect()
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}, this.options.reconnectDelay)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 清理资源
|
|
310
|
+
*/
|
|
311
|
+
private cleanup(): void {
|
|
312
|
+
if (this.reconnectTimer) {
|
|
313
|
+
clearTimeout(this.reconnectTimer)
|
|
314
|
+
this.reconnectTimer = null
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (this.socket) {
|
|
318
|
+
// 移除所有事件监听器
|
|
319
|
+
this.socket.onopen = null
|
|
320
|
+
this.socket.onmessage = null
|
|
321
|
+
this.socket.onerror = null
|
|
322
|
+
this.socket.onclose = null
|
|
323
|
+
|
|
324
|
+
// 如果连接还在,关闭它
|
|
325
|
+
if (this.socket.readyState === WebSocket.OPEN ||
|
|
326
|
+
this.socket.readyState === WebSocket.CONNECTING) {
|
|
327
|
+
this.socket.close()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
this.socket = null
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 日志输出
|
|
336
|
+
*/
|
|
337
|
+
private log(...args: any[]): void {
|
|
338
|
+
if (this.options.enableLog) {
|
|
339
|
+
console.log('[WebSocketManager]', ...args)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 销毁实例,清理所有资源
|
|
345
|
+
*/
|
|
346
|
+
destroy(): void {
|
|
347
|
+
this.disconnect()
|
|
348
|
+
this.callbacks = {}
|
|
349
|
+
this.MessagePb = null
|
|
350
|
+
this.reconnectAttempts = 0
|
|
351
|
+
}
|
|
352
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ProtoBufManager } from './ProtoBufManager'
|
|
2
|
+
import { WebSocketManager, WebSocketStatus, MessageFormat } from './WebSocketManager'
|
|
3
|
+
import { ProtobufWebSocketClient } from './ProtobufWebSocketClient'
|
|
4
|
+
import { ProtobufPlaybackClient } from './ProtobufPlaybackClient'
|
|
5
|
+
import type { WebSocketManagerOptions, WebSocketCallbacks } from './WebSocketManager'
|
|
6
|
+
import type { ProtobufWebSocketClientConfig } from './ProtobufWebSocketClient'
|
|
7
|
+
import type { ProtobufPlaybackClientConfig, ProtobufPlaybackCallbacks } from './ProtobufPlaybackClient'
|
|
8
|
+
import type { ProtoBufManagerOptions } from './ProtoBufManager'
|
|
9
|
+
import { TimerManager } from './TimerManager'
|
|
10
|
+
export { ProtoBufManager, WebSocketManager, WebSocketStatus, MessageFormat, ProtobufWebSocketClient, ProtobufPlaybackClient, TimerManager }
|
|
11
|
+
export type { Type as ProtobufType } from 'protobufjs'
|
|
12
|
+
export type { WebSocketManagerOptions, WebSocketCallbacks, ProtobufWebSocketClientConfig, ProtobufPlaybackClientConfig, ProtobufPlaybackCallbacks, ProtoBufManagerOptions }
|
|
13
|
+
|