更新network库及core库优化

This commit is contained in:
YHH
2025-08-12 09:39:07 +08:00
parent c178e2fbcc
commit 9f76d37a82
117 changed files with 17988 additions and 4099 deletions

View File

@@ -0,0 +1,101 @@
# ECS Framework 网络库 - 客户端
该包提供了完整的网络客户端功能,包括连接管理、预测、插值等现代网络游戏必需的特性。
## 主要功能
-**传输层支持**: WebSocket 和 HTTP 两种传输方式
-**网络客户端**: 完整的连接、认证、房间管理
-**网络行为**: ClientNetworkBehaviour 基类和 NetworkIdentity 组件
-**装饰器系统**: @SyncVar, @ClientRpc, @ServerRpc 装饰器
-**客户端预测**: 减少网络延迟感知的预测系统
-**插值系统**: 平滑的网络对象状态同步
-**TypeScript**: 完整的类型支持
## 安装
```bash
npm install @esengine/ecs-framework-network-client
```
## 快速开始
```typescript
import {
NetworkClient,
WebSocketClientTransport,
ClientNetworkBehaviour,
SyncVar,
ServerRpc
} from '@esengine/ecs-framework-network-client';
// 创建网络客户端
const client = new NetworkClient({
transport: 'websocket',
transportConfig: {
host: 'localhost',
port: 8080,
secure: false
}
});
// 连接到服务器
await client.connect();
// 认证
const userInfo = await client.authenticate('username', 'password');
// 获取房间列表
const rooms = await client.getRoomList();
// 加入房间
const roomInfo = await client.joinRoom('room-id');
```
## 网络行为示例
```typescript
class PlayerController extends ClientNetworkBehaviour {
@SyncVar({ clientCanModify: true })
position: { x: number; y: number } = { x: 0, y: 0 };
@SyncVar()
health: number = 100;
@ServerRpc({ requireLocalPlayer: true })
async move(direction: string): Promise<void> {
// 这个方法会被发送到服务器执行
}
@ClientRpc()
onDamaged(damage: number): void {
// 这个方法会被服务器调用
console.log(`Received damage: ${damage}`);
}
}
```
## 预测和插值
```typescript
import { PredictionSystem, InterpolationSystem } from '@esengine/ecs-framework-network-client';
// 启用预测系统
const predictionSystem = new PredictionSystem(scene, 64, 500);
scene.addSystem(predictionSystem);
// 启用插值系统
const interpolationSystem = new InterpolationSystem(scene, {
delay: 100,
enableExtrapolation: false
});
scene.addSystem(interpolationSystem);
```
## 编译状态
**编译成功** - 所有 TypeScript 错误已修复,包生成完成
## License
MIT

View File

@@ -0,0 +1,117 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
console.log('🚀 使用 Rollup 构建 network-client 包...');
async function main() {
try {
if (fs.existsSync('./dist')) {
console.log('🧹 清理旧的构建文件...');
execSync('rimraf ./dist', { stdio: 'inherit' });
}
console.log('📦 执行 Rollup 构建...');
execSync('rollup -c rollup.config.cjs', { stdio: 'inherit' });
console.log('📋 生成 package.json...');
generatePackageJson();
console.log('📁 复制必要文件...');
copyFiles();
showBuildResults();
console.log('✅ network-client 构建完成!');
console.log('\n🚀 发布命令:');
console.log('cd dist && npm publish');
} catch (error) {
console.error('❌ 构建失败:', error.message);
process.exit(1);
}
}
function generatePackageJson() {
const sourcePackage = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
const distPackage = {
name: sourcePackage.name,
version: sourcePackage.version,
description: sourcePackage.description,
main: 'index.cjs',
module: 'index.mjs',
unpkg: 'index.umd.js',
types: 'index.d.ts',
exports: {
'.': {
import: './index.mjs',
require: './index.cjs',
types: './index.d.ts'
}
},
files: [
'index.mjs',
'index.mjs.map',
'index.cjs',
'index.cjs.map',
'index.umd.js',
'index.umd.js.map',
'index.d.ts',
'README.md',
'LICENSE'
],
keywords: [
'ecs',
'networking',
'client',
'prediction',
'interpolation',
'game-engine',
'typescript'
],
author: sourcePackage.author,
license: sourcePackage.license,
repository: sourcePackage.repository,
dependencies: sourcePackage.dependencies,
peerDependencies: sourcePackage.peerDependencies,
engines: {
node: '>=16.0.0'
},
sideEffects: false
};
fs.writeFileSync('./dist/package.json', JSON.stringify(distPackage, null, 2));
}
function copyFiles() {
const filesToCopy = [
{ src: './README.md', dest: './dist/README.md' },
{ src: '../../LICENSE', dest: './dist/LICENSE' }
];
filesToCopy.forEach(({ src, dest }) => {
if (fs.existsSync(src)) {
fs.copyFileSync(src, dest);
console.log(` ✓ 复制: ${path.basename(dest)}`);
} else {
console.log(` ⚠️ 文件不存在: ${src}`);
}
});
}
function showBuildResults() {
const distDir = './dist';
const files = ['index.mjs', 'index.cjs', 'index.umd.js', 'index.d.ts'];
console.log('\n📊 构建结果:');
files.forEach(file => {
const filePath = path.join(distDir, file);
if (fs.existsSync(filePath)) {
const size = fs.statSync(filePath).size;
console.log(` ${file}: ${(size / 1024).toFixed(1)}KB`);
}
});
}
main().catch(console.error);

View File

@@ -0,0 +1,53 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom', // 客户端库使用 jsdom 环境
roots: ['<rootDir>/tests'],
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
testPathIgnorePatterns: ['/node_modules/'],
collectCoverage: false,
collectCoverageFrom: [
'src/**/*.ts',
'!src/index.ts',
'!src/**/index.ts',
'!**/*.d.ts',
'!src/**/*.test.ts',
'!src/**/*.spec.ts'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
coverageThreshold: {
global: {
branches: 60,
functions: 70,
lines: 70,
statements: 70
},
'./src/core/': {
branches: 70,
functions: 80,
lines: 80,
statements: 80
}
},
verbose: true,
transform: {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: 'tsconfig.json',
useESM: false,
}],
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
testTimeout: 10000,
clearMocks: true,
restoreMocks: true,
modulePathIgnorePatterns: [
'<rootDir>/bin/',
'<rootDir>/dist/',
'<rootDir>/node_modules/'
]
};

View File

@@ -0,0 +1,86 @@
{
"name": "@esengine/ecs-framework-network-client",
"version": "1.0.17",
"description": "ECS Framework 网络库 - 客户端实现",
"type": "module",
"main": "bin/index.js",
"types": "bin/index.d.ts",
"exports": {
".": {
"types": "./bin/index.d.ts",
"import": "./bin/index.js",
"development": {
"types": "./src/index.ts",
"import": "./src/index.ts"
}
}
},
"files": [
"bin/**/*",
"README.md",
"LICENSE"
],
"keywords": [
"ecs",
"networking",
"client",
"prediction",
"interpolation",
"game-engine",
"typescript"
],
"scripts": {
"clean": "rimraf bin dist",
"build:ts": "tsc",
"prebuild": "npm run clean",
"build": "npm run build:ts",
"build:watch": "tsc --watch",
"rebuild": "npm run clean && npm run build",
"build:npm": "npm run build && node build-rollup.cjs",
"publish:npm": "npm run build:npm && cd dist && npm publish",
"publish:patch": "npm version patch && npm run build:npm && cd dist && npm publish",
"publish:minor": "npm version minor && npm run build:npm && cd dist && npm publish",
"publish:major": "npm version major && npm run build:npm && cd dist && npm publish",
"preversion": "npm run rebuild",
"test": "jest --config jest.config.cjs",
"test:watch": "jest --watch --config jest.config.cjs",
"test:coverage": "jest --coverage --config jest.config.cjs",
"test:ci": "jest --ci --coverage --config jest.config.cjs",
"test:clear": "jest --clearCache"
},
"author": "yhh",
"license": "MIT",
"dependencies": {
"ws": "^8.18.0"
},
"peerDependencies": {
"@esengine/ecs-framework": ">=2.1.29",
"@esengine/ecs-framework-network-shared": ">=1.0.0"
},
"devDependencies": {
"@esengine/ecs-framework": "*",
"@esengine/ecs-framework-network-shared": "*",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@types/jest": "^29.5.14",
"@types/node": "^20.19.0",
"@types/ws": "^8.5.13",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"rimraf": "^5.0.0",
"rollup": "^4.42.0",
"rollup-plugin-dts": "^6.2.1",
"ts-jest": "^29.4.0",
"typescript": "^5.8.3"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "https://github.com/esengine/ecs-framework.git",
"directory": "packages/network-client"
}
}

View File

@@ -0,0 +1,133 @@
const resolve = require('@rollup/plugin-node-resolve');
const commonjs = require('@rollup/plugin-commonjs');
const terser = require('@rollup/plugin-terser');
const dts = require('rollup-plugin-dts').default;
const { readFileSync } = require('fs');
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
const banner = `/**
* @esengine/ecs-framework-network-client v${pkg.version}
* ECS Framework 网络库 - 客户端实现
*
* @author ${pkg.author}
* @license ${pkg.license}
*/`;
const external = [
'ws',
'@esengine/ecs-framework',
'@esengine/ecs-framework-network-shared'
];
const commonPlugins = [
resolve({
browser: true,
preferBuiltins: false
}),
commonjs({
include: /node_modules/
})
];
module.exports = [
// ES模块构建
{
input: 'bin/index.js',
output: {
file: 'dist/index.mjs',
format: 'es',
banner,
sourcemap: true,
exports: 'named'
},
plugins: [
...commonPlugins,
terser({
format: {
comments: /^!/
}
})
],
external,
treeshake: {
moduleSideEffects: false,
propertyReadSideEffects: false,
unknownGlobalSideEffects: false
}
},
// CommonJS构建
{
input: 'bin/index.js',
output: {
file: 'dist/index.cjs',
format: 'cjs',
banner,
sourcemap: true,
exports: 'named'
},
plugins: [
...commonPlugins,
terser({
format: {
comments: /^!/
}
})
],
external,
treeshake: {
moduleSideEffects: false
}
},
// UMD构建
{
input: 'bin/index.js',
output: {
file: 'dist/index.umd.js',
format: 'umd',
name: 'ECSNetworkClient',
banner,
sourcemap: true,
exports: 'named',
globals: {
'ws': 'WebSocket',
'uuid': 'uuid',
'@esengine/ecs-framework': 'ECS',
'@esengine/ecs-framework-network-shared': 'ECSNetworkShared'
}
},
plugins: [
...commonPlugins,
terser({
format: {
comments: /^!/
}
})
],
external,
treeshake: {
moduleSideEffects: false
}
},
// 类型定义构建
{
input: 'bin/index.d.ts',
output: {
file: 'dist/index.d.ts',
format: 'es',
banner: `/**
* @esengine/ecs-framework-network-client v${pkg.version}
* TypeScript definitions
*/`
},
plugins: [
dts({
respectExternal: true
})
],
external
}
];

View File

@@ -0,0 +1,179 @@
/**
* 客户端网络行为基类
*
* 类似Unity Mirror的NetworkBehaviour提供网络功能
*/
import { Component, Entity } from '@esengine/ecs-framework';
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
import { NetworkClient } from './NetworkClient';
import { NetworkIdentity } from './NetworkIdentity';
/**
* 客户端网络行为基类
*/
export abstract class ClientNetworkBehaviour extends Component {
/** 网络标识组件 */
protected networkIdentity: NetworkIdentity | null = null;
/** 网络客户端实例 */
protected networkClient: NetworkClient | null = null;
/**
* 组件初始化
*/
initialize(): void {
// 获取网络标识组件
this.networkIdentity = this.entity.getComponent(NetworkIdentity);
if (!this.networkIdentity) {
throw new Error('NetworkBehaviour requires NetworkIdentity component');
}
// 从全局获取网络客户端实例
this.networkClient = this.getNetworkClient();
}
/**
* 获取网络客户端实例
*/
protected getNetworkClient(): NetworkClient | null {
// 这里需要实现从全局管理器获取客户端实例的逻辑
// 暂时返回null在实际使用时需要通过单例模式或依赖注入获取
return null;
}
/**
* 是否为本地玩家
*/
get isLocalPlayer(): boolean {
return this.networkIdentity?.isLocalPlayer ?? false;
}
/**
* 是否为服务器权威
*/
get hasAuthority(): boolean {
return this.networkIdentity?.hasAuthority ?? false;
}
/**
* 网络ID
*/
get networkId(): string {
return this.networkIdentity?.networkId ?? '';
}
/**
* 是否已连接
*/
get isConnected(): boolean {
return this.networkClient?.isInRoom() ?? false;
}
/**
* 发送RPC到服务器
*/
protected async sendServerRpc(methodName: string, ...args: NetworkValue[]): Promise<NetworkValue> {
if (!this.networkClient || !this.networkIdentity) {
throw new Error('Network client or identity not available');
}
return this.networkClient.sendRpc(this.networkIdentity.networkId, methodName, args, true);
}
/**
* 发送不可靠RPC到服务器
*/
protected async sendServerRpcUnreliable(methodName: string, ...args: NetworkValue[]): Promise<void> {
if (!this.networkClient || !this.networkIdentity) {
throw new Error('Network client or identity not available');
}
await this.networkClient.sendRpc(this.networkIdentity.networkId, methodName, args, false);
}
/**
* 更新SyncVar
*/
protected async updateSyncVar(fieldName: string, value: NetworkValue): Promise<void> {
if (!this.networkClient || !this.networkIdentity) {
throw new Error('Network client or identity not available');
}
await this.networkClient.updateSyncVar(this.networkIdentity.networkId, fieldName, value);
}
/**
* 当收到RPC调用时
*/
onRpcReceived(methodName: string, args: NetworkValue[]): void {
// 尝试调用对应的方法
const method = (this as any)[methodName];
if (typeof method === 'function') {
try {
method.apply(this, args);
} catch (error) {
console.error(`Error calling RPC method ${methodName}:`, error);
}
} else {
console.warn(`RPC method ${methodName} not found on ${this.constructor.name}`);
}
}
/**
* 当SyncVar更新时
*/
onSyncVarChanged(fieldName: string, oldValue: NetworkValue, newValue: NetworkValue): void {
// 子类可以重写此方法来处理SyncVar变化
}
/**
* 当获得权威时
*/
onStartAuthority(): void {
// 子类可以重写此方法
}
/**
* 当失去权威时
*/
onStopAuthority(): void {
// 子类可以重写此方法
}
/**
* 当成为本地玩家时
*/
onStartLocalPlayer(): void {
// 子类可以重写此方法
}
/**
* 当不再是本地玩家时
*/
onStopLocalPlayer(): void {
// 子类可以重写此方法
}
/**
* 网络启动时调用
*/
onNetworkStart(): void {
// 子类可以重写此方法
}
/**
* 网络停止时调用
*/
onNetworkStop(): void {
// 子类可以重写此方法
}
/**
* 组件销毁
*/
onDestroy(): void {
this.networkIdentity = null;
this.networkClient = null;
}
}

View File

