避免throw导致的中止运行,增加fallback回退json的序列化
This commit is contained in:
@@ -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<string, number>;
|
||||
/** 默认值 */
|
||||
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<string, ProtoFieldDefinition>)
|
||||
|| new Map<string, ProtoFieldDefinition>();
|
||||
|
||||
@@ -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<string, number>;
|
||||
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<string, number>, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||
ProtoField(fieldNumber, ProtoFieldType.ENUM, { ...options, enumValues });
|
||||
|
||||
/**
|
||||
* 检查组件是否支持protobuf序列化
|
||||
*/
|
||||
|
||||
@@ -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<string, any> = new Map();
|
||||
|
||||
/** 组件序列化数据缓存 */
|
||||
private componentDataCache: Map<string, any> = new Map();
|
||||
|
||||
/** 缓存访问计数器 */
|
||||
private cacheAccessCount: Map<string, number> = 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,17 +230,153 @@ 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 = this.groupComponentsByType(components, continueOnError, errors);
|
||||
|
||||
// 按组分别序列化
|
||||
for (const [protoName, groupComponents] of componentGroups) {
|
||||
const definition = this.registry.getComponentDefinition(protoName);
|
||||
const MessageType = this.getMessageType(protoName);
|
||||
|
||||
if (!definition || !MessageType) {
|
||||
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 result = this.serializeSingleComponent(component, definition, compiledType);
|
||||
results.push(result);
|
||||
} catch (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, 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<string, Component[]> {
|
||||
const componentGroups = new Map<string, Component[]>();
|
||||
|
||||
for (const component of components) {
|
||||
try {
|
||||
if (!isProtoSerializable(component)) {
|
||||
throw new Error(`[ProtobufSerializer] 组件 ${component.constructor.name} 不支持protobuf序列化`);
|
||||
}
|
||||
@@ -209,45 +390,35 @@ export class ProtobufSerializer {
|
||||
componentGroups.set(protoName, []);
|
||||
}
|
||||
componentGroups.get(protoName)!.push(component);
|
||||
}
|
||||
|
||||
// 按组分别序列化
|
||||
for (const [protoName, groupComponents] of componentGroups) {
|
||||
const definition = this.registry.getComponentDefinition(protoName);
|
||||
const MessageType = this.getMessageType(protoName);
|
||||
|
||||
if (!definition || !MessageType) {
|
||||
throw new Error(`[ProtobufSerializer] 组件类型 ${protoName} 未正确注册`);
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`[ProtobufSerializer] 批量序列化失败: ${component.constructor.name} - ${error}`);
|
||||
if (continueOnError) {
|
||||
errors.push(error instanceof Error ? error : new Error(String(error)));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
return componentGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取编译后的消息类型
|
||||
*/
|
||||
private getCompiledMessageType(_protoName: string, _definition: ProtoComponentDefinition, MessageType: any): any {
|
||||
// TODO: 实现消息类型编译和缓存优化
|
||||
return MessageType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数组分割成批次
|
||||
*/
|
||||
private splitIntoBatches<T>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,11 @@
|
||||
*/
|
||||
export interface SerializedData {
|
||||
/** 序列化类型 */
|
||||
type: 'protobuf';
|
||||
type: 'protobuf' | 'json';
|
||||
/** 组件类型名称 */
|
||||
componentType: string;
|
||||
/** 序列化后的数据 */
|
||||
data: Uint8Array;
|
||||
data: Uint8Array | any;
|
||||
/** 数据大小(字节) */
|
||||
size: number;
|
||||
}
|
||||
413
tests/Utils/Serialization/ProtobufSerializerEdgeCases.test.ts
Normal file
413
tests/Utils/Serialization/ProtobufSerializerEdgeCases.test.ts
Normal file
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user