@threejs-shared/protobuf 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,311 +0,0 @@
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 * as 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
-
@@ -1,238 +0,0 @@
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
-
@@ -1,125 +0,0 @@
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
-