@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 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
+