@@ -0,0 +1,638 @@
/**
* 网络客户端主类
*
* 管理连接、认证、房间加入等功能
*/
import { Scene, EntityManager, Emitter, ITimer, Core } from '@esengine/ecs-framework';
import {
NetworkIdentity as SharedNetworkIdentity,
NetworkValue,
RpcMessage,
SyncVarMessage
} from '@esengine/ecs-framework-network-shared';
import {
ClientTransport,
WebSocketClientTransport,
HttpClientTransport,
ConnectionState,
ClientMessage,
ClientTransportConfig,
WebSocketClientConfig,
HttpClientConfig
} from '../transport';
/**
* 网络客户端配置
*/
export interface NetworkClientConfig {
/** 传输类型 */
transport: 'websocket' | 'http';
/** 传输配置 */
transportConfig: WebSocketClientConfig | HttpClientConfig;
/** 是否启用预测 */
enablePrediction?: boolean;
/** 预测缓冲区大小 */
predictionBuffer?: number;
/** 是否启用插值 */
enableInterpolation?: boolean;
/** 插值延迟(毫秒) */
interpolationDelay?: number;
/** 网络对象同步间隔(毫秒) */
syncInterval?: number;
/** 是否启用调试 */
debug?: boolean;
}
/**
* 用户信息
*/
export interface UserInfo {
/** 用户ID */
userId: string;
/** 用户名 */
username: string;
/** 用户数据 */
data?: NetworkValue;
}
/**
* 房间信息
*/
export interface RoomInfo {
/** 房间ID */
roomId: string;
/** 房间名称 */
name: string;
/** 当前人数 */
playerCount: number;
/** 最大人数 */
maxPlayers: number;
/** 房间元数据 */
metadata?: NetworkValue;
/** 是否私有房间 */
isPrivate?: boolean;
}
/**
* 认证消息
*/
export interface AuthMessage {
action: string;
username: string;
password?: string;
userData?: NetworkValue;
}
/**
* 房间消息
*/
export interface RoomMessage {
action: string;
roomId?: string;
name?: string;
maxPlayers?: number;
metadata?: NetworkValue;
isPrivate?: boolean;
password?: string;
}
/**
* 网络客户端事件
*/
export interface NetworkClientEvents {
/** 连接建立 */
'connected': () => void;
/** 连接断开 */
'disconnected': (reason: string) => void;
/** 认证成功 */
'authenticated': (userInfo: UserInfo) => void;
/** 加入房间成功 */
'joined-room': (roomInfo: RoomInfo) => void;
/** 离开房间 */
'left-room': (roomId: string) => void;
/** 房间列表更新 */
'room-list-updated': (rooms: RoomInfo[]) => void;
/** 玩家加入房间 */
'player-joined': (userId: string, userInfo: UserInfo) => void;
/** 玩家离开房间 */
'player-left': (userId: string) => void;
/** 网络对象创建 */
'network-object-created': (networkId: string, data: NetworkValue) => void;
/** 网络对象销毁 */
'network-object-destroyed': (networkId: string) => void;
/** SyncVar 更新 */
'syncvar-updated': (networkId: string, fieldName: string, value: NetworkValue) => void;
/** RPC 调用 */
'rpc-received': (networkId: string, methodName: string, args: NetworkValue[]) => void;
/** 错误发生 */
'error': (error: Error) => void;
}
/**
* 网络客户端主类
*/
export class NetworkClient {
private transport: ClientTransport;
private config: NetworkClientConfig;
private currentUser: UserInfo | null = null;
private currentRoom: RoomInfo | null = null;
private availableRooms: Map<string, RoomInfo> = new Map();
private networkObjects: Map<string, SharedNetworkIdentity> = new Map();
private pendingRpcs: Map<string, { resolve: Function; reject: Function; timeout: ITimer<any> }> = new Map();
private scene: Scene | null = null;
private eventEmitter: Emitter<keyof NetworkClientEvents, any>;
constructor(config: NetworkClientConfig) {
this.eventEmitter = new Emitter();
this.config = {
enablePrediction: true,
predictionBuffer: 64,
enableInterpolation: true,
interpolationDelay: 100,
syncInterval: 50,
debug: false,
...config
};
this.transport = this.createTransport();
this.setupTransportEvents();
}
/**
* 创建传输层
*/
private createTransport(): ClientTransport {
switch (this.config.transport) {
case 'websocket':
return new WebSocketClientTransport(this.config.transportConfig as WebSocketClientConfig);
case 'http':
return new HttpClientTransport(this.config.transportConfig as HttpClientConfig);
default:
throw new Error(`Unsupported transport type: ${this.config.transport}`);
}
}
/**
* 设置传输层事件监听
*/
private setupTransportEvents(): void {
this.transport.on('connected', () => {
this.eventEmitter.emit('connected');
});
this.transport.on('disconnected', (reason) => {
this.handleDisconnected(reason);
});
this.transport.on('message', (message) => {
this.handleMessage(message);
});
this.transport.on('error', (error) => {
this.eventEmitter.emit('error', error);
});
}
/**
* 连接到服务器
*/
async connect(): Promise<void> {
return this.transport.connect();
}
/**
* 断开连接
*/
async disconnect(): Promise<void> {
await this.transport.disconnect();
this.cleanup();
}
/**
* 用户认证
*/
async authenticate(username: string, password?: string, userData?: NetworkValue): Promise<UserInfo> {
if (!this.transport.isConnected()) {
throw new Error('Not connected to server');
}
const authMessage: AuthMessage = {
action: 'login',
username,
password,
userData
};
const response = await this.sendRequestWithResponse('system', authMessage as any);
if (response.success && response.userInfo) {
this.currentUser = response.userInfo as UserInfo;
this.eventEmitter.emit('authenticated', this.currentUser);
return this.currentUser;
} else {
throw new Error(response.error || 'Authentication failed');
}
}
/**
* 获取房间列表
*/
async getRoomList(): Promise<RoomInfo[]> {
if (!this.isAuthenticated()) {
throw new Error('Not authenticated');
}
const roomMessage: RoomMessage = {
action: 'list-rooms'
};
const response = await this.sendRequestWithResponse('system', roomMessage as any);
if (response.success && response.rooms) {
this.availableRooms.clear();
response.rooms.forEach((room: RoomInfo) => {
this.availableRooms.set(room.roomId, room);
});
this.eventEmitter.emit('room-list-updated', response.rooms);
return response.rooms;
} else {
throw new Error(response.error || 'Failed to get room list');
}
}
/**
* 创建房间
*/
async createRoom(name: string, maxPlayers: number = 8, metadata?: NetworkValue, isPrivate = false): Promise<RoomInfo> {
if (!this.isAuthenticated()) {
throw new Error('Not authenticated');
}
const roomMessage: RoomMessage = {
action: 'create-room',
name,
maxPlayers,
metadata,
isPrivate
};
const response = await this.sendRequestWithResponse('system', roomMessage as any);
if (response.success && response.room) {
this.currentRoom = response.room as RoomInfo;
this.eventEmitter.emit('joined-room', this.currentRoom);
return this.currentRoom;
} else {
throw new Error(response.error || 'Failed to create room');
}
}
/**
* 加入房间
*/
async joinRoom(roomId: string, password?: string): Promise<RoomInfo> {
if (!this.isAuthenticated()) {
throw new Error('Not authenticated');
}
const roomMessage: RoomMessage = {
action: 'join-room',
roomId,
password
};
const response = await this.sendRequestWithResponse('system', roomMessage as any);
if (response.success && response.room) {
this.currentRoom = response.room as RoomInfo;
this.eventEmitter.emit('joined-room', this.currentRoom);
return this.currentRoom;
} else {
throw new Error(response.error || 'Failed to join room');
}
}
/**
* 离开房间
*/
async leaveRoom(): Promise<void> {
if (!this.currentRoom) {
return;
}
const roomMessage: RoomMessage = {
action: 'leave-room',
roomId: this.currentRoom.roomId
};
try {
await this.sendRequestWithResponse('system', roomMessage as any);
} finally {
const roomId = this.currentRoom.roomId;
this.currentRoom = null;
this.networkObjects.clear();
this.eventEmitter.emit('left-room', roomId);
}
}
/**
* 发送RPC调用
*/
async sendRpc(networkId: string, methodName: string, args: NetworkValue[] = [], reliable = true): Promise<NetworkValue> {
if (!this.isInRoom()) {
throw new Error('Not in a room');
}
const rpcMessage: any = {
networkId,
methodName,
args,
isServer: false,
messageId: this.generateMessageId()
};
if (reliable) {
return this.sendRequestWithResponse('rpc', rpcMessage);
} else {
await this.transport.sendMessage({
type: 'rpc',
data: rpcMessage as NetworkValue,
reliable: false
});
return {};
}
}
/**
* 更新SyncVar
*/
async updateSyncVar(networkId: string, fieldName: string, value: NetworkValue): Promise<void> {
if (!this.isInRoom()) {
throw new Error('Not in a room');
}
const syncMessage: any = {
networkId,
propertyName: fieldName,
value,
isServer: false
};
await this.transport.sendMessage({
type: 'syncvar',
data: syncMessage as NetworkValue,
reliable: true
});
}
/**
* 设置ECS场景
*/
setScene(scene: Scene): void {
this.scene = scene;
}
/**
* 获取当前用户信息
*/
getCurrentUser(): UserInfo | null {
return this.currentUser;
}
/**
* 获取当前房间信息
*/
getCurrentRoom(): RoomInfo | null {
return this.currentRoom;
}
/**
* 获取连接状态
*/
getConnectionState(): ConnectionState {
return this.transport.getState();
}
/**
* 是否已认证
*/
isAuthenticated(): boolean {
return this.currentUser !== null && this.transport.isConnected();
}
/**
* 是否在房间中
*/
isInRoom(): boolean {
return this.isAuthenticated() && this.currentRoom !== null;
}
/**
* 获取网络对象
*/
getNetworkObject(networkId: string): SharedNetworkIdentity | null {
return this.networkObjects.get(networkId) || null;
}
/**
* 获取所有网络对象
*/
getAllNetworkObjects(): SharedNetworkIdentity[] {
return Array.from(this.networkObjects.values());
}
/**
* 处理断开连接
*/
private handleDisconnected(reason: string): void {
this.cleanup();
this.eventEmitter.emit('disconnected', reason);
}
/**
* 处理接收到的消息
*/
private handleMessage(message: ClientMessage): void {
try {
switch (message.type) {
case 'system':
this.handleSystemMessage(message);
break;
case 'rpc':
this.handleRpcMessage(message);
break;
case 'syncvar':
this.handleSyncVarMessage(message);
break;
case 'custom':
this.handleCustomMessage(message);
break;
}
} catch (error) {
console.error('Error handling message:', error);
this.eventEmitter.emit('error', error as Error);
}
}
/**
* 处理系统消息
*/
private handleSystemMessage(message: ClientMessage): void {
const data = message.data as any;
// 处理响应消息
if (message.messageId && this.pendingRpcs.has(message.messageId)) {
const pending = this.pendingRpcs.get(message.messageId)!;
pending.timeout.stop();
this.pendingRpcs.delete(message.messageId);
if (data.success) {
pending.resolve(data);
} else {
pending.reject(new Error(data.error || 'Request failed'));
}
return;
}
// 处理广播消息
switch (data.action) {
case 'player-joined':
this.eventEmitter.emit('player-joined', data.userId, data.userInfo);
break;
case 'player-left':
this.eventEmitter.emit('player-left', data.userId);
break;
case 'network-object-created':
this.handleNetworkObjectCreated(data);
break;
case 'network-object-destroyed':
this.handleNetworkObjectDestroyed(data);
break;
}
}
/**
* 处理RPC消息
*/
private handleRpcMessage(message: ClientMessage): void {
const rpcData = message.data as any;
this.eventEmitter.emit('rpc-received', rpcData.networkId, rpcData.methodName, rpcData.args || []);
}
/**
* 处理SyncVar消息
*/
private handleSyncVarMessage(message: ClientMessage): void {
const syncData = message.data as any;
this.eventEmitter.emit('syncvar-updated', syncData.networkId, syncData.propertyName, syncData.value);
}
/**
* 处理自定义消息
*/
private handleCustomMessage(message: ClientMessage): void {
// 可扩展的自定义消息处理
}
/**
* 处理网络对象创建
*/
private handleNetworkObjectCreated(data: any): void {
const networkObject = new SharedNetworkIdentity();
this.networkObjects.set(data.networkId, networkObject);
this.eventEmitter.emit('network-object-created', data.networkId, data.data || {});
}
/**
* 处理网络对象销毁
*/
private handleNetworkObjectDestroyed(data: any): void {
this.networkObjects.delete(data.networkId);
this.eventEmitter.emit('network-object-destroyed', data.networkId);
}
/**
* 发送请求并等待响应
*/
private sendRequestWithResponse(type: ClientMessage['type'], data: NetworkValue, timeout = 30000): Promise<any> {
return new Promise((resolve, reject) => {
const messageId = this.generateMessageId();
const timeoutTimer = Core.schedule(timeout / 1000, false, this, () => {
this.pendingRpcs.delete(messageId);
reject(new Error('Request timeout'));
});
this.pendingRpcs.set(messageId, {
resolve,
reject,
timeout: timeoutTimer
});
this.transport.sendMessage({
type,
data,
messageId,
reliable: true
}).catch(reject);
});
}
/**
* 生成消息ID
*/
private generateMessageId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
/**
* 清理资源
*/
private cleanup(): void {
this.currentUser = null;
this.currentRoom = null;
this.availableRooms.clear();
this.networkObjects.clear();
// 取消所有待处理的RPC
this.pendingRpcs.forEach(pending => {
pending.timeout.stop();
pending.reject(new Error('Connection closed'));
});
this.pendingRpcs.clear();
}
/**
* 销毁客户端
*/
destroy(): void {
this.disconnect();
this.transport.destroy();
// 清理事件监听器由于Emitter没有clear方法我们重新创建一个
this.eventEmitter = new Emitter();
}
/**
* 类型安全的事件监听
*/
on<K extends keyof NetworkClientEvents>(event: K, listener: NetworkClientEvents[K]): void {
this.eventEmitter.addObserver(event, listener, this);
}
/**
* 移除事件监听
*/
off<K extends keyof NetworkClientEvents>(event: K, listener: NetworkClientEvents[K]): void {
this.eventEmitter.removeObserver(event, listener);
}
/**
* 类型安全的事件触发
*/
emit<K extends keyof NetworkClientEvents>(event: K, ...args: Parameters<NetworkClientEvents[K]>): void {
this.eventEmitter.emit(event, ...args);
}
}

View File

