diff --git a/packages/framework/server/package.json b/packages/framework/server/package.json index 4389ab48..45488158 100644 --- a/packages/framework/server/package.json +++ b/packages/framework/server/package.json @@ -46,23 +46,19 @@ "test:watch": "vitest" }, "dependencies": { - "@esengine/rpc": "workspace:*" + "@esengine/rpc": "workspace:*", + "@esengine/ecs-framework": "workspace:*" }, "peerDependencies": { "ws": ">=8.0.0", - "jsonwebtoken": ">=9.0.0", - "@esengine/ecs-framework": ">=2.7.1" + "jsonwebtoken": ">=9.0.0" }, "peerDependenciesMeta": { "jsonwebtoken": { "optional": true - }, - "@esengine/ecs-framework": { - "optional": true } }, "devDependencies": { - "@esengine/ecs-framework": "workspace:*", "@types/jsonwebtoken": "^9.0.0", "@types/node": "^20.0.0", "@types/ws": "^8.5.13", diff --git a/packages/framework/server/src/auth/__tests__/MockAuthProvider.test.ts b/packages/framework/server/src/auth/__tests__/MockAuthProvider.test.ts index 7218932a..36b99f93 100644 --- a/packages/framework/server/src/auth/__tests__/MockAuthProvider.test.ts +++ b/packages/framework/server/src/auth/__tests__/MockAuthProvider.test.ts @@ -168,9 +168,9 @@ describe('MockAuthProvider', () => { it('should get all users', () => { const users = provider.getUsers(); expect(users).toHaveLength(3); - expect(users.map(u => u.id)).toContain('1'); - expect(users.map(u => u.id)).toContain('2'); - expect(users.map(u => u.id)).toContain('3'); + expect(users.map((u) => u.id)).toContain('1'); + expect(users.map((u) => u.id)).toContain('2'); + expect(users.map((u) => u.id)).toContain('3'); }); }); diff --git a/packages/framework/server/src/auth/__tests__/providers.test.ts b/packages/framework/server/src/auth/__tests__/providers.test.ts index 77ef7258..c3d9ece6 100644 --- a/packages/framework/server/src/auth/__tests__/providers.test.ts +++ b/packages/framework/server/src/auth/__tests__/providers.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; import { JwtAuthProvider, createJwtAuthProvider } from '../providers/JwtAuthProvider'; import { SessionAuthProvider, createSessionAuthProvider, type ISessionStorage } from '../providers/SessionAuthProvider'; @@ -125,7 +125,7 @@ describe('JwtAuthProvider', () => { const token = provider.sign({ sub: '123', name: 'Alice' }); // Wait a bit so iat changes - await new Promise(resolve => setTimeout(resolve, 1100)); + await new Promise((resolve) => setTimeout(resolve, 1100)); const result = await provider.refresh(token); @@ -239,7 +239,7 @@ describe('SessionAuthProvider', () => { it('should validate user on verify', async () => { const validatingProvider = createSessionAuthProvider({ storage, - validateUser: (user) => user.id !== 'banned' + validateUser: (user: { id: string; name?: string }) => user.id !== 'banned' }); const sessionId = await validatingProvider.createSession({ id: 'banned', name: 'Bad User' }); @@ -252,7 +252,7 @@ describe('SessionAuthProvider', () => { it('should pass validation for valid user', async () => { const validatingProvider = createSessionAuthProvider({ storage, - validateUser: (user) => user.id !== 'banned' + validateUser: (user: { id: string; name?: string }) => user.id !== 'banned' }); const sessionId = await validatingProvider.createSession({ id: '123', name: 'Good User' }); @@ -269,7 +269,7 @@ describe('SessionAuthProvider', () => { const session1 = await provider.getSession(sessionId); const lastActive1 = session1?.lastActiveAt; - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 10)); const result = await provider.refresh(sessionId); expect(result.success).toBe(true); diff --git a/packages/framework/server/src/auth/context.ts b/packages/framework/server/src/auth/context.ts index c426b8e8..f8d99344 100644 --- a/packages/framework/server/src/auth/context.ts +++ b/packages/framework/server/src/auth/context.ts @@ -138,7 +138,7 @@ export class AuthContext implements IAuthContext { * @en Check if has any of specified roles */ hasAnyRole(roles: string[]): boolean { - return roles.some(role => this._roles.includes(role)); + return roles.some((role) => this._roles.includes(role)); } /** @@ -146,7 +146,7 @@ export class AuthContext implements IAuthContext { * @en Check if has all specified roles */ hasAllRoles(roles: string[]): boolean { - return roles.every(role => this._roles.includes(role)); + return roles.every((role) => this._roles.includes(role)); } /** diff --git a/packages/framework/server/src/auth/mixin/withAuth.ts b/packages/framework/server/src/auth/mixin/withAuth.ts index eb7c1f36..f1ed649c 100644 --- a/packages/framework/server/src/auth/mixin/withAuth.ts +++ b/packages/framework/server/src/auth/mixin/withAuth.ts @@ -4,6 +4,7 @@ */ import type { ServerConnection, GameServer } from '../../types/index.js'; +import { createLogger } from '../../logger.js'; import type { IAuthProvider, AuthResult, @@ -14,6 +15,8 @@ import type { } from '../types.js'; import { AuthContext } from '../context.js'; +const logger = createLogger('Auth'); + /** * @zh 认证数据键 * @en Auth data key @@ -155,7 +158,7 @@ export function withAuth( } } } catch (error) { - console.error('[Auth] Error during auto-authentication:', error); + logger.error('Error during auto-authentication:', error); } } diff --git a/packages/framework/server/src/auth/mixin/withRoomAuth.ts b/packages/framework/server/src/auth/mixin/withRoomAuth.ts index 5eadb435..838f2cef 100644 --- a/packages/framework/server/src/auth/mixin/withRoomAuth.ts +++ b/packages/framework/server/src/auth/mixin/withRoomAuth.ts @@ -5,9 +5,12 @@ import type { Room, Player } from '../../room/index.js'; import type { IAuthContext, AuthRoomConfig } from '../types.js'; +import { createLogger } from '../../logger.js'; import { getAuthContext } from './withAuth.js'; import { createGuestContext } from '../context.js'; +const logger = createLogger('AuthRoom'); + /** * @zh 带认证的玩家 * @en Player with authentication @@ -181,7 +184,7 @@ export function withRoomAuth(); if (requireAuth && !authContext.isAuthenticated) { - console.warn(`[AuthRoom] Rejected unauthenticated player: ${player.id}`); + logger.warn(`Rejected unauthenticated player: ${player.id}`); this.kick(player as any, 'Authentication required'); return; } @@ -192,7 +195,7 @@ export function withRoomAuth[] { - return this.getAuthPlayers().filter(p => p.auth?.hasRole(role)); + return this.getAuthPlayers().filter((p) => p.auth?.hasRole(role)); } /** @@ -250,7 +253,7 @@ export function withRoomAuth | undefined { - return this.getAuthPlayers().find(p => p.auth?.userId === userId); + return this.getAuthPlayers().find((p) => p.auth?.userId === userId); } } @@ -281,7 +284,7 @@ export function withRoomAuth> - implements IAuthRoom { +implements IAuthRoom { /** * @zh 认证配置(子类可覆盖) diff --git a/packages/framework/server/src/auth/testing/MockAuthProvider.ts b/packages/framework/server/src/auth/testing/MockAuthProvider.ts index 08a94d8a..23d1f2ec 100644 --- a/packages/framework/server/src/auth/testing/MockAuthProvider.ts +++ b/packages/framework/server/src/auth/testing/MockAuthProvider.ts @@ -77,7 +77,7 @@ export interface MockAuthConfig { * ``` */ export class MockAuthProvider - implements IAuthProvider { +implements IAuthProvider { readonly name = 'mock'; @@ -102,7 +102,7 @@ export class MockAuthProvider */ private async _delay(): Promise { if (this._config.delay && this._config.delay > 0) { - await new Promise(resolve => setTimeout(resolve, this._config.delay)); + await new Promise((resolve) => setTimeout(resolve, this._config.delay)); } } diff --git a/packages/framework/server/src/core/server.ts b/packages/framework/server/src/core/server.ts index c319cad2..0c8af690 100644 --- a/packages/framework/server/src/core/server.ts +++ b/packages/framework/server/src/core/server.ts @@ -3,10 +3,11 @@ * @en Game server core */ -import * as path from 'node:path' -import { createServer as createHttpServer, type Server as HttpServer } from 'node:http' -import { serve, type RpcServer } from '@esengine/rpc/server' -import { rpc } from '@esengine/rpc' +import * as path from 'node:path'; +import { createServer as createHttpServer, type Server as HttpServer } from 'node:http'; +import { serve, type RpcServer } from '@esengine/rpc/server'; +import { rpc } from '@esengine/rpc'; +import { createLogger } from '../logger.js'; import type { ServerConfig, ServerConnection, @@ -15,12 +16,12 @@ import type { MsgContext, LoadedApiHandler, LoadedMsgHandler, - LoadedHttpHandler, -} from '../types/index.js' -import type { HttpRoutes, HttpHandler } from '../http/types.js' -import { loadApiHandlers, loadMsgHandlers, loadHttpHandlers } from '../router/loader.js' -import { RoomManager, type RoomClass, type Room } from '../room/index.js' -import { createHttpRouter } from '../http/router.js' + LoadedHttpHandler +} from '../types/index.js'; +import type { HttpRoutes, HttpHandler } from '../http/types.js'; +import { loadApiHandlers, loadMsgHandlers, loadHttpHandlers } from '../router/loader.js'; +import { RoomManager, type RoomClass, type Room } from '../room/index.js'; +import { createHttpRouter } from '../http/router.js'; /** * @zh 默认配置 @@ -32,8 +33,8 @@ const DEFAULT_CONFIG: Required { - const opts = { ...DEFAULT_CONFIG, ...config } - const cwd = process.cwd() + const opts = { ...DEFAULT_CONFIG, ...config }; + const cwd = process.cwd(); + const logger = createLogger('Server'); // 加载文件路由处理器 - const apiHandlers = await loadApiHandlers(path.resolve(cwd, opts.apiDir)) - const msgHandlers = await loadMsgHandlers(path.resolve(cwd, opts.msgDir)) + const apiHandlers = await loadApiHandlers(path.resolve(cwd, opts.apiDir)); + const msgHandlers = await loadMsgHandlers(path.resolve(cwd, opts.msgDir)); // 加载 HTTP 文件路由 - const httpDir = config.httpDir ?? opts.httpDir - const httpPrefix = config.httpPrefix ?? opts.httpPrefix - const httpHandlers = await loadHttpHandlers(path.resolve(cwd, httpDir), httpPrefix) + const httpDir = config.httpDir ?? opts.httpDir; + const httpPrefix = config.httpPrefix ?? opts.httpPrefix; + const httpHandlers = await loadHttpHandlers(path.resolve(cwd, httpDir), httpPrefix); if (apiHandlers.length > 0) { - console.log(`[Server] Loaded ${apiHandlers.length} API handlers`) + logger.info(`Loaded ${apiHandlers.length} API handlers`); } if (msgHandlers.length > 0) { - console.log(`[Server] Loaded ${msgHandlers.length} message handlers`) + logger.info(`Loaded ${msgHandlers.length} message handlers`); } if (httpHandlers.length > 0) { - console.log(`[Server] Loaded ${httpHandlers.length} HTTP handlers`) + logger.info(`Loaded ${httpHandlers.length} HTTP handlers`); } // 合并 HTTP 路由(文件路由 + 内联路由) - const mergedHttpRoutes: HttpRoutes = {} + const mergedHttpRoutes: HttpRoutes = {}; // 先添加文件路由 for (const handler of httpHandlers) { - const existingRoute = mergedHttpRoutes[handler.route] + const existingRoute = mergedHttpRoutes[handler.route]; if (existingRoute && typeof existingRoute !== 'function') { - (existingRoute as Record)[handler.method] = handler.definition.handler + (existingRoute as Record)[handler.method] = handler.definition.handler; } else { mergedHttpRoutes[handler.route] = { - [handler.method]: handler.definition.handler, - } + [handler.method]: handler.definition.handler + }; } } @@ -96,64 +98,64 @@ export async function createServer(config: ServerConfig = {}): Promise 0 + const hasHttpRoutes = Object.keys(mergedHttpRoutes).length > 0; // 动态构建协议 const apiDefs: Record> = { // 内置 API JoinRoom: rpc.api(), - LeaveRoom: rpc.api(), - } + LeaveRoom: rpc.api() + }; const msgDefs: Record> = { // 内置消息(房间消息透传) - RoomMessage: rpc.msg(), - } + RoomMessage: rpc.msg() + }; for (const handler of apiHandlers) { - apiDefs[handler.name] = rpc.api() + apiDefs[handler.name] = rpc.api(); } for (const handler of msgHandlers) { - msgDefs[handler.name] = rpc.msg() + msgDefs[handler.name] = rpc.msg(); } const protocol = rpc.define({ api: apiDefs, - msg: msgDefs, - }) + msg: msgDefs + }); // 服务器状态 - let currentTick = 0 - let tickInterval: ReturnType | null = null - let rpcServer: RpcServer> | null = null - let httpServer: HttpServer | null = null + let currentTick = 0; + let tickInterval: ReturnType | null = null; + let rpcServer: RpcServer> | null = null; + let httpServer: HttpServer | null = null; // 房间管理器(立即初始化,以便 define() 可在 start() 前调用) const roomManager = new RoomManager((conn, type, data) => { - rpcServer?.send(conn, 'RoomMessage' as any, { type, data } as any) - }) + rpcServer?.send(conn, 'RoomMessage' as any, { type, data } as any); + }); // 构建 API 处理器映射 - const apiMap: Record = {} + const apiMap: Record = {}; for (const handler of apiHandlers) { - apiMap[handler.name] = handler + apiMap[handler.name] = handler; } // 构建消息处理器映射 - const msgMap: Record = {} + const msgMap: Record = {}; for (const handler of msgHandlers) { - msgMap[handler.name] = handler + msgMap[handler.name] = handler; } // 游戏服务器实例 @@ -161,15 +163,15 @@ export async function createServer(config: ServerConfig = {}): Promise + return (rpcServer?.connections ?? []) as ReadonlyArray; }, get tick() { - return currentTick + return currentTick; }, get rooms() { - return roomManager + return roomManager; }, /** @@ -177,12 +179,12 @@ export async function createServer(config: ServerConfig = {}): Promise unknown): void { - roomManager.define(name, roomClass as RoomClass) + roomManager.define(name, roomClass as RoomClass); }, async start() { // 构建 API handlers - const apiHandlersObj: Record Promise> = {} + const apiHandlersObj: Record Promise> = {}; // 内置 JoinRoom API apiHandlersObj['JoinRoom'] = async (input: any, conn) => { @@ -190,163 +192,163 @@ export async function createServer(config: ServerConfig = {}): Promise - } + }; if (roomId) { - const result = await roomManager.joinById(roomId, conn.id, conn) + const result = await roomManager.joinById(roomId, conn.id, conn); if (!result) { - throw new Error('Failed to join room') + throw new Error('Failed to join room'); } - return { roomId: result.room.id, playerId: result.player.id } + return { roomId: result.room.id, playerId: result.player.id }; } if (roomType) { - const result = await roomManager.joinOrCreate(roomType, conn.id, conn, options) + const result = await roomManager.joinOrCreate(roomType, conn.id, conn, options); if (!result) { - throw new Error('Failed to join or create room') + throw new Error('Failed to join or create room'); } - return { roomId: result.room.id, playerId: result.player.id } + return { roomId: result.room.id, playerId: result.player.id }; } - throw new Error('roomType or roomId required') - } + throw new Error('roomType or roomId required'); + }; // 内置 LeaveRoom API apiHandlersObj['LeaveRoom'] = async (_input, conn) => { - await roomManager.leave(conn.id) - return { success: true } - } + await roomManager.leave(conn.id); + return { success: true }; + }; // 文件路由 API for (const [name, handler] of Object.entries(apiMap)) { apiHandlersObj[name] = async (input, conn) => { const ctx: ApiContext = { conn: conn as ServerConnection, - server: gameServer, - } - return handler.definition.handler(input, ctx) - } + server: gameServer + }; + return handler.definition.handler(input, ctx); + }; } // 构建消息 handlers - const msgHandlersObj: Record void | Promise> = {} + const msgHandlersObj: Record void | Promise> = {}; // 内置 RoomMessage 处理 msgHandlersObj['RoomMessage'] = async (data: any, conn) => { - const { type, data: payload } = data as { type: string; data: unknown } - roomManager.handleMessage(conn.id, type, payload) - } + const { type, data: payload } = data as { type: string; data: unknown }; + roomManager.handleMessage(conn.id, type, payload); + }; // 文件路由消息 for (const [name, handler] of Object.entries(msgMap)) { msgHandlersObj[name] = async (data, conn) => { const ctx: MsgContext = { conn: conn as ServerConnection, - server: gameServer, - } - await handler.definition.handler(data, ctx) - } + server: gameServer + }; + await handler.definition.handler(data, ctx); + }; } // 如果有 HTTP 路由,创建 HTTP 服务器 if (hasHttpRoutes) { - const httpRouter = createHttpRouter(mergedHttpRoutes, config.cors ?? true) + const httpRouter = createHttpRouter(mergedHttpRoutes, config.cors ?? true); httpServer = createHttpServer(async (req, res) => { // 先尝试 HTTP 路由 - const handled = await httpRouter(req, res) + const handled = await httpRouter(req, res); if (!handled) { // 未匹配的请求返回 404 - res.statusCode = 404 - res.setHeader('Content-Type', 'application/json') - res.end(JSON.stringify({ error: 'Not Found' })) + res.statusCode = 404; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ error: 'Not Found' })); } - }) + }); // 使用 HTTP 服务器创建 RPC rpcServer = serve(protocol, { server: httpServer, createConnData: () => ({}), onStart: () => { - console.log(`[Server] Started on http://localhost:${opts.port}`) - opts.onStart?.(opts.port) + logger.info(`Started on http://localhost:${opts.port}`); + opts.onStart?.(opts.port); }, onConnect: async (conn) => { - await config.onConnect?.(conn as ServerConnection) + await config.onConnect?.(conn as ServerConnection); }, onDisconnect: async (conn) => { - await roomManager?.leave(conn.id, 'disconnected') - await config.onDisconnect?.(conn as ServerConnection) + await roomManager?.leave(conn.id, 'disconnected'); + await config.onDisconnect?.(conn as ServerConnection); }, api: apiHandlersObj as any, - msg: msgHandlersObj as any, - }) + msg: msgHandlersObj as any + }); - await rpcServer.start() + await rpcServer.start(); // 启动 HTTP 服务器 await new Promise((resolve) => { - httpServer!.listen(opts.port, () => resolve()) - }) + httpServer!.listen(opts.port, () => resolve()); + }); } else { // 仅 WebSocket 模式 rpcServer = serve(protocol, { port: opts.port, createConnData: () => ({}), onStart: (p) => { - console.log(`[Server] Started on ws://localhost:${p}`) - opts.onStart?.(p) + logger.info(`Started on ws://localhost:${p}`); + opts.onStart?.(p); }, onConnect: async (conn) => { - await config.onConnect?.(conn as ServerConnection) + await config.onConnect?.(conn as ServerConnection); }, onDisconnect: async (conn) => { - await roomManager?.leave(conn.id, 'disconnected') - await config.onDisconnect?.(conn as ServerConnection) + await roomManager?.leave(conn.id, 'disconnected'); + await config.onDisconnect?.(conn as ServerConnection); }, api: apiHandlersObj as any, - msg: msgHandlersObj as any, - }) + msg: msgHandlersObj as any + }); - await rpcServer.start() + await rpcServer.start(); } // 启动 tick 循环 if (opts.tickRate > 0) { tickInterval = setInterval(() => { - currentTick++ - }, 1000 / opts.tickRate) + currentTick++; + }, 1000 / opts.tickRate); } }, async stop() { if (tickInterval) { - clearInterval(tickInterval) - tickInterval = null + clearInterval(tickInterval); + tickInterval = null; } if (rpcServer) { - await rpcServer.stop() - rpcServer = null + await rpcServer.stop(); + rpcServer = null; } if (httpServer) { await new Promise((resolve, reject) => { httpServer!.close((err) => { - if (err) reject(err) - else resolve() - }) - }) - httpServer = null + if (err) reject(err); + else resolve(); + }); + }); + httpServer = null; } }, broadcast(name, data) { - rpcServer?.broadcast(name as any, data as any) + rpcServer?.broadcast(name as any, data as any); }, send(conn, name, data) { - rpcServer?.send(conn as any, name as any, data as any) - }, - } + rpcServer?.send(conn as any, name as any, data as any); + } + }; - return gameServer as GameServer + return gameServer as GameServer; } diff --git a/packages/framework/server/src/ecs/ECSRoom.test.ts b/packages/framework/server/src/ecs/ECSRoom.test.ts index 0b610806..0130d3e0 100644 --- a/packages/framework/server/src/ecs/ECSRoom.test.ts +++ b/packages/framework/server/src/ecs/ECSRoom.test.ts @@ -3,20 +3,17 @@ * @en ECSRoom integration tests */ -import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest' +import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest'; import { Core, Component, ECSComponent, - sync, - initChangeTracker, - getSyncMetadata, - registerSyncComponent, -} from '@esengine/ecs-framework' -import { createTestEnv, type TestEnvironment, wait } from '../testing/TestServer.js' -import { ECSRoom } from './ECSRoom.js' -import type { Player } from '../room/Player.js' -import { onMessage } from '../room/decorators.js' + sync +} from '@esengine/ecs-framework'; +import { createTestEnv, type TestEnvironment, wait } from '../testing/TestServer.js'; +import { ECSRoom } from './ECSRoom.js'; +import type { Player } from '../room/Player.js'; +import { onMessage } from '../room/decorators.js'; // ============================================================================ // Test Components | 测试组件 @@ -24,16 +21,10 @@ import { onMessage } from '../room/decorators.js' @ECSComponent('ECSRoomTest_PlayerComponent') class PlayerComponent extends Component { - @sync('string') name: string = '' - @sync('uint16') score: number = 0 - @sync('float32') x: number = 0 - @sync('float32') y: number = 0 -} - -@ECSComponent('ECSRoomTest_HealthComponent') -class HealthComponent extends Component { - @sync('int32') current: number = 100 - @sync('int32') max: number = 100 + @sync('string') name: string = ''; + @sync('uint16') score: number = 0; + @sync('float32') x: number = 0; + @sync('float32') y: number = 0; } // ============================================================================ @@ -50,69 +41,69 @@ interface TestPlayerData { class TestECSRoom extends ECSRoom { state: TestRoomState = { - gameStarted: false, - } + gameStarted: false + }; onCreate(): void { // 可以在这里添加系统 } onJoin(player: Player): void { - const entity = this.createPlayerEntity(player.id) - const comp = entity.addComponent(new PlayerComponent()) - comp.name = player.data.nickname || `Player_${player.id.slice(-4)}` - comp.x = Math.random() * 100 - comp.y = Math.random() * 100 + const entity = this.createPlayerEntity(player.id); + const comp = entity.addComponent(new PlayerComponent()); + comp.name = player.data.nickname || `Player_${player.id.slice(-4)}`; + comp.x = Math.random() * 100; + comp.y = Math.random() * 100; this.broadcast('PlayerJoined', { playerId: player.id, - name: comp.name, - }) + name: comp.name + }); } async onLeave(player: Player, reason?: string): Promise { - await super.onLeave(player, reason) - this.broadcast('PlayerLeft', { playerId: player.id }) + await super.onLeave(player, reason); + this.broadcast('PlayerLeft', { playerId: player.id }); } @onMessage('Move') handleMove(data: { x: number; y: number }, player: Player): void { - const entity = this.getPlayerEntity(player.id) + const entity = this.getPlayerEntity(player.id); if (entity) { - const comp = entity.getComponent(PlayerComponent) + const comp = entity.getComponent(PlayerComponent); if (comp) { - comp.x = data.x - comp.y = data.y + comp.x = data.x; + comp.y = data.y; } } } @onMessage('AddScore') handleAddScore(data: { amount: number }, player: Player): void { - const entity = this.getPlayerEntity(player.id) + const entity = this.getPlayerEntity(player.id); if (entity) { - const comp = entity.getComponent(PlayerComponent) + const comp = entity.getComponent(PlayerComponent); if (comp) { - comp.score += data.amount + comp.score += data.amount; } } } @onMessage('Ping') handlePing(_data: unknown, player: Player): void { - player.send('Pong', { timestamp: Date.now() }) + player.send('Pong', { timestamp: Date.now() }); } getWorld() { - return this.world + return this.world; } getScene() { - return this.scene + return this.scene; } getPlayerEntityCount(): number { - return this.scene.entities.buffer.length + return this.scene.entities.buffer.length; } } @@ -121,25 +112,24 @@ class TestECSRoom extends ECSRoom { // ============================================================================ describe('ECSRoom Integration Tests', () => { - let env: TestEnvironment + let env: TestEnvironment; beforeAll(() => { - Core.create() - registerSyncComponent('ECSRoomTest_PlayerComponent', PlayerComponent) - registerSyncComponent('ECSRoomTest_HealthComponent', HealthComponent) - }) + Core.create(); + // @ECSComponent 装饰器已自动注册组件 + }); afterAll(() => { - Core.destroy() - }) + Core.destroy(); + }); beforeEach(async () => { - env = await createTestEnv({ tickRate: 20 }) - }) + env = await createTestEnv({ tickRate: 20 }); + }); afterEach(async () => { - await env.cleanup() - }) + await env.cleanup(); + }); // ======================================================================== // Room Creation | 房间创建 @@ -147,28 +137,28 @@ describe('ECSRoom Integration Tests', () => { describe('Room Creation', () => { it('should create ECSRoom with World and Scene', async () => { - env.server.define('ecs-test', TestECSRoom) + env.server.define('ecs-test', TestECSRoom); - const client = await env.createClient() - await client.joinRoom('ecs-test') + const client = await env.createClient(); + await client.joinRoom('ecs-test'); - expect(client.roomId).toBeDefined() - }) + expect(client.roomId).toBeDefined(); + }); it('should have World managed by Core.worldManager', async () => { - env.server.define('ecs-test', TestECSRoom) + env.server.define('ecs-test', TestECSRoom); - const client = await env.createClient() - await client.joinRoom('ecs-test') + const client = await env.createClient(); + await client.joinRoom('ecs-test'); // 验证 World 正常创建(通过消息通信验证) - const pongPromise = client.waitForRoomMessage<{ timestamp: number }>('Pong') - client.sendToRoom('Ping', {}) - const pong = await pongPromise + const pongPromise = client.waitForRoomMessage<{ timestamp: number }>('Pong'); + client.sendToRoom('Ping', {}); + const pong = await pongPromise; - expect(pong.timestamp).toBeGreaterThan(0) - }) - }) + expect(pong.timestamp).toBeGreaterThan(0); + }); + }); // ======================================================================== // Player Entity Management | 玩家实体管理 @@ -176,41 +166,41 @@ describe('ECSRoom Integration Tests', () => { describe('Player Entity Management', () => { it('should create player entity on join', async () => { - env.server.define('ecs-test', TestECSRoom) + env.server.define('ecs-test', TestECSRoom); - const client1 = await env.createClient() - const { roomId } = await client1.joinRoom('ecs-test') + const client1 = await env.createClient(); + const { roomId } = await client1.joinRoom('ecs-test'); // 等待第二个玩家加入时收到广播 const joinPromise = client1.waitForRoomMessage<{ playerId: string; name: string }>( 'PlayerJoined' - ) + ); - const client2 = await env.createClient() - await client2.joinRoomById(roomId) + const client2 = await env.createClient(); + await client2.joinRoomById(roomId); - const joinMsg = await joinPromise - expect(joinMsg.playerId).toBe(client2.playerId) - expect(joinMsg.name).toContain('Player_') - }) + const joinMsg = await joinPromise; + expect(joinMsg.playerId).toBe(client2.playerId); + expect(joinMsg.name).toContain('Player_'); + }); it('should destroy player entity on leave', async () => { - env.server.define('ecs-test', TestECSRoom) + env.server.define('ecs-test', TestECSRoom); - const client1 = await env.createClient() - const { roomId } = await client1.joinRoom('ecs-test') + const client1 = await env.createClient(); + const { roomId } = await client1.joinRoom('ecs-test'); - const client2 = await env.createClient() - await client2.joinRoomById(roomId) + const client2 = await env.createClient(); + await client2.joinRoomById(roomId); - const leavePromise = client1.waitForRoomMessage<{ playerId: string }>('PlayerLeft') + const leavePromise = client1.waitForRoomMessage<{ playerId: string }>('PlayerLeft'); - await client2.leaveRoom() + await client2.leaveRoom(); - const leaveMsg = await leavePromise - expect(leaveMsg.playerId).toBeDefined() - }) - }) + const leaveMsg = await leavePromise; + expect(leaveMsg.playerId).toBeDefined(); + }); + }); // ======================================================================== // Component Sync | 组件同步 @@ -218,41 +208,41 @@ describe('ECSRoom Integration Tests', () => { describe('Component State Updates', () => { it('should update component via message handler', async () => { - env.server.define('ecs-test', TestECSRoom) + env.server.define('ecs-test', TestECSRoom); - const client = await env.createClient() - await client.joinRoom('ecs-test') + const client = await env.createClient(); + await client.joinRoom('ecs-test'); - client.sendToRoom('Move', { x: 100, y: 200 }) + client.sendToRoom('Move', { x: 100, y: 200 }); // 等待处理 - await wait(50) + await wait(50); // 验证 Ping/Pong 仍能工作(房间仍活跃) - const pongPromise = client.waitForRoomMessage<{ timestamp: number }>('Pong') - client.sendToRoom('Ping', {}) - const pong = await pongPromise + const pongPromise = client.waitForRoomMessage<{ timestamp: number }>('Pong'); + client.sendToRoom('Ping', {}); + const pong = await pongPromise; - expect(pong.timestamp).toBeGreaterThan(0) - }) + expect(pong.timestamp).toBeGreaterThan(0); + }); it('should handle AddScore message', async () => { - env.server.define('ecs-test', TestECSRoom) + env.server.define('ecs-test', TestECSRoom); - const client = await env.createClient() - await client.joinRoom('ecs-test') + const client = await env.createClient(); + await client.joinRoom('ecs-test'); - client.sendToRoom('AddScore', { amount: 50 }) - client.sendToRoom('AddScore', { amount: 25 }) + client.sendToRoom('AddScore', { amount: 50 }); + client.sendToRoom('AddScore', { amount: 25 }); - await wait(50) + await wait(50); // 确认房间仍然活跃 - const pongPromise = client.waitForRoomMessage<{ timestamp: number }>('Pong') - client.sendToRoom('Ping', {}) - await pongPromise - }) - }) + const pongPromise = client.waitForRoomMessage<{ timestamp: number }>('Pong'); + client.sendToRoom('Ping', {}); + await pongPromise; + }); + }); // ======================================================================== // Sync Broadcast | 同步广播 @@ -260,22 +250,22 @@ describe('ECSRoom Integration Tests', () => { describe('State Sync Broadcast', () => { it('should receive $sync messages when enabled', async () => { - env.server.define('ecs-test', TestECSRoom) + env.server.define('ecs-test', TestECSRoom); - const client = await env.createClient() - await client.joinRoom('ecs-test') + const client = await env.createClient(); + await client.joinRoom('ecs-test'); // 触发状态变更 - client.sendToRoom('Move', { x: 50, y: 75 }) + client.sendToRoom('Move', { x: 50, y: 75 }); // 等待 tick 处理 - await wait(200) + await wait(200); // 检查是否收到 $sync 消息 - const hasSync = client.hasReceivedMessage('RoomMessage') - expect(hasSync).toBe(true) - }) - }) + const hasSync = client.hasReceivedMessage('RoomMessage'); + expect(hasSync).toBe(true); + }); + }); // ======================================================================== // Multi-player Sync | 多玩家同步 @@ -283,47 +273,47 @@ describe('ECSRoom Integration Tests', () => { describe('Multi-player Scenarios', () => { it('should handle multiple players in same room', async () => { - env.server.define('ecs-test', TestECSRoom) + env.server.define('ecs-test', TestECSRoom); - const client1 = await env.createClient() - const { roomId } = await client1.joinRoom('ecs-test') + const client1 = await env.createClient(); + const { roomId } = await client1.joinRoom('ecs-test'); - const client2 = await env.createClient() - const joinPromise = client1.waitForRoomMessage<{ playerId: string }>('PlayerJoined') - await client2.joinRoomById(roomId) + const client2 = await env.createClient(); + const joinPromise = client1.waitForRoomMessage<{ playerId: string }>('PlayerJoined'); + await client2.joinRoomById(roomId); - const joinMsg = await joinPromise - expect(joinMsg.playerId).toBe(client2.playerId) - }) + const joinMsg = await joinPromise; + expect(joinMsg.playerId).toBe(client2.playerId); + }); it('should broadcast to all players on state change', async () => { - env.server.define('ecs-test', TestECSRoom) + env.server.define('ecs-test', TestECSRoom); - const client1 = await env.createClient() - const { roomId } = await client1.joinRoom('ecs-test') + const client1 = await env.createClient(); + const { roomId } = await client1.joinRoom('ecs-test'); - const client2 = await env.createClient() + const client2 = await env.createClient(); // client1 等待收到 client2 加入的广播 - const joinPromise = client1.waitForRoomMessage<{ playerId: string }>('PlayerJoined') + const joinPromise = client1.waitForRoomMessage<{ playerId: string }>('PlayerJoined'); - await client2.joinRoomById(roomId) + await client2.joinRoomById(roomId); - const joinMsg = await joinPromise - expect(joinMsg.playerId).toBe(client2.playerId) + const joinMsg = await joinPromise; + expect(joinMsg.playerId).toBe(client2.playerId); // 验证每个客户端都能独立通信 - const pong1Promise = client1.waitForRoomMessage<{ timestamp: number }>('Pong') - client1.sendToRoom('Ping', {}) - const pong1 = await pong1Promise - expect(pong1.timestamp).toBeGreaterThan(0) + const pong1Promise = client1.waitForRoomMessage<{ timestamp: number }>('Pong'); + client1.sendToRoom('Ping', {}); + const pong1 = await pong1Promise; + expect(pong1.timestamp).toBeGreaterThan(0); - const pong2Promise = client2.waitForRoomMessage<{ timestamp: number }>('Pong') - client2.sendToRoom('Ping', {}) - const pong2 = await pong2Promise - expect(pong2.timestamp).toBeGreaterThan(0) - }) - }) + const pong2Promise = client2.waitForRoomMessage<{ timestamp: number }>('Pong'); + client2.sendToRoom('Ping', {}); + const pong2 = await pong2Promise; + expect(pong2.timestamp).toBeGreaterThan(0); + }); + }); // ======================================================================== // Cleanup | 清理 @@ -331,18 +321,18 @@ describe('ECSRoom Integration Tests', () => { describe('Room Cleanup', () => { it('should cleanup World on dispose', async () => { - env.server.define('ecs-test', TestECSRoom) + env.server.define('ecs-test', TestECSRoom); - const client = await env.createClient() - await client.joinRoom('ecs-test') + const client = await env.createClient(); + await client.joinRoom('ecs-test'); - await client.leaveRoom() + await client.leaveRoom(); // 等待自动销毁 - await wait(100) + await wait(100); // 房间应该已销毁 - expect(client.roomId).toBeNull() - }) - }) -}) + expect(client.roomId).toBeNull(); + }); + }); +}); diff --git a/packages/framework/server/src/ecs/ECSRoom.ts b/packages/framework/server/src/ecs/ECSRoom.ts index b556824c..99835659 100644 --- a/packages/framework/server/src/ecs/ECSRoom.ts +++ b/packages/framework/server/src/ecs/ECSRoom.ts @@ -24,7 +24,7 @@ import { NETWORK_ENTITY_METADATA, type NetworkEntityMetadata, // Events - ECSEventType, + ECSEventType } from '@esengine/ecs-framework'; import { Room, type RoomOptions } from '../room/Room.js'; @@ -62,7 +62,7 @@ export interface ECSRoomConfig { const DEFAULT_ECS_CONFIG: ECSRoomConfig = { syncInterval: 50, // 20 Hz enableDeltaSync: true, - enableAutoNetworkEntity: true, + enableAutoNetworkEntity: true }; /** @@ -305,7 +305,7 @@ export abstract class ECSRoom this._hasChanges(entity)); + const changedEntities = entities.filter((entity) => this._hasChanges(entity)); if (changedEntities.length === 0) return; diff --git a/packages/framework/server/src/ecs/index.ts b/packages/framework/server/src/ecs/index.ts index ddd6ccbc..6fccf7f2 100644 --- a/packages/framework/server/src/ecs/index.ts +++ b/packages/framework/server/src/ecs/index.ts @@ -41,7 +41,7 @@ export type { Component, EntitySystem, Scene, - World, + World } from '@esengine/ecs-framework'; // Re-export sync types @@ -55,7 +55,7 @@ export { SyncOperation, type SyncType, type SyncFieldMetadata, - type SyncMetadata, + type SyncMetadata } from '@esengine/ecs-framework'; // Re-export room decorators diff --git a/packages/framework/server/src/helpers/define.ts b/packages/framework/server/src/helpers/define.ts index d5fea51d..28daebf2 100644 --- a/packages/framework/server/src/helpers/define.ts +++ b/packages/framework/server/src/helpers/define.ts @@ -3,7 +3,7 @@ * @en API, message, and HTTP definition helpers */ -import type { ApiDefinition, MsgDefinition, HttpDefinition } from '../types/index.js' +import type { ApiDefinition, MsgDefinition, HttpDefinition } from '../types/index.js'; /** * @zh 定义 API 处理器 @@ -25,7 +25,7 @@ import type { ApiDefinition, MsgDefinition, HttpDefinition } from '../types/inde export function defineApi>( definition: ApiDefinition ): ApiDefinition { - return definition + return definition; } /** @@ -47,7 +47,7 @@ export function defineApi>( export function defineMsg>( definition: MsgDefinition ): MsgDefinition { - return definition + return definition; } /** @@ -77,5 +77,5 @@ export function defineMsg>( export function defineHttp( definition: HttpDefinition ): HttpDefinition { - return definition + return definition; } diff --git a/packages/framework/server/src/http/router.ts b/packages/framework/server/src/http/router.ts index ebb75e0b..36106668 100644 --- a/packages/framework/server/src/http/router.ts +++ b/packages/framework/server/src/http/router.ts @@ -7,14 +7,17 @@ */ import type { IncomingMessage, ServerResponse } from 'node:http'; +import { createLogger } from '../logger.js'; import type { HttpRequest, HttpResponse, HttpHandler, HttpRoutes, - CorsOptions, + CorsOptions } from './types.js'; +const logger = createLogger('HTTP'); + /** * @zh 创建 HTTP 请求对象 * @en Create HTTP request object @@ -47,7 +50,7 @@ async function createRequest(req: IncomingMessage): Promise { query, headers: req.headers as Record, body, - ip, + ip }; } @@ -133,7 +136,7 @@ function createResponse(res: ServerResponse): HttpResponse { res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.statusCode = code; res.end(JSON.stringify({ error: message })); - }, + } }; return response; @@ -253,7 +256,7 @@ export function createHttpRouter(routes: HttpRoutes, cors?: CorsOptions | boolea await route.handler(httpReq, httpRes); return true; } catch (error) { - console.error('[HTTP] Route handler error:', error); + logger.error('Route handler error:', error); res.statusCode = 500; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'Internal Server Error' })); diff --git a/packages/framework/server/src/index.ts b/packages/framework/server/src/index.ts index 5a42dd55..6153f59b 100644 --- a/packages/framework/server/src/index.ts +++ b/packages/framework/server/src/index.ts @@ -27,15 +27,15 @@ */ // Core -export { createServer } from './core/server.js' +export { createServer } from './core/server.js'; // Helpers -export { defineApi, defineMsg, defineHttp } from './helpers/define.js' +export { defineApi, defineMsg, defineHttp } from './helpers/define.js'; // Room System -export { Room, type RoomOptions } from './room/Room.js' -export { Player, type IPlayer } from './room/Player.js' -export { onMessage } from './room/decorators.js' +export { Room, type RoomOptions } from './room/Room.js'; +export { Player, type IPlayer } from './room/Player.js'; +export { onMessage } from './room/decorators.js'; // Types export type { @@ -47,18 +47,18 @@ export type { ApiDefinition, MsgDefinition, HttpDefinition, - HttpMethod, -} from './types/index.js' + HttpMethod +} from './types/index.js'; // HTTP -export { createHttpRouter } from './http/router.js' +export { createHttpRouter } from './http/router.js'; export type { HttpRequest, HttpResponse, HttpHandler, HttpRoutes, - CorsOptions, -} from './http/types.js' + CorsOptions +} from './http/types.js'; // Re-export useful types from @esengine/rpc -export { RpcError, ErrorCode } from '@esengine/rpc' +export { RpcError, ErrorCode } from '@esengine/rpc'; diff --git a/packages/framework/server/src/logger.ts b/packages/framework/server/src/logger.ts new file mode 100644 index 00000000..8e5ba09d --- /dev/null +++ b/packages/framework/server/src/logger.ts @@ -0,0 +1,34 @@ +/** + * @zh 日志模块 - 直接使用 @esengine/ecs-framework 的 Logger + * @en Logger module - Uses @esengine/ecs-framework Logger directly + */ + +import { createLogger as ecsCreateLogger, type ILogger } from '@esengine/ecs-framework'; + +export type { ILogger }; + +/** + * @zh 创建命名日志器 + * @en Create a named logger + * + * @param name - @zh 日志器名称 @en Logger name + * @returns @zh 日志器实例 @en Logger instance + * + * @example + * ```typescript + * import { createLogger } from './logger.js' + * + * const logger = createLogger('Server') + * logger.info('Started on port 3000') + * logger.error('Connection failed:', error) + * ``` + */ +export function createLogger(name: string): ILogger { + return ecsCreateLogger(name); +} + +/** + * @zh 默认服务器日志器 + * @en Default server logger + */ +export const serverLogger = createLogger('Server'); diff --git a/packages/framework/server/src/ratelimit/__tests__/strategies.test.ts b/packages/framework/server/src/ratelimit/__tests__/strategies.test.ts index 7675a549..7d1c0679 100644 --- a/packages/framework/server/src/ratelimit/__tests__/strategies.test.ts +++ b/packages/framework/server/src/ratelimit/__tests__/strategies.test.ts @@ -42,7 +42,7 @@ describe('TokenBucketStrategy', () => { strategy.consume('user-1'); } - await new Promise(resolve => setTimeout(resolve, 150)); + await new Promise((resolve) => setTimeout(resolve, 150)); const result = strategy.consume('user-1'); expect(result.allowed).toBe(true); @@ -92,7 +92,7 @@ describe('TokenBucketStrategy', () => { it('should clean up full buckets', async () => { strategy.consume('user-1'); - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); strategy.cleanup(); }); @@ -131,7 +131,7 @@ describe('SlidingWindowStrategy', () => { strategy.consume('user-1'); } - await new Promise(resolve => setTimeout(resolve, 1100)); + await new Promise((resolve) => setTimeout(resolve, 1100)); const result = strategy.consume('user-1'); expect(result.allowed).toBe(true); @@ -192,7 +192,7 @@ describe('FixedWindowStrategy', () => { strategy.consume('user-1'); } - await new Promise(resolve => setTimeout(resolve, 1100)); + await new Promise((resolve) => setTimeout(resolve, 1100)); const result = strategy.consume('user-1'); expect(result.allowed).toBe(true); @@ -224,7 +224,7 @@ describe('FixedWindowStrategy', () => { it('should clean up old windows', async () => { strategy.consume('user-1'); - await new Promise(resolve => setTimeout(resolve, 2100)); + await new Promise((resolve) => setTimeout(resolve, 2100)); strategy.cleanup(); }); diff --git a/packages/framework/server/src/ratelimit/decorators/rateLimit.ts b/packages/framework/server/src/ratelimit/decorators/rateLimit.ts index 252a2019..aa89842e 100644 --- a/packages/framework/server/src/ratelimit/decorators/rateLimit.ts +++ b/packages/framework/server/src/ratelimit/decorators/rateLimit.ts @@ -100,7 +100,7 @@ function getMessageTypeFromMethod(target: any, methodName: string): string | und */ export function rateLimit(config?: MessageRateLimitConfig): MethodDecorator { return function ( - target: Object, + target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor ): PropertyDescriptor { @@ -159,7 +159,7 @@ export function rateLimit(config?: MessageRateLimitConfig): MethodDecorator { */ export function noRateLimit(): MethodDecorator { return function ( - target: Object, + target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor ): PropertyDescriptor { @@ -202,7 +202,7 @@ export function rateLimitMessage( config?: MessageRateLimitConfig ): MethodDecorator { return function ( - target: Object, + target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor ): PropertyDescriptor { @@ -232,7 +232,7 @@ export function rateLimitMessage( */ export function noRateLimitMessage(messageType: string): MethodDecorator { return function ( - target: Object, + target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor ): PropertyDescriptor { diff --git a/packages/framework/server/src/ratelimit/mixin/withRateLimit.ts b/packages/framework/server/src/ratelimit/mixin/withRateLimit.ts index dd24c4eb..dd46d9f3 100644 --- a/packages/framework/server/src/ratelimit/mixin/withRateLimit.ts +++ b/packages/framework/server/src/ratelimit/mixin/withRateLimit.ts @@ -108,6 +108,50 @@ function setPlayerRateLimitContext(player: Player, context: IRateLimitContext): }); } +/** + * @zh 抽象构造器类型 + * @en Abstract constructor type + */ +type AbstractConstructor = abstract new (...args: any[]) => T; + +/** + * @zh 可混入的 Room 构造器类型(支持抽象和具体类) + * @en Mixable Room constructor type (supports both abstract and concrete classes) + */ +type RoomConstructor = AbstractConstructor; + +// ============================================================================ +// Mixin 类型辅助函数 | Mixin Type Helpers +// ============================================================================ +// TypeScript 的 mixin 模式存在类型系统限制: +// 1. ES6 class 语法不支持 `extends` 抽象类型参数 +// 2. 泛型类型参数无法直接用于 class extends 子句 +// 以下辅助函数封装了必要的类型转换,使 mixin 实现更清晰 +// +// TypeScript mixin pattern has type system limitations: +// 1. ES6 class syntax doesn't support `extends` with abstract type parameters +// 2. Generic type parameters cannot be used directly in class extends clause +// The following helpers encapsulate necessary type casts for cleaner mixin implementation +// ============================================================================ + +/** + * @zh 将抽象 Room 构造器转换为可继承的具体构造器 + * @en Convert abstract Room constructor to extendable concrete constructor + */ +function toExtendable(Base: T): new (...args: any[]) => Room { + return Base as unknown as new (...args: any[]) => Room; +} + +/** + * @zh 将 mixin 类转换为正确的返回类型 + * @en Cast mixin class to correct return type + */ +function toMixinResult( + MixinClass: AbstractConstructor +): TBase & AbstractConstructor { + return MixinClass as unknown as TBase & AbstractConstructor; +} + /** * @zh 包装房间类添加速率限制功能 * @en Wrap room class with rate limit functionality @@ -148,10 +192,10 @@ function setPlayerRateLimitContext(player: Player, context: IRateLimitContext): * } * ``` */ -export function withRateLimit Room = new (...args: any[]) => Room>( +export function withRateLimit( Base: TBase, config: RateLimitConfig = {} -): TBase & (new (...args: any[]) => IRateLimitRoom) { +): TBase & AbstractConstructor { const { messagesPerSecond = 10, burstSize = 20, @@ -163,7 +207,9 @@ export function withRateLimit Room = new ( cleanupInterval = 60000 } = config; - abstract class RateLimitRoom extends (Base as new (...args: any[]) => Room) implements IRateLimitRoom { + const BaseRoom = toExtendable(Base); + + abstract class RateLimitRoom extends BaseRoom implements IRateLimitRoom { private _rateLimitStrategy: IRateLimitStrategy; private _playerContexts: WeakMap = new WeakMap(); private _cleanupTimer: ReturnType | null = null; @@ -381,5 +427,5 @@ export function withRateLimit Room = new ( } } - return RateLimitRoom as unknown as TBase & (new (...args: any[]) => IRateLimitRoom); + return toMixinResult(RateLimitRoom); } diff --git a/packages/framework/server/src/ratelimit/strategies/SlidingWindow.ts b/packages/framework/server/src/ratelimit/strategies/SlidingWindow.ts index 5b0953da..67e583bc 100644 --- a/packages/framework/server/src/ratelimit/strategies/SlidingWindow.ts +++ b/packages/framework/server/src/ratelimit/strategies/SlidingWindow.ts @@ -168,7 +168,7 @@ export class SlidingWindowStrategy implements IRateLimitStrategy { */ private _cleanExpiredTimestamps(window: WindowState, now: number): void { const cutoff = now - this._windowMs; - window.timestamps = window.timestamps.filter(ts => ts > cutoff); + window.timestamps = window.timestamps.filter((ts) => ts > cutoff); } /** diff --git a/packages/framework/server/src/room/Player.ts b/packages/framework/server/src/room/Player.ts index 08c300f0..0bb83109 100644 --- a/packages/framework/server/src/room/Player.ts +++ b/packages/framework/server/src/room/Player.ts @@ -3,7 +3,7 @@ * @en Player class */ -import type { Connection } from '@esengine/rpc' +import type { Connection } from '@esengine/rpc'; /** * @zh 玩家接口 @@ -22,13 +22,13 @@ export interface IPlayer> { * @en Player implementation */ export class Player> implements IPlayer { - readonly id: string - readonly roomId: string - data: TData + readonly id: string; + readonly roomId: string; + data: TData; - private _conn: Connection - private _sendFn: (conn: Connection, type: string, data: unknown) => void - private _leaveFn: (player: Player, reason?: string) => void + private _conn: Connection; + private _sendFn: (conn: Connection, type: string, data: unknown) => void; + private _leaveFn: (player: Player, reason?: string) => void; constructor(options: { id: string @@ -38,12 +38,12 @@ export class Player> implements IPlayer { leaveFn: (player: Player, reason?: string) => void initialData?: TData }) { - this.id = options.id - this.roomId = options.roomId - this._conn = options.conn - this._sendFn = options.sendFn - this._leaveFn = options.leaveFn - this.data = options.initialData ?? ({} as TData) + this.id = options.id; + this.roomId = options.roomId; + this._conn = options.conn; + this._sendFn = options.sendFn; + this._leaveFn = options.leaveFn; + this.data = options.initialData ?? ({} as TData); } /** @@ -51,7 +51,7 @@ export class Player> implements IPlayer { * @en Get underlying connection */ get connection(): Connection { - return this._conn + return this._conn; } /** @@ -59,7 +59,7 @@ export class Player> implements IPlayer { * @en Send message to player */ send(type: string, data: T): void { - this._sendFn(this._conn, type, data) + this._sendFn(this._conn, type, data); } /** @@ -67,6 +67,6 @@ export class Player> implements IPlayer { * @en Make player leave the room */ leave(reason?: string): void { - this._leaveFn(this, reason) + this._leaveFn(this, reason); } } diff --git a/packages/framework/server/src/room/Room.ts b/packages/framework/server/src/room/Room.ts index c649fb9d..25861763 100644 --- a/packages/framework/server/src/room/Room.ts +++ b/packages/framework/server/src/room/Room.ts @@ -3,7 +3,7 @@ * @en Room base class */ -import { Player } from './Player.js' +import { Player } from './Player.js'; /** * @zh 房间配置 @@ -26,7 +26,7 @@ interface MessageHandlerMeta { * @zh 消息处理器存储 key * @en Message handler storage key */ -const MESSAGE_HANDLERS = Symbol('messageHandlers') +const MESSAGE_HANDLERS = Symbol('messageHandlers'); /** * @zh 房间基类 @@ -58,19 +58,19 @@ export abstract class Room> * @zh 最大玩家数 * @en Maximum players */ - maxPlayers = 16 + maxPlayers = 16; /** * @zh Tick 速率(每秒),0 = 不自动 tick * @en Tick rate (per second), 0 = no auto tick */ - tickRate = 0 + tickRate = 0; /** * @zh 空房间自动销毁 * @en Auto dispose when empty */ - autoDispose = true + autoDispose = true; // ======================================================================== // 状态 | State @@ -80,21 +80,21 @@ export abstract class Room> * @zh 房间状态 * @en Room state */ - state: TState = {} as TState + state: TState = {} as TState; // ======================================================================== // 内部属性 | Internal properties // ======================================================================== - private _id: string = '' - private _players: Map> = new Map() - private _locked = false - private _disposed = false - private _tickInterval: ReturnType | null = null - private _lastTickTime = 0 - private _broadcastFn: ((type: string, data: unknown) => void) | null = null - private _sendFn: ((conn: any, type: string, data: unknown) => void) | null = null - private _disposeFn: (() => void) | null = null + private _id: string = ''; + private _players: Map> = new Map(); + private _locked = false; + private _disposed = false; + private _tickInterval: ReturnType | null = null; + private _lastTickTime = 0; + private _broadcastFn: ((type: string, data: unknown) => void) | null = null; + private _sendFn: ((conn: any, type: string, data: unknown) => void) | null = null; + private _disposeFn: (() => void) | null = null; // ======================================================================== // 只读属性 | Readonly properties @@ -105,7 +105,7 @@ export abstract class Room> * @en Room ID */ get id(): string { - return this._id + return this._id; } /** @@ -113,7 +113,7 @@ export abstract class Room> * @en All players */ get players(): ReadonlyArray> { - return Array.from(this._players.values()) + return Array.from(this._players.values()); } /** @@ -121,7 +121,7 @@ export abstract class Room> * @en Player count */ get playerCount(): number { - return this._players.size + return this._players.size; } /** @@ -129,7 +129,7 @@ export abstract class Room> * @en Is full */ get isFull(): boolean { - return this._players.size >= this.maxPlayers + return this._players.size >= this.maxPlayers; } /** @@ -137,7 +137,7 @@ export abstract class Room> * @en Is locked */ get isLocked(): boolean { - return this._locked + return this._locked; } /** @@ -145,7 +145,7 @@ export abstract class Room> * @en Is disposed */ get isDisposed(): boolean { - return this._disposed + return this._disposed; } // ======================================================================== @@ -192,7 +192,7 @@ export abstract class Room> */ broadcast(type: string, data: T): void { for (const player of this._players.values()) { - player.send(type, data) + player.send(type, data); } } @@ -203,7 +203,7 @@ export abstract class Room> broadcastExcept(except: Player, type: string, data: T): void { for (const player of this._players.values()) { if (player.id !== except.id) { - player.send(type, data) + player.send(type, data); } } } @@ -213,7 +213,7 @@ export abstract class Room> * @en Get player by id */ getPlayer(id: string): Player | undefined { - return this._players.get(id) + return this._players.get(id); } /** @@ -221,7 +221,7 @@ export abstract class Room> * @en Kick player */ kick(player: Player, reason?: string): void { - player.leave(reason ?? 'kicked') + player.leave(reason ?? 'kicked'); } /** @@ -229,7 +229,7 @@ export abstract class Room> * @en Lock room */ lock(): void { - this._locked = true + this._locked = true; } /** @@ -237,7 +237,7 @@ export abstract class Room> * @en Unlock room */ unlock(): void { - this._locked = false + this._locked = false; } /** @@ -245,18 +245,18 @@ export abstract class Room> * @en Manually dispose room */ dispose(): void { - if (this._disposed) return - this._disposed = true + if (this._disposed) return; + this._disposed = true; - this._stopTick() + this._stopTick(); for (const player of this._players.values()) { - player.leave('room_disposed') + player.leave('room_disposed'); } - this._players.clear() + this._players.clear(); - this.onDispose() - this._disposeFn?.() + this.onDispose(); + this._disposeFn?.(); } // ======================================================================== @@ -272,18 +272,18 @@ export abstract class Room> broadcastFn: (type: string, data: unknown) => void disposeFn: () => void }): void { - this._id = options.id - this._sendFn = options.sendFn - this._broadcastFn = options.broadcastFn - this._disposeFn = options.disposeFn + this._id = options.id; + this._sendFn = options.sendFn; + this._broadcastFn = options.broadcastFn; + this._disposeFn = options.disposeFn; } /** * @internal */ async _create(options?: RoomOptions): Promise { - await this.onCreate(options) - this._startTick() + await this.onCreate(options); + this._startTick(); } /** @@ -291,7 +291,7 @@ export abstract class Room> */ async _addPlayer(id: string, conn: any): Promise | null> { if (this._locked || this.isFull || this._disposed) { - return null + return null; } const player = new Player({ @@ -299,27 +299,27 @@ export abstract class Room> roomId: this._id, conn, sendFn: this._sendFn!, - leaveFn: (p, reason) => this._removePlayer(p.id, reason), - }) + leaveFn: (p, reason) => this._removePlayer(p.id, reason) + }); - this._players.set(id, player) - await this.onJoin(player) + this._players.set(id, player); + await this.onJoin(player); - return player + return player; } /** * @internal */ async _removePlayer(id: string, reason?: string): Promise { - const player = this._players.get(id) - if (!player) return + const player = this._players.get(id); + if (!player) return; - this._players.delete(id) - await this.onLeave(player, reason) + this._players.delete(id); + await this.onLeave(player, reason); if (this.autoDispose && this._players.size === 0) { - this.dispose() + this.dispose(); } } @@ -327,16 +327,16 @@ export abstract class Room> * @internal */ _handleMessage(type: string, data: unknown, playerId: string): void { - const player = this._players.get(playerId) - if (!player) return + const player = this._players.get(playerId); + if (!player) return; - const handlers = (this.constructor as any)[MESSAGE_HANDLERS] as MessageHandlerMeta[] | undefined + const handlers = (this.constructor as any)[MESSAGE_HANDLERS] as MessageHandlerMeta[] | undefined; if (handlers) { for (const handler of handlers) { if (handler.type === type) { - const method = (this as any)[handler.method] + const method = (this as any)[handler.method]; if (typeof method === 'function') { - method.call(this, data, player) + method.call(this, data, player); } } } @@ -344,21 +344,21 @@ export abstract class Room> } private _startTick(): void { - if (this.tickRate <= 0) return + if (this.tickRate <= 0) return; - this._lastTickTime = performance.now() + this._lastTickTime = performance.now(); this._tickInterval = setInterval(() => { - const now = performance.now() - const dt = (now - this._lastTickTime) / 1000 - this._lastTickTime = now - this.onTick(dt) - }, 1000 / this.tickRate) + const now = performance.now(); + const dt = (now - this._lastTickTime) / 1000; + this._lastTickTime = now; + this.onTick(dt); + }, 1000 / this.tickRate); } private _stopTick(): void { if (this._tickInterval) { - clearInterval(this._tickInterval) - this._tickInterval = null + clearInterval(this._tickInterval); + this._tickInterval = null; } } } @@ -368,7 +368,7 @@ export abstract class Room> * @en Get message handler metadata */ export function getMessageHandlers(target: any): MessageHandlerMeta[] { - return target[MESSAGE_HANDLERS] || [] + return target[MESSAGE_HANDLERS] || []; } /** @@ -377,7 +377,7 @@ export function getMessageHandlers(target: any): MessageHandlerMeta[] { */ export function registerMessageHandler(target: any, type: string, method: string): void { if (!target[MESSAGE_HANDLERS]) { - target[MESSAGE_HANDLERS] = [] + target[MESSAGE_HANDLERS] = []; } - target[MESSAGE_HANDLERS].push({ type, method }) + target[MESSAGE_HANDLERS].push({ type, method }); } diff --git a/packages/framework/server/src/room/RoomManager.ts b/packages/framework/server/src/room/RoomManager.ts index 9f80813a..5c6f8611 100644 --- a/packages/framework/server/src/room/RoomManager.ts +++ b/packages/framework/server/src/room/RoomManager.ts @@ -3,8 +3,11 @@ * @en Room manager */ -import { Room, type RoomOptions } from './Room.js' -import type { Player } from './Player.js' +import { Room, type RoomOptions } from './Room.js'; +import type { Player } from './Player.js'; +import { createLogger } from '../logger.js'; + +const logger = createLogger('Room'); /** * @zh 房间类型 @@ -25,15 +28,15 @@ interface RoomDefinition { * @en Room manager */ export class RoomManager { - private _definitions: Map = new Map() - private _rooms: Map = new Map() - private _playerToRoom: Map = new Map() - private _nextRoomId = 1 + private _definitions: Map = new Map(); + private _rooms: Map = new Map(); + private _playerToRoom: Map = new Map(); + private _nextRoomId = 1; - private _sendFn: (conn: any, type: string, data: unknown) => void + private _sendFn: (conn: any, type: string, data: unknown) => void; constructor(sendFn: (conn: any, type: string, data: unknown) => void) { - this._sendFn = sendFn + this._sendFn = sendFn; } /** @@ -41,7 +44,7 @@ export class RoomManager { * @en Define room type */ define(name: string, roomClass: RoomClass): void { - this._definitions.set(name, { roomClass }) + this._definitions.set(name, { roomClass }); } /** @@ -49,33 +52,33 @@ export class RoomManager { * @en Create room */ async create(name: string, options?: RoomOptions): Promise { - const def = this._definitions.get(name) + const def = this._definitions.get(name); if (!def) { - console.warn(`[RoomManager] Room type not found: ${name}`) - return null + logger.warn(`Room type not found: ${name}`); + return null; } - const roomId = this._generateRoomId() - const room = new def.roomClass() + const roomId = this._generateRoomId(); + const room = new def.roomClass(); room._init({ id: roomId, sendFn: this._sendFn, broadcastFn: (type, data) => { for (const player of room.players) { - player.send(type, data) + player.send(type, data); } }, disposeFn: () => { - this._rooms.delete(roomId) - }, - }) + this._rooms.delete(roomId); + } + }); - this._rooms.set(roomId, room) - await room._create(options) + this._rooms.set(roomId, room); + await room._create(options); - console.log(`[Room] Created: ${name} (${roomId})`) - return room + logger.info(`Created: ${name} (${roomId})`); + return room; } /** @@ -89,22 +92,22 @@ export class RoomManager { options?: RoomOptions ): Promise<{ room: Room; player: Player } | null> { // 查找可加入的房间 - let room = this._findAvailableRoom(name) + let room = this._findAvailableRoom(name); // 没有则创建 if (!room) { - room = await this.create(name, options) - if (!room) return null + room = await this.create(name, options); + if (!room) return null; } // 加入房间 - const player = await room._addPlayer(playerId, conn) - if (!player) return null + const player = await room._addPlayer(playerId, conn); + if (!player) return null; - this._playerToRoom.set(playerId, room.id) + this._playerToRoom.set(playerId, room.id); - console.log(`[Room] Player ${playerId} joined ${room.id}`) - return { room, player } + logger.info(`Player ${playerId} joined ${room.id}`); + return { room, player }; } /** @@ -116,16 +119,16 @@ export class RoomManager { playerId: string, conn: any ): Promise<{ room: Room; player: Player } | null> { - const room = this._rooms.get(roomId) - if (!room) return null + const room = this._rooms.get(roomId); + if (!room) return null; - const player = await room._addPlayer(playerId, conn) - if (!player) return null + const player = await room._addPlayer(playerId, conn); + if (!player) return null; - this._playerToRoom.set(playerId, room.id) + this._playerToRoom.set(playerId, room.id); - console.log(`[Room] Player ${playerId} joined ${room.id}`) - return { room, player } + logger.info(`Player ${playerId} joined ${room.id}`); + return { room, player }; } /** @@ -133,16 +136,16 @@ export class RoomManager { * @en Player leave */ async leave(playerId: string, reason?: string): Promise { - const roomId = this._playerToRoom.get(playerId) - if (!roomId) return + const roomId = this._playerToRoom.get(playerId); + if (!roomId) return; - const room = this._rooms.get(roomId) + const room = this._rooms.get(roomId); if (room) { - await room._removePlayer(playerId, reason) + await room._removePlayer(playerId, reason); } - this._playerToRoom.delete(playerId) - console.log(`[Room] Player ${playerId} left ${roomId}`) + this._playerToRoom.delete(playerId); + logger.info(`Player ${playerId} left ${roomId}`); } /** @@ -150,12 +153,12 @@ export class RoomManager { * @en Handle message */ handleMessage(playerId: string, type: string, data: unknown): void { - const roomId = this._playerToRoom.get(playerId) - if (!roomId) return + const roomId = this._playerToRoom.get(playerId); + if (!roomId) return; - const room = this._rooms.get(roomId) + const room = this._rooms.get(roomId); if (room) { - room._handleMessage(type, data, playerId) + room._handleMessage(type, data, playerId); } } @@ -164,7 +167,7 @@ export class RoomManager { * @en Get room */ getRoom(roomId: string): Room | undefined { - return this._rooms.get(roomId) + return this._rooms.get(roomId); } /** @@ -172,8 +175,8 @@ export class RoomManager { * @en Get player's room */ getPlayerRoom(playerId: string): Room | undefined { - const roomId = this._playerToRoom.get(playerId) - return roomId ? this._rooms.get(roomId) : undefined + const roomId = this._playerToRoom.get(playerId); + return roomId ? this._rooms.get(roomId) : undefined; } /** @@ -181,7 +184,7 @@ export class RoomManager { * @en Get all rooms */ getRooms(): ReadonlyArray { - return Array.from(this._rooms.values()) + return Array.from(this._rooms.values()); } /** @@ -189,17 +192,17 @@ export class RoomManager { * @en Get all rooms of a type */ getRoomsByType(name: string): Room[] { - const def = this._definitions.get(name) - if (!def) return [] + const def = this._definitions.get(name); + if (!def) return []; return Array.from(this._rooms.values()).filter( - room => room instanceof def.roomClass - ) + (room) => room instanceof def.roomClass + ); } private _findAvailableRoom(name: string): Room | null { - const def = this._definitions.get(name) - if (!def) return null + const def = this._definitions.get(name); + if (!def) return null; for (const room of this._rooms.values()) { if ( @@ -208,14 +211,14 @@ export class RoomManager { !room.isLocked && !room.isDisposed ) { - return room + return room; } } - return null + return null; } private _generateRoomId(): string { - return `room_${this._nextRoomId++}` + return `room_${this._nextRoomId++}`; } } diff --git a/packages/framework/server/src/room/decorators.ts b/packages/framework/server/src/room/decorators.ts index c3a9e3a8..ca116089 100644 --- a/packages/framework/server/src/room/decorators.ts +++ b/packages/framework/server/src/room/decorators.ts @@ -3,7 +3,7 @@ * @en Room decorators */ -import { registerMessageHandler } from './Room.js' +import { registerMessageHandler } from './Room.js'; /** * @zh 消息处理器装饰器 @@ -30,6 +30,6 @@ export function onMessage(type: string): MethodDecorator { propertyKey: string | symbol, _descriptor: PropertyDescriptor ) { - registerMessageHandler(target.constructor, type, propertyKey as string) - } + registerMessageHandler(target.constructor, type, propertyKey as string); + }; } diff --git a/packages/framework/server/src/room/index.ts b/packages/framework/server/src/room/index.ts index ac3bddb8..dca631e4 100644 --- a/packages/framework/server/src/room/index.ts +++ b/packages/framework/server/src/room/index.ts @@ -3,7 +3,7 @@ * @en Room system */ -export { Room, type RoomOptions } from './Room.js' -export { Player, type IPlayer } from './Player.js' -export { RoomManager, type RoomClass } from './RoomManager.js' -export { onMessage } from './decorators.js' +export { Room, type RoomOptions } from './Room.js'; +export { Player, type IPlayer } from './Player.js'; +export { RoomManager, type RoomClass } from './RoomManager.js'; +export { onMessage } from './decorators.js'; diff --git a/packages/framework/server/src/router/loader.ts b/packages/framework/server/src/router/loader.ts index 7e785d0a..50cabf1c 100644 --- a/packages/framework/server/src/router/loader.ts +++ b/packages/framework/server/src/router/loader.ts @@ -3,9 +3,10 @@ * @en File-based router loader */ -import * as fs from 'node:fs' -import * as path from 'node:path' -import { pathToFileURL } from 'node:url' +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { createLogger } from '../logger.js'; import type { ApiDefinition, MsgDefinition, @@ -13,8 +14,10 @@ import type { LoadedApiHandler, LoadedMsgHandler, LoadedHttpHandler, - HttpMethod, -} from '../types/index.js' + HttpMethod +} from '../types/index.js'; + +const logger = createLogger('Server'); /** * @zh 将文件名转换为 API/消息名称 @@ -26,12 +29,12 @@ import type { * 'save_blueprint.ts' -> 'SaveBlueprint' */ function fileNameToHandlerName(fileName: string): string { - const baseName = fileName.replace(/\.(ts|js|mts|mjs)$/, '') + const baseName = fileName.replace(/\.(ts|js|mts|mjs)$/, ''); return baseName .split(/[-_]/) - .map(part => part.charAt(0).toUpperCase() + part.slice(1)) - .join('') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(''); } /** @@ -40,23 +43,23 @@ function fileNameToHandlerName(fileName: string): string { */ function scanDirectory(dir: string): string[] { if (!fs.existsSync(dir)) { - return [] + return []; } - const files: string[] = [] - const entries = fs.readdirSync(dir, { withFileTypes: true }) + const files: string[] = []; + const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name)) { // 跳过 index 和下划线开头的文件 if (entry.name.startsWith('_') || entry.name.startsWith('index.')) { - continue + continue; } - files.push(path.join(dir, entry.name)) + files.push(path.join(dir, entry.name)); } } - return files + return files; } /** @@ -64,29 +67,29 @@ function scanDirectory(dir: string): string[] { * @en Load API handlers */ export async function loadApiHandlers(apiDir: string): Promise { - const files = scanDirectory(apiDir) - const handlers: LoadedApiHandler[] = [] + const files = scanDirectory(apiDir); + const handlers: LoadedApiHandler[] = []; for (const filePath of files) { try { - const fileUrl = pathToFileURL(filePath).href - const module = await import(fileUrl) - const definition = module.default as ApiDefinition + const fileUrl = pathToFileURL(filePath).href; + const module = await import(fileUrl); + const definition = module.default as ApiDefinition; if (definition && typeof definition.handler === 'function') { - const name = fileNameToHandlerName(path.basename(filePath)) + const name = fileNameToHandlerName(path.basename(filePath)); handlers.push({ name, path: filePath, - definition, - }) + definition + }); } } catch (err) { - console.warn(`[Server] Failed to load API handler: ${filePath}`, err) + logger.warn(`Failed to load API handler: ${filePath}`, err); } } - return handlers + return handlers; } /** @@ -94,29 +97,29 @@ export async function loadApiHandlers(apiDir: string): Promise { - const files = scanDirectory(msgDir) - const handlers: LoadedMsgHandler[] = [] + const files = scanDirectory(msgDir); + const handlers: LoadedMsgHandler[] = []; for (const filePath of files) { try { - const fileUrl = pathToFileURL(filePath).href - const module = await import(fileUrl) - const definition = module.default as MsgDefinition + const fileUrl = pathToFileURL(filePath).href; + const module = await import(fileUrl); + const definition = module.default as MsgDefinition; if (definition && typeof definition.handler === 'function') { - const name = fileNameToHandlerName(path.basename(filePath)) + const name = fileNameToHandlerName(path.basename(filePath)); handlers.push({ name, path: filePath, - definition, - }) + definition + }); } } catch (err) { - console.warn(`[Server] Failed to load msg handler: ${filePath}`, err) + logger.warn(`Failed to load msg handler: ${filePath}`, err); } } - return handlers + return handlers; } /** @@ -125,27 +128,27 @@ export async function loadMsgHandlers(msgDir: string): Promise { if (!fs.existsSync(dir)) { - return [] + return []; } - const files: Array<{ filePath: string; relativePath: string }> = [] - const entries = fs.readdirSync(dir, { withFileTypes: true }) + const files: Array<{ filePath: string; relativePath: string }> = []; + const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { - const fullPath = path.join(dir, entry.name) + const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { - files.push(...scanDirectoryRecursive(fullPath, baseDir)) + files.push(...scanDirectoryRecursive(fullPath, baseDir)); } else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name)) { if (entry.name.startsWith('_') || entry.name.startsWith('index.')) { - continue + continue; } - const relativePath = path.relative(baseDir, fullPath) - files.push({ filePath: fullPath, relativePath }) + const relativePath = path.relative(baseDir, fullPath); + files.push({ filePath: fullPath, relativePath }); } } - return files + return files; } /** @@ -161,17 +164,17 @@ function filePathToRoute(relativePath: string, prefix: string): string { let route = relativePath .replace(/\.(ts|js|mts|mjs)$/, '') .replace(/\\/g, '/') - .replace(/\[([^\]]+)\]/g, ':$1') + .replace(/\[(\w+)\]/g, ':$1'); if (!route.startsWith('/')) { - route = '/' + route + route = '/' + route; } const fullRoute = prefix.endsWith('/') ? prefix.slice(0, -1) + route - : prefix + route + : prefix + route; - return fullRoute + return fullRoute; } /** @@ -194,30 +197,30 @@ export async function loadHttpHandlers( httpDir: string, prefix: string = '/api' ): Promise { - const files = scanDirectoryRecursive(httpDir) - const handlers: LoadedHttpHandler[] = [] + const files = scanDirectoryRecursive(httpDir); + const handlers: LoadedHttpHandler[] = []; for (const { filePath, relativePath } of files) { try { - const fileUrl = pathToFileURL(filePath).href - const module = await import(fileUrl) - const definition = module.default as HttpDefinition + const fileUrl = pathToFileURL(filePath).href; + const module = await import(fileUrl); + const definition = module.default as HttpDefinition; if (definition && typeof definition.handler === 'function') { - const route = filePathToRoute(relativePath, prefix) - const method: HttpMethod = definition.method ?? 'POST' + const route = filePathToRoute(relativePath, prefix); + const method: HttpMethod = definition.method ?? 'POST'; handlers.push({ route, method, path: filePath, - definition, - }) + definition + }); } } catch (err) { - console.warn(`[Server] Failed to load HTTP handler: ${filePath}`, err) + logger.warn(`Failed to load HTTP handler: ${filePath}`, err); } } - return handlers + return handlers; } diff --git a/packages/framework/server/src/testing/MockRoom.ts b/packages/framework/server/src/testing/MockRoom.ts index 0cce03fd..ff440e88 100644 --- a/packages/framework/server/src/testing/MockRoom.ts +++ b/packages/framework/server/src/testing/MockRoom.ts @@ -3,7 +3,7 @@ * @en Mock room for testing */ -import { Room, onMessage, type Player } from '../room/index.js' +import { Room, onMessage, type Player } from '../room/index.js'; /** * @zh 模拟房间状态 @@ -41,27 +41,27 @@ export class MockRoom extends Room { state: MockRoomState = { messages: [], joinCount: 0, - leaveCount: 0, - } + leaveCount: 0 + }; onCreate(): void { // 房间创建 } onJoin(player: Player): void { - this.state.joinCount++ + this.state.joinCount++; this.broadcast('PlayerJoined', { playerId: player.id, - joinCount: this.state.joinCount, - }) + joinCount: this.state.joinCount + }); } onLeave(player: Player): void { - this.state.leaveCount++ + this.state.leaveCount++; this.broadcast('PlayerLeft', { playerId: player.id, - leaveCount: this.state.leaveCount, - }) + leaveCount: this.state.leaveCount + }); } @onMessage('*') @@ -69,31 +69,31 @@ export class MockRoom extends Room { this.state.messages.push({ type, data, - playerId: player.id, - }) + playerId: player.id + }); // 回显消息给所有玩家 this.broadcast('MessageReceived', { type, data, - from: player.id, - }) + from: player.id + }); } @onMessage('Echo') handleEcho(data: unknown, player: Player): void { // 只回复给发送者 - player.send('EchoReply', data) + player.send('EchoReply', data); } @onMessage('Broadcast') handleBroadcast(data: unknown, _player: Player): void { - this.broadcast('BroadcastMessage', data) + this.broadcast('BroadcastMessage', data); } @onMessage('Ping') handlePing(_data: unknown, player: Player): void { - player.send('Pong', { timestamp: Date.now() }) + player.send('Pong', { timestamp: Date.now() }); } } @@ -107,7 +107,7 @@ export class MockRoom extends Room { export class EchoRoom extends Room { @onMessage('*') handleAnyMessage(data: unknown, player: Player, type: string): void { - player.send(type, data) + player.send(type, data); } } @@ -120,15 +120,15 @@ export class EchoRoom extends Room { */ export class BroadcastRoom extends Room { onJoin(player: Player): void { - this.broadcast('PlayerJoined', { id: player.id }) + this.broadcast('PlayerJoined', { id: player.id }); } onLeave(player: Player): void { - this.broadcast('PlayerLeft', { id: player.id }) + this.broadcast('PlayerLeft', { id: player.id }); } @onMessage('*') handleAnyMessage(data: unknown, player: Player, type: string): void { - this.broadcast(type, { from: player.id, data }) + this.broadcast(type, { from: player.id, data }); } } diff --git a/packages/framework/server/src/testing/Room.test.ts b/packages/framework/server/src/testing/Room.test.ts index 8a412da1..9932775e 100644 --- a/packages/framework/server/src/testing/Room.test.ts +++ b/packages/framework/server/src/testing/Room.test.ts @@ -6,10 +6,10 @@ * @en This file demonstrates how to use testing utilities for server testing */ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { createTestEnv, type TestEnvironment, wait } from './TestServer.js' -import { MockRoom, BroadcastRoom } from './MockRoom.js' -import { Room, onMessage, type Player } from '../room/index.js' +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { createTestEnv, type TestEnvironment, wait } from './TestServer.js'; +import { MockRoom, BroadcastRoom } from './MockRoom.js'; +import { Room, onMessage, type Player } from '../room/index.js'; // ============================================================================ // Custom Room for Testing | 自定义测试房间 @@ -21,52 +21,52 @@ interface GameState { } class GameRoom extends Room { - maxPlayers = 4 + maxPlayers = 4; state: GameState = { players: new Map(), - scores: new Map(), - } + scores: new Map() + }; onJoin(player: Player): void { - this.state.players.set(player.id, { x: 0, y: 0 }) - this.state.scores.set(player.id, 0) + this.state.players.set(player.id, { x: 0, y: 0 }); + this.state.scores.set(player.id, 0); this.broadcast('PlayerJoined', { playerId: player.id, - playerCount: this.state.players.size, - }) + playerCount: this.state.players.size + }); } onLeave(player: Player): void { - this.state.players.delete(player.id) - this.state.scores.delete(player.id) + this.state.players.delete(player.id); + this.state.scores.delete(player.id); this.broadcast('PlayerLeft', { playerId: player.id, - playerCount: this.state.players.size, - }) + playerCount: this.state.players.size + }); } @onMessage('Move') handleMove(data: { x: number; y: number }, player: Player): void { - const pos = this.state.players.get(player.id) + const pos = this.state.players.get(player.id); if (pos) { - pos.x = data.x - pos.y = data.y + pos.x = data.x; + pos.y = data.y; this.broadcast('PlayerMoved', { playerId: player.id, x: data.x, - y: data.y, - }) + y: data.y + }); } } @onMessage('Score') handleScore(data: { points: number }, player: Player): void { - const current = this.state.scores.get(player.id) ?? 0 - this.state.scores.set(player.id, current + data.points) + const current = this.state.scores.get(player.id) ?? 0; + this.state.scores.set(player.id, current + data.points); player.send('ScoreUpdated', { - score: this.state.scores.get(player.id), - }) + score: this.state.scores.get(player.id) + }); } } @@ -75,15 +75,15 @@ class GameRoom extends Room { // ============================================================================ describe('Room Integration Tests', () => { - let env: TestEnvironment + let env: TestEnvironment; beforeEach(async () => { - env = await createTestEnv() - }) + env = await createTestEnv(); + }); afterEach(async () => { - await env.cleanup() - }) + await env.cleanup(); + }); // ======================================================================== // Basic Tests | 基础测试 @@ -91,39 +91,39 @@ describe('Room Integration Tests', () => { describe('Basic Room Operations', () => { it('should create and join room', async () => { - env.server.define('game', GameRoom) + env.server.define('game', GameRoom); - const client = await env.createClient() - const result = await client.joinRoom('game') + const client = await env.createClient(); + const result = await client.joinRoom('game'); - expect(result.roomId).toBeDefined() - expect(result.playerId).toBeDefined() - expect(client.roomId).toBe(result.roomId) - }) + expect(result.roomId).toBeDefined(); + expect(result.playerId).toBeDefined(); + expect(client.roomId).toBe(result.roomId); + }); it('should leave room', async () => { - env.server.define('game', GameRoom) + env.server.define('game', GameRoom); - const client = await env.createClient() - await client.joinRoom('game') + const client = await env.createClient(); + await client.joinRoom('game'); - await client.leaveRoom() + await client.leaveRoom(); - expect(client.roomId).toBeNull() - }) + expect(client.roomId).toBeNull(); + }); it('should join existing room by id', async () => { - env.server.define('game', GameRoom) + env.server.define('game', GameRoom); - const client1 = await env.createClient() - const { roomId } = await client1.joinRoom('game') + const client1 = await env.createClient(); + const { roomId } = await client1.joinRoom('game'); - const client2 = await env.createClient() - const result = await client2.joinRoomById(roomId) + const client2 = await env.createClient(); + const result = await client2.joinRoomById(roomId); - expect(result.roomId).toBe(roomId) - }) - }) + expect(result.roomId).toBe(roomId); + }); + }); // ======================================================================== // Message Tests | 消息测试 @@ -131,66 +131,66 @@ describe('Room Integration Tests', () => { describe('Room Messages', () => { it('should receive room messages', async () => { - env.server.define('game', GameRoom) + env.server.define('game', GameRoom); - const client = await env.createClient() - await client.joinRoom('game') + const client = await env.createClient(); + await client.joinRoom('game'); - const movePromise = client.waitForRoomMessage('PlayerMoved') - client.sendToRoom('Move', { x: 100, y: 200 }) + const movePromise = client.waitForRoomMessage('PlayerMoved'); + client.sendToRoom('Move', { x: 100, y: 200 }); - const msg = await movePromise + const msg = await movePromise; expect(msg).toEqual({ playerId: client.playerId, x: 100, - y: 200, - }) - }) + y: 200 + }); + }); it('should receive broadcast messages', async () => { - env.server.define('game', GameRoom) + env.server.define('game', GameRoom); - const [client1, client2] = await env.createClients(2) + const [client1, client2] = await env.createClients(2); - const { roomId } = await client1.joinRoom('game') - await client2.joinRoomById(roomId) + const { roomId } = await client1.joinRoom('game'); + await client2.joinRoomById(roomId); // client1 等待收到 client2 的移动消息 - const movePromise = client1.waitForRoomMessage('PlayerMoved') - client2.sendToRoom('Move', { x: 50, y: 75 }) + const movePromise = client1.waitForRoomMessage('PlayerMoved'); + client2.sendToRoom('Move', { x: 50, y: 75 }); - const msg = await movePromise + const msg = await movePromise; expect(msg).toMatchObject({ playerId: client2.playerId, x: 50, - y: 75, - }) - }) + y: 75 + }); + }); it('should handle player join/leave broadcasts', async () => { - env.server.define('broadcast', BroadcastRoom) + env.server.define('broadcast', BroadcastRoom); - const client1 = await env.createClient() - const { roomId } = await client1.joinRoom('broadcast') + const client1 = await env.createClient(); + const { roomId } = await client1.joinRoom('broadcast'); // 等待 client2 加入的广播 - const joinPromise = client1.waitForRoomMessage<{ id: string }>('PlayerJoined') + const joinPromise = client1.waitForRoomMessage<{ id: string }>('PlayerJoined'); - const client2 = await env.createClient() - const client2Result = await client2.joinRoomById(roomId) + const client2 = await env.createClient(); + const client2Result = await client2.joinRoomById(roomId); - const joinMsg = await joinPromise - expect(joinMsg).toMatchObject({ id: client2Result.playerId }) + const joinMsg = await joinPromise; + expect(joinMsg).toMatchObject({ id: client2Result.playerId }); // 等待 client2 离开的广播 - const leavePromise = client1.waitForRoomMessage<{ id: string }>('PlayerLeft') - const client2PlayerId = client2.playerId // 保存 playerId - await client2.leaveRoom() + const leavePromise = client1.waitForRoomMessage<{ id: string }>('PlayerLeft'); + const client2PlayerId = client2.playerId; // 保存 playerId + await client2.leaveRoom(); - const leaveMsg = await leavePromise - expect(leaveMsg).toMatchObject({ id: client2PlayerId }) - }) - }) + const leaveMsg = await leavePromise; + expect(leaveMsg).toMatchObject({ id: client2PlayerId }); + }); + }); // ======================================================================== // MockRoom Tests | 模拟房间测试 @@ -198,45 +198,45 @@ describe('Room Integration Tests', () => { describe('MockRoom', () => { it('should record messages', async () => { - env.server.define('mock', MockRoom) + env.server.define('mock', MockRoom); - const client = await env.createClient() - await client.joinRoom('mock') + const client = await env.createClient(); + await client.joinRoom('mock'); // 使用 Echo 消息,因为它是明确定义的 - const echoPromise = client.waitForRoomMessage('EchoReply') - client.sendToRoom('Echo', { value: 123 }) - await echoPromise + const echoPromise = client.waitForRoomMessage('EchoReply'); + client.sendToRoom('Echo', { value: 123 }); + await echoPromise; - expect(client.hasReceivedMessage('RoomMessage')).toBe(true) - }) + expect(client.hasReceivedMessage('RoomMessage')).toBe(true); + }); it('should handle echo', async () => { - env.server.define('mock', MockRoom) + env.server.define('mock', MockRoom); - const client = await env.createClient() - await client.joinRoom('mock') + const client = await env.createClient(); + await client.joinRoom('mock'); - const echoPromise = client.waitForRoomMessage('EchoReply') - client.sendToRoom('Echo', { message: 'hello' }) + const echoPromise = client.waitForRoomMessage('EchoReply'); + client.sendToRoom('Echo', { message: 'hello' }); - const reply = await echoPromise - expect(reply).toEqual({ message: 'hello' }) - }) + const reply = await echoPromise; + expect(reply).toEqual({ message: 'hello' }); + }); it('should handle ping/pong', async () => { - env.server.define('mock', MockRoom) + env.server.define('mock', MockRoom); - const client = await env.createClient() - await client.joinRoom('mock') + const client = await env.createClient(); + await client.joinRoom('mock'); - const pongPromise = client.waitForRoomMessage<{ timestamp: number }>('Pong') - client.sendToRoom('Ping', {}) + const pongPromise = client.waitForRoomMessage<{ timestamp: number }>('Pong'); + client.sendToRoom('Ping', {}); - const pong = await pongPromise - expect(pong.timestamp).toBeGreaterThan(0) - }) - }) + const pong = await pongPromise; + expect(pong.timestamp).toBeGreaterThan(0); + }); + }); // ======================================================================== // Multiple Clients Tests | 多客户端测试 @@ -244,45 +244,45 @@ describe('Room Integration Tests', () => { describe('Multiple Clients', () => { it('should handle multiple clients in same room', async () => { - env.server.define('game', GameRoom) + env.server.define('game', GameRoom); - const clients = await env.createClients(3) - const { roomId } = await clients[0].joinRoom('game') + const clients = await env.createClients(3); + const { roomId } = await clients[0].joinRoom('game'); for (let i = 1; i < clients.length; i++) { - await clients[i].joinRoomById(roomId) + await clients[i].joinRoomById(roomId); } // 所有客户端都应该能收到消息 - const promises = clients.map((c) => c.waitForRoomMessage('PlayerMoved')) + const promises = clients.map((c) => c.waitForRoomMessage('PlayerMoved')); - clients[0].sendToRoom('Move', { x: 1, y: 2 }) + clients[0].sendToRoom('Move', { x: 1, y: 2 }); - const results = await Promise.all(promises) + const results = await Promise.all(promises); for (const result of results) { - expect(result).toMatchObject({ x: 1, y: 2 }) + expect(result).toMatchObject({ x: 1, y: 2 }); } - }) + }); it('should handle concurrent room operations', async () => { - env.server.define('game', GameRoom) + env.server.define('game', GameRoom); - const clients = await env.createClients(4) // maxPlayers = 4 + const clients = await env.createClients(4); // maxPlayers = 4 // 顺序加入房间(避免并发创建多个房间) - const { roomId } = await clients[0].joinRoom('game') + const { roomId } = await clients[0].joinRoom('game'); // 其余客户端加入同一房间 const results = await Promise.all( clients.slice(1).map((c) => c.joinRoomById(roomId)) - ) + ); // 验证所有客户端都在同一房间 for (const result of results) { - expect(result.roomId).toBe(roomId) + expect(result.roomId).toBe(roomId); } - }) - }) + }); + }); // ======================================================================== // Error Handling Tests | 错误处理测试 @@ -290,31 +290,31 @@ describe('Room Integration Tests', () => { describe('Error Handling', () => { it('should reject joining non-existent room type', async () => { - const client = await env.createClient() + const client = await env.createClient(); - await expect(client.joinRoom('nonexistent')).rejects.toThrow() - }) + await expect(client.joinRoom('nonexistent')).rejects.toThrow(); + }); it('should handle client disconnect gracefully', async () => { - env.server.define('game', GameRoom) + env.server.define('game', GameRoom); - const client1 = await env.createClient() - const { roomId } = await client1.joinRoom('game') + const client1 = await env.createClient(); + const { roomId } = await client1.joinRoom('game'); - const client2 = await env.createClient() - await client2.joinRoomById(roomId) + const client2 = await env.createClient(); + await client2.joinRoomById(roomId); // 等待 client2 离开的广播 - const leavePromise = client1.waitForRoomMessage('PlayerLeft') + const leavePromise = client1.waitForRoomMessage('PlayerLeft'); // 强制断开 client2 - await client2.disconnect() + await client2.disconnect(); // client1 应该收到离开消息 - const msg = await leavePromise - expect(msg).toBeDefined() - }) - }) + const msg = await leavePromise; + expect(msg).toBeDefined(); + }); + }); // ======================================================================== // Assertion Helpers Tests | 断言辅助测试 @@ -322,50 +322,50 @@ describe('Room Integration Tests', () => { describe('TestClient Assertions', () => { it('should track received messages', async () => { - env.server.define('mock', MockRoom) + env.server.define('mock', MockRoom); - const client = await env.createClient() - await client.joinRoom('mock') + const client = await env.createClient(); + await client.joinRoom('mock'); // 发送多条消息 - client.sendToRoom('Test', { n: 1 }) - client.sendToRoom('Test', { n: 2 }) - client.sendToRoom('Test', { n: 3 }) + client.sendToRoom('Test', { n: 1 }); + client.sendToRoom('Test', { n: 2 }); + client.sendToRoom('Test', { n: 3 }); // 等待消息处理 - await wait(100) + await wait(100); - expect(client.getMessageCount()).toBeGreaterThan(0) - expect(client.hasReceivedMessage('RoomMessage')).toBe(true) - }) + expect(client.getMessageCount()).toBeGreaterThan(0); + expect(client.hasReceivedMessage('RoomMessage')).toBe(true); + }); it('should get messages of specific type', async () => { - env.server.define('mock', MockRoom) + env.server.define('mock', MockRoom); - const client = await env.createClient() - await client.joinRoom('mock') + const client = await env.createClient(); + await client.joinRoom('mock'); - client.sendToRoom('Ping', {}) - await client.waitForRoomMessage('Pong') + client.sendToRoom('Ping', {}); + await client.waitForRoomMessage('Pong'); - const pongs = client.getMessagesOfType('RoomMessage') - expect(pongs.length).toBeGreaterThan(0) - }) + const pongs = client.getMessagesOfType('RoomMessage'); + expect(pongs.length).toBeGreaterThan(0); + }); it('should clear message history', async () => { - env.server.define('mock', MockRoom) + env.server.define('mock', MockRoom); - const client = await env.createClient() - await client.joinRoom('mock') + const client = await env.createClient(); + await client.joinRoom('mock'); - client.sendToRoom('Test', {}) - await wait(50) + client.sendToRoom('Test', {}); + await wait(50); - expect(client.getMessageCount()).toBeGreaterThan(0) + expect(client.getMessageCount()).toBeGreaterThan(0); - client.clearMessages() + client.clearMessages(); - expect(client.getMessageCount()).toBe(0) - }) - }) -}) + expect(client.getMessageCount()).toBe(0); + }); + }); +}); diff --git a/packages/framework/server/src/testing/TestClient.ts b/packages/framework/server/src/testing/TestClient.ts index aac8b303..1a3e9d68 100644 --- a/packages/framework/server/src/testing/TestClient.ts +++ b/packages/framework/server/src/testing/TestClient.ts @@ -3,9 +3,12 @@ * @en Test client for server testing */ -import WebSocket from 'ws' -import { json } from '@esengine/rpc/codec' -import type { Codec } from '@esengine/rpc/codec' +import WebSocket from 'ws'; +import { json } from '@esengine/rpc/codec'; +import type { Codec } from '@esengine/rpc/codec'; +import { createLogger } from '../logger.js'; + +const logger = createLogger('TestClient'); // ============================================================================ // Types | 类型定义 @@ -65,8 +68,8 @@ const PacketType = { ApiRequest: 0, ApiResponse: 1, ApiError: 2, - Message: 3, -} as const + Message: 3 +} as const; // ============================================================================ // TestClient Class | 测试客户端类 @@ -106,26 +109,26 @@ interface PendingCall { * ``` */ export class TestClient { - private readonly _port: number - private readonly _codec: Codec - private readonly _timeout: number - private readonly _connectTimeout: number + private readonly _port: number; + private readonly _codec: Codec; + private readonly _timeout: number; + private readonly _connectTimeout: number; - private _ws: WebSocket | null = null - private _callIdCounter = 0 - private _connected = false - private _currentRoomId: string | null = null - private _currentPlayerId: string | null = null + private _ws: WebSocket | null = null; + private _callIdCounter = 0; + private _connected = false; + private _currentRoomId: string | null = null; + private _currentPlayerId: string | null = null; - private readonly _pendingCalls = new Map() - private readonly _msgHandlers = new Map void>>() - private readonly _receivedMessages: ReceivedMessage[] = [] + private readonly _pendingCalls = new Map(); + private readonly _msgHandlers = new Map void>>(); + private readonly _receivedMessages: ReceivedMessage[] = []; constructor(port: number, options: TestClientOptions = {}) { - this._port = port - this._codec = options.codec ?? json() - this._timeout = options.timeout ?? 5000 - this._connectTimeout = options.connectTimeout ?? 5000 + this._port = port; + this._codec = options.codec ?? json(); + this._timeout = options.timeout ?? 5000; + this._connectTimeout = options.connectTimeout ?? 5000; } // ======================================================================== @@ -137,7 +140,7 @@ export class TestClient { * @en Whether connected */ get isConnected(): boolean { - return this._connected + return this._connected; } /** @@ -145,7 +148,7 @@ export class TestClient { * @en Current room ID */ get roomId(): string | null { - return this._currentRoomId + return this._currentRoomId; } /** @@ -153,7 +156,7 @@ export class TestClient { * @en Current player ID */ get playerId(): string | null { - return this._currentPlayerId + return this._currentPlayerId; } /** @@ -161,7 +164,7 @@ export class TestClient { * @en All received messages */ get receivedMessages(): ReadonlyArray { - return this._receivedMessages + return this._receivedMessages; } // ======================================================================== @@ -174,36 +177,36 @@ export class TestClient { */ connect(): Promise { return new Promise((resolve, reject) => { - const url = `ws://localhost:${this._port}` - this._ws = new WebSocket(url) + const url = `ws://localhost:${this._port}`; + this._ws = new WebSocket(url); const timeout = setTimeout(() => { - this._ws?.close() - reject(new Error(`Connection timeout after ${this._connectTimeout}ms`)) - }, this._connectTimeout) + this._ws?.close(); + reject(new Error(`Connection timeout after ${this._connectTimeout}ms`)); + }, this._connectTimeout); this._ws.on('open', () => { - clearTimeout(timeout) - this._connected = true - resolve(this) - }) + clearTimeout(timeout); + this._connected = true; + resolve(this); + }); this._ws.on('close', () => { - this._connected = false - this._rejectAllPending('Connection closed') - }) + this._connected = false; + this._rejectAllPending('Connection closed'); + }); this._ws.on('error', (err) => { - clearTimeout(timeout) + clearTimeout(timeout); if (!this._connected) { - reject(err) + reject(err); } - }) + }); this._ws.on('message', (data: Buffer) => { - this._handleMessage(data) - }) - }) + this._handleMessage(data); + }); + }); } /** @@ -213,18 +216,18 @@ export class TestClient { async disconnect(): Promise { return new Promise((resolve) => { if (!this._ws || this._ws.readyState === WebSocket.CLOSED) { - resolve() - return + resolve(); + return; } this._ws.once('close', () => { - this._connected = false - this._ws = null - resolve() - }) + this._connected = false; + this._ws = null; + resolve(); + }); - this._ws.close() - }) + this._ws.close(); + }); } // ======================================================================== @@ -236,10 +239,10 @@ export class TestClient { * @en Join a room */ async joinRoom(roomType: string, options?: Record): Promise { - const result = await this.call('JoinRoom', { roomType, options }) - this._currentRoomId = result.roomId - this._currentPlayerId = result.playerId - return result + const result = await this.call('JoinRoom', { roomType, options }); + this._currentRoomId = result.roomId; + this._currentPlayerId = result.playerId; + return result; } /** @@ -247,10 +250,10 @@ export class TestClient { * @en Join a room by ID */ async joinRoomById(roomId: string): Promise { - const result = await this.call('JoinRoom', { roomId }) - this._currentRoomId = result.roomId - this._currentPlayerId = result.playerId - return result + const result = await this.call('JoinRoom', { roomId }); + this._currentRoomId = result.roomId; + this._currentPlayerId = result.playerId; + return result; } /** @@ -258,9 +261,9 @@ export class TestClient { * @en Leave room */ async leaveRoom(): Promise { - await this.call('LeaveRoom', {}) - this._currentRoomId = null - this._currentPlayerId = null + await this.call('LeaveRoom', {}); + this._currentRoomId = null; + this._currentPlayerId = null; } /** @@ -268,7 +271,7 @@ export class TestClient { * @en Send message to room */ sendToRoom(type: string, data: unknown): void { - this.send('RoomMessage', { type, data }) + this.send('RoomMessage', { type, data }); } // ======================================================================== @@ -282,26 +285,26 @@ export class TestClient { call(name: string, input: unknown): Promise { return new Promise((resolve, reject) => { if (!this._connected || !this._ws) { - reject(new Error('Not connected')) - return + reject(new Error('Not connected')); + return; } - const id = ++this._callIdCounter + const id = ++this._callIdCounter; const timer = setTimeout(() => { - this._pendingCalls.delete(id) - reject(new Error(`API call '${name}' timeout after ${this._timeout}ms`)) - }, this._timeout) + this._pendingCalls.delete(id); + reject(new Error(`API call '${name}' timeout after ${this._timeout}ms`)); + }, this._timeout); this._pendingCalls.set(id, { resolve: resolve as (v: unknown) => void, reject, - timer, - }) + timer + }); - const packet = [PacketType.ApiRequest, id, name, input] + const packet = [PacketType.ApiRequest, id, name, input]; // eslint-disable-next-line @typescript-eslint/no-explicit-any - this._ws.send(this._codec.encode(packet as any) as Buffer) - }) + this._ws.send(this._codec.encode(packet as any) as Buffer); + }); } /** @@ -309,10 +312,10 @@ export class TestClient { * @en Send message */ send(name: string, data: unknown): void { - if (!this._connected || !this._ws) return - const packet = [PacketType.Message, name, data] + if (!this._connected || !this._ws) return; + const packet = [PacketType.Message, name, data]; // eslint-disable-next-line @typescript-eslint/no-explicit-any - this._ws.send(this._codec.encode(packet as any) as Buffer) + this._ws.send(this._codec.encode(packet as any) as Buffer); } // ======================================================================== @@ -324,13 +327,13 @@ export class TestClient { * @en Listen for message */ on(name: string, handler: (data: unknown) => void): this { - let handlers = this._msgHandlers.get(name) + let handlers = this._msgHandlers.get(name); if (!handlers) { - handlers = new Set() - this._msgHandlers.set(name, handlers) + handlers = new Set(); + this._msgHandlers.set(name, handlers); } - handlers.add(handler) - return this + handlers.add(handler); + return this; } /** @@ -339,11 +342,11 @@ export class TestClient { */ off(name: string, handler?: (data: unknown) => void): this { if (handler) { - this._msgHandlers.get(name)?.delete(handler) + this._msgHandlers.get(name)?.delete(handler); } else { - this._msgHandlers.delete(name) + this._msgHandlers.delete(name); } - return this + return this; } /** @@ -352,21 +355,21 @@ export class TestClient { */ waitForMessage(type: string, timeout?: number): Promise { return new Promise((resolve, reject) => { - const timeoutMs = timeout ?? this._timeout + const timeoutMs = timeout ?? this._timeout; const timer = setTimeout(() => { - this.off(type, handler) - reject(new Error(`Timeout waiting for message '${type}' after ${timeoutMs}ms`)) - }, timeoutMs) + this.off(type, handler); + reject(new Error(`Timeout waiting for message '${type}' after ${timeoutMs}ms`)); + }, timeoutMs); const handler = (data: unknown) => { - clearTimeout(timer) - this.off(type, handler) - resolve(data as T) - } + clearTimeout(timer); + this.off(type, handler); + resolve(data as T); + }; - this.on(type, handler) - }) + this.on(type, handler); + }); } /** @@ -375,24 +378,24 @@ export class TestClient { */ waitForRoomMessage(type: string, timeout?: number): Promise { return new Promise((resolve, reject) => { - const timeoutMs = timeout ?? this._timeout + const timeoutMs = timeout ?? this._timeout; const timer = setTimeout(() => { - this.off('RoomMessage', handler) - reject(new Error(`Timeout waiting for room message '${type}' after ${timeoutMs}ms`)) - }, timeoutMs) + this.off('RoomMessage', handler); + reject(new Error(`Timeout waiting for room message '${type}' after ${timeoutMs}ms`)); + }, timeoutMs); const handler = (data: unknown) => { - const msg = data as { type: string; data: unknown } + const msg = data as { type: string; data: unknown }; if (msg.type === type) { - clearTimeout(timer) - this.off('RoomMessage', handler) - resolve(msg.data as T) + clearTimeout(timer); + this.off('RoomMessage', handler); + resolve(msg.data as T); } - } + }; - this.on('RoomMessage', handler) - }) + this.on('RoomMessage', handler); + }); } // ======================================================================== @@ -404,7 +407,7 @@ export class TestClient { * @en Whether received a specific message */ hasReceivedMessage(type: string): boolean { - return this._receivedMessages.some((m) => m.type === type) + return this._receivedMessages.some((m) => m.type === type); } /** @@ -414,7 +417,7 @@ export class TestClient { getMessagesOfType(type: string): T[] { return this._receivedMessages .filter((m) => m.type === type) - .map((m) => m.data as T) + .map((m) => m.data as T); } /** @@ -424,10 +427,10 @@ export class TestClient { getLastMessage(type: string): T | undefined { for (let i = this._receivedMessages.length - 1; i >= 0; i--) { if (this._receivedMessages[i].type === type) { - return this._receivedMessages[i].data as T + return this._receivedMessages[i].data as T; } } - return undefined + return undefined; } /** @@ -435,7 +438,7 @@ export class TestClient { * @en Clear message records */ clearMessages(): void { - this._receivedMessages.length = 0 + this._receivedMessages.length = 0; } /** @@ -444,9 +447,9 @@ export class TestClient { */ getMessageCount(type?: string): number { if (type) { - return this._receivedMessages.filter((m) => m.type === type).length + return this._receivedMessages.filter((m) => m.type === type).length; } - return this._receivedMessages.length + return this._receivedMessages.length; } // ======================================================================== @@ -455,40 +458,40 @@ export class TestClient { private _handleMessage(raw: Buffer): void { try { - const packet = this._codec.decode(raw) as unknown[] - const type = packet[0] as number + const packet = this._codec.decode(raw) as unknown[]; + const type = packet[0] as number; switch (type) { case PacketType.ApiResponse: - this._handleApiResponse([packet[0], packet[1], packet[2]] as [number, number, unknown]) - break + this._handleApiResponse([packet[0], packet[1], packet[2]] as [number, number, unknown]); + break; case PacketType.ApiError: - this._handleApiError([packet[0], packet[1], packet[2], packet[3]] as [number, number, string, string]) - break + this._handleApiError([packet[0], packet[1], packet[2], packet[3]] as [number, number, string, string]); + break; case PacketType.Message: - this._handleMsg([packet[0], packet[1], packet[2]] as [number, string, unknown]) - break + this._handleMsg([packet[0], packet[1], packet[2]] as [number, string, unknown]); + break; } } catch (err) { - console.error('[TestClient] Failed to handle message:', err) + logger.error('Failed to handle message:', err); } } private _handleApiResponse([, id, result]: [number, number, unknown]): void { - const pending = this._pendingCalls.get(id) + const pending = this._pendingCalls.get(id); if (pending) { - clearTimeout(pending.timer) - this._pendingCalls.delete(id) - pending.resolve(result) + clearTimeout(pending.timer); + this._pendingCalls.delete(id); + pending.resolve(result); } } private _handleApiError([, id, code, message]: [number, number, string, string]): void { - const pending = this._pendingCalls.get(id) + const pending = this._pendingCalls.get(id); if (pending) { - clearTimeout(pending.timer) - this._pendingCalls.delete(id) - pending.reject(new Error(`[${code}] ${message}`)) + clearTimeout(pending.timer); + this._pendingCalls.delete(id); + pending.reject(new Error(`[${code}] ${message}`)); } } @@ -497,17 +500,17 @@ export class TestClient { this._receivedMessages.push({ type: name, data, - timestamp: Date.now(), - }) + timestamp: Date.now() + }); // 触发处理器 - const handlers = this._msgHandlers.get(name) + const handlers = this._msgHandlers.get(name); if (handlers) { for (const handler of handlers) { try { - handler(data) + handler(data); } catch (err) { - console.error('[TestClient] Handler error:', err) + logger.error('Handler error:', err); } } } @@ -515,9 +518,9 @@ export class TestClient { private _rejectAllPending(reason: string): void { for (const [, pending] of this._pendingCalls) { - clearTimeout(pending.timer) - pending.reject(new Error(reason)) + clearTimeout(pending.timer); + pending.reject(new Error(reason)); } - this._pendingCalls.clear() + this._pendingCalls.clear(); } } diff --git a/packages/framework/server/src/testing/TestServer.ts b/packages/framework/server/src/testing/TestServer.ts index 9eab034d..aa3ebd30 100644 --- a/packages/framework/server/src/testing/TestServer.ts +++ b/packages/framework/server/src/testing/TestServer.ts @@ -3,9 +3,10 @@ * @en Test server utilities */ -import { createServer } from '../core/server.js' -import type { GameServer } from '../types/index.js' -import { TestClient, type TestClientOptions } from './TestClient.js' +import { createServer } from '../core/server.js'; +import type { GameServer } from '../types/index.js'; +import { TestClient, type TestClientOptions } from './TestClient.js'; +import { LoggerManager, LogLevel } from '@esengine/ecs-framework'; // ============================================================================ // Types | 类型定义 @@ -89,20 +90,20 @@ export interface TestEnvironment { * @en Get a random available port */ async function getRandomPort(): Promise { - const net = await import('node:net') + const net = await import('node:net'); return new Promise((resolve, reject) => { - const server = net.createServer() + const server = net.createServer(); server.listen(0, () => { - const address = server.address() + const address = server.address(); if (address && typeof address === 'object') { - const port = address.port - server.close(() => resolve(port)) + const port = address.port; + server.close(() => resolve(port)); } else { - server.close(() => reject(new Error('Failed to get port'))) + server.close(() => reject(new Error('Failed to get port'))); } - }) - server.on('error', reject) - }) + }); + server.on('error', reject); + }); } /** @@ -110,7 +111,7 @@ async function getRandomPort(): Promise { * @en Wait for specified milliseconds */ export function wait(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) + return new Promise((resolve) => setTimeout(resolve, ms)); } // ============================================================================ @@ -137,36 +138,38 @@ export function wait(ms: number): Promise { export async function createTestServer( options: TestServerOptions = {} ): Promise<{ server: GameServer; port: number; cleanup: () => Promise }> { - const port = options.port || (await getRandomPort()) - const silent = options.silent ?? true + const port = options.port || (await getRandomPort()); + const silent = options.silent ?? true; - // 临时禁用 console.log - const originalLog = console.log + // 临时设置日志级别为 None(禁用所有日志) + const loggerManager = LoggerManager.getInstance(); + let originalLevel: LogLevel | undefined; if (silent) { - console.log = () => {} + originalLevel = LogLevel.Info; + loggerManager.setGlobalLevel(LogLevel.None); } const server = await createServer({ port, tickRate: options.tickRate ?? 0, apiDir: '__non_existent_api__', - msgDir: '__non_existent_msg__', - }) + msgDir: '__non_existent_msg__' + }); - await server.start() + await server.start(); - // 恢复 console.log - if (silent) { - console.log = originalLog + // 恢复日志级别 + if (silent && originalLevel !== undefined) { + loggerManager.setGlobalLevel(originalLevel); } return { server, port, cleanup: async () => { - await server.stop() - }, - } + await server.stop(); + } + }; } /** @@ -211,8 +214,8 @@ export async function createTestServer( * ``` */ export async function createTestEnv(options: TestServerOptions = {}): Promise { - const { server, port, cleanup: serverCleanup } = await createTestServer(options) - const clients: TestClient[] = [] + const { server, port, cleanup: serverCleanup } = await createTestServer(options); + const clients: TestClient[] = []; return { server, @@ -220,30 +223,30 @@ export async function createTestEnv(options: TestServerOptions = {}): Promise { - const client = new TestClient(port, clientOptions) - await client.connect() - clients.push(client) - return client + const client = new TestClient(port, clientOptions); + await client.connect(); + clients.push(client); + return client; }, async createClients(count: number, clientOptions?: TestClientOptions): Promise { - const newClients: TestClient[] = [] + const newClients: TestClient[] = []; for (let i = 0; i < count; i++) { - const client = new TestClient(port, clientOptions) - await client.connect() - clients.push(client) - newClients.push(client) + const client = new TestClient(port, clientOptions); + await client.connect(); + clients.push(client); + newClients.push(client); } - return newClients + return newClients; }, async cleanup(): Promise { // 断开所有客户端 - await Promise.all(clients.map((c) => c.disconnect().catch(() => {}))) - clients.length = 0 + await Promise.all(clients.map((c) => c.disconnect().catch(() => {}))); + clients.length = 0; // 停止服务器 - await serverCleanup() - }, - } + await serverCleanup(); + } + }; } diff --git a/packages/framework/server/src/testing/index.ts b/packages/framework/server/src/testing/index.ts index 2979d4da..0c1ac9a2 100644 --- a/packages/framework/server/src/testing/index.ts +++ b/packages/framework/server/src/testing/index.ts @@ -27,11 +27,11 @@ * ``` */ -export { TestClient, type TestClientOptions } from './TestClient.js' +export { TestClient, type TestClientOptions } from './TestClient.js'; export { createTestServer, createTestEnv, type TestServerOptions, - type TestEnvironment, -} from './TestServer.js' -export { MockRoom } from './MockRoom.js' + type TestEnvironment +} from './TestServer.js'; +export { MockRoom } from './MockRoom.js'; diff --git a/packages/framework/server/src/types/index.ts b/packages/framework/server/src/types/index.ts index e1663067..a6c43ae5 100644 --- a/packages/framework/server/src/types/index.ts +++ b/packages/framework/server/src/types/index.ts @@ -3,8 +3,8 @@ * @en ESEngine Server type definitions */ -import type { Connection, ProtocolDef } from '@esengine/rpc' -import type { HttpRoutes, CorsOptions, HttpRequest, HttpResponse } from '../http/types.js' +import type { Connection, ProtocolDef } from '@esengine/rpc'; +import type { HttpRoutes, CorsOptions, HttpRequest, HttpResponse } from '../http/types.js'; // ============================================================================ // Server Config diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c328ce1a..df9e4b35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1730,13 +1730,13 @@ importers: packages/framework/server: dependencies: + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core/dist '@esengine/rpc': specifier: workspace:* version: link:../rpc devDependencies: - '@esengine/ecs-framework': - specifier: workspace:* - version: link:../core/dist '@types/jsonwebtoken': specifier: ^9.0.0 version: 9.0.10