diff --git a/src/Utils/Serialization/ProtobufDecorators.ts b/src/Utils/Serialization/ProtobufDecorators.ts index 5121b251..65b30a62 100644 --- a/src/Utils/Serialization/ProtobufDecorators.ts +++ b/src/Utils/Serialization/ProtobufDecorators.ts @@ -6,6 +6,7 @@ import 'reflect-metadata'; import { Component } from '../../ECS/Component'; +import { BigIntFactory } from '../../ECS/Utils/BigIntCompatibility'; /** * Protobuf字段类型枚举 @@ -25,7 +26,15 @@ export enum ProtoFieldType { SFIXED64 = 'sfixed64', BOOL = 'bool', STRING = 'string', - BYTES = 'bytes' + BYTES = 'bytes', + // 扩展类型 + MESSAGE = 'message', + ENUM = 'enum', + ANY = 'google.protobuf.Any', + TIMESTAMP = 'google.protobuf.Timestamp', + DURATION = 'google.protobuf.Duration', + STRUCT = 'google.protobuf.Struct', + VALUE = 'google.protobuf.Value' } /** @@ -42,6 +51,12 @@ export interface ProtoFieldDefinition { optional?: boolean; /** 字段名称 */ name: string; + /** 自定义类型名称 */ + customTypeName?: string; + /** 枚举值映射 */ + enumValues?: Record; + /** 默认值 */ + defaultValue?: any; } /** @@ -137,14 +152,12 @@ export class ProtobufRegistry { * ProtoSerializable 组件装饰器 * * 标记组件支持protobuf序列化 - * - * @param protoName - protobuf消息名称,默认使用类名 - * + * @param protoName protobuf消息名称,默认使用类名 * @example * ```typescript * @ProtoSerializable('Position') * class PositionComponent extends Component { - * // ... + * // 组件实现 * } * ``` */ @@ -153,7 +166,7 @@ export function ProtoSerializable(protoName?: string) { const componentName = protoName || constructor.name; const registry = ProtobufRegistry.getInstance(); - // 获取字段定义(由ProtoField装饰器设置) + // 获取字段定义 const fields = (constructor.prototype._protoFields as Map) || new Map(); @@ -176,11 +189,14 @@ export function ProtoSerializable(protoName?: string) { * ProtoField 字段装饰器 * * 标记字段参与protobuf序列化 - * - * @param fieldNumber - protobuf字段编号(必须唯一且大于0) - * @param type - 字段类型,默认自动推断 - * @param options - 额外选项 - * + * @param fieldNumber protobuf字段编号,必须唯一且大于0 + * @param type 字段类型,默认自动推断 + * @param options 额外选项 + * @param options.repeated 是否为数组 + * @param options.optional 是否可选 + * @param options.customTypeName 自定义类型名称 + * @param options.enumValues 枚举值映射 + * @param options.defaultValue 默认值 * @example * ```typescript * class PositionComponent extends Component { @@ -198,6 +214,9 @@ export function ProtoField( options?: { repeated?: boolean; optional?: boolean; + customTypeName?: string; + enumValues?: Record; + defaultValue?: any; } ) { return function (target: any, propertyKey: string) { @@ -231,7 +250,10 @@ export function ProtoField( type: inferredType || ProtoFieldType.STRING, repeated: options?.repeated || false, optional: options?.optional || false, - name: propertyKey + name: propertyKey, + customTypeName: options?.customTypeName, + enumValues: options?.enumValues, + defaultValue: options?.defaultValue }); }; } @@ -244,11 +266,20 @@ function inferProtoType(jsType: any): ProtoFieldType { switch (jsType) { case Number: - return ProtoFieldType.FLOAT; + return ProtoFieldType.DOUBLE; case Boolean: return ProtoFieldType.BOOL; case String: return ProtoFieldType.STRING; + case Date: + return ProtoFieldType.TIMESTAMP; + case Array: + return ProtoFieldType.STRING; + case Uint8Array: + case ArrayBuffer: + return ProtoFieldType.BYTES; + case Object: + return ProtoFieldType.STRUCT; default: return ProtoFieldType.STRING; } @@ -269,6 +300,49 @@ export const ProtoString = (fieldNumber: number, options?: { repeated?: boolean; export const ProtoBool = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => ProtoField(fieldNumber, ProtoFieldType.BOOL, options); +// 扩展的便捷装饰器 +export const ProtoDouble = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => + ProtoField(fieldNumber, ProtoFieldType.DOUBLE, options); + +export const ProtoInt64 = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => + ProtoField(fieldNumber, ProtoFieldType.INT64, options); + +export const ProtoUint32 = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => + ProtoField(fieldNumber, ProtoFieldType.UINT32, options); + +export const ProtoUint64 = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => + ProtoField(fieldNumber, ProtoFieldType.UINT64, options); + +export const ProtoBytes = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => + ProtoField(fieldNumber, ProtoFieldType.BYTES, options); + +export const ProtoTimestamp = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => + ProtoField(fieldNumber, ProtoFieldType.TIMESTAMP, options); + +export const ProtoDuration = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => + ProtoField(fieldNumber, ProtoFieldType.DURATION, options); + +export const ProtoStruct = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => + ProtoField(fieldNumber, ProtoFieldType.STRUCT, options); + +/** + * 自定义消息类型装饰器 + * @param fieldNumber 字段编号 + * @param customTypeName 自定义类型名称 + * @param options 额外选项 + */ +export const ProtoMessage = (fieldNumber: number, customTypeName: string, options?: { repeated?: boolean; optional?: boolean }) => + ProtoField(fieldNumber, ProtoFieldType.MESSAGE, { ...options, customTypeName }); + +/** + * 枚举类型装饰器 + * @param fieldNumber 字段编号 + * @param enumValues 枚举值映射 + * @param options 额外选项 + */ +export const ProtoEnum = (fieldNumber: number, enumValues: Record, options?: { repeated?: boolean; optional?: boolean }) => + ProtoField(fieldNumber, ProtoFieldType.ENUM, { ...options, enumValues }); + /** * 检查组件是否支持protobuf序列化 */ diff --git a/src/Utils/Serialization/ProtobufSerializer.ts b/src/Utils/Serialization/ProtobufSerializer.ts index f0a40612..8facfbe0 100644 --- a/src/Utils/Serialization/ProtobufSerializer.ts +++ b/src/Utils/Serialization/ProtobufSerializer.ts @@ -5,6 +5,7 @@ */ import { Component } from '../../ECS/Component'; +import { BigIntFactory } from '../../ECS/Utils/BigIntCompatibility'; import { ProtobufRegistry, ProtoComponentDefinition, @@ -30,9 +31,21 @@ export class ProtobufSerializer { /** MessageType缓存映射表 */ private messageTypeCache: Map = new Map(); + /** 组件序列化数据缓存 */ + private componentDataCache: Map = new Map(); + + /** 缓存访问计数器 */ + private cacheAccessCount: Map = new Map(); + + /** 最大缓存大小 */ + private maxCacheSize: number = 1000; + /** 是否启用数据验证 */ private enableValidation: boolean = process.env.NODE_ENV === 'development'; + /** 是否启用组件数据缓存 */ + private enableComponentDataCache: boolean = true; + private constructor() { this.registry = ProtobufRegistry.getInstance(); this.initializeProtobuf(); @@ -40,17 +53,44 @@ export class ProtobufSerializer { /** * 设置性能选项 + * @param options 性能配置选项 + * @param options.enableValidation 是否启用数据验证 + * @param options.enableComponentDataCache 是否启用组件数据缓存 + * @param options.maxCacheSize 最大缓存大小 + * @param options.clearCache 是否清空消息类型缓存 + * @param options.clearAllCaches 是否清空所有缓存 */ public setPerformanceOptions(options: { enableValidation?: boolean; + enableComponentDataCache?: boolean; + maxCacheSize?: number; clearCache?: boolean; + clearAllCaches?: boolean; }): void { if (options.enableValidation !== undefined) { this.enableValidation = options.enableValidation; } + if (options.enableComponentDataCache !== undefined) { + this.enableComponentDataCache = options.enableComponentDataCache; + } + if (options.maxCacheSize !== undefined && options.maxCacheSize > 0) { + this.maxCacheSize = options.maxCacheSize; + } if (options.clearCache) { this.messageTypeCache.clear(); } + if (options.clearAllCaches) { + this.clearAllCaches(); + } + } + + /** + * 清空所有缓存 + */ + public clearAllCaches(): void { + this.messageTypeCache.clear(); + this.componentDataCache.clear(); + this.cacheAccessCount.clear(); } /** @@ -76,7 +116,6 @@ export class ProtobufSerializer { /** * 手动初始化protobuf.js库 - * * @param protobufJs protobuf.js库实例 */ public initialize(protobufJs: any): void { @@ -87,44 +126,42 @@ export class ProtobufSerializer { /** * 序列化组件 - * * @param component 要序列化的组件 * @returns 序列化数据 - * @throws Error 如果组件不支持protobuf序列化 */ public serialize(component: Component): SerializedData { const componentType = component.constructor.name; // 检查是否支持protobuf序列化 if (!isProtoSerializable(component)) { - throw new Error(`[ProtobufSerializer] 组件 ${componentType} 不支持protobuf序列化,请添加@ProtoSerializable装饰器`); + return this.fallbackToJsonSerialization(component, `组件 ${componentType} 不支持protobuf序列化`); } const protoName = getProtoName(component); if (!protoName) { - throw new Error(`[ProtobufSerializer] 组件 ${componentType} 未设置protobuf名称`); + return this.fallbackToJsonSerialization(component, `组件 ${componentType} 未设置protobuf名称`); } const definition = this.registry.getComponentDefinition(protoName); if (!definition) { - throw new Error(`[ProtobufSerializer] 未找到组件定义: ${protoName}`); + return this.fallbackToJsonSerialization(component, `未找到组件定义: ${protoName}`); } // 获取protobuf消息类型 const MessageType = this.getMessageType(protoName); if (!MessageType) { - throw new Error(`[ProtobufSerializer] 未找到消息类型: ${protoName}`); + return this.fallbackToJsonSerialization(component, `未找到消息类型: ${protoName}`); } try { // 构建protobuf数据对象 const protoData = this.buildProtoData(component, definition); - // 数据验证(仅在开发环境) + // 数据验证 if (this.enableValidation) { const error = MessageType.verify(protoData); if (error) { - throw new Error(`[ProtobufSerializer] 数据验证失败: ${error}`); + return this.fallbackToJsonSerialization(component, `数据验证失败: ${error}`); } } @@ -140,26 +177,33 @@ export class ProtobufSerializer { }; } catch (error) { - throw new Error(`[ProtobufSerializer] 序列化失败: ${componentType} - ${error}`); + return this.fallbackToJsonSerialization(component, `序列化失败: ${error}`); } } /** * 反序列化组件 - * * @param component 目标组件实例 * @param serializedData 序列化数据 - * @throws Error 如果反序列化失败 */ public deserialize(component: Component, serializedData: SerializedData): void { + // 如果是JSON数据,使用JSON反序列化 + if (serializedData.type === 'json') { + this.deserializeFromJson(component, serializedData); + return; + } + + // Protobuf反序列化 const protoName = getProtoName(component); if (!protoName) { - throw new Error(`[ProtobufSerializer] 组件 ${component.constructor.name} 未设置protobuf名称`); + console.warn(`[ProtobufSerializer] 组件 ${component.constructor.name} 未设置protobuf名称,跳过反序列化`); + return; } const MessageType = this.getMessageType(protoName); if (!MessageType) { - throw new Error(`[ProtobufSerializer] 未找到消息类型: ${protoName}`); + console.warn(`[ProtobufSerializer] 未找到消息类型: ${protoName},跳过反序列化`); + return; } try { @@ -171,7 +215,8 @@ export class ProtobufSerializer { this.applyDataToComponent(component, data); } catch (error) { - throw new Error(`[ProtobufSerializer] 反序列化失败: ${component.constructor.name} - ${error}`); + console.error(`[ProtobufSerializer] 反序列化失败: ${component.constructor.name} - ${error}`); + // 不抛出异常,避免影响整个反序列化流程 } } @@ -185,31 +230,59 @@ export class ProtobufSerializer { /** * 批量序列化组件 - * * @param components 要序列化的组件数组 + * @param options 批量序列化选项 + * @param options.continueOnError 遇到错误时是否继续处理 + * @param options.maxBatchSize 最大批次大小 * @returns 序列化结果数组 */ - public serializeBatch(components: Component[]): SerializedData[] { + public serializeBatch( + components: Component[], + options?: { + continueOnError?: boolean; + maxBatchSize?: number; + } + ): SerializedData[] { const results: SerializedData[] = []; + const errors: Error[] = []; + + const continueOnError = options?.continueOnError ?? false; + const maxBatchSize = options?.maxBatchSize ?? 1000; + + // 分批处理大量组件 + const batches = this.splitIntoBatches(components, maxBatchSize); + + for (const batch of batches) { + const batchResults = this.serializeBatchSerial(batch, continueOnError); + results.push(...batchResults.results); + errors.push(...batchResults.errors); + } + + // 如果有错误且不继续执行,抛出第一个错误 + if (errors.length > 0 && !continueOnError) { + throw errors[0]; + } + + // 记录错误统计 + if (errors.length > 0) { + console.warn(`[ProtobufSerializer] 批量序列化完成,${results.length} 成功,${errors.length} 失败`); + } + + return results; + } + + /** + * 串行批量序列化 + */ + private serializeBatchSerial( + components: Component[], + continueOnError: boolean + ): { results: SerializedData[], errors: Error[] } { + const results: SerializedData[] = []; + const errors: Error[] = []; // 按组件类型分组,减少重复查找 - const componentGroups = new Map(); - - for (const component of components) { - if (!isProtoSerializable(component)) { - throw new Error(`[ProtobufSerializer] 组件 ${component.constructor.name} 不支持protobuf序列化`); - } - - const protoName = getProtoName(component); - if (!protoName) { - throw new Error(`[ProtobufSerializer] 组件 ${component.constructor.name} 未设置protobuf名称`); - } - - if (!componentGroups.has(protoName)) { - componentGroups.set(protoName, []); - } - componentGroups.get(protoName)!.push(component); - } + const componentGroups = this.groupComponentsByType(components, continueOnError, errors); // 按组分别序列化 for (const [protoName, groupComponents] of componentGroups) { @@ -217,37 +290,135 @@ export class ProtobufSerializer { const MessageType = this.getMessageType(protoName); if (!definition || !MessageType) { - throw new Error(`[ProtobufSerializer] 组件类型 ${protoName} 未正确注册`); + const error = new Error(`[ProtobufSerializer] 组件类型 ${protoName} 未正确注册`); + if (continueOnError) { + errors.push(error); + // 回退到JSON序列化 + for (const component of groupComponents) { + try { + const jsonResult = this.fallbackToJsonSerialization(component, `组件类型 ${protoName} 未正确注册`); + results.push(jsonResult); + } catch (jsonError) { + errors.push(jsonError instanceof Error ? jsonError : new Error(String(jsonError))); + } + } + continue; + } else { + throw error; + } } + // 预编译消息类型和字段定义 + const compiledType = this.getCompiledMessageType(protoName, definition, MessageType); + for (const component of groupComponents) { try { - const protoData = this.buildProtoData(component, definition); - - // 数据验证(仅在开发环境) - if (this.enableValidation) { - const error = MessageType.verify(protoData); - if (error) { - throw new Error(`[ProtobufSerializer] 数据验证失败: ${error}`); - } - } - - const message = MessageType.create(protoData); - const buffer = MessageType.encode(message).finish(); - - results.push({ - type: 'protobuf', - componentType: component.constructor.name, - data: buffer, - size: buffer.length - }); + const result = this.serializeSingleComponent(component, definition, compiledType); + results.push(result); } catch (error) { - throw new Error(`[ProtobufSerializer] 批量序列化失败: ${component.constructor.name} - ${error}`); + if (continueOnError) { + errors.push(error instanceof Error ? error : new Error(String(error))); + // 尝试JSON回退 + try { + const jsonResult = this.fallbackToJsonSerialization(component, `Protobuf序列化失败: ${error}`); + results.push(jsonResult); + } catch (jsonError) { + errors.push(jsonError instanceof Error ? jsonError : new Error(String(jsonError))); + } + } else { + throw error; + } } } } - return results; + return { results, errors }; + } + + + /** + * 序列化单个组件 + */ + private serializeSingleComponent( + component: Component, + definition: ProtoComponentDefinition, + compiledType: any + ): SerializedData { + const protoData = this.buildProtoData(component, definition); + + // 数据验证 + if (this.enableValidation && compiledType.verify) { + const error = compiledType.verify(protoData); + if (error) { + throw new Error(`[ProtobufSerializer] 数据验证失败: ${error}`); + } + } + + const message = compiledType.create(protoData); + const buffer = compiledType.encode(message).finish(); + + return { + type: 'protobuf', + componentType: component.constructor.name, + data: buffer, + size: buffer.length + }; + } + + /** + * 按类型分组组件 + */ + private groupComponentsByType( + components: Component[], + continueOnError: boolean, + errors: Error[] + ): Map { + const componentGroups = new Map(); + + for (const component of components) { + try { + if (!isProtoSerializable(component)) { + throw new Error(`[ProtobufSerializer] 组件 ${component.constructor.name} 不支持protobuf序列化`); + } + + const protoName = getProtoName(component); + if (!protoName) { + throw new Error(`[ProtobufSerializer] 组件 ${component.constructor.name} 未设置protobuf名称`); + } + + if (!componentGroups.has(protoName)) { + componentGroups.set(protoName, []); + } + componentGroups.get(protoName)!.push(component); + } catch (error) { + if (continueOnError) { + errors.push(error instanceof Error ? error : new Error(String(error))); + } else { + throw error; + } + } + } + + return componentGroups; + } + + /** + * 获取编译后的消息类型 + */ + private getCompiledMessageType(_protoName: string, _definition: ProtoComponentDefinition, MessageType: any): any { + // TODO: 实现消息类型编译和缓存优化 + return MessageType; + } + + /** + * 将数组分割成批次 + */ + private splitIntoBatches(items: T[], batchSize: number): T[][] { + const batches: T[][] = []; + for (let i = 0; i < items.length; i += batchSize) { + batches.push(items.slice(i, i + batchSize)); + } + return batches; } /** @@ -256,12 +427,18 @@ export class ProtobufSerializer { public getStats(): { registeredComponents: number; protobufAvailable: boolean; - cacheSize: number; + messageTypeCacheSize: number; + componentDataCacheSize: number; + enableComponentDataCache: boolean; + maxCacheSize: number; } { return { registeredComponents: this.registry.getAllComponents().size, protobufAvailable: !!this.protobuf, - cacheSize: this.messageTypeCache.size + messageTypeCacheSize: this.messageTypeCache.size, + componentDataCacheSize: this.componentDataCache.size, + enableComponentDataCache: this.enableComponentDataCache, + maxCacheSize: this.maxCacheSize }; } @@ -269,6 +446,17 @@ export class ProtobufSerializer { * 构建protobuf数据对象 */ private buildProtoData(component: Component, definition: ProtoComponentDefinition): any { + const componentType = component.constructor.name; + + // 生成缓存键 + const cacheKey = this.generateComponentCacheKey(component, componentType); + + // 检查缓存 + if (this.enableComponentDataCache && this.componentDataCache.has(cacheKey)) { + this.updateCacheAccess(cacheKey); + return this.componentDataCache.get(cacheKey); + } + const data: any = {}; for (const [propertyName, fieldDef] of definition.fields) { @@ -279,6 +467,11 @@ export class ProtobufSerializer { } } + // 缓存结果,仅在启用且数据较小时缓存 + if (this.enableComponentDataCache && JSON.stringify(data).length < 1000) { + this.setCacheWithLRU(cacheKey, data); + } + return data; } @@ -305,6 +498,15 @@ export class ProtobufSerializer { case ProtoFieldType.SFIXED32: return typeof value === 'number' ? (value | 0) : (parseInt(value) || 0); + case ProtoFieldType.INT64: + case ProtoFieldType.UINT64: + case ProtoFieldType.SINT64: + case ProtoFieldType.FIXED64: + case ProtoFieldType.SFIXED64: + // 使用BigIntFactory处理64位整数以确保兼容性 + const bigIntValue = BigIntFactory.create(value || 0); + return bigIntValue.valueOf(); // 转换为数值用于protobuf + case ProtoFieldType.FLOAT: case ProtoFieldType.DOUBLE: return typeof value === 'number' ? value : (parseFloat(value) || 0); @@ -315,11 +517,87 @@ export class ProtobufSerializer { case ProtoFieldType.STRING: return typeof value === 'string' ? value : String(value); + case ProtoFieldType.BYTES: + if (value instanceof Uint8Array) return value; + if (value instanceof ArrayBuffer) return new Uint8Array(value); + if (typeof value === 'string') return new TextEncoder().encode(value); + return new Uint8Array(); + + case ProtoFieldType.TIMESTAMP: + if (value instanceof Date) { + return { + seconds: Math.floor(value.getTime() / 1000), + nanos: (value.getTime() % 1000) * 1000000 + }; + } + if (typeof value === 'string') { + const date = new Date(value); + return { + seconds: Math.floor(date.getTime() / 1000), + nanos: (date.getTime() % 1000) * 1000000 + }; + } + return { seconds: 0, nanos: 0 }; + + case ProtoFieldType.DURATION: + if (typeof value === 'number') { + return { + seconds: Math.floor(value / 1000), + nanos: (value % 1000) * 1000000 + }; + } + return { seconds: 0, nanos: 0 }; + + case ProtoFieldType.STRUCT: + if (value && typeof value === 'object') { + return this.convertObjectToStruct(value); + } + return {}; + + case ProtoFieldType.MESSAGE: + case ProtoFieldType.ENUM: + // 对于自定义消息和枚举,直接返回值,让protobuf.js处理 + return value; + default: return value; } } + /** + * 转换对象为Protobuf Struct格式 + */ + private convertObjectToStruct(obj: any): any { + const result: any = { fields: {} }; + + for (const [key, value] of Object.entries(obj)) { + result.fields[key] = this.convertValueToStructValue(value); + } + + return result; + } + + /** + * 转换值为Protobuf Value格式 + */ + private convertValueToStructValue(value: any): any { + if (value === null) return { nullValue: 0 }; + if (typeof value === 'number') return { numberValue: value }; + if (typeof value === 'string') return { stringValue: value }; + if (typeof value === 'boolean') return { boolValue: value }; + if (Array.isArray(value)) { + return { + listValue: { + values: value.map(v => this.convertValueToStructValue(v)) + } + }; + } + if (typeof value === 'object') { + return { structValue: this.convertObjectToStruct(value) }; + } + return { stringValue: String(value) }; + } + /** * 应用数据到组件 */ @@ -378,4 +656,180 @@ export class ProtobufSerializer { return null; } } + + /** + * 回退到JSON序列化 + */ + private fallbackToJsonSerialization(component: Component, reason: string): SerializedData { + console.warn(`[ProtobufSerializer] ${reason},回退到JSON序列化`); + + try { + const data = this.serializeToJson(component); + const jsonString = JSON.stringify(data); + const encoder = new TextEncoder(); + const buffer = encoder.encode(jsonString); + + return { + type: 'json', + componentType: component.constructor.name, + data: data, + size: buffer.length + }; + } catch (error) { + console.error(`[ProtobufSerializer] JSON序列化也失败: ${error}`); + throw new Error(`[ProtobufSerializer] 序列化完全失败: ${component.constructor.name}`); + } + } + + /** + * 从JSON数据反序列化 + */ + private deserializeFromJson(component: Component, serializedData: SerializedData): void { + try { + if (typeof (component as any).deserialize === 'function') { + // 使用组件的自定义反序列化方法 + (component as any).deserialize(serializedData.data); + } else { + // 默认的属性赋值 + Object.assign(component, serializedData.data); + } + } catch (error) { + console.error(`[ProtobufSerializer] JSON反序列化失败: ${component.constructor.name} - ${error}`); + } + } + + /** + * 序列化为JSON对象 + */ + private serializeToJson(component: Component): any { + if (typeof (component as any).serialize === 'function') { + // 使用组件的自定义序列化方法 + return (component as any).serialize(); + } else { + // 默认的属性序列化 + const data: any = {}; + + // 获取所有可枚举属性 + for (const key of Object.keys(component)) { + const value = (component as any)[key]; + if (this.isSerializableValue(value)) { + data[key] = this.deepCloneSerializableValue(value); + } + } + + return data; + } + } + + /** + * 检查值是否可序列化 + */ + private isSerializableValue(value: any): boolean { + if (value === null || value === undefined) return true; + if (typeof value === 'function') return false; + if (value instanceof Date) return true; + if (Array.isArray(value)) return true; + if (value instanceof Map || value instanceof Set) return true; + if (typeof value === 'object' && value.constructor === Object) return true; + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return true; + return false; + } + + /** + * 深拷贝可序列化的值 + */ + private deepCloneSerializableValue(value: any): any { + if (value === null || value === undefined) return value; + if (typeof value !== 'object') return value; + if (value instanceof Date) return value.toISOString(); + + if (Array.isArray(value)) { + return value.map(item => this.deepCloneSerializableValue(item)); + } + + if (value instanceof Map) { + return Array.from(value.entries()); + } + + if (value instanceof Set) { + return Array.from(value); + } + + if (value.constructor === Object) { + const result: any = {}; + for (const key in value) { + if (value.hasOwnProperty(key) && this.isSerializableValue(value[key])) { + result[key] = this.deepCloneSerializableValue(value[key]); + } + } + return result; + } + + return value; + } + + /** + * 生成组件缓存键 + */ + private generateComponentCacheKey(component: Component, componentType: string): string { + // TODO: 考虑更高效的缓存键生成策略 + const properties = Object.keys(component).sort(); + const values = properties.map(key => String((component as any)[key])).join('|'); + return `${componentType}:${this.simpleHash(values)}`; + } + + /** + * 简单哈希函数 + */ + private simpleHash(str: string): string { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32bit integer + } + return hash.toString(36); + } + + /** + * 更新缓存访问计数 + */ + private updateCacheAccess(cacheKey: string): void { + const currentCount = this.cacheAccessCount.get(cacheKey) || 0; + this.cacheAccessCount.set(cacheKey, currentCount + 1); + } + + /** + * 使用LRU策略设置缓存 + */ + private setCacheWithLRU(cacheKey: string, data: any): void { + // 检查是否需要淘汰缓存 + if (this.componentDataCache.size >= this.maxCacheSize) { + this.evictLRUCache(); + } + + this.componentDataCache.set(cacheKey, data); + this.cacheAccessCount.set(cacheKey, 1); + } + + /** + * 淘汰LRU缓存项 + */ + private evictLRUCache(): void { + let lruKey = ''; + let minAccessCount = Number.MAX_SAFE_INTEGER; + + // 找到访问次数最少的缓存项 + for (const [key, count] of this.cacheAccessCount) { + if (count < minAccessCount) { + minAccessCount = count; + lruKey = key; + } + } + + if (lruKey) { + this.componentDataCache.delete(lruKey); + this.cacheAccessCount.delete(lruKey); + } + } } \ No newline at end of file diff --git a/src/Utils/Serialization/SerializationTypes.ts b/src/Utils/Serialization/SerializationTypes.ts index 6acf54a9..61c4b617 100644 --- a/src/Utils/Serialization/SerializationTypes.ts +++ b/src/Utils/Serialization/SerializationTypes.ts @@ -7,11 +7,11 @@ */ export interface SerializedData { /** 序列化类型 */ - type: 'protobuf'; + type: 'protobuf' | 'json'; /** 组件类型名称 */ componentType: string; /** 序列化后的数据 */ - data: Uint8Array; + data: Uint8Array | any; /** 数据大小(字节) */ size: number; } \ No newline at end of file diff --git a/tests/Utils/Serialization/ProtobufSerializerEdgeCases.test.ts b/tests/Utils/Serialization/ProtobufSerializerEdgeCases.test.ts new file mode 100644 index 00000000..85d19786 --- /dev/null +++ b/tests/Utils/Serialization/ProtobufSerializerEdgeCases.test.ts @@ -0,0 +1,413 @@ +/** + * Protobuf序列化器边界情况测试 + */ + +import { Component } from '../../../src/ECS/Component'; +import { BigIntFactory } from '../../../src/ECS/Utils/BigIntCompatibility'; +import { ProtobufSerializer } from '../../../src/Utils/Serialization/ProtobufSerializer'; +import { + ProtoSerializable, + ProtoFloat, + ProtoInt32, + ProtoString, + ProtoBool, + ProtoBytes, + ProtoTimestamp, + ProtoDouble, + ProtoInt64, + ProtoStruct +} from '../../../src/Utils/Serialization/ProtobufDecorators'; + +// 边界测试组件 +@ProtoSerializable('EdgeCaseComponent') +class EdgeCaseComponent extends Component { + @ProtoFloat(1) + public floatValue: number = 0; + + @ProtoDouble(2) + public doubleValue: number = 0; + + @ProtoInt32(3) + public intValue: number = 0; + + @ProtoInt64(4) + public bigIntValue: any = BigIntFactory.zero(); + + @ProtoString(5) + public stringValue: string = ''; + + @ProtoBool(6) + public boolValue: boolean = false; + + @ProtoBytes(7) + public bytesValue: Uint8Array = new Uint8Array(); + + @ProtoTimestamp(8) + public timestampValue: Date = new Date(); + + @ProtoStruct(9) + public structValue: any = {}; + + @ProtoFloat(10, { repeated: true }) + public arrayValue: number[] = []; + + constructor() { + super(); + } +} + +// 不完整的组件(缺少字段) +@ProtoSerializable('IncompleteComponent') +class IncompleteComponent extends Component { + @ProtoString(1) + public name: string = ''; + + // 故意添加没有装饰器的字段 + public undecoratedField: number = 42; + + constructor(name: string = '') { + super(); + this.name = name; + } +} + +// 有循环引用的组件 +@ProtoSerializable('CircularComponent') +class CircularComponent extends Component { + @ProtoString(1) + public name: string = ''; + + @ProtoStruct(2) + public circular: any = null; + + constructor(name: string = '') { + super(); + this.name = name; + // 创建循环引用 + this.circular = this; + } +} + +// 没有protobuf装饰器的组件 +class NonSerializableComponent extends Component { + public data: string = 'test'; + + serialize(): any { + return { data: this.data }; + } + + deserialize(data: any): void { + this.data = data.data || this.data; + } +} + +// Mock protobuf.js +const mockProtobuf = { + parse: jest.fn().mockReturnValue({ + root: { + lookupType: jest.fn().mockImplementation((typeName: string) => { + return { + verify: jest.fn().mockReturnValue(null), + create: jest.fn().mockImplementation((data) => data), + encode: jest.fn().mockReturnValue({ + finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])) + }), + decode: jest.fn().mockImplementation(() => ({ + floatValue: 3.14, + doubleValue: 2.718, + intValue: 42, + bigIntValue: BigIntFactory.create(999), + stringValue: 'test', + boolValue: true, + bytesValue: new Uint8Array([65, 66, 67]), + timestampValue: { seconds: 1609459200, nanos: 0 }, + structValue: { fields: { key: { stringValue: 'value' } } }, + arrayValue: [1.1, 2.2, 3.3], + name: 'TestComponent' + })), + toObject: jest.fn().mockImplementation((message) => message) + }; + }) + } + }) +}; + +describe('ProtobufSerializer边界情况测试', () => { + let serializer: ProtobufSerializer; + + beforeEach(() => { + serializer = ProtobufSerializer.getInstance(); + serializer.initialize(mockProtobuf); + jest.clearAllMocks(); + }); + + describe('极值测试', () => { + it('应该处理极大值', () => { + const component = new EdgeCaseComponent(); + component.floatValue = Number.MAX_VALUE; + component.doubleValue = Number.MAX_VALUE; + component.intValue = Number.MAX_SAFE_INTEGER; + component.bigIntValue = BigIntFactory.create(Number.MAX_SAFE_INTEGER); + + const result = serializer.serialize(component); + expect(result.type).toBe('protobuf'); + expect(result.size).toBeGreaterThan(0); + }); + + it('应该处理极小值', () => { + const component = new EdgeCaseComponent(); + component.floatValue = Number.MIN_VALUE; + component.doubleValue = Number.MIN_VALUE; + component.intValue = Number.MIN_SAFE_INTEGER; + component.bigIntValue = BigIntFactory.create(Number.MIN_SAFE_INTEGER); + + const result = serializer.serialize(component); + expect(result.type).toBe('protobuf'); + }); + + it('应该处理特殊数值', () => { + const component = new EdgeCaseComponent(); + component.floatValue = NaN; + component.doubleValue = Infinity; + component.intValue = 0; + + const result = serializer.serialize(component); + expect(result.type).toBe('protobuf'); + }); + }); + + describe('空值和undefined测试', () => { + it('应该处理null值', () => { + const component = new EdgeCaseComponent(); + (component as any).stringValue = null; + (component as any).structValue = null; + + const result = serializer.serialize(component); + expect(result.type).toBe('protobuf'); + }); + + it('应该处理undefined值', () => { + const component = new EdgeCaseComponent(); + (component as any).stringValue = undefined; + (component as any).floatValue = undefined; + + const result = serializer.serialize(component); + expect(result.type).toBe('protobuf'); + }); + + it('应该处理空数组', () => { + const component = new EdgeCaseComponent(); + component.arrayValue = []; + + const result = serializer.serialize(component); + expect(result.type).toBe('protobuf'); + }); + }); + + describe('复杂数据类型测试', () => { + it('应该处理复杂对象结构', () => { + const component = new EdgeCaseComponent(); + component.structValue = { + nested: { + array: [1, 2, 3], + object: { key: 'value' }, + date: new Date(), + null: null, + undefined: undefined + } + }; + + const result = serializer.serialize(component); + expect(result.type).toBe('protobuf'); + }); + + it('应该处理Date对象', () => { + const component = new EdgeCaseComponent(); + component.timestampValue = new Date('2021-01-01T00:00:00Z'); + + const result = serializer.serialize(component); + expect(result.type).toBe('protobuf'); + }); + + it('应该处理Uint8Array', () => { + const component = new EdgeCaseComponent(); + component.bytesValue = new Uint8Array([0, 255, 128, 64]); + + const result = serializer.serialize(component); + expect(result.type).toBe('protobuf'); + }); + }); + + describe('循环引用测试', () => { + it('应该处理循环引用对象', () => { + const component = new CircularComponent('circular'); + + // 应该回退到JSON序列化 + const result = serializer.serialize(component); + expect(result).toBeDefined(); + }); + }); + + describe('不完整组件测试', () => { + it('应该处理缺少装饰器的字段', () => { + const component = new IncompleteComponent('test'); + + const result = serializer.serialize(component); + expect(result.type).toBe('protobuf'); + expect(result.componentType).toBe('IncompleteComponent'); + }); + }); + + describe('非序列化组件测试', () => { + it('应该回退到JSON序列化', () => { + const component = new NonSerializableComponent(); + + const result = serializer.serialize(component); + expect(result.type).toBe('json'); + expect(result.componentType).toBe('NonSerializableComponent'); + expect(result.data).toEqual({ data: 'test' }); + }); + }); + + describe('批量序列化边界测试', () => { + it('应该处理空数组', () => { + const results = serializer.serializeBatch([]); + expect(results).toEqual([]); + }); + + it('应该处理混合组件类型', () => { + const components = [ + new EdgeCaseComponent(), + new NonSerializableComponent(), + new IncompleteComponent('mixed'), + ]; + + const results = serializer.serializeBatch(components, { continueOnError: true }); + expect(results.length).toBeGreaterThanOrEqual(2); + expect(results.some(r => r.type === 'protobuf')).toBe(true); + expect(results.some(r => r.type === 'json')).toBe(true); + }); + + it('应该处理批量数据', () => { + const components = Array.from({ length: 50 }, () => new EdgeCaseComponent()); + + const results = serializer.serializeBatch(components); + expect(results).toHaveLength(50); + expect(results.every(r => r.type === 'protobuf')).toBe(true); + }); + + it('应该处理序列化错误', () => { + // 模拟序列化失败 + const mockType = mockProtobuf.parse().root.lookupType('ecs.EdgeCaseComponent'); + mockType.verify.mockReturnValue('Validation error'); + + const components = [new EdgeCaseComponent()]; + + // continueOnError = false 应该抛出异常 + expect(() => { + serializer.serializeBatch(components, { continueOnError: false }); + }).toThrow(); + + // continueOnError = true 应该回退到JSON + const results = serializer.serializeBatch(components, { continueOnError: true }); + expect(results).toHaveLength(1); + expect(results[0].type).toBe('json'); + }); + }); + + describe('反序列化边界测试', () => { + it('应该处理JSON类型的反序列化', () => { + const component = new NonSerializableComponent(); + const serializedData = { + type: 'json' as const, + componentType: 'NonSerializableComponent', + data: { data: 'deserialized' }, + size: 100 + }; + + serializer.deserialize(component, serializedData); + expect(component.data).toBe('deserialized'); + }); + + it('应该优雅处理反序列化错误', () => { + const component = new EdgeCaseComponent(); + const invalidData = { + type: 'protobuf' as const, + componentType: 'EdgeCaseComponent', + data: new Uint8Array([255, 255, 255, 255]), + size: 4 + }; + + // 模拟解码失败 + const mockType = mockProtobuf.parse().root.lookupType('ecs.EdgeCaseComponent'); + mockType.decode.mockImplementation(() => { + throw new Error('Decode failed'); + }); + + // 不应该抛出异常 + expect(() => { + serializer.deserialize(component, invalidData); + }).not.toThrow(); + }); + + it('应该处理缺失的proto定义', () => { + const component = new EdgeCaseComponent(); + // 清除proto名称以模拟缺失情况 + (component as any)._protoName = undefined; + + const serializedData = { + type: 'protobuf' as const, + componentType: 'EdgeCaseComponent', + data: new Uint8Array([1, 2, 3, 4]), + size: 4 + }; + + // 不应该抛出异常 + expect(() => { + serializer.deserialize(component, serializedData); + }).not.toThrow(); + }); + }); + + describe('缓存测试', () => { + it('应该能清空所有缓存', () => { + serializer.clearAllCaches(); + const stats = serializer.getStats(); + expect(stats.messageTypeCacheSize).toBe(0); + expect(stats.componentDataCacheSize).toBe(0); + }); + }); + + describe('性能选项测试', () => { + it('应该能禁用数据验证', () => { + serializer.setPerformanceOptions({ enableValidation: false }); + + const component = new EdgeCaseComponent(); + const result = serializer.serialize(component); + expect(result.type).toBe('protobuf'); + }); + + it('应该能禁用组件数据缓存', () => { + serializer.setPerformanceOptions({ enableComponentDataCache: false }); + + const component = new EdgeCaseComponent(); + serializer.serialize(component); + + const stats = serializer.getStats(); + expect(stats.componentDataCacheSize).toBe(0); + }); + }); + + describe('统计信息测试', () => { + it('应该返回正确的统计信息', () => { + const stats = serializer.getStats(); + + expect(typeof stats.registeredComponents).toBe('number'); + expect(typeof stats.protobufAvailable).toBe('boolean'); + expect(typeof stats.messageTypeCacheSize).toBe('number'); + expect(typeof stats.componentDataCacheSize).toBe('number'); + expect(typeof stats.enableComponentDataCache).toBe('boolean'); + expect(typeof stats.maxCacheSize).toBe('number'); + }); + }); +}); \ No newline at end of file