@@ -0,0 +1,378 @@
/**
* 客户端网络标识组件
*
* 标识网络对象并管理其状态
*/
import { Component, Entity } from '@esengine/ecs-framework';
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
import { ClientNetworkBehaviour } from './ClientNetworkBehaviour';
/**
* 网络权威类型
*/
export enum NetworkAuthority {
/** 服务器权威 */
SERVER = 'server',
/** 客户端权威 */
CLIENT = 'client',
/** 所有者权威 */
OWNER = 'owner'
}
/**
* SyncVar信息
*/
export interface SyncVarInfo {
/** 字段名 */
fieldName: string;
/** 当前值 */
currentValue: NetworkValue;
/** 上一个值 */
previousValue: NetworkValue;
/** 最后更新时间 */
lastUpdateTime: number;
/** 是否已变更 */
isDirty: boolean;
}
/**
* 网络标识组件
*/
export class NetworkIdentity extends Component {
/** 网络ID */
private _networkId: string = '';
/** 所有者用户ID */
private _ownerId: string = '';
/** 是否为本地玩家 */
private _isLocalPlayer: boolean = false;
/** 权威类型 */
private _authority: NetworkAuthority = NetworkAuthority.SERVER;
/** 是否有权威 */
private _hasAuthority: boolean = false;
/** 网络行为组件列表 */
private networkBehaviours: ClientNetworkBehaviour[] = [];
/** SyncVar信息映射 */
private syncVars: Map<string, SyncVarInfo> = new Map();
/** 预测状态 */
private predictionEnabled: boolean = false;
/** 插值状态 */
private interpolationEnabled: boolean = true;
/**
* 网络ID
*/
get networkId(): string {
return this._networkId;
}
set networkId(value: string) {
this._networkId = value;
}
/**
* 所有者用户ID
*/
get ownerId(): string {
return this._ownerId;
}
set ownerId(value: string) {
this._ownerId = value;
}
/**
* 是否为本地玩家
*/
get isLocalPlayer(): boolean {
return this._isLocalPlayer;
}
set isLocalPlayer(value: boolean) {
if (this._isLocalPlayer !== value) {
this._isLocalPlayer = value;
this.notifyLocalPlayerChanged();
}
}
/**
* 权威类型
*/
get authority(): NetworkAuthority {
return this._authority;
}
set authority(value: NetworkAuthority) {
if (this._authority !== value) {
this._authority = value;
this.updateAuthorityStatus();
}
}
/**
* 是否有权威
*/
get hasAuthority(): boolean {
return this._hasAuthority;
}
/**
* 是否启用预测
*/
get isPredictionEnabled(): boolean {
return this.predictionEnabled;
}
set isPredictionEnabled(value: boolean) {
this.predictionEnabled = value;
}
/**
* 是否启用插值
*/
get isInterpolationEnabled(): boolean {
return this.interpolationEnabled;
}
set isInterpolationEnabled(value: boolean) {
this.interpolationEnabled = value;
}
/**
* 组件初始化
*/
initialize(): void {
this.collectNetworkBehaviours();
this.notifyNetworkStart();
}
/**
* 收集网络行为组件
*/
private collectNetworkBehaviours(): void {
// 暂时留空,等待实际集成时实现
this.networkBehaviours = [];
}
/**
* 更新权威状态
*/
private updateAuthorityStatus(): void {
const oldHasAuthority = this._hasAuthority;
// 根据权威类型计算是否有权威
switch (this._authority) {
case NetworkAuthority.SERVER:
this._hasAuthority = false; // 客户端永远没有服务器权威
break;
case NetworkAuthority.CLIENT:
this._hasAuthority = true; // 客户端权威
break;
case NetworkAuthority.OWNER:
this._hasAuthority = this._isLocalPlayer; // 本地玩家才有权威
break;
}
// 通知权威变化
if (oldHasAuthority !== this._hasAuthority) {
this.notifyAuthorityChanged();
}
}
/**
* 通知权威变化
*/
private notifyAuthorityChanged(): void {
this.networkBehaviours.forEach(behaviour => {
if (this._hasAuthority) {
behaviour.onStartAuthority();
} else {
behaviour.onStopAuthority();
}
});
}
/**
* 通知本地玩家状态变化
*/
private notifyLocalPlayerChanged(): void {
this.updateAuthorityStatus(); // 本地玩家状态影响权威
this.networkBehaviours.forEach(behaviour => {
if (this._isLocalPlayer) {
behaviour.onStartLocalPlayer();
} else {
behaviour.onStopLocalPlayer();
}
});
}
/**
* 通知网络启动
*/
private notifyNetworkStart(): void {
this.networkBehaviours.forEach(behaviour => {
behaviour.onNetworkStart();
});
}
/**
* 通知网络停止
*/
private notifyNetworkStop(): void {
this.networkBehaviours.forEach(behaviour => {
behaviour.onNetworkStop();
});
}
/**
* 处理RPC调用
*/
handleRpcCall(methodName: string, args: NetworkValue[]): void {
// 将RPC调用分发给所有网络行为组件
this.networkBehaviours.forEach(behaviour => {
behaviour.onRpcReceived(methodName, args);
});
}
/**
* 注册SyncVar
*/
registerSyncVar(fieldName: string, initialValue: NetworkValue): void {
this.syncVars.set(fieldName, {
fieldName,
currentValue: initialValue,
previousValue: initialValue,
lastUpdateTime: Date.now(),
isDirty: false
});
}
/**
* 更新SyncVar
*/
updateSyncVar(fieldName: string, newValue: NetworkValue): void {
const syncVar = this.syncVars.get(fieldName);
if (!syncVar) {
console.warn(`SyncVar ${fieldName} not registered on ${this._networkId}`);
return;
}
const oldValue = syncVar.currentValue;
syncVar.previousValue = oldValue;
syncVar.currentValue = newValue;
syncVar.lastUpdateTime = Date.now();
syncVar.isDirty = true;
// 通知所有网络行为组件
this.networkBehaviours.forEach(behaviour => {
behaviour.onSyncVarChanged(fieldName, oldValue, newValue);
});
}
/**
* 获取SyncVar值
*/
getSyncVar(fieldName: string): NetworkValue | undefined {
return this.syncVars.get(fieldName)?.currentValue;
}
/**
* 获取所有SyncVar
*/
getAllSyncVars(): Map<string, SyncVarInfo> {
return new Map(this.syncVars);
}
/**
* 获取脏SyncVar
*/
getDirtySyncVars(): SyncVarInfo[] {
return Array.from(this.syncVars.values()).filter(syncVar => syncVar.isDirty);
}
/**
* 清除脏标记
*/
clearDirtyFlags(): void {
this.syncVars.forEach(syncVar => {
syncVar.isDirty = false;
});
}
/**
* 序列化网络状态
*/
serializeState(): NetworkValue {
const state: any = {
networkId: this._networkId,
ownerId: this._ownerId,
isLocalPlayer: this._isLocalPlayer,
authority: this._authority,
syncVars: {}
};
// 序列化SyncVar
this.syncVars.forEach((syncVar, fieldName) => {
state.syncVars[fieldName] = syncVar.currentValue;
});
return state;
}
/**
* 反序列化网络状态
*/
deserializeState(state: any): void {
if (state.networkId) this._networkId = state.networkId;
if (state.ownerId) this._ownerId = state.ownerId;
if (typeof state.isLocalPlayer === 'boolean') this.isLocalPlayer = state.isLocalPlayer;
if (state.authority) this.authority = state.authority;
// 反序列化SyncVar
if (state.syncVars) {
Object.entries(state.syncVars).forEach(([fieldName, value]) => {
if (this.syncVars.has(fieldName)) {
this.updateSyncVar(fieldName, value as NetworkValue);
}
});
}
}
/**
* 设置预测状态
*/
setPredictionState(enabled: boolean): void {
this.predictionEnabled = enabled;
}
/**
* 设置插值状态
*/
setInterpolationState(enabled: boolean): void {
this.interpolationEnabled = enabled;
}
/**
* 检查是否可以发送RPC
*/
canSendRpc(): boolean {
return this._hasAuthority || this._isLocalPlayer;
}
/**
* 检查是否可以更新SyncVar
*/
canUpdateSyncVar(): boolean {
return this._hasAuthority;
}
/**
* 组件销毁
*/
onDestroy(): void {
this.notifyNetworkStop();
this.networkBehaviours = [];
this.syncVars.clear();
}
}

View File

@@ -0,0 +1,7 @@
/**
* 核心模块导出
*/
export * from './NetworkClient';
export * from './ClientNetworkBehaviour';
export * from './NetworkIdentity';

View File

@@ -0,0 +1,108 @@
/**
* ClientRpc装饰器 - 客户端版本
*
* 用于标记可以从服务器调用的客户端方法
*/
import 'reflect-metadata';
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
/**
* ClientRpc配置选项
*/
export interface ClientRpcOptions {
/** 是否可靠传输 */
reliable?: boolean;
/** 超时时间(毫秒) */
timeout?: number;
/** 是否仅发送给所有者 */
ownerOnly?: boolean;
/** 是否包含发送者 */
includeSender?: boolean;
/** 权限要求 */
requireAuthority?: boolean;
}
/**
* ClientRpc元数据键
*/
export const CLIENT_RPC_METADATA_KEY = Symbol('client_rpc');
/**
* ClientRpc元数据
*/
export interface ClientRpcMetadata {
/** 方法名 */
methodName: string;
/** 配置选项 */
options: ClientRpcOptions;
/** 原始方法 */
originalMethod: Function;
}
/**
* ClientRpc装饰器
*/
export function ClientRpc(options: ClientRpcOptions = {}): MethodDecorator {
return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
const methodName = propertyKey as string;
const originalMethod = descriptor.value;
// 获取已有的ClientRpc元数据
const existingMetadata: ClientRpcMetadata[] = Reflect.getMetadata(CLIENT_RPC_METADATA_KEY, target.constructor) || [];
// 添加新的ClientRpc元数据
existingMetadata.push({
methodName,
options: {
reliable: true,
timeout: 30000,
ownerOnly: false,
includeSender: false,
requireAuthority: false,
...options
},
originalMethod
});
// 设置元数据
Reflect.defineMetadata(CLIENT_RPC_METADATA_KEY, existingMetadata, target.constructor);
// 包装原方法客户端接收RPC调用时执行
descriptor.value = function (this: any, ...args: NetworkValue[]) {
try {
// 直接调用原方法客户端接收RPC调用
return originalMethod.apply(this, args);
} catch (error) {
console.error(`Error executing ClientRpc ${methodName}:`, error);
throw error;
}
};
return descriptor;
};
}
/**
* 获取类的所有ClientRpc元数据
*/
export function getClientRpcMetadata(target: any): ClientRpcMetadata[] {
return Reflect.getMetadata(CLIENT_RPC_METADATA_KEY, target) || [];
}
/**
* 检查方法是否为ClientRpc
*/
export function isClientRpc(target: any, methodName: string): boolean {
const metadata = getClientRpcMetadata(target);
return metadata.some(meta => meta.methodName === methodName);
}
/**
* 获取特定方法的ClientRpc选项
*/
export function getClientRpcOptions(target: any, methodName: string): ClientRpcOptions | null {
const metadata = getClientRpcMetadata(target);
const rpc = metadata.find(meta => meta.methodName === methodName);
return rpc ? rpc.options : null;
}

View File

@@ -0,0 +1,138 @@
/**
* ServerRpc装饰器 - 客户端版本
*
* 用于标记可以向服务器发送的RPC方法
*/
import 'reflect-metadata';
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
import { ClientNetworkBehaviour } from '../core/ClientNetworkBehaviour';
/**
* ServerRpc配置选项
*/
export interface ServerRpcOptions {
/** 是否可靠传输 */
reliable?: boolean;
/** 超时时间(毫秒) */
timeout?: number;
/** 是否需要权威 */
requireAuthority?: boolean;
/** 是否需要是本地玩家 */
requireLocalPlayer?: boolean;
}
/**
* ServerRpc元数据键
*/
export const SERVER_RPC_METADATA_KEY = Symbol('server_rpc');
/**
* ServerRpc元数据
*/
export interface ServerRpcMetadata {
/** 方法名 */
methodName: string;
/** 配置选项 */
options: ServerRpcOptions;
/** 原始方法 */
originalMethod: Function;
}
/**
* ServerRpc装饰器
*/
export function ServerRpc(options: ServerRpcOptions = {}): MethodDecorator {
return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
const methodName = propertyKey as string;
const originalMethod = descriptor.value;
// 获取已有的ServerRpc元数据
const existingMetadata: ServerRpcMetadata[] = Reflect.getMetadata(SERVER_RPC_METADATA_KEY, target.constructor) || [];
// 添加新的ServerRpc元数据
existingMetadata.push({
methodName,
options: {
reliable: true,
timeout: 30000,
requireAuthority: false,
requireLocalPlayer: false,
...options
},
originalMethod
});
// 设置元数据
Reflect.defineMetadata(SERVER_RPC_METADATA_KEY, existingMetadata, target.constructor);
// 替换方法实现为发送RPC调用
descriptor.value = async function (this: ClientNetworkBehaviour, ...args: NetworkValue[]) {
try {
// 获取NetworkIdentity
const networkIdentity = this.entity?.getComponent('NetworkIdentity' as any);
if (!networkIdentity) {
throw new Error('NetworkIdentity component not found');
}
// 检查权限要求
if (options.requireAuthority && !(networkIdentity as any).hasAuthority) {
throw new Error(`ServerRpc ${methodName} requires authority`);
}
if (options.requireLocalPlayer && !(networkIdentity as any).isLocalPlayer) {
throw new Error(`ServerRpc ${methodName} requires local player`);
}
// 发送RPC到服务器
if (options.reliable) {
const result = await this.sendServerRpc(methodName, ...args);
return result;
} else {
await this.sendServerRpcUnreliable(methodName, ...args);
return null;
}
} catch (error) {
console.error(`Error sending ServerRpc ${methodName}:`, error);
throw error;
}
};
// 保存原方法到特殊属性,用于本地预测或调试
(descriptor.value as any).__originalMethod = originalMethod;
return descriptor;
};
}
/**
* 获取类的所有ServerRpc元数据
*/
export function getServerRpcMetadata(target: any): ServerRpcMetadata[] {
return Reflect.getMetadata(SERVER_RPC_METADATA_KEY, target) || [];
}
/**
* 检查方法是否为ServerRpc
*/
export function isServerRpc(target: any, methodName: string): boolean {
const metadata = getServerRpcMetadata(target);
return metadata.some(meta => meta.methodName === methodName);
}
/**
* 获取特定方法的ServerRpc选项
*/
export function getServerRpcOptions(target: any, methodName: string): ServerRpcOptions | null {
const metadata = getServerRpcMetadata(target);
const rpc = metadata.find(meta => meta.methodName === methodName);
return rpc ? rpc.options : null;
}
/**
* 获取方法的原始实现(未被装饰器修改的版本)
*/
export function getOriginalMethod(method: Function): Function | null {
return (method as any).__originalMethod || null;
}

View File

@@ -0,0 +1,146 @@
/**
* SyncVar装饰器 - 客户端版本
*
* 用于标记需要同步的变量
*/
import 'reflect-metadata';
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
import { ClientNetworkBehaviour } from '../core/ClientNetworkBehaviour';
/**
* SyncVar配置选项
*/
export interface SyncVarOptions {
/** 是否可从客户端修改 */
clientCanModify?: boolean;
/** 同步间隔(毫秒)0表示立即同步 */
syncInterval?: number;
/** 是否仅同步给所有者 */
ownerOnly?: boolean;
/** 自定义序列化器 */
serializer?: (value: any) => NetworkValue;
/** 自定义反序列化器 */
deserializer?: (value: NetworkValue) => any;
}
/**
* SyncVar元数据键
*/
export const SYNCVAR_METADATA_KEY = Symbol('syncvar');
/**
* SyncVar元数据
*/
export interface SyncVarMetadata {
/** 属性名 */
propertyKey: string;
/** 配置选项 */
options: SyncVarOptions;
}
/**
* SyncVar装饰器
*/
export function SyncVar(options: SyncVarOptions = {}): PropertyDecorator {
return function (target: any, propertyKey: string | symbol) {
const key = propertyKey as string;
// 获取已有的SyncVar元数据
const existingMetadata: SyncVarMetadata[] = Reflect.getMetadata(SYNCVAR_METADATA_KEY, target.constructor) || [];
// 添加新的SyncVar元数据
existingMetadata.push({
propertyKey: key,
options: {
clientCanModify: false,
syncInterval: 0,
ownerOnly: false,
...options
}
});
// 设置元数据
Reflect.defineMetadata(SYNCVAR_METADATA_KEY, existingMetadata, target.constructor);
// 存储原始属性名(用于内部存储)
const privateKey = `_syncvar_${key}`;
// 创建属性访问器
Object.defineProperty(target, key, {
get: function (this: ClientNetworkBehaviour) {
// 从NetworkIdentity获取SyncVar值
const networkIdentity = this.entity?.getComponent('NetworkIdentity' as any);
if (networkIdentity) {
const syncVarValue = (networkIdentity as any).getSyncVar(key);
if (syncVarValue !== undefined) {
return options.deserializer ? options.deserializer(syncVarValue) : syncVarValue;
}
}
// 如果网络值不存在,返回本地存储的值
return (this as any)[privateKey];
},
set: function (this: ClientNetworkBehaviour, value: any) {
const oldValue = (this as any)[privateKey];
const newValue = options.serializer ? options.serializer(value) : value;
// 存储到本地
(this as any)[privateKey] = value;
// 获取NetworkIdentity
const networkIdentity = this.entity?.getComponent('NetworkIdentity' as any);
if (!networkIdentity) {
return;
}
// 检查是否可以修改
if (!options.clientCanModify && !(networkIdentity as any).hasAuthority) {
console.warn(`Cannot modify SyncVar ${key} without authority`);
return;
}
// 注册SyncVar如果尚未注册
(networkIdentity as any).registerSyncVar(key, newValue);
// 更新NetworkIdentity中的值
(networkIdentity as any).updateSyncVar(key, newValue);
// 如果有权威且值发生变化,发送到服务器
if ((networkIdentity as any).hasAuthority && oldValue !== value) {
this.updateSyncVar(key, newValue).catch(error => {
console.error(`Failed to sync variable ${key}:`, error);
});
}
},
enumerable: true,
configurable: true
});
};
}
/**
* 获取类的所有SyncVar元数据
*/
export function getSyncVarMetadata(target: any): SyncVarMetadata[] {
return Reflect.getMetadata(SYNCVAR_METADATA_KEY, target) || [];
}
/**
* 检查属性是否为SyncVar
*/
export function isSyncVar(target: any, propertyKey: string): boolean {
const metadata = getSyncVarMetadata(target);
return metadata.some(meta => meta.propertyKey === propertyKey);
}
/**
* 获取特定属性的SyncVar选项
*/
export function getSyncVarOptions(target: any, propertyKey: string): SyncVarOptions | null {
const metadata = getSyncVarMetadata(target);
const syncVar = metadata.find(meta => meta.propertyKey === propertyKey);
return syncVar ? syncVar.options : null;
}

