Files
esengine/packages/framework/server/src/schema/composites.ts
YHH f333b81298 feat(server): add Schema validation system and binary encoding optimization (#421)
* feat(server): add distributed room support

- Add DistributedRoomManager for multi-server room management
- Add MemoryAdapter for testing and standalone mode
- Add RedisAdapter for production multi-server deployments
- Add LoadBalancedRouter with 5 load balancing strategies
- Add distributed config option to createServer
- Add $redirect message for cross-server player redirection
- Add failover mechanism for automatic room recovery
- Add room:migrated and server:draining event types
- Update documentation (zh/en)

* feat(server): add Schema validation system and binary encoding optimization

## Schema Validation System
- Add lightweight schema validation system (s.object, s.string, s.number, etc.)
- Support auto type inference with Infer<> generic
- Integrate schema validation into API/message handlers
- Add defineApiWithSchema and defineMsgWithSchema helpers

## Binary Encoding Optimization
- Add native WebSocket binary frame support via sendBinary()
- Add PacketType.Binary for efficient binary data transmission
- Optimize ECSRoom.broadcastBinary() to use native binary

## Architecture Improvements
- Extract BaseValidator to separate file to eliminate code duplication
- Add ECSRoom export to main index.ts for better discoverability
- Add Core.worldManager initialization check in ECSRoom constructor
- Remove deprecated validate field from ApiDefinition (use schema instead)

## Documentation
- Add Schema validation documentation in Chinese and English

* fix(rpc): resolve ESLint warnings with proper types

- Replace `any` with proper WebSocket type in connection.ts
- Add IncomingMessage type for request handling in index.ts
- Use Record<string, Handler> pattern instead of `any` casting
- Replace `any` with `unknown` in ProtocolDef and type inference
2026-01-02 17:18:13 +08:00

559 lines
16 KiB
TypeScript

/**
* @zh 复合类型验证器
* @en Composite type validators
*/
import type {
Validator,
ValidationResult,
ObjectShape,
InferShape
} from './types.js';
import { BaseValidator } from './base.js';
// ============================================================================
// Object Validator
// ============================================================================
/**
* @zh 对象验证选项
* @en Object validation options
*/
export interface ObjectValidatorOptions {
strict?: boolean;
}
/**
* @zh 对象验证器
* @en Object validator
*/
export class ObjectValidator<T extends ObjectShape> extends BaseValidator<InferShape<T>> {
readonly typeName = 'object';
private readonly _shape: T;
private _objectOptions: ObjectValidatorOptions = {};
constructor(shape: T) {
super();
this._shape = shape;
}
protected _validate(value: unknown, path: string[]): ValidationResult<InferShape<T>> {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return {
success: false,
error: {
path,
message: `Expected object, received ${Array.isArray(value) ? 'array' : typeof value}`,
expected: 'object',
received: value
}
};
}
const result: Record<string, unknown> = {};
const obj = value as Record<string, unknown>;
// Validate each field in shape
for (const [key, validator] of Object.entries(this._shape)) {
const fieldValue = obj[key];
const fieldPath = [...path, key];
const fieldResult = validator.validate(fieldValue, fieldPath);
if (!fieldResult.success) {
return fieldResult as ValidationResult<InferShape<T>>;
}
result[key] = fieldResult.data;
}
// Strict mode: check for unknown keys
if (this._objectOptions.strict) {
const knownKeys = new Set(Object.keys(this._shape));
for (const key of Object.keys(obj)) {
if (!knownKeys.has(key)) {
return {
success: false,
error: {
path: [...path, key],
message: `Unknown key "${key}"`,
expected: 'known key',
received: key
}
};
}
}
}
return { success: true, data: result as InferShape<T> };
}
protected _clone(): ObjectValidator<T> {
const clone = new ObjectValidator(this._shape);
clone._options = { ...this._options };
clone._objectOptions = { ...this._objectOptions };
return clone;
}
/**
* @zh 严格模式(不允许额外字段)
* @en Strict mode (no extra fields allowed)
*/
strict(): ObjectValidator<T> {
const clone = this._clone();
clone._objectOptions.strict = true;
return clone;
}
/**
* @zh 部分模式(所有字段可选)
* @en Partial mode (all fields optional)
*/
partial(): ObjectValidator<{
[K in keyof T]: ReturnType<T[K]['optional']>;
}> {
const partialShape: Record<string, Validator<unknown>> = {};
for (const [key, validator] of Object.entries(this._shape)) {
partialShape[key] = validator.optional();
}
return new ObjectValidator(partialShape) as any;
}
/**
* @zh 选择部分字段
* @en Pick specific fields
*/
pick<K extends keyof T>(...keys: K[]): ObjectValidator<Pick<T, K>> {
const pickedShape: Record<string, Validator<unknown>> = {};
for (const key of keys) {
pickedShape[key as string] = this._shape[key];
}
return new ObjectValidator(pickedShape) as any;
}
/**
* @zh 排除部分字段
* @en Omit specific fields
*/
omit<K extends keyof T>(...keys: K[]): ObjectValidator<Omit<T, K>> {
const keySet = new Set(keys as string[]);
const omittedShape: Record<string, Validator<unknown>> = {};
for (const [key, validator] of Object.entries(this._shape)) {
if (!keySet.has(key)) {
omittedShape[key] = validator;
}
}
return new ObjectValidator(omittedShape) as any;
}
/**
* @zh 扩展对象 Schema
* @en Extend object schema
*/
extend<U extends ObjectShape>(shape: U): ObjectValidator<T & U> {
const extendedShape = { ...this._shape, ...shape };
return new ObjectValidator(extendedShape) as any;
}
}
// ============================================================================
// Array Validator
// ============================================================================
/**
* @zh 数组验证选项
* @en Array validation options
*/
export interface ArrayValidatorOptions {
minLength?: number;
maxLength?: number;
}
/**
* @zh 数组验证器
* @en Array validator
*/
export class ArrayValidator<T> extends BaseValidator<T[]> {
readonly typeName = 'array';
private readonly _element: Validator<T>;
private _arrayOptions: ArrayValidatorOptions = {};
constructor(element: Validator<T>) {
super();
this._element = element;
}
protected _validate(value: unknown, path: string[]): ValidationResult<T[]> {
if (!Array.isArray(value)) {
return {
success: false,
error: {
path,
message: `Expected array, received ${typeof value}`,
expected: 'array',
received: value
}
};
}
const { minLength, maxLength } = this._arrayOptions;
if (minLength !== undefined && value.length < minLength) {
return {
success: false,
error: {
path,
message: `Array must have at least ${minLength} items`,
expected: `array(minLength: ${minLength})`,
received: value
}
};
}
if (maxLength !== undefined && value.length > maxLength) {
return {
success: false,
error: {
path,
message: `Array must have at most ${maxLength} items`,
expected: `array(maxLength: ${maxLength})`,
received: value
}
};
}
const result: T[] = [];
for (let i = 0; i < value.length; i++) {
const itemPath = [...path, String(i)];
const itemResult = this._element.validate(value[i], itemPath);
if (!itemResult.success) {
return itemResult as ValidationResult<T[]>;
}
result.push(itemResult.data);
}
return { success: true, data: result };
}
protected _clone(): ArrayValidator<T> {
const clone = new ArrayValidator(this._element);
clone._options = { ...this._options };
clone._arrayOptions = { ...this._arrayOptions };
return clone;
}
/**
* @zh 设置最小长度
* @en Set minimum length
*/
min(length: number): ArrayValidator<T> {
const clone = this._clone();
clone._arrayOptions.minLength = length;
return clone;
}
/**
* @zh 设置最大长度
* @en Set maximum length
*/
max(length: number): ArrayValidator<T> {
const clone = this._clone();
clone._arrayOptions.maxLength = length;
return clone;
}
/**
* @zh 设置长度范围
* @en Set length range
*/
length(min: number, max: number): ArrayValidator<T> {
const clone = this._clone();
clone._arrayOptions.minLength = min;
clone._arrayOptions.maxLength = max;
return clone;
}
/**
* @zh 要求非空数组
* @en Require non-empty array
*/
nonempty(): ArrayValidator<T> {
return this.min(1);
}
}
// ============================================================================
// Tuple Validator
// ============================================================================
/**
* @zh 元组验证器
* @en Tuple validator
*/
export class TupleValidator<T extends readonly Validator<unknown>[]> extends BaseValidator<{
[K in keyof T]: T[K] extends Validator<infer U> ? U : never;
}> {
readonly typeName = 'tuple';
private readonly _elements: T;
constructor(elements: T) {
super();
this._elements = elements;
}
protected _validate(value: unknown, path: string[]): ValidationResult<{
[K in keyof T]: T[K] extends Validator<infer U> ? U : never;
}> {
if (!Array.isArray(value)) {
return {
success: false,
error: {
path,
message: `Expected tuple, received ${typeof value}`,
expected: 'tuple',
received: value
}
};
}
if (value.length !== this._elements.length) {
return {
success: false,
error: {
path,
message: `Expected tuple of length ${this._elements.length}, received length ${value.length}`,
expected: `tuple(length: ${this._elements.length})`,
received: value
}
};
}
const result: unknown[] = [];
for (let i = 0; i < this._elements.length; i++) {
const itemPath = [...path, String(i)];
const itemResult = this._elements[i].validate(value[i], itemPath);
if (!itemResult.success) {
return itemResult as any;
}
result.push(itemResult.data);
}
return { success: true, data: result as any };
}
protected _clone(): TupleValidator<T> {
const clone = new TupleValidator(this._elements);
clone._options = { ...this._options };
return clone;
}
}
// ============================================================================
// Union Validator
// ============================================================================
/**
* @zh 联合类型验证器
* @en Union type validator
*/
export class UnionValidator<T extends readonly Validator<unknown>[]> extends BaseValidator<
T[number] extends Validator<infer U> ? U : never
> {
readonly typeName: string;
private readonly _variants: T;
constructor(variants: T) {
super();
this._variants = variants;
this.typeName = `union(${variants.map(v => v.typeName).join(' | ')})`;
}
protected _validate(value: unknown, path: string[]): ValidationResult<
T[number] extends Validator<infer U> ? U : never
> {
const errors: string[] = [];
for (const variant of this._variants) {
const result = variant.validate(value, path);
if (result.success) {
return result as any;
}
errors.push(variant.typeName);
}
return {
success: false,
error: {
path,
message: `Expected one of: ${errors.join(', ')}`,
expected: this.typeName,
received: value
}
};
}
protected _clone(): UnionValidator<T> {
const clone = new UnionValidator(this._variants);
clone._options = { ...this._options };
return clone;
}
}
// ============================================================================
// Record Validator
// ============================================================================
/**
* @zh 记录类型验证器
* @en Record type validator
*/
export class RecordValidator<T> extends BaseValidator<Record<string, T>> {
readonly typeName = 'record';
private readonly _valueValidator: Validator<T>;
constructor(valueValidator: Validator<T>) {
super();
this._valueValidator = valueValidator;
}
protected _validate(value: unknown, path: string[]): ValidationResult<Record<string, T>> {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return {
success: false,
error: {
path,
message: `Expected object, received ${Array.isArray(value) ? 'array' : typeof value}`,
expected: 'record',
received: value
}
};
}
const result: Record<string, T> = {};
const obj = value as Record<string, unknown>;
for (const [key, val] of Object.entries(obj)) {
const fieldPath = [...path, key];
const fieldResult = this._valueValidator.validate(val, fieldPath);
if (!fieldResult.success) {
return fieldResult as ValidationResult<Record<string, T>>;
}
result[key] = fieldResult.data;
}
return { success: true, data: result };
}
protected _clone(): RecordValidator<T> {
const clone = new RecordValidator(this._valueValidator);
clone._options = { ...this._options };
return clone;
}
}
// ============================================================================
// Enum Validator
// ============================================================================
/**
* @zh 枚举验证器
* @en Enum validator
*/
export class EnumValidator<T extends readonly (string | number)[]> extends BaseValidator<T[number]> {
readonly typeName: string;
private readonly _values: Set<string | number>;
private readonly _valuesArray: T;
constructor(values: T) {
super();
this._valuesArray = values;
this._values = new Set(values);
this.typeName = `enum(${values.map(v => JSON.stringify(v)).join(', ')})`;
}
protected _validate(value: unknown, path: string[]): ValidationResult<T[number]> {
if (!this._values.has(value as string | number)) {
return {
success: false,
error: {
path,
message: `Expected one of: ${this._valuesArray.map(v => JSON.stringify(v)).join(', ')}`,
expected: this.typeName,
received: value
}
};
}
return { success: true, data: value as T[number] };
}
protected _clone(): EnumValidator<T> {
const clone = new EnumValidator(this._valuesArray);
clone._options = { ...this._options };
return clone;
}
}
// ============================================================================
// Factory Functions
// ============================================================================
/**
* @zh 创建对象验证器
* @en Create object validator
*/
export function object<T extends ObjectShape>(shape: T): ObjectValidator<T> {
return new ObjectValidator(shape);
}
/**
* @zh 创建数组验证器
* @en Create array validator
*/
export function array<T>(element: Validator<T>): ArrayValidator<T> {
return new ArrayValidator(element);
}
/**
* @zh 创建元组验证器
* @en Create tuple validator
*/
export function tuple<T extends readonly Validator<unknown>[]>(
elements: T
): TupleValidator<T> {
return new TupleValidator(elements);
}
/**
* @zh 创建联合类型验证器
* @en Create union type validator
*/
export function union<T extends readonly Validator<unknown>[]>(
variants: T
): UnionValidator<T> {
return new UnionValidator(variants);
}
/**
* @zh 创建记录类型验证器
* @en Create record type validator
*/
export function record<T>(valueValidator: Validator<T>): RecordValidator<T> {
return new RecordValidator(valueValidator);
}
/**
* @zh 创建枚举验证器
* @en Create enum validator
*/
export function nativeEnum<T extends readonly (string | number)[]>(
values: T
): EnumValidator<T> {
return new EnumValidator(values);
}