* 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
559 lines
16 KiB
TypeScript
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);
|
|
}
|