View File

@@ -0,0 +1,7 @@
/**
* 装饰器导出
*/
export * from './SyncVar';
export * from './ClientRpc';
export * from './ServerRpc';

View File

@@ -0,0 +1,23 @@
/**
* ECS Framework 网络库 - 客户端
*
* 提供网络客户端功能,包括连接管理、预测、插值等
*/
// 核心模块
export * from './core';
// 传输层
export * from './transport';
// 装饰器
export * from './decorators';
// 系统
export * from './systems';
// 接口
export * from './interfaces';
// 版本信息
export const VERSION = '1.0.11';

View File

@@ -0,0 +1,34 @@
/**
* 网络系统相关接口
*/
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
/**
* 可预测组件接口
*
* 实现此接口的组件可以参与客户端预测系统
*/
export interface IPredictable {
/**
* 预测更新
*
* @param inputs 输入数据
* @param timestamp 时间戳
*/
predictUpdate(inputs: NetworkValue, timestamp: number): void;
}
/**
* 可插值组件接口
*
* 实现此接口的组件可以参与插值系统
*/
export interface IInterpolatable {
/**
* 应用插值状态
*
* @param state 插值后的状态数据
*/
applyInterpolatedState(state: NetworkValue): void;
}

View File

@@ -0,0 +1,5 @@
/**
* 接口导出
*/
export * from './NetworkInterfaces';

View File

@@ -0,0 +1,520 @@
/**
* 客户端插值系统
*
* 实现网络对象的平滑插值
*/
import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework';
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
import { NetworkIdentity } from '../core/NetworkIdentity';
import { IInterpolatable } from '../interfaces/NetworkInterfaces';
/**
* 插值状态快照
*/
export interface InterpolationSnapshot {
/** 时间戳 */
timestamp: number;
/** 网络ID */
networkId: string;
/** 状态数据 */
state: NetworkValue;
}
/**
* 插值目标
*/
export interface InterpolationTarget {
/** 网络ID */
networkId: string;
/** 起始状态 */
fromState: NetworkValue;
/** 目标状态 */
toState: NetworkValue;
/** 起始时间 */
fromTime: number;
/** 结束时间 */
toTime: number;
/** 当前插值进度 (0-1) */
progress: number;
}
/**
* 插值配置
*/
export interface InterpolationConfig {
/** 插值延迟(毫秒) */
delay: number;
/** 最大插值时间(毫秒) */
maxTime: number;
/** 插值缓冲区大小 */
bufferSize: number;
/** 外推是否启用 */
enableExtrapolation: boolean;
/** 最大外推时间(毫秒) */
maxExtrapolationTime: number;
}
/**
* 插值算法类型
*/
export enum InterpolationType {
/** 线性插值 */
LINEAR = 'linear',
/** 平滑插值 */
SMOOTHSTEP = 'smoothstep',
/** 三次贝塞尔插值 */
CUBIC = 'cubic'
}
/**
* 客户端插值系统
*/
export class InterpolationSystem extends EntitySystem {
/** 插值状态缓冲区 */
private stateBuffer: Map<string, InterpolationSnapshot[]> = new Map();
/** 当前插值目标 */
private interpolationTargets: Map<string, InterpolationTarget> = new Map();
/** 插值配置 */
private config: InterpolationConfig;
/** 当前时间 */
private currentTime: number = 0;
constructor(config?: Partial<InterpolationConfig>) {
// 使用Matcher查询具有NetworkIdentity的实体
super(Matcher.all(NetworkIdentity));
this.config = {
delay: 100,
maxTime: 500,
bufferSize: 32,
enableExtrapolation: false,
maxExtrapolationTime: 50,
...config
};
this.currentTime = Date.now();
}
/**
* 系统初始化
*/
override initialize(): void {
super.initialize();
this.currentTime = Date.now();
}
/**
* 系统更新
*/
override update(): void {
this.currentTime = Date.now();
this.cleanupOldStates();
// 调用父类update会自动调用process方法处理匹配的实体
super.update();
}
/**
* 处理匹配的实体
*/
protected override process(entities: Entity[]): void {
const interpolationTime = this.currentTime - this.config.delay;
for (const entity of entities) {
const networkIdentity = entity.getComponent(NetworkIdentity);
if (networkIdentity && networkIdentity.isInterpolationEnabled) {
const networkId = networkIdentity.networkId;
const target = this.interpolationTargets.get(networkId);
if (target) {
// 计算插值进度
const duration = target.toTime - target.fromTime;
if (duration > 0) {
const elapsed = interpolationTime - target.fromTime;
target.progress = Math.max(0, Math.min(1, elapsed / duration));
// 执行插值
const interpolatedState = this.interpolateStates(
target.fromState,
target.toState,
target.progress,
InterpolationType.LINEAR
);
// 应用插值状态
this.applyInterpolatedState(entity, interpolatedState);
// 检查是否需要外推
if (target.progress >= 1 && this.config.enableExtrapolation) {
this.performExtrapolation(entity, target, interpolationTime);
}
}
}
}
}
}
/**
* 添加网络状态快照
*/
addStateSnapshot(networkId: string, state: NetworkValue, timestamp: number): void {
// 获取或创建缓冲区
if (!this.stateBuffer.has(networkId)) {
this.stateBuffer.set(networkId, []);
}
const buffer = this.stateBuffer.get(networkId)!;
const snapshot: InterpolationSnapshot = {
timestamp,
networkId,
state
};
// 插入到正确的位置(按时间戳排序)
const insertIndex = this.findInsertIndex(buffer, timestamp);
buffer.splice(insertIndex, 0, snapshot);
// 保持缓冲区大小
if (buffer.length > this.config.bufferSize) {
buffer.shift();
}
// 更新插值目标
this.updateInterpolationTarget(networkId);
}
/**
* 更新插值目标
*/
private updateInterpolationTarget(networkId: string): void {
const buffer = this.stateBuffer.get(networkId);
if (!buffer || buffer.length < 2) {
return;
}
const interpolationTime = this.currentTime - this.config.delay;
// 查找插值区间
const { from, to } = this.findInterpolationRange(buffer, interpolationTime);
if (!from || !to) {
return;
}
// 更新或创建插值目标
this.interpolationTargets.set(networkId, {
networkId,
fromState: from.state,
toState: to.state,
fromTime: from.timestamp,
toTime: to.timestamp,
progress: 0
});
}
/**
* 查找插值区间
*/
private findInterpolationRange(buffer: InterpolationSnapshot[], time: number): {
from: InterpolationSnapshot | null;
to: InterpolationSnapshot | null;
} {
let from: InterpolationSnapshot | null = null;
let to: InterpolationSnapshot | null = null;
for (let i = 0; i < buffer.length - 1; i++) {
const current = buffer[i];
const next = buffer[i + 1];
if (time >= current.timestamp && time <= next.timestamp) {
from = current;
to = next;
break;
}
}
// 如果没有找到区间,使用最近的两个状态
if (!from && !to && buffer.length >= 2) {
if (time < buffer[0].timestamp) {
// 时间过早,使用前两个状态
from = buffer[0];
to = buffer[1];
} else if (time > buffer[buffer.length - 1].timestamp) {
// 时间过晚,使用后两个状态
from = buffer[buffer.length - 2];
to = buffer[buffer.length - 1];
}
}
return { from, to };
}
/**
* 状态插值
*/
private interpolateStates(
fromState: NetworkValue,
toState: NetworkValue,
progress: number,
type: InterpolationType
): NetworkValue {
// 调整插值进度曲线
const adjustedProgress = this.adjustProgress(progress, type);
try {
return this.interpolateValue(fromState, toState, adjustedProgress);
} catch (error) {
console.error('Error interpolating states:', error);
return toState; // 出错时返回目标状态
}
}
/**
* 递归插值值
*/
private interpolateValue(from: NetworkValue, to: NetworkValue, progress: number): NetworkValue {
// 如果类型不同,直接返回目标值
if (typeof from !== typeof to) {
return to;
}
// 数字插值
if (typeof from === 'number' && typeof to === 'number') {
return from + (to - from) * progress;
}
// 字符串插值(直接切换)
if (typeof from === 'string' && typeof to === 'string') {
return progress < 0.5 ? from : to;
}
// 布尔插值(直接切换)
if (typeof from === 'boolean' && typeof to === 'boolean') {
return progress < 0.5 ? from : to;
}
// 数组插值
if (Array.isArray(from) && Array.isArray(to)) {
const result: NetworkValue[] = [];
const maxLength = Math.max(from.length, to.length);
for (let i = 0; i < maxLength; i++) {
const fromValue = i < from.length ? from[i] : to[i];
const toValue = i < to.length ? to[i] : from[i];
result[i] = this.interpolateValue(fromValue, toValue, progress);
}
return result;
}
// 对象插值
if (from && to && typeof from === 'object' && typeof to === 'object') {
const result: any = {};
const allKeys = new Set([...Object.keys(from), ...Object.keys(to)]);
for (const key of allKeys) {
const fromValue = (from as any)[key];
const toValue = (to as any)[key];
if (fromValue !== undefined && toValue !== undefined) {
result[key] = this.interpolateValue(fromValue, toValue, progress);
} else {
result[key] = toValue !== undefined ? toValue : fromValue;
}
}
return result;
}
// 其他类型直接返回目标值
return to;
}
/**
* 调整插值进度曲线
*/
private adjustProgress(progress: number, type: InterpolationType): number {
switch (type) {
case InterpolationType.LINEAR:
return progress;
case InterpolationType.SMOOTHSTEP:
return progress * progress * (3 - 2 * progress);
case InterpolationType.CUBIC:
return progress < 0.5
? 4 * progress * progress * progress
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
default:
return progress;
}
}
/**
* 应用插值状态到实体
*/
private applyInterpolatedState(entity: Entity, state: NetworkValue): void {
// 获取所有可插值的组件
const components: any[] = [];
for (const component of components) {
if (this.isInterpolatable(component)) {
try {
(component as IInterpolatable).applyInterpolatedState(state);
} catch (error) {
console.error('Error applying interpolated state:', error);
}
}
}
// 更新NetworkIdentity中的状态
const networkIdentity = entity.getComponent(NetworkIdentity);
if (networkIdentity && typeof networkIdentity.deserializeState === 'function') {
try {
networkIdentity.deserializeState(state);
} catch (error) {
console.error('Error deserializing interpolated state:', error);
}
}
}
/**
* 检查组件是否实现了IInterpolatable接口
*/
private isInterpolatable(component: any): component is IInterpolatable {
return component && typeof component.applyInterpolatedState === 'function';
}
/**
* 执行外推
*/
private performExtrapolation(entity: Entity, target: InterpolationTarget, currentTime: number): void {
if (!this.config.enableExtrapolation) {
return;
}
const extrapolationTime = currentTime - target.toTime;
if (extrapolationTime > this.config.maxExtrapolationTime) {
return;
}
// 计算外推状态
const extrapolationProgress = extrapolationTime / (target.toTime - target.fromTime);
const extrapolatedState = this.extrapolateState(
target.fromState,
target.toState,
1 + extrapolationProgress
);
// 应用外推状态
this.applyInterpolatedState(entity, extrapolatedState);
}
/**
* 状态外推
*/
private extrapolateState(fromState: NetworkValue, toState: NetworkValue, progress: number): NetworkValue {
// 简单的线性外推
return this.interpolateValue(fromState, toState, progress);
}
/**
* 查找插入位置
*/
private findInsertIndex(buffer: InterpolationSnapshot[], timestamp: number): number {
let left = 0;
let right = buffer.length;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (buffer[mid].timestamp < timestamp) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
/**
* 清理过期状态
*/
private cleanupOldStates(): void {
const cutoffTime = this.currentTime - this.config.maxTime;
this.stateBuffer.forEach((buffer, networkId) => {
// 移除过期的状态
const validStates = buffer.filter(snapshot => snapshot.timestamp > cutoffTime);
if (validStates.length !== buffer.length) {
this.stateBuffer.set(networkId, validStates);
}
// 如果缓冲区为空,移除它
if (validStates.length === 0) {
this.stateBuffer.delete(networkId);
this.interpolationTargets.delete(networkId);
}
});
}
/**
* 根据网络ID查找实体
*/
private findEntityByNetworkId(networkId: string): Entity | null {
// 使用系统的entities属性来查找
for (const entity of this.entities) {
const networkIdentity = entity.getComponent(NetworkIdentity);
if (networkIdentity && networkIdentity.networkId === networkId) {
return entity;
}
}
return null;
}
/**
* 设置插值配置
*/
setInterpolationConfig(config: Partial<InterpolationConfig>): void {
this.config = { ...this.config, ...config };
}
/**
* 获取插值统计信息
*/
getInterpolationStats(): { [networkId: string]: { bufferSize: number; progress: number } } {
const stats: { [networkId: string]: { bufferSize: number; progress: number } } = {};
this.stateBuffer.forEach((buffer, networkId) => {
const target = this.interpolationTargets.get(networkId);
stats[networkId] = {
bufferSize: buffer.length,
progress: target ? target.progress : 0
};
});
return stats;
}
/**
* 清空所有插值数据
*/
clearInterpolationData(): void {
this.stateBuffer.clear();
this.interpolationTargets.clear();
}
/**
* 系统销毁
*/
onDestroy(): void {
this.clearInterpolationData();
}
}

View File

@@ -0,0 +1,362 @@
/**
* 客户端预测系统
*
* 实现客户端预测和服务器和解
*/
import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework';
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
import { NetworkIdentity } from '../core/NetworkIdentity';
import { IPredictable } from '../interfaces/NetworkInterfaces';
/**
* 预测状态快照
*/
export interface PredictionSnapshot {
/** 时间戳 */
timestamp: number;
/** 网络ID */
networkId: string;
/** 状态数据 */
state: NetworkValue;
/** 输入数据 */
inputs?: NetworkValue;
}
/**
* 预测输入
*/
export interface PredictionInput {
/** 时间戳 */
timestamp: number;
/** 输入数据 */
data: NetworkValue;
}
/**
* 客户端预测系统
*/
export class PredictionSystem extends EntitySystem {
/** 预测状态缓冲区 */
private predictionBuffer: Map<string, PredictionSnapshot[]> = new Map();
/** 输入缓冲区 */
private inputBuffer: PredictionInput[] = [];
/** 最大缓冲区大小 */
private maxBufferSize: number = 64;
/** 预测时间窗口(毫秒) */
private predictionWindow: number = 500;
/** 当前预测时间戳 */
private currentPredictionTime: number = 0;
constructor(maxBufferSize = 64, predictionWindow = 500) {
// 使用Matcher查询具有NetworkIdentity的实体
super(Matcher.all(NetworkIdentity));
this.maxBufferSize = maxBufferSize;
this.predictionWindow = predictionWindow;
this.currentPredictionTime = Date.now();
}
/**
* 系统初始化
*/
override initialize(): void {
super.initialize();
this.currentPredictionTime = Date.now();
}
/**
* 系统更新
*/
override update(): void {
this.currentPredictionTime = Date.now();
this.cleanupOldSnapshots();
// 调用父类update会自动调用process方法处理匹配的实体
super.update();
}
/**
* 处理匹配的实体
*/
protected override process(entities: Entity[]): void {
for (const entity of entities) {
const networkIdentity = entity.getComponent(NetworkIdentity);
if (networkIdentity &&
networkIdentity.isPredictionEnabled &&
networkIdentity.isLocalPlayer) {
// 保存当前状态快照
this.saveSnapshot(entity);
// 应用当前输入进行预测
const currentInputs = this.getCurrentInputs();
if (currentInputs) {
this.applyInputs(entity, currentInputs, this.currentPredictionTime);
}
}
}
}
/**
* 添加预测输入
*/
addInput(input: PredictionInput): void {
this.inputBuffer.push(input);
// 保持输入缓冲区大小
if (this.inputBuffer.length > this.maxBufferSize) {
this.inputBuffer.shift();
}
// 按时间戳排序
this.inputBuffer.sort((a, b) => a.timestamp - b.timestamp);
}
/**
* 保存预测状态快照
*/
saveSnapshot(entity: Entity): void {
const networkIdentity = entity.getComponent(NetworkIdentity);
if (!networkIdentity || !networkIdentity.isPredictionEnabled) {
return;
}
const networkId = networkIdentity.networkId;
const snapshot: PredictionSnapshot = {
timestamp: this.currentPredictionTime,
networkId,
state: networkIdentity.serializeState(),
inputs: this.getCurrentInputs() || undefined
};
// 获取或创建缓冲区
if (!this.predictionBuffer.has(networkId)) {
this.predictionBuffer.set(networkId, []);
}
const buffer = this.predictionBuffer.get(networkId)!;
buffer.push(snapshot);
// 保持缓冲区大小
if (buffer.length > this.maxBufferSize) {
buffer.shift();
}
}
/**
* 从服务器接收权威状态进行和解
*/
reconcileWithServer(networkId: string, serverState: NetworkValue, serverTimestamp: number): void {
const buffer = this.predictionBuffer.get(networkId);
if (!buffer || buffer.length === 0) {
return;
}
// 查找对应时间戳的预测状态
const predictionSnapshot = this.findSnapshot(buffer, serverTimestamp);
if (!predictionSnapshot) {
return;
}
// 比较预测状态和服务器状态
if (this.statesMatch(predictionSnapshot.state, serverState)) {
// 预测正确,移除已确认的快照
this.removeSnapshotsBeforeTimestamp(buffer, serverTimestamp);
return;
}
// 预测错误,需要进行和解
this.performReconciliation(networkId, serverState, serverTimestamp);
}
/**
* 执行预测和解
*/
private performReconciliation(networkId: string, serverState: NetworkValue, serverTimestamp: number): void {
const entity = this.findEntityByNetworkId(networkId);
if (!entity) {
return;
}
const networkIdentity = entity.getComponent(NetworkIdentity);
if (!networkIdentity) {
return;
}
// 回滚到服务器状态
if (typeof networkIdentity.deserializeState === 'function') {
networkIdentity.deserializeState(serverState);
}
// 重新应用服务器时间戳之后的输入
const buffer = this.predictionBuffer.get(networkId)!;
const snapshotsToReplay = buffer.filter(snapshot => snapshot.timestamp > serverTimestamp);
for (const snapshot of snapshotsToReplay) {
if (snapshot.inputs) {
this.applyInputs(entity, snapshot.inputs, snapshot.timestamp);
}
}
// 清理已和解的快照
this.removeSnapshotsBeforeTimestamp(buffer, serverTimestamp);
}
/**
* 应用输入进行预测计算
*/
private applyInputs(entity: Entity, inputs: NetworkValue, timestamp: number): void {
const networkIdentity = entity.getComponent(NetworkIdentity);
if (!networkIdentity) return;
// 获取实体的所有组件并检查是否实现了IPredictable接口
const components: any[] = [];
for (const component of components) {
if (this.isPredictable(component)) {
try {
(component as IPredictable).predictUpdate(inputs, timestamp);
} catch (error) {
console.error('Error applying prediction:', error);
}
}
}
}
/**
* 检查组件是否实现了IPredictable接口
*/
private isPredictable(component: any): component is IPredictable {
return component && typeof component.predictUpdate === 'function';
}
/**
* 获取当前输入
*/
private getCurrentInputs(): NetworkValue | null {
if (this.inputBuffer.length === 0) {
return null;
}
// 获取最新的输入
return this.inputBuffer[this.inputBuffer.length - 1].data;
}
/**
* 查找指定时间戳的快照
*/
private findSnapshot(buffer: PredictionSnapshot[], timestamp: number): PredictionSnapshot | null {
// 查找最接近的快照
let closest: PredictionSnapshot | null = null;
let minDiff = Number.MAX_SAFE_INTEGER;
for (const snapshot of buffer) {
const diff = Math.abs(snapshot.timestamp - timestamp);
if (diff < minDiff) {
minDiff = diff;
closest = snapshot;
}
}
return closest;
}
/**
* 比较两个状态是否匹配
*/
private statesMatch(predictedState: NetworkValue, serverState: NetworkValue): boolean {
try {
// 简单的JSON比较实际应用中可能需要更精确的比较
return JSON.stringify(predictedState) === JSON.stringify(serverState);
} catch (error) {
return false;
}
}
/**
* 移除指定时间戳之前的快照
*/
private removeSnapshotsBeforeTimestamp(buffer: PredictionSnapshot[], timestamp: number): void {
for (let i = buffer.length - 1; i >= 0; i--) {
if (buffer[i].timestamp < timestamp) {
buffer.splice(0, i + 1);
break;
}
}
}
/**
* 清理过期的快照
*/
private cleanupOldSnapshots(): void {
const cutoffTime = this.currentPredictionTime - this.predictionWindow;
this.predictionBuffer.forEach((buffer, networkId) => {
this.removeSnapshotsBeforeTimestamp(buffer, cutoffTime);
// 如果缓冲区为空,移除它
if (buffer.length === 0) {
this.predictionBuffer.delete(networkId);
}
});
// 清理过期的输入
this.inputBuffer = this.inputBuffer.filter(input =>
input.timestamp > cutoffTime
);
}
/**
* 根据网络ID查找实体
*/
private findEntityByNetworkId(networkId: string): Entity | null {
// 使用系统的entities属性来查找
for (const entity of this.entities) {
const networkIdentity = entity.getComponent(NetworkIdentity);
if (networkIdentity && networkIdentity.networkId === networkId) {
return entity;
}
}
return null;
}
/**
* 设置预测配置
*/
setPredictionConfig(maxBufferSize: number, predictionWindow: number): void {
this.maxBufferSize = maxBufferSize;
this.predictionWindow = predictionWindow;
}
/**
* 获取预测统计信息
*/
getPredictionStats(): { [networkId: string]: number } {
const stats: { [networkId: string]: number } = {};
this.predictionBuffer.forEach((buffer, networkId) => {
stats[networkId] = buffer.length;
});
return stats;
}
/**
* 清空所有预测数据
*/
clearPredictionData(): void {
this.predictionBuffer.clear();
this.inputBuffer = [];
}
/**
* 系统销毁
*/
onDestroy(): void {
this.clearPredictionData();
}
}

View File

@@ -0,0 +1,6 @@
/**
* 系统导出
*/
export * from './PredictionSystem';
export * from './InterpolationSystem';

View File

@@ -0,0 +1,445 @@
/**
* 客户端传输层抽象接口
*/
import { Emitter, ITimer, Core } from '@esengine/ecs-framework';
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
/**
* 客户端传输配置
*/
export interface ClientTransportConfig {
/** 服务器地址 */
host: string;
/** 服务器端口 */
port: number;
/** 是否使用安全连接 */
secure?: boolean;
/** 连接超时时间(毫秒) */
connectionTimeout?: number;
/** 重连间隔(毫秒) */
reconnectInterval?: number;
/** 最大重连次数 */
maxReconnectAttempts?: number;
/** 心跳间隔(毫秒) */
heartbeatInterval?: number;
/** 消息队列最大大小 */
maxQueueSize?: number;
}
/**
* 连接状态
*/
export enum ConnectionState {
/** 断开连接 */
DISCONNECTED = 'disconnected',
/** 连接中 */
CONNECTING = 'connecting',
/** 已连接 */
CONNECTED = 'connected',
/** 认证中 */
AUTHENTICATING = 'authenticating',
/** 已认证 */
AUTHENTICATED = 'authenticated',
/** 重连中 */
RECONNECTING = 'reconnecting',
/** 连接错误 */
ERROR = 'error'
}
/**
* 客户端消息
*/
export interface ClientMessage {
/** 消息类型 */
type: 'rpc' | 'syncvar' | 'system' | 'custom';
/** 消息数据 */
data: NetworkValue;
/** 消息ID用于响应匹配 */
messageId?: string;
/** 是否可靠传输 */
reliable?: boolean;
/** 时间戳 */
timestamp?: number;
}
/**
* 连接统计信息
*/
export interface ConnectionStats {
/** 连接时间 */
connectedAt: Date | null;
/** 连接持续时间(毫秒) */
connectionDuration: number;
/** 发送消息数 */
messagesSent: number;
/** 接收消息数 */
messagesReceived: number;
/** 发送字节数 */
bytesSent: number;
/** 接收字节数 */
bytesReceived: number;
/** 重连次数 */
reconnectCount: number;
/** 丢失消息数 */
messagesLost: number;
/** 平均延迟(毫秒) */
averageLatency: number;
}
/**
* 客户端传输事件
*/
export interface ClientTransportEvents {
/** 连接建立 */
'connected': () => void;
/** 连接断开 */
'disconnected': (reason: string) => void;
/** 连接状态变化 */
'state-changed': (oldState: ConnectionState, newState: ConnectionState) => void;
/** 收到消息 */
'message': (message: ClientMessage) => void;
/** 连接错误 */
'error': (error: Error) => void;
/** 重连开始 */
'reconnecting': (attempt: number, maxAttempts: number) => void;
/** 重连成功 */
'reconnected': () => void;
/** 重连失败 */
'reconnect-failed': () => void;
/** 延迟更新 */
'latency-updated': (latency: number) => void;
}
/**
* 客户端传输层抽象类
*/
export abstract class ClientTransport {
protected config: ClientTransportConfig;
protected state: ConnectionState = ConnectionState.DISCONNECTED;
protected stats: ConnectionStats;
protected messageQueue: ClientMessage[] = [];
protected reconnectAttempts = 0;
protected reconnectTimer: ITimer<any> | null = null;
protected heartbeatTimer: ITimer<any> | null = null;
private latencyMeasurements: number[] = [];
private eventEmitter: Emitter<keyof ClientTransportEvents, any>;
constructor(config: ClientTransportConfig) {
this.eventEmitter = new Emitter<keyof ClientTransportEvents, any>();
this.config = {
secure: false,
connectionTimeout: 10000, // 10秒
reconnectInterval: 3000, // 3秒
maxReconnectAttempts: 10,
heartbeatInterval: 30000, // 30秒
maxQueueSize: 1000,
...config
};
this.stats = {
connectedAt: null,
connectionDuration: 0,
messagesSent: 0,
messagesReceived: 0,
bytesSent: 0,
bytesReceived: 0,
reconnectCount: 0,
messagesLost: 0,
averageLatency: 0
};
}
/**
* 连接到服务器
*/
abstract connect(): Promise<void>;
/**
* 断开连接
*/
abstract disconnect(): Promise<void>;
/**
* 发送消息
*/
abstract sendMessage(message: ClientMessage): Promise<boolean>;
/**
* 获取当前连接状态
*/
getState(): ConnectionState {
return this.state;
}
/**
* 检查是否已连接
*/
isConnected(): boolean {
return this.state === ConnectionState.CONNECTED ||
this.state === ConnectionState.AUTHENTICATED;
}
/**
* 获取连接统计信息
*/
getStats(): ConnectionStats {
if (this.stats.connectedAt) {
this.stats.connectionDuration = Date.now() - this.stats.connectedAt.getTime();
}
return { ...this.stats };
}
/**
* 获取配置
*/
getConfig(): Readonly<ClientTransportConfig> {
return this.config;
}
/**
* 设置状态
*/
protected setState(newState: ConnectionState): void {
if (this.state !== newState) {
const oldState = this.state;
this.state = newState;
this.eventEmitter.emit('state-changed', oldState, newState);
// 特殊状态处理
if (newState === ConnectionState.CONNECTED) {
this.stats.connectedAt = new Date();
this.reconnectAttempts = 0;
this.startHeartbeat();
this.processMessageQueue();
this.eventEmitter.emit('connected');
if (oldState === ConnectionState.RECONNECTING) {
this.eventEmitter.emit('reconnected');
}
} else if (newState === ConnectionState.DISCONNECTED) {
this.stats.connectedAt = null;
this.stopHeartbeat();
}
}
}
/**
* 处理接收到的消息
*/
protected handleMessage(message: ClientMessage): void {
this.stats.messagesReceived++;
if (message.data) {
try {
const messageSize = JSON.stringify(message.data).length;
this.stats.bytesReceived += messageSize;
} catch (error) {
// 忽略序列化错误
}
}
// 处理系统消息
if (message.type === 'system') {
this.handleSystemMessage(message);
return;
}
this.eventEmitter.emit('message', message);
}
/**
* 处理系统消息
*/
protected handleSystemMessage(message: ClientMessage): void {
const data = message.data as any;
switch (data.action) {
case 'ping':
// 响应ping
this.sendMessage({
type: 'system',
data: { action: 'pong', timestamp: data.timestamp }
});
break;
case 'pong':
// 计算延迟
if (data.timestamp) {
const latency = Date.now() - data.timestamp;
this.updateLatency(latency);
}
break;
}
}
/**
* 处理连接错误
*/
protected handleError(error: Error): void {
console.error('Transport error:', error.message);
this.eventEmitter.emit('error', error);
if (this.isConnected()) {
this.setState(ConnectionState.ERROR);
this.startReconnect();
}
}
/**
* 开始重连
*/
protected startReconnect(): void {
if (this.reconnectAttempts >= this.config.maxReconnectAttempts!) {
this.eventEmitter.emit('reconnect-failed');
return;
}
this.setState(ConnectionState.RECONNECTING);
this.reconnectAttempts++;
this.stats.reconnectCount++;
this.eventEmitter.emit('reconnecting', this.reconnectAttempts, this.config.maxReconnectAttempts!);
this.reconnectTimer = Core.schedule(this.config.reconnectInterval! / 1000, false, this, async () => {
try {
await this.connect();
} catch (error) {
this.startReconnect(); // 继续重连
}
});
}
/**
* 停止重连
*/
protected stopReconnect(): void {
if (this.reconnectTimer) {
this.reconnectTimer.stop();
this.reconnectTimer = null;
}
}
/**
* 将消息加入队列
*/
protected queueMessage(message: ClientMessage): boolean {
if (this.messageQueue.length >= this.config.maxQueueSize!) {
this.stats.messagesLost++;
return false;
}
this.messageQueue.push(message);
return true;
}
/**
* 处理消息队列
*/
protected async processMessageQueue(): Promise<void> {
while (this.messageQueue.length > 0 && this.isConnected()) {
const message = this.messageQueue.shift()!;
await this.sendMessage(message);
}
}
/**
* 开始心跳
*/
protected startHeartbeat(): void {
if (this.config.heartbeatInterval && this.config.heartbeatInterval > 0) {
this.heartbeatTimer = Core.schedule(this.config.heartbeatInterval / 1000, true, this, () => {
this.sendHeartbeat();
});
}
}
/**
* 停止心跳
*/
protected stopHeartbeat(): void {
if (this.heartbeatTimer) {
this.heartbeatTimer.stop();
this.heartbeatTimer = null;
}
}
/**
* 发送心跳
*/
protected sendHeartbeat(): void {
this.sendMessage({
type: 'system',
data: { action: 'ping', timestamp: Date.now() }
}).catch(() => {
// 心跳发送失败,可能连接有问题
});
}
/**
* 更新延迟统计
*/
protected updateLatency(latency: number): void {
this.latencyMeasurements.push(latency);
// 只保留最近的10个测量值
if (this.latencyMeasurements.length > 10) {
this.latencyMeasurements.shift();
}
// 计算平均延迟
const sum = this.latencyMeasurements.reduce((a, b) => a + b, 0);
this.stats.averageLatency = sum / this.latencyMeasurements.length;
this.eventEmitter.emit('latency-updated', latency);
}
/**
* 更新发送统计
*/
protected updateSendStats(message: ClientMessage): void {
this.stats.messagesSent++;
if (message.data) {
try {
const messageSize = JSON.stringify(message.data).length;
this.stats.bytesSent += messageSize;
} catch (error) {
// 忽略序列化错误
}
}
}
/**
* 销毁传输层
*/
destroy(): void {
this.stopReconnect();
this.stopHeartbeat();
this.messageQueue = [];
}
/**
* 类型安全的事件监听
*/
on<K extends keyof ClientTransportEvents>(event: K, listener: ClientTransportEvents[K]): this {
this.eventEmitter.addObserver(event, listener, this);
return this;
}
/**
* 移除事件监听
*/
off<K extends keyof ClientTransportEvents>(event: K, listener: ClientTransportEvents[K]): this {
this.eventEmitter.removeObserver(event, listener);
return this;
}
/**
* 类型安全的事件触发
*/
emit<K extends keyof ClientTransportEvents>(event: K, ...args: Parameters<ClientTransportEvents[K]>): void {
this.eventEmitter.emit(event, ...args);
}
}

View File

@@ -0,0 +1,427 @@
/**
* HTTP 客户端传输实现
*
* 支持 REST API 和长轮询
*/
import { Core, ITimer } from '@esengine/ecs-framework';
import {
ClientTransport,
ClientTransportConfig,
ConnectionState,
ClientMessage
} from './ClientTransport';
/**
* HTTP 客户端配置
*/
export interface HttpClientConfig extends ClientTransportConfig {
/** API 路径前缀 */
apiPrefix?: string;
/** 请求超时时间(毫秒) */
requestTimeout?: number;
/** 长轮询超时时间(毫秒) */
longPollTimeout?: number;
/** 是否启用长轮询 */
enableLongPolling?: boolean;
/** 额外的请求头 */
headers?: Record<string, string>;
/** 认证令牌 */
authToken?: string;
}
/**
* HTTP 响应接口
*/
interface HttpResponse {
success: boolean;
data?: any;
error?: string;
messages?: ClientMessage[];
}
/**
* HTTP 客户端传输
*/
export class HttpClientTransport extends ClientTransport {
private connectionId: string | null = null;
private longPollController: AbortController | null = null;
private longPollRunning = false;
private connectPromise: Promise<void> | null = null;
private requestTimers: Set<ITimer<any>> = new Set();
protected override config: HttpClientConfig;
constructor(config: HttpClientConfig) {
super(config);
this.config = {
apiPrefix: '/api',
requestTimeout: 30000, // 30秒
longPollTimeout: 25000, // 25秒
enableLongPolling: true,
headers: {
'Content-Type': 'application/json'
},
...config
};
}
/**
* 连接到服务器
*/
async connect(): Promise<void> {
if (this.state === ConnectionState.CONNECTING ||
this.state === ConnectionState.CONNECTED) {
return this.connectPromise || Promise.resolve();
}
this.setState(ConnectionState.CONNECTING);
this.stopReconnect();
this.connectPromise = this.performConnect();
return this.connectPromise;
}
/**
* 执行连接
*/
private async performConnect(): Promise<void> {
try {
// 发送连接请求
const response = await this.makeRequest('/connect', 'POST', {});
if (response.success && response.data.connectionId) {
this.connectionId = response.data.connectionId;
this.setState(ConnectionState.CONNECTED);
// 启动长轮询
if (this.config.enableLongPolling) {
this.startLongPolling();
}
} else {
throw new Error(response.error || 'Connection failed');
}
} catch (error) {
this.setState(ConnectionState.ERROR);
throw error;
}
}
/**
* 断开连接
*/
async disconnect(): Promise<void> {
this.stopReconnect();
this.stopLongPolling();
if (this.connectionId) {
try {
await this.makeRequest('/disconnect', 'POST', {
connectionId: this.connectionId
});
} catch (error) {
// 忽略断开连接时的错误
}
this.connectionId = null;
}
this.setState(ConnectionState.DISCONNECTED);
this.connectPromise = null;
}
/**
* 发送消息
*/
async sendMessage(message: ClientMessage): Promise<boolean> {
if (!this.connectionId) {
// 如果未连接,将消息加入队列
if (this.state === ConnectionState.CONNECTING ||
this.state === ConnectionState.RECONNECTING) {
return this.queueMessage(message);
}
return false;
}
try {
const response = await this.makeRequest('/send', 'POST', {
connectionId: this.connectionId,
message: {
...message,
timestamp: message.timestamp || Date.now()
}
});
if (response.success) {
this.updateSendStats(message);
return true;
} else {
console.error('Send message failed:', response.error);
return false;
}
} catch (error) {
this.handleError(error as Error);
return false;
}
}
/**
* 启动长轮询
*/
private startLongPolling(): void {
if (this.longPollRunning || !this.connectionId) {
return;
}
this.longPollRunning = true;
this.performLongPoll();
}
/**
* 停止长轮询
*/
private stopLongPolling(): void {
this.longPollRunning = false;
if (this.longPollController) {
this.longPollController.abort();
this.longPollController = null;
}
}
/**
* 执行长轮询
*/
private async performLongPoll(): Promise<void> {
while (this.longPollRunning && this.connectionId) {
try {
this.longPollController = new AbortController();
const response = await this.makeRequest('/poll', 'GET', {
connectionId: this.connectionId
}, {
signal: this.longPollController.signal,
timeout: this.config.longPollTimeout
});
if (response.success && response.messages && response.messages.length > 0) {
// 处理接收到的消息
for (const message of response.messages) {
this.handleMessage(message);
}
}
// 如果服务器指示断开连接
if (response.data && response.data.disconnected) {
this.handleServerDisconnect();
break;
}
} catch (error) {
if ((error as any).name === 'AbortError') {
// 被主动取消,正常情况
break;
}
console.warn('Long polling error:', (error as Error).message);
// 如果是网络错误,尝试重连
if (this.isNetworkError(error as Error)) {
this.handleError(error as Error);
break;
}
// 短暂等待后重试
await this.delay(1000);
}
this.longPollController = null;
}
}
/**
* 处理服务器主动断开连接
*/
private handleServerDisconnect(): void {
this.connectionId = null;
this.stopLongPolling();
this.emit('disconnected', 'Server disconnect');
if (this.reconnectAttempts < this.config.maxReconnectAttempts!) {
this.startReconnect();
} else {
this.setState(ConnectionState.DISCONNECTED);
}
}
/**
* 发送 HTTP 请求
*/
private async makeRequest(
path: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
data?: any,
options: {
signal?: AbortSignal;
timeout?: number;
} = {}
): Promise<HttpResponse> {
const url = this.buildUrl(path);
const headers = this.buildHeaders();
const requestOptions: RequestInit = {
method,
headers,
signal: options.signal
};
// 添加请求体
if (method !== 'GET' && data) {
requestOptions.body = JSON.stringify(data);
} else if (method === 'GET' && data) {
// GET 请求将数据作为查询参数
const params = new URLSearchParams();
Object.entries(data).forEach(([key, value]) => {
params.append(key, String(value));
});
const separator = url.includes('?') ? '&' : '?';
return this.fetchWithTimeout(`${url}${separator}${params}`, requestOptions, options.timeout);
}
return this.fetchWithTimeout(url, requestOptions, options.timeout);
}
/**
* 带超时的 fetch 请求
*/
private async fetchWithTimeout(
url: string,
options: RequestInit,
timeout?: number
): Promise<HttpResponse> {
const actualTimeout = timeout || this.config.requestTimeout!;
const controller = new AbortController();
let timeoutTimer: ITimer<any> | null = null;
// 创建超时定时器
timeoutTimer = Core.schedule(actualTimeout / 1000, false, this, () => {
controller.abort();
if (timeoutTimer) {
this.requestTimers.delete(timeoutTimer);
}
});
this.requestTimers.add(timeoutTimer);
try {
const response = await fetch(url, {
...options,
signal: options.signal || controller.signal
});
// 清理定时器
if (timeoutTimer) {
timeoutTimer.stop();
this.requestTimers.delete(timeoutTimer);
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
return result as HttpResponse;
} catch (error) {
// 清理定时器
if (timeoutTimer) {
timeoutTimer.stop();
this.requestTimers.delete(timeoutTimer);
}
throw error;
}
}
/**
* 构建请求URL
*/
private buildUrl(path: string): string {
const protocol = this.config.secure ? 'https' : 'http';
const basePath = this.config.apiPrefix || '';
const cleanPath = path.startsWith('/') ? path : `/${path}`;
return `${protocol}://${this.config.host}:${this.config.port}${basePath}${cleanPath}`;
}
/**
* 构建请求头
*/
private buildHeaders(): Record<string, string> {
const headers = { ...this.config.headers };
if (this.config.authToken) {
headers['Authorization'] = `Bearer ${this.config.authToken}`;
}
return headers;
}
/**
* 检查是否为网络错误
*/
private isNetworkError(error: Error): boolean {
return error.message.includes('fetch') ||
error.message.includes('network') ||
error.message.includes('timeout') ||
error.name === 'TypeError';
}
/**
* 延迟函数
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => {
const timer = Core.schedule(ms / 1000, false, this, () => {
this.requestTimers.delete(timer);
resolve();
});
this.requestTimers.add(timer);
});
}
/**
* 设置认证令牌
*/
setAuthToken(token: string): void {
this.config.authToken = token;
}
/**
* 获取连接ID
*/
getConnectionId(): string | null {
return this.connectionId;
}
/**
* 检查是否支持 Fetch API
*/
static isSupported(): boolean {
return typeof fetch !== 'undefined';
}
/**
* 销毁传输层
*/
override destroy(): void {
// 清理所有请求定时器
this.requestTimers.forEach(timer => timer.stop());
this.requestTimers.clear();
this.disconnect();
super.destroy();
}
}

View File

@@ -0,0 +1,282 @@
/**
* WebSocket 客户端传输实现
*/
import { Core, ITimer } from '@esengine/ecs-framework';
import {
ClientTransport,
ClientTransportConfig,
ConnectionState,
ClientMessage
} from './ClientTransport';
/**
* WebSocket 客户端配置
*/
export interface WebSocketClientConfig extends ClientTransportConfig {
/** WebSocket 路径 */
path?: string;
/** 协议列表 */
protocols?: string | string[];
/** 额外的请求头 */
headers?: Record<string, string>;
/** 是否启用二进制消息 */
binaryType?: 'blob' | 'arraybuffer';
/** WebSocket 扩展 */
extensions?: any;
}
/**
* WebSocket 客户端传输
*/
export class WebSocketClientTransport extends ClientTransport {
private websocket: WebSocket | null = null;
private connectionPromise: Promise<void> | null = null;
private connectionTimeoutTimer: ITimer<any> | null = null;
protected override config: WebSocketClientConfig;
constructor(config: WebSocketClientConfig) {
super(config);
this.config = {
path: '/ws',
protocols: [],
headers: {},
binaryType: 'arraybuffer',
...config
};
}
/**
* 连接到服务器
*/
async connect(): Promise<void> {
if (this.state === ConnectionState.CONNECTING ||
this.state === ConnectionState.CONNECTED) {
return this.connectionPromise || Promise.resolve();
}
this.setState(ConnectionState.CONNECTING);
this.stopReconnect(); // 停止任何正在进行的重连
this.connectionPromise = new Promise((resolve, reject) => {
try {
// 构建WebSocket URL
const protocol = this.config.secure ? 'wss' : 'ws';
const url = `${protocol}://${this.config.host}:${this.config.port}${this.config.path}`;
// 创建WebSocket连接
this.websocket = new WebSocket(url, this.config.protocols);
if (this.config.binaryType) {
this.websocket.binaryType = this.config.binaryType;
}
// 设置连接超时
this.connectionTimeoutTimer = Core.schedule(this.config.connectionTimeout! / 1000, false, this, () => {
if (this.websocket && this.websocket.readyState === WebSocket.CONNECTING) {
this.websocket.close();
reject(new Error('Connection timeout'));
}
});
// WebSocket 事件处理
this.websocket.onopen = () => {
if (this.connectionTimeoutTimer) {
this.connectionTimeoutTimer.stop();
this.connectionTimeoutTimer = null;
}
this.setState(ConnectionState.CONNECTED);
resolve();
};
this.websocket.onclose = (event) => {
if (this.connectionTimeoutTimer) {
this.connectionTimeoutTimer.stop();
this.connectionTimeoutTimer = null;
}
this.handleClose(event.code, event.reason);
if (this.state === ConnectionState.CONNECTING) {
reject(new Error(`Connection failed: ${event.reason || 'Unknown error'}`));
}
};
this.websocket.onerror = (event) => {
if (this.connectionTimeoutTimer) {
this.connectionTimeoutTimer.stop();
this.connectionTimeoutTimer = null;
}
const error = new Error('WebSocket error');
this.handleError(error);
if (this.state === ConnectionState.CONNECTING) {
reject(error);
}
};
this.websocket.onmessage = (event) => {
this.handleWebSocketMessage(event);
};
} catch (error) {
this.setState(ConnectionState.ERROR);
reject(error);
}
});
return this.connectionPromise;
}
/**
* 断开连接
*/
async disconnect(): Promise<void> {
this.stopReconnect();
if (this.websocket) {
// 设置状态为断开连接,避免触发重连
this.setState(ConnectionState.DISCONNECTED);
if (this.websocket.readyState === WebSocket.OPEN ||
this.websocket.readyState === WebSocket.CONNECTING) {
this.websocket.close(1000, 'Client disconnect');
}
this.websocket = null;
}
this.connectionPromise = null;
}
/**
* 发送消息
*/
async sendMessage(message: ClientMessage): Promise<boolean> {
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
// 如果未连接,将消息加入队列
if (this.state === ConnectionState.CONNECTING ||
this.state === ConnectionState.RECONNECTING) {
return this.queueMessage(message);
}
return false;
}
try {
// 序列化消息
const serialized = JSON.stringify({
...message,
timestamp: message.timestamp || Date.now()
});
// 发送消息
this.websocket.send(serialized);
this.updateSendStats(message);
return true;
} catch (error) {
this.handleError(error as Error);
return false;
}
}
/**
* 处理 WebSocket 消息
*/
private handleWebSocketMessage(event: MessageEvent): void {
try {
let data: string;
if (event.data instanceof ArrayBuffer) {
// 处理二进制数据
data = new TextDecoder().decode(event.data);
} else if (event.data instanceof Blob) {
// Blob 需要异步处理
event.data.text().then(text => {
this.processMessage(text);
});
return;
} else {
// 字符串数据
data = event.data;
}
this.processMessage(data);
} catch (error) {
console.error('Error processing WebSocket message:', error);
}
}
/**
* 处理消息内容
*/
private processMessage(data: string): void {
try {
const message: ClientMessage = JSON.parse(data);
this.handleMessage(message);
} catch (error) {
console.error('Error parsing message:', error);
}
}
/**
* 处理连接关闭
*/
private handleClose(code: number, reason: string): void {
this.websocket = null;
this.connectionPromise = null;
const wasConnected = this.isConnected();
// 根据关闭代码决定是否重连
if (code === 1000) {
// 正常关闭,不重连
this.setState(ConnectionState.DISCONNECTED);
this.emit('disconnected', reason || 'Normal closure');
} else if (wasConnected && this.reconnectAttempts < this.config.maxReconnectAttempts!) {
// 异常关闭,尝试重连
this.emit('disconnected', reason || `Abnormal closure (${code})`);
this.startReconnect();
} else {
// 达到最大重连次数或其他情况
this.setState(ConnectionState.DISCONNECTED);
this.emit('disconnected', reason || `Connection lost (${code})`);
}
}
/**
* 获取 WebSocket 就绪状态
*/
getReadyState(): number {
return this.websocket?.readyState ?? WebSocket.CLOSED;
}
/**
* 获取 WebSocket 实例
*/
getWebSocket(): WebSocket | null {
return this.websocket;
}
/**
* 检查是否支持 WebSocket
*/
static isSupported(): boolean {
return typeof WebSocket !== 'undefined';
}
/**
* 销毁传输层
*/
override destroy(): void {
if (this.connectionTimeoutTimer) {
this.connectionTimeoutTimer.stop();
this.connectionTimeoutTimer = null;
}
this.disconnect();
super.destroy();
}
}

View File

@@ -0,0 +1,7 @@
/**
* 传输层导出
*/
export * from './ClientTransport';
export * from './WebSocketClientTransport';
export * from './HttpClientTransport';

View File

@@ -0,0 +1,384 @@
/**
* NetworkClient 集成测试
* 测试网络客户端的完整功能,包括依赖注入和错误处理
*/
import { NetworkClient } from '../src/core/NetworkClient';
// Mock 所有外部依赖
jest.mock('@esengine/ecs-framework', () => ({
Core: {
scene: null,
schedule: {
scheduleRepeating: jest.fn((callback: Function, interval: number) => ({
stop: jest.fn()
}))
}
},
Emitter: jest.fn().mockImplementation(() => ({
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
removeAllListeners: jest.fn()
}))
}));
jest.mock('@esengine/ecs-framework-network-shared', () => ({
NetworkValue: {},
generateMessageId: jest.fn(() => 'test-message-id-123'),
generateNetworkId: jest.fn(() => 12345),
NetworkUtils: {
generateMessageId: jest.fn(() => 'test-message-id-456'),
calculateDistance: jest.fn(() => 100),
isNodeEnvironment: jest.fn(() => false),
isBrowserEnvironment: jest.fn(() => true)
}
}));
// Mock WebSocket
class MockWebSocket {
public readyState: number = WebSocket.CONNECTING;
public onopen: ((event: Event) => void) | null = null;
public onclose: ((event: CloseEvent) => void) | null = null;
public onmessage: ((event: MessageEvent) => void) | null = null;
public onerror: ((event: Event) => void) | null = null;
constructor(public url: string, public protocols?: string | string[]) {}
send(data: string | ArrayBuffer | Blob): void {}
close(code?: number, reason?: string): void {
this.readyState = WebSocket.CLOSED;
if (this.onclose) {
this.onclose(new CloseEvent('close', { code: code || 1000, reason: reason || '' }));
}
}
}
(global as any).WebSocket = MockWebSocket;
(global as any).WebSocket.CONNECTING = 0;
(global as any).WebSocket.OPEN = 1;
(global as any).WebSocket.CLOSING = 2;
(global as any).WebSocket.CLOSED = 3;
describe('NetworkClient 集成测试', () => {
let client: NetworkClient;
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
if (client) {
client.disconnect().catch(() => {});
client = null as any;
}
});
describe('依赖注入测试', () => {
it('应该正确处理所有依赖模块', () => {
expect(() => {
client = new NetworkClient({
transport: 'websocket',
transportConfig: {
host: 'localhost',
port: 8080
}
});
}).not.toThrow();
expect(client).toBeInstanceOf(NetworkClient);
});
it('应该正确使用network-shared中的工具函数', () => {
const { generateMessageId, NetworkUtils } = require('@esengine/ecs-framework-network-shared');
client = new NetworkClient({
transport: 'websocket',
transportConfig: {
host: 'localhost',
port: 8080
}
});
// 验证network-shared模块被正确导入
expect(generateMessageId).toBeDefined();
expect(NetworkUtils).toBeDefined();
});
it('应该正确使用ecs-framework中的Core模块', () => {
const { Core } = require('@esengine/ecs-framework');
client = new NetworkClient({
transport: 'websocket',
transportConfig: {
host: 'localhost',
port: 8080
}
});
expect(Core).toBeDefined();
expect(Core.schedule).toBeDefined();
});
});
describe('构造函数错误处理', () => {
it('应该处理network-shared模块导入失败', () => {
// 重置模块并模拟导入失败
jest.resetModules();
jest.doMock('@esengine/ecs-framework-network-shared', () => {
throw new Error('network-shared模块导入失败');
});
expect(() => {
const { NetworkClient } = require('../src/core/NetworkClient');
new NetworkClient({
transportType: 'websocket',
host: 'localhost',
port: 8080
});
}).toThrow();
});
it('应该处理ecs-framework模块导入失败', () => {
// 重置模块并模拟导入失败
jest.resetModules();
jest.doMock('@esengine/ecs-framework', () => {
throw new Error('ecs-framework模块导入失败');
});
expect(() => {
const { NetworkClient } = require('../src/core/NetworkClient');
new NetworkClient({
transportType: 'websocket',
host: 'localhost',
port: 8080
});
}).toThrow();
});
it('应该处理传输层构造失败', () => {
// Mock传输层构造函数抛出异常
const originalWebSocket = (global as any).WebSocket;
(global as any).WebSocket = jest.fn(() => {
throw new Error('WebSocket不可用');
});
client = new NetworkClient({
transport: 'websocket',
transportConfig: {
host: 'localhost',
port: 8080
}
});
expect(client.connect()).rejects.toThrow();
// 恢复原始WebSocket
(global as any).WebSocket = originalWebSocket;
});
});
describe('功能测试', () => {
beforeEach(() => {
client = new NetworkClient({
transport: 'websocket',
transportConfig: {
host: 'localhost',
port: 8080
}
});
});
it('应该能够成功连接', async () => {
const connectPromise = client.connect();
// 模拟连接成功
setTimeout(() => {
const transport = (client as any).transport;
if (transport && transport.websocket && transport.websocket.onopen) {
transport.websocket.readyState = WebSocket.OPEN;
transport.websocket.onopen(new Event('open'));
}
}, 0);
await expect(connectPromise).resolves.toBeUndefined();
});
it('应该能够发送消息', async () => {
// 先连接
const connectPromise = client.connect();
setTimeout(() => {
const transport = (client as any).transport;
if (transport && transport.websocket && transport.websocket.onopen) {
transport.websocket.readyState = WebSocket.OPEN;
transport.websocket.onopen(new Event('open'));
}
}, 0);
await connectPromise;
// 发送消息
const message = {
type: 'custom' as const,
data: { test: 'message' },
reliable: true
};
// NetworkClient没有直接的sendMessage方法它通过RPC调用
});
it('应该能够正确断开连接', async () => {
await expect(client.disconnect()).resolves.toBeUndefined();
});
it('应该返回正确的认证状态', () => {
expect(client.isAuthenticated()).toBe(false);
});
it('应该能够获取网络对象列表', () => {
const networkObjects = client.getAllNetworkObjects();
expect(Array.isArray(networkObjects)).toBe(true);
expect(networkObjects.length).toBe(0);
});
});
describe('消息ID生成测试', () => {
beforeEach(() => {
client = new NetworkClient({
transport: 'websocket',
transportConfig: {
host: 'localhost',
port: 8080
}
});
});
it('应该能够生成唯一的消息ID', () => {
const messageId1 = (client as any).generateMessageId();
const messageId2 = (client as any).generateMessageId();
expect(typeof messageId1).toBe('string');
expect(typeof messageId2).toBe('string');
expect(messageId1).not.toBe(messageId2);
});
it('生成的消息ID应该符合预期格式', () => {
const messageId = (client as any).generateMessageId();
// 检查消息ID格式时间戳 + 随机字符串)
expect(messageId).toMatch(/^[a-z0-9]+$/);
expect(messageId.length).toBeGreaterThan(10);
});
});
describe('错误恢复测试', () => {
beforeEach(() => {
client = new NetworkClient({
transportType: 'websocket',
host: 'localhost',
port: 8080,
maxReconnectAttempts: 2,
reconnectInterval: 100
});
});
it('连接失败后应该尝试重连', async () => {
let connectAttempts = 0;
const originalWebSocket = (global as any).WebSocket;
(global as any).WebSocket = jest.fn().mockImplementation(() => {
connectAttempts++;
const ws = new originalWebSocket('ws://localhost:8080');
// 模拟连接失败
setTimeout(() => {
if (ws.onerror) {
ws.onerror(new Event('error'));
}
}, 0);
return ws;
});
await expect(client.connect()).rejects.toThrow();
// 等待重连尝试
await new Promise(resolve => setTimeout(resolve, 300));
expect(connectAttempts).toBeGreaterThan(1);
// 恢复原始WebSocket
(global as any).WebSocket = originalWebSocket;
});
it('达到最大重连次数后应该停止重连', async () => {
const maxAttempts = 2;
client = new NetworkClient({
transportType: 'websocket',
host: 'localhost',
port: 8080,
maxReconnectAttempts: maxAttempts,
reconnectInterval: 50
});
let connectAttempts = 0;
const originalWebSocket = (global as any).WebSocket;
(global as any).WebSocket = jest.fn().mockImplementation(() => {
connectAttempts++;
const ws = new originalWebSocket('ws://localhost:8080');
setTimeout(() => {
if (ws.onerror) {
ws.onerror(new Event('error'));
}
}, 0);
return ws;
});
await expect(client.connect()).rejects.toThrow();
// 等待所有重连尝试完成
await new Promise(resolve => setTimeout(resolve, 200));
expect(connectAttempts).toBeLessThanOrEqual(maxAttempts + 1);
// 恢复原始WebSocket
(global as any).WebSocket = originalWebSocket;
});
});
describe('内存泄漏防护测试', () => {
it('断开连接时应该清理所有资源', async () => {
client = new NetworkClient({
transport: 'websocket',
transportConfig: {
host: 'localhost',
port: 8080
}
});
const { Emitter } = require('@esengine/ecs-framework');
const emitterInstance = Emitter.mock.results[Emitter.mock.results.length - 1].value;
await client.disconnect();
expect(emitterInstance.removeAllListeners).toHaveBeenCalled();
});
it('多次创建和销毁客户端不应该造成内存泄漏', () => {
const initialEmitterCallCount = require('@esengine/ecs-framework').Emitter.mock.calls.length;
// 创建和销毁多个客户端实例
for (let i = 0; i < 5; i++) {
const tempClient = new NetworkClient({
transportType: 'websocket',
host: 'localhost',
port: 8080
});
tempClient.disconnect().catch(() => {});
}
const finalEmitterCallCount = require('@esengine/ecs-framework').Emitter.mock.calls.length;
// 验证Emitter实例数量符合预期
expect(finalEmitterCallCount - initialEmitterCallCount).toBe(5);
});
});
});

View File

@@ -0,0 +1,27 @@
import 'reflect-metadata';
// Mock WebSocket for testing
(global as any).WebSocket = class MockWebSocket {
onopen: ((event: Event) => void) | null = null;
onmessage: ((event: MessageEvent) => void) | null = null;
onclose: ((event: CloseEvent) => void) | null = null;
onerror: ((event: Event) => void) | null = null;
constructor(public url: string) {}
send(data: string | ArrayBuffer | Blob) {
// Mock implementation
}
close() {
// Mock implementation
}
};
global.beforeEach(() => {
jest.clearAllMocks();
});
global.afterEach(() => {
jest.restoreAllMocks();
});

View File

@@ -0,0 +1,374 @@
/**
* ClientTransport 基类测试
* 测试客户端传输层基类的构造函数和依赖问题
*/
import { ClientTransport, ClientTransportConfig, ConnectionState } from '../../src/transport/ClientTransport';
// Mock Emitter 和 Core
jest.mock('@esengine/ecs-framework', () => ({
Emitter: jest.fn().mockImplementation(() => ({
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
removeAllListeners: jest.fn()
})),
Core: {
schedule: {
scheduleRepeating: jest.fn((callback: Function, interval: number) => ({
stop: jest.fn()
}))
}
}
}));
// Mock network-shared
jest.mock('@esengine/ecs-framework-network-shared', () => ({
NetworkValue: {}
}));
// 创建测试用的具体实现类
class TestClientTransport extends ClientTransport {
async connect(): Promise<void> {
return Promise.resolve();
}
async disconnect(): Promise<void> {
return Promise.resolve();
}
async sendMessage(message: any): Promise<void> {
return Promise.resolve();
}
}
describe('ClientTransport', () => {
let transport: TestClientTransport;
const defaultConfig: ClientTransportConfig = {
host: 'localhost',
port: 8080
};
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
if (transport) {
transport = null as any;
}
});
describe('构造函数测试', () => {
it('应该能够成功创建ClientTransport实例', () => {
expect(() => {
transport = new TestClientTransport(defaultConfig);
}).not.toThrow();
expect(transport).toBeInstanceOf(ClientTransport);
});
it('应该正确设置默认配置', () => {
transport = new TestClientTransport(defaultConfig);
const config = (transport as any).config;
expect(config.host).toBe('localhost');
expect(config.port).toBe(8080);
expect(config.secure).toBe(false);
expect(config.connectionTimeout).toBe(10000);
expect(config.reconnectInterval).toBe(3000);
expect(config.maxReconnectAttempts).toBe(10);
expect(config.heartbeatInterval).toBe(30000);
expect(config.maxQueueSize).toBe(1000);
});
it('应该允许自定义配置覆盖默认值', () => {
const customConfig: ClientTransportConfig = {
host: 'example.com',
port: 9090,
secure: true,
connectionTimeout: 15000,
reconnectInterval: 5000,
maxReconnectAttempts: 5,
heartbeatInterval: 60000,
maxQueueSize: 500
};
transport = new TestClientTransport(customConfig);
const config = (transport as any).config;
expect(config.host).toBe('example.com');
expect(config.port).toBe(9090);
expect(config.secure).toBe(true);
expect(config.connectionTimeout).toBe(15000);
expect(config.reconnectInterval).toBe(5000);
expect(config.maxReconnectAttempts).toBe(5);
expect(config.heartbeatInterval).toBe(60000);
expect(config.maxQueueSize).toBe(500);
});
it('应该正确初始化内部状态', () => {
transport = new TestClientTransport(defaultConfig);
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
expect((transport as any).messageQueue).toEqual([]);
expect((transport as any).reconnectAttempts).toBe(0);
expect((transport as any).reconnectTimer).toBeNull();
expect((transport as any).heartbeatTimer).toBeNull();
expect((transport as any).latencyMeasurements).toEqual([]);
});
it('应该正确初始化统计信息', () => {
transport = new TestClientTransport(defaultConfig);
const stats = transport.getStats();
expect(stats.connectedAt).toBeNull();
expect(stats.connectionDuration).toBe(0);
expect(stats.messagesSent).toBe(0);
expect(stats.messagesReceived).toBe(0);
expect(stats.bytesSent).toBe(0);
expect(stats.bytesReceived).toBe(0);
expect(stats.averageLatency).toBe(0);
expect(stats.averageLatency).toBe(0);
expect(stats.reconnectCount).toBe(0);
});
});
describe('依赖注入测试', () => {
it('应该正确处理@esengine/ecs-framework中的Emitter', () => {
const { Emitter } = require('@esengine/ecs-framework');
expect(() => {
transport = new TestClientTransport(defaultConfig);
}).not.toThrow();
expect(Emitter).toHaveBeenCalled();
});
it('构造函数中Emitter初始化失败应该抛出异常', () => {
// Mock Emitter构造函数抛出异常
const { Emitter } = require('@esengine/ecs-framework');
Emitter.mockImplementation(() => {
throw new Error('Emitter初始化失败');
});
expect(() => {
transport = new TestClientTransport(defaultConfig);
}).toThrow('Emitter初始化失败');
});
it('应该正确处理@esengine/ecs-framework-network-shared依赖', () => {
const networkShared = require('@esengine/ecs-framework-network-shared');
expect(() => {
transport = new TestClientTransport(defaultConfig);
}).not.toThrow();
expect(networkShared).toBeDefined();
expect(networkShared.NetworkValue).toBeDefined();
});
});
describe('事件系统测试', () => {
beforeEach(() => {
transport = new TestClientTransport(defaultConfig);
});
it('应该能够注册事件监听器', () => {
const mockCallback = jest.fn();
const { Emitter } = require('@esengine/ecs-framework');
const emitterInstance = Emitter.mock.results[0].value;
transport.on('connected', mockCallback);
expect(emitterInstance.on).toHaveBeenCalledWith('connected', mockCallback);
});
it('应该能够移除事件监听器', () => {
const mockCallback = jest.fn();
const { Emitter } = require('@esengine/ecs-framework');
const emitterInstance = Emitter.mock.results[0].value;
transport.off('connected', mockCallback);
expect(emitterInstance.off).toHaveBeenCalledWith('connected', mockCallback);
});
it('应该能够发出事件', () => {
const { Emitter } = require('@esengine/ecs-framework');
const emitterInstance = Emitter.mock.results[0].value;
(transport as any).emit('connected');
expect(emitterInstance.emit).toHaveBeenCalledWith('connected');
});
});
describe('消息队列测试', () => {
beforeEach(() => {
transport = new TestClientTransport(defaultConfig);
});
it('应该能够将消息加入队列', async () => {
const message = {
type: 'custom' as const,
data: { test: 'data' },
reliable: true,
timestamp: Date.now()
};
await transport.sendMessage(message);
const messageQueue = (transport as any).messageQueue;
expect(messageQueue).toHaveLength(1);
expect(messageQueue[0]).toEqual(message);
});
it('消息队列达到最大大小时应该移除旧消息', async () => {
// 设置较小的队列大小
const smallQueueConfig = { ...defaultConfig, maxQueueSize: 2 };
transport = new TestClientTransport(smallQueueConfig);
const message1 = { type: 'custom' as const, data: { id: 1 }, reliable: true, timestamp: Date.now() };
const message2 = { type: 'custom' as const, data: { id: 2 }, reliable: true, timestamp: Date.now() };
const message3 = { type: 'custom' as const, data: { id: 3 }, reliable: true, timestamp: Date.now() };
await transport.sendMessage(message1);
await transport.sendMessage(message2);
await transport.sendMessage(message3);
const messageQueue = (transport as any).messageQueue;
expect(messageQueue).toHaveLength(2);
expect(messageQueue[0]).toEqual(message2);
expect(messageQueue[1]).toEqual(message3);
});
});
describe('连接状态测试', () => {
beforeEach(() => {
transport = new TestClientTransport(defaultConfig);
});
it('应该正确获取连接状态', () => {
expect(transport.getState()).toBe(ConnectionState.DISCONNECTED);
});
it('应该正确检查连接状态', () => {
expect(transport.isConnected()).toBe(false);
(transport as any).state = ConnectionState.CONNECTED;
expect(transport.isConnected()).toBe(true);
(transport as any).state = ConnectionState.AUTHENTICATED;
expect(transport.isConnected()).toBe(true);
});
it('状态变化时应该发出事件', () => {
const { Emitter } = require('@esengine/ecs-framework');
const emitterInstance = Emitter.mock.results[0].value;
(transport as any).setState(ConnectionState.CONNECTING);
expect(emitterInstance.emit).toHaveBeenCalledWith(
'state-changed',
ConnectionState.DISCONNECTED,
ConnectionState.CONNECTING
);
});
});
describe('延迟测量测试', () => {
beforeEach(() => {
transport = new TestClientTransport(defaultConfig);
});
it('应该能够更新延迟测量', () => {
(transport as any).updateLatency(100);
(transport as any).updateLatency(200);
(transport as any).updateLatency(150);
const stats = transport.getStats();
expect(stats.averageLatency).toBe(150);
});
it('应该限制延迟测量样本数量', () => {
// 添加超过最大样本数的测量
for (let i = 0; i < 150; i++) {
(transport as any).updateLatency(i * 10);
}
const latencyMeasurements = (transport as any).latencyMeasurements;
expect(latencyMeasurements.length).toBeLessThanOrEqual(100);
});
});
describe('配置验证测试', () => {
it('应该拒绝无效的主机名', () => {
expect(() => {
transport = new TestClientTransport({ host: '', port: 8080 });
}).toThrow();
});
it('应该拒绝无效的端口号', () => {
expect(() => {
transport = new TestClientTransport({ host: 'localhost', port: 0 });
}).toThrow();
expect(() => {
transport = new TestClientTransport({ host: 'localhost', port: 65536 });
}).toThrow();
});
it('应该拒绝负数的超时配置', () => {
expect(() => {
transport = new TestClientTransport({
host: 'localhost',
port: 8080,
connectionTimeout: -1000
});
}).toThrow();
});
});
describe('资源清理测试', () => {
beforeEach(() => {
transport = new TestClientTransport(defaultConfig);
});
it('应该能够清理所有定时器', () => {
const { Core } = require('@esengine/ecs-framework');
const mockTimer = { stop: jest.fn() };
Core.schedule.scheduleRepeating.mockReturnValue(mockTimer);
// 设置一些定时器
(transport as any).reconnectTimer = mockTimer;
(transport as any).heartbeatTimer = mockTimer;
// 调用清理方法
(transport as any).cleanup();
expect(mockTimer.stop).toHaveBeenCalledTimes(2);
expect((transport as any).reconnectTimer).toBeNull();
expect((transport as any).heartbeatTimer).toBeNull();
});
it('应该能够清理消息队列', () => {
(transport as any).messageQueue = [
{ type: 'custom', data: {}, reliable: true, timestamp: Date.now() }
];
(transport as any).cleanup();
expect((transport as any).messageQueue).toHaveLength(0);
});
it('应该能够移除所有事件监听器', () => {
const { Emitter } = require('@esengine/ecs-framework');
const emitterInstance = Emitter.mock.results[0].value;
(transport as any).cleanup();
expect(emitterInstance.removeAllListeners).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,348 @@
/**
* WebSocketClientTransport 测试
* 测试WebSocket客户端传输层的构造函数和依赖问题
*/
import { WebSocketClientTransport, WebSocketClientConfig } from '../../src/transport/WebSocketClientTransport';
import { ConnectionState } from '../../src/transport/ClientTransport';
// Mock WebSocket
class MockWebSocket {
public readyState: number = WebSocket.CONNECTING;
public onopen: ((event: Event) => void) | null = null;
public onclose: ((event: CloseEvent) => void) | null = null;
public onmessage: ((event: MessageEvent) => void) | null = null;
public onerror: ((event: Event) => void) | null = null;
constructor(public url: string, public protocols?: string | string[]) {}
send(data: string | ArrayBuffer | Blob): void {}
close(code?: number, reason?: string): void {
this.readyState = WebSocket.CLOSED;
if (this.onclose) {
this.onclose(new CloseEvent('close', { code: code || 1000, reason: reason || '' }));
}
}
}
// Mock依赖 - 直接创建mock对象而不依赖外部模块
const mockCore = {
schedule: {
scheduleRepeating: jest.fn((callback: Function, interval: number) => ({
stop: jest.fn()
}))
}
};
const mockEmitter = {
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
removeAllListeners: jest.fn()
};
const mockNetworkShared = {
NetworkValue: {},
generateMessageId: jest.fn(() => 'mock-message-id-123')
};
// 设置模块mock
jest.doMock('@esengine/ecs-framework', () => ({
Core: mockCore,
Emitter: jest.fn(() => mockEmitter)
}));
jest.doMock('@esengine/ecs-framework-network-shared', () => mockNetworkShared);
// 设置全局WebSocket mock
(global as any).WebSocket = MockWebSocket;
(global as any).WebSocket.CONNECTING = 0;
(global as any).WebSocket.OPEN = 1;
(global as any).WebSocket.CLOSING = 2;
(global as any).WebSocket.CLOSED = 3;
describe('WebSocketClientTransport', () => {
let transport: WebSocketClientTransport;
const defaultConfig: WebSocketClientConfig = {
host: 'localhost',
port: 8080,
secure: false,
connectionTimeout: 5000,
reconnectInterval: 1000,
maxReconnectAttempts: 3,
heartbeatInterval: 30000
};
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
if (transport) {
transport.disconnect().catch(() => {});
transport = null as any;
}
});
describe('构造函数测试', () => {
it('应该能够成功创建WebSocketClientTransport实例', () => {
expect(() => {
transport = new WebSocketClientTransport(defaultConfig);
}).not.toThrow();
expect(transport).toBeInstanceOf(WebSocketClientTransport);
});
it('应该正确合并默认配置', () => {
transport = new WebSocketClientTransport(defaultConfig);
const config = (transport as any).config;
expect(config.path).toBe('/ws');
expect(config.protocols).toEqual([]);
expect(config.headers).toEqual({});
expect(config.binaryType).toBe('arraybuffer');
expect(config.host).toBe('localhost');
expect(config.port).toBe(8080);
});
it('应该允许自定义配置覆盖默认值', () => {
const customConfig: WebSocketClientConfig = {
...defaultConfig,
path: '/custom-ws',
protocols: ['custom-protocol'],
headers: { 'X-Custom': 'value' },
binaryType: 'blob'
};
transport = new WebSocketClientTransport(customConfig);
const config = (transport as any).config;
expect(config.path).toBe('/custom-ws');
expect(config.protocols).toEqual(['custom-protocol']);
expect(config.headers).toEqual({ 'X-Custom': 'value' });
expect(config.binaryType).toBe('blob');
});
it('应该正确初始化内部状态', () => {
transport = new WebSocketClientTransport(defaultConfig);
expect((transport as any).websocket).toBeNull();
expect((transport as any).connectionPromise).toBeNull();
expect((transport as any).connectionTimeoutTimer).toBeNull();
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
});
});
describe('依赖注入测试', () => {
it('应该正确处理@esengine/ecs-framework依赖', () => {
const { Core } = require('@esengine/ecs-framework');
expect(() => {
transport = new WebSocketClientTransport(defaultConfig);
}).not.toThrow();
expect(Core).toBeDefined();
});
it('应该正确处理@esengine/ecs-framework-network-shared依赖', () => {
const { generateMessageId } = require('@esengine/ecs-framework-network-shared');
expect(() => {
transport = new WebSocketClientTransport(defaultConfig);
}).not.toThrow();
expect(generateMessageId).toBeDefined();
expect(typeof generateMessageId).toBe('function');
});
});
describe('连接功能测试', () => {
beforeEach(() => {
transport = new WebSocketClientTransport(defaultConfig);
});
it('应该能够发起连接', async () => {
const connectPromise = transport.connect();
expect((transport as any).websocket).toBeInstanceOf(MockWebSocket);
expect((transport as any).state).toBe(ConnectionState.CONNECTING);
// 模拟连接成功
const ws = (transport as any).websocket as MockWebSocket;
ws.readyState = WebSocket.OPEN;
if (ws.onopen) {
ws.onopen(new Event('open'));
}
await expect(connectPromise).resolves.toBeUndefined();
});
it('应该构造正确的WebSocket URL', async () => {
transport.connect();
const ws = (transport as any).websocket as MockWebSocket;
expect(ws.url).toBe('ws://localhost:8080/ws');
});
it('使用安全连接时应该构造HTTPS URL', async () => {
const secureConfig = { ...defaultConfig, secure: true };
transport = new WebSocketClientTransport(secureConfig);
transport.connect();
const ws = (transport as any).websocket as MockWebSocket;
expect(ws.url).toBe('wss://localhost:8080/ws');
});
it('应该设置WebSocket事件处理器', async () => {
transport.connect();
const ws = (transport as any).websocket as MockWebSocket;
expect(ws.onopen).toBeDefined();
expect(ws.onclose).toBeDefined();
expect(ws.onmessage).toBeDefined();
expect(ws.onerror).toBeDefined();
});
it('连接超时应该被正确处理', async () => {
const shortTimeoutConfig = { ...defaultConfig, connectionTimeout: 100 };
transport = new WebSocketClientTransport(shortTimeoutConfig);
const connectPromise = transport.connect();
// 不触发onopen事件让连接超时
await expect(connectPromise).rejects.toThrow('连接超时');
});
it('应该能够正确断开连接', async () => {
transport.connect();
// 模拟连接成功
const ws = (transport as any).websocket as MockWebSocket;
ws.readyState = WebSocket.OPEN;
if (ws.onopen) {
ws.onopen(new Event('open'));
}
await transport.disconnect();
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
});
});
describe('消息发送测试', () => {
beforeEach(async () => {
transport = new WebSocketClientTransport(defaultConfig);
});
it('未连接时发送消息应该加入队列', async () => {
const message = {
type: 'custom' as const,
data: { test: 'data' },
reliable: true,
timestamp: Date.now()
};
await transport.sendMessage(message);
const messageQueue = (transport as any).messageQueue;
expect(messageQueue).toHaveLength(1);
expect(messageQueue[0]).toEqual(message);
});
it('连接后应该发送队列中的消息', async () => {
const message = {
type: 'custom' as const,
data: { test: 'data' },
reliable: true,
timestamp: Date.now()
};
// 先发送消息到队列
await transport.sendMessage(message);
// 然后连接
transport.connect();
const ws = (transport as any).websocket as MockWebSocket;
const sendSpy = jest.spyOn(ws, 'send');
// 模拟连接成功
ws.readyState = WebSocket.OPEN;
if (ws.onopen) {
ws.onopen(new Event('open'));
}
expect(sendSpy).toHaveBeenCalled();
expect((transport as any).messageQueue).toHaveLength(0);
});
});
describe('错误处理测试', () => {
it('应该处理WebSocket构造函数异常', () => {
// Mock WebSocket构造函数抛出异常
const originalWebSocket = (global as any).WebSocket;
(global as any).WebSocket = jest.fn(() => {
throw new Error('WebSocket构造失败');
});
transport = new WebSocketClientTransport(defaultConfig);
expect(transport.connect()).rejects.toThrow('WebSocket构造失败');
// 恢复原始WebSocket
(global as any).WebSocket = originalWebSocket;
});
it('应该处理网络连接错误', async () => {
transport = new WebSocketClientTransport(defaultConfig);
const connectPromise = transport.connect();
// 模拟连接错误
const ws = (transport as any).websocket as MockWebSocket;
if (ws.onerror) {
ws.onerror(new Event('error'));
}
await expect(connectPromise).rejects.toThrow();
});
it('应该处理意外的连接关闭', () => {
transport = new WebSocketClientTransport(defaultConfig);
transport.connect();
const ws = (transport as any).websocket as MockWebSocket;
// 模拟连接意外关闭
if (ws.onclose) {
ws.onclose(new CloseEvent('close', { code: 1006, reason: '意外关闭' }));
}
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
});
});
describe('统计信息测试', () => {
it('应该正确计算连接统计信息', async () => {
transport = new WebSocketClientTransport(defaultConfig);
const initialStats = transport.getStats();
expect(initialStats.connectedAt).toBeNull();
expect(initialStats.messagesSent).toBe(0);
expect(initialStats.messagesReceived).toBe(0);
});
it('连接后应该更新统计信息', async () => {
transport = new WebSocketClientTransport(defaultConfig);
transport.connect();
const ws = (transport as any).websocket as MockWebSocket;
ws.readyState = WebSocket.OPEN;
if (ws.onopen) {
ws.onopen(new Event('open'));
}
const stats = transport.getStats();
expect(stats.connectedAt).toBeInstanceOf(Date);
});
});
});

View File

@@ -0,0 +1,45 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "node",
"allowImportingTsExtensions": false,
"lib": ["ES2020", "DOM"],
"outDir": "./bin",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"exactOptionalPropertyTypes": false,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": false,
"noUncheckedIndexedAccess": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"importHelpers": false,
"downlevelIteration": true,
"isolatedModules": false,
"allowJs": true,
"resolveJsonModule": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"bin",
"**/*.test.ts",
"**/*.spec.ts"
]
}