Files
esengine/packages/framework/rpc/src/server/index.ts

398 lines
11 KiB
TypeScript
Raw Normal View History

feat(rpc,network): 新增 RPC 库并迁移网络模块 (#364) * feat(rpc,network): 新增 RPC 库并迁移网络模块 ## @esengine/rpc (新增) - 新增类型安全的 RPC 库,支持 WebSocket 通信 - 新增 RpcClient 类:connect/disconnect, call/send/on/off/once 方法 - 新增 RpcServer 类:Node.js WebSocket 服务端 - 新增编解码系统:支持 JSON 和 MessagePack - 新增 TextEncoder/TextDecoder polyfill,兼容微信小游戏平台 - 新增 WebSocketAdapter 接口,支持跨平台 WebSocket 抽象 ## @esengine/network (重构) - 重构 NetworkService:拆分为 RpcService 基类和 GameNetworkService - 新增 gameProtocol:类型安全的 API 和消息定义 - 新增类型安全便捷方法:sendInput(), onSync(), onSpawn(), onDespawn() - 更新 NetworkPlugin 使用新的服务架构 - 移除 TSRPC 依赖,改用 @esengine/rpc ## 文档 - 新增 RPC 模块文档(中英文) - 更新 Network 模块文档(中英文) - 更新侧边栏导航 * fix(network,cli): 修复 CI 构建和更新 CLI 适配器 ## 修复 - 在 tsconfig.build.json 添加 rpc 引用,修复类型声明生成 ## CLI 更新 - 更新 nodejs 适配器使用新的 @esengine/rpc - 生成的服务器代码使用 RpcServer 替代旧的 GameServer - 添加 ws 和 @types/ws 依赖 - 更新 README 模板中的客户端连接示例 * chore: 添加 CLI changeset * fix(ci): add @esengine/rpc to build and check scripts - Add rpc package to CI build step (must build before network) - Add rpc to type-check:framework, lint:framework, test:ci:framework * fix(rpc,network): fix tsconfig for declaration generation - Remove composite mode from rpc (not needed, causes CI issues) - Remove rpc from network project references (resolves via node_modules) - Remove unused references from network tsconfig.build.json
2025-12-28 10:54:51 +08:00
/**
* @zh RPC
* @en RPC Server Module
*/
import { WebSocketServer, WebSocket } from 'ws'
import type { Server as HttpServer } from 'node:http'
feat(rpc,network): 新增 RPC 库并迁移网络模块 (#364) * feat(rpc,network): 新增 RPC 库并迁移网络模块 ## @esengine/rpc (新增) - 新增类型安全的 RPC 库,支持 WebSocket 通信 - 新增 RpcClient 类:connect/disconnect, call/send/on/off/once 方法 - 新增 RpcServer 类:Node.js WebSocket 服务端 - 新增编解码系统:支持 JSON 和 MessagePack - 新增 TextEncoder/TextDecoder polyfill,兼容微信小游戏平台 - 新增 WebSocketAdapter 接口,支持跨平台 WebSocket 抽象 ## @esengine/network (重构) - 重构 NetworkService:拆分为 RpcService 基类和 GameNetworkService - 新增 gameProtocol:类型安全的 API 和消息定义 - 新增类型安全便捷方法:sendInput(), onSync(), onSpawn(), onDespawn() - 更新 NetworkPlugin 使用新的服务架构 - 移除 TSRPC 依赖,改用 @esengine/rpc ## 文档 - 新增 RPC 模块文档(中英文) - 更新 Network 模块文档(中英文) - 更新侧边栏导航 * fix(network,cli): 修复 CI 构建和更新 CLI 适配器 ## 修复 - 在 tsconfig.build.json 添加 rpc 引用,修复类型声明生成 ## CLI 更新 - 更新 nodejs 适配器使用新的 @esengine/rpc - 生成的服务器代码使用 RpcServer 替代旧的 GameServer - 添加 ws 和 @types/ws 依赖 - 更新 README 模板中的客户端连接示例 * chore: 添加 CLI changeset * fix(ci): add @esengine/rpc to build and check scripts - Add rpc package to CI build step (must build before network) - Add rpc to type-check:framework, lint:framework, test:ci:framework * fix(rpc,network): fix tsconfig for declaration generation - Remove composite mode from rpc (not needed, causes CI issues) - Remove rpc from network project references (resolves via node_modules) - Remove unused references from network tsconfig.build.json
2025-12-28 10:54:51 +08:00
import type {
ProtocolDef,
ApiNames,
MsgNames,
ApiInput,
ApiOutput,
MsgData,
Packet,
PacketType,
Connection,
} from '../types'
import { RpcError, ErrorCode } from '../types'
import { json } from '../codec/json'
import type { Codec } from '../codec/types'
import { ServerConnection } from './connection'
// ============ Types ============
/**
* @zh API
* @en API handler function
*/
type ApiHandler<TInput, TOutput, TConnData> = (
input: TInput,
conn: Connection<TConnData>
) => TOutput | Promise<TOutput>
/**
* @zh
* @en Message handler function
*/
type MsgHandler<TData, TConnData> = (
data: TData,
conn: Connection<TConnData>
) => void | Promise<void>
/**
* @zh API
* @en API handlers map
*/
type ApiHandlers<P extends ProtocolDef, TConnData> = {
[K in ApiNames<P>]: ApiHandler<
ApiInput<P['api'][K]>,
ApiOutput<P['api'][K]>,
TConnData
>
}
/**
* @zh
* @en Message handlers map
*/
type MsgHandlers<P extends ProtocolDef, TConnData> = {
[K in MsgNames<P>]?: MsgHandler<MsgData<P['msg'][K]>, TConnData>
}
/**
* @zh
* @en Server options
*/
export interface ServeOptions<P extends ProtocolDef, TConnData = unknown> {
/**
* @zh server
* @en Listen port (mutually exclusive with server)
feat(rpc,network): 新增 RPC 库并迁移网络模块 (#364) * feat(rpc,network): 新增 RPC 库并迁移网络模块 ## @esengine/rpc (新增) - 新增类型安全的 RPC 库,支持 WebSocket 通信 - 新增 RpcClient 类:connect/disconnect, call/send/on/off/once 方法 - 新增 RpcServer 类:Node.js WebSocket 服务端 - 新增编解码系统:支持 JSON 和 MessagePack - 新增 TextEncoder/TextDecoder polyfill,兼容微信小游戏平台 - 新增 WebSocketAdapter 接口,支持跨平台 WebSocket 抽象 ## @esengine/network (重构) - 重构 NetworkService:拆分为 RpcService 基类和 GameNetworkService - 新增 gameProtocol:类型安全的 API 和消息定义 - 新增类型安全便捷方法:sendInput(), onSync(), onSpawn(), onDespawn() - 更新 NetworkPlugin 使用新的服务架构 - 移除 TSRPC 依赖,改用 @esengine/rpc ## 文档 - 新增 RPC 模块文档(中英文) - 更新 Network 模块文档(中英文) - 更新侧边栏导航 * fix(network,cli): 修复 CI 构建和更新 CLI 适配器 ## 修复 - 在 tsconfig.build.json 添加 rpc 引用,修复类型声明生成 ## CLI 更新 - 更新 nodejs 适配器使用新的 @esengine/rpc - 生成的服务器代码使用 RpcServer 替代旧的 GameServer - 添加 ws 和 @types/ws 依赖 - 更新 README 模板中的客户端连接示例 * chore: 添加 CLI changeset * fix(ci): add @esengine/rpc to build and check scripts - Add rpc package to CI build step (must build before network) - Add rpc to type-check:framework, lint:framework, test:ci:framework * fix(rpc,network): fix tsconfig for declaration generation - Remove composite mode from rpc (not needed, causes CI issues) - Remove rpc from network project references (resolves via node_modules) - Remove unused references from network tsconfig.build.json
2025-12-28 10:54:51 +08:00
*/
port?: number
/**
* @zh HTTP port
* @en Existing HTTP server (mutually exclusive with port)
*
* @zh 使 HTTP WebSocket
* @en Use this option to support both HTTP and WebSocket on the same port
*/
server?: HttpServer
feat(rpc,network): 新增 RPC 库并迁移网络模块 (#364) * feat(rpc,network): 新增 RPC 库并迁移网络模块 ## @esengine/rpc (新增) - 新增类型安全的 RPC 库,支持 WebSocket 通信 - 新增 RpcClient 类:connect/disconnect, call/send/on/off/once 方法 - 新增 RpcServer 类:Node.js WebSocket 服务端 - 新增编解码系统:支持 JSON 和 MessagePack - 新增 TextEncoder/TextDecoder polyfill,兼容微信小游戏平台 - 新增 WebSocketAdapter 接口,支持跨平台 WebSocket 抽象 ## @esengine/network (重构) - 重构 NetworkService:拆分为 RpcService 基类和 GameNetworkService - 新增 gameProtocol:类型安全的 API 和消息定义 - 新增类型安全便捷方法:sendInput(), onSync(), onSpawn(), onDespawn() - 更新 NetworkPlugin 使用新的服务架构 - 移除 TSRPC 依赖,改用 @esengine/rpc ## 文档 - 新增 RPC 模块文档(中英文) - 更新 Network 模块文档(中英文) - 更新侧边栏导航 * fix(network,cli): 修复 CI 构建和更新 CLI 适配器 ## 修复 - 在 tsconfig.build.json 添加 rpc 引用,修复类型声明生成 ## CLI 更新 - 更新 nodejs 适配器使用新的 @esengine/rpc - 生成的服务器代码使用 RpcServer 替代旧的 GameServer - 添加 ws 和 @types/ws 依赖 - 更新 README 模板中的客户端连接示例 * chore: 添加 CLI changeset * fix(ci): add @esengine/rpc to build and check scripts - Add rpc package to CI build step (must build before network) - Add rpc to type-check:framework, lint:framework, test:ci:framework * fix(rpc,network): fix tsconfig for declaration generation - Remove composite mode from rpc (not needed, causes CI issues) - Remove rpc from network project references (resolves via node_modules) - Remove unused references from network tsconfig.build.json
2025-12-28 10:54:51 +08:00
/**
* @zh API
* @en API handlers
*/
api: ApiHandlers<P, TConnData>
/**
* @zh
* @en Message handlers
*/
msg?: MsgHandlers<P, TConnData>
/**
* @zh
* @en Codec
* @defaultValue json()
*/
codec?: Codec
/**
* @zh
* @en Connection initial data factory
*/
createConnData?: () => TConnData
/**
* @zh
* @en Connection established callback
*/
onConnect?: (conn: Connection<TConnData>) => void | Promise<void>
/**
* @zh
* @en Connection closed callback
*/
onDisconnect?: (conn: Connection<TConnData>, reason?: string) => void | Promise<void>
/**
* @zh
* @en Error callback
*/
onError?: (error: Error, conn?: Connection<TConnData>) => void
/**
* @zh
* @en Server started callback
*/
onStart?: (port: number) => void
}
/**
* @zh RPC
* @en RPC Server instance
*/
export interface RpcServer<P extends ProtocolDef, TConnData = unknown> {
/**
* @zh
* @en Start server
*/
start(): Promise<void>
/**
* @zh
* @en Stop server
*/
stop(): Promise<void>
/**
* @zh
* @en Get all connections
*/
readonly connections: ReadonlyArray<Connection<TConnData>>
/**
* @zh
* @en Send message to a single connection
*/
send<K extends MsgNames<P>>(
conn: Connection<TConnData>,
name: K,
data: MsgData<P['msg'][K]>
): void
/**
* @zh 广
* @en Broadcast message to all connections
*/
broadcast<K extends MsgNames<P>>(
name: K,
data: MsgData<P['msg'][K]>,
options?: { exclude?: Connection<TConnData> | Connection<TConnData>[] }
): void
}
// ============ Implementation ============
const PT = {
ApiRequest: 0,
ApiResponse: 1,
ApiError: 2,
Message: 3,
Heartbeat: 9,
} as const
/**
* @zh RPC
* @en Create RPC server
*
* @example
* ```typescript
* const server = serve(protocol, {
* port: 3000,
* api: {
* join: async (input, conn) => {
* return { id: conn.id }
* },
* },
* })
* await server.start()
* ```
*/
export function serve<P extends ProtocolDef, TConnData = unknown>(
_protocol: P,
options: ServeOptions<P, TConnData>
): RpcServer<P, TConnData> {
const codec = options.codec ?? json()
const connections: ServerConnection<TConnData>[] = []
let wss: WebSocketServer | null = null
let connIdCounter = 0
const getClientIp = (ws: WebSocket, req: any): string => {
return req?.headers?.['x-forwarded-for']?.split(',')[0]?.trim()
|| req?.socket?.remoteAddress
|| 'unknown'
}
const handleMessage = async (
conn: ServerConnection<TConnData>,
data: string | Buffer
): Promise<void> => {
try {
const packet = codec.decode(
typeof data === 'string' ? data : new Uint8Array(data)
)
const type = packet[0]
if (type === PT.ApiRequest) {
const [, id, path, input] = packet as [number, number, string, unknown]
await handleApiRequest(conn, id, path, input)
} else if (type === PT.Message) {
const [, path, msgData] = packet as [number, string, unknown]
await handleMsg(conn, path, msgData)
} else if (type === PT.Heartbeat) {
conn.send(codec.encode([PT.Heartbeat]))
}
} catch (err) {
options.onError?.(err as Error, conn)
}
}
const handleApiRequest = async (
conn: ServerConnection<TConnData>,
id: number,
path: string,
input: unknown
): Promise<void> => {
const handler = (options.api as any)[path]
if (!handler) {
const errPacket: Packet = [PT.ApiError, id, ErrorCode.NOT_FOUND, `API not found: ${path}`]
conn.send(codec.encode(errPacket))
return
}
try {
const result = await handler(input, conn)
const resPacket: Packet = [PT.ApiResponse, id, result]
conn.send(codec.encode(resPacket))
} catch (err) {
if (err instanceof RpcError) {
const errPacket: Packet = [PT.ApiError, id, err.code, err.message]
conn.send(codec.encode(errPacket))
} else {
const errPacket: Packet = [PT.ApiError, id, ErrorCode.INTERNAL_ERROR, 'Internal server error']
conn.send(codec.encode(errPacket))
options.onError?.(err as Error, conn)
}
}
}
const handleMsg = async (
conn: ServerConnection<TConnData>,
path: string,
data: unknown
): Promise<void> => {
const handler = options.msg?.[path as MsgNames<P>]
if (handler) {
await (handler as any)(data, conn)
}
}
const server: RpcServer<P, TConnData> = {
get connections() {
return connections as ReadonlyArray<Connection<TConnData>>
},
async start() {
return new Promise((resolve) => {
// 根据配置创建 WebSocketServer
if (options.server) {
// 附加到已有的 HTTP 服务器
wss = new WebSocketServer({ server: options.server })
} else if (options.port) {
// 独立创建
wss = new WebSocketServer({ port: options.port })
} else {
throw new Error('Either port or server must be provided')
}
feat(rpc,network): 新增 RPC 库并迁移网络模块 (#364) * feat(rpc,network): 新增 RPC 库并迁移网络模块 ## @esengine/rpc (新增) - 新增类型安全的 RPC 库,支持 WebSocket 通信 - 新增 RpcClient 类:connect/disconnect, call/send/on/off/once 方法 - 新增 RpcServer 类:Node.js WebSocket 服务端 - 新增编解码系统:支持 JSON 和 MessagePack - 新增 TextEncoder/TextDecoder polyfill,兼容微信小游戏平台 - 新增 WebSocketAdapter 接口,支持跨平台 WebSocket 抽象 ## @esengine/network (重构) - 重构 NetworkService:拆分为 RpcService 基类和 GameNetworkService - 新增 gameProtocol:类型安全的 API 和消息定义 - 新增类型安全便捷方法:sendInput(), onSync(), onSpawn(), onDespawn() - 更新 NetworkPlugin 使用新的服务架构 - 移除 TSRPC 依赖,改用 @esengine/rpc ## 文档 - 新增 RPC 模块文档(中英文) - 更新 Network 模块文档(中英文) - 更新侧边栏导航 * fix(network,cli): 修复 CI 构建和更新 CLI 适配器 ## 修复 - 在 tsconfig.build.json 添加 rpc 引用,修复类型声明生成 ## CLI 更新 - 更新 nodejs 适配器使用新的 @esengine/rpc - 生成的服务器代码使用 RpcServer 替代旧的 GameServer - 添加 ws 和 @types/ws 依赖 - 更新 README 模板中的客户端连接示例 * chore: 添加 CLI changeset * fix(ci): add @esengine/rpc to build and check scripts - Add rpc package to CI build step (must build before network) - Add rpc to type-check:framework, lint:framework, test:ci:framework * fix(rpc,network): fix tsconfig for declaration generation - Remove composite mode from rpc (not needed, causes CI issues) - Remove rpc from network project references (resolves via node_modules) - Remove unused references from network tsconfig.build.json
2025-12-28 10:54:51 +08:00
wss.on('connection', async (ws, req) => {
const id = String(++connIdCounter)
const ip = getClientIp(ws, req)
const initialData = options.createConnData?.() ?? ({} as TConnData)
const conn = new ServerConnection<TConnData>({
id,
ip,
socket: ws,
initialData,
onClose: () => {
const idx = connections.indexOf(conn)
if (idx !== -1) connections.splice(idx, 1)
},
})
connections.push(conn)
ws.on('message', (data) => {
handleMessage(conn, data as string | Buffer)
})
ws.on('close', async (code, reason) => {
conn._markClosed()
const idx = connections.indexOf(conn)
if (idx !== -1) connections.splice(idx, 1)
await options.onDisconnect?.(conn, reason?.toString())
})
ws.on('error', (err) => {
options.onError?.(err, conn)
})
await options.onConnect?.(conn)
})
// 如果使用已有的 HTTP 服务器WebSocketServer 不会触发 listening 事件
if (options.server) {
options.onStart?.(0) // 端口由 HTTP 服务器管理
feat(rpc,network): 新增 RPC 库并迁移网络模块 (#364) * feat(rpc,network): 新增 RPC 库并迁移网络模块 ## @esengine/rpc (新增) - 新增类型安全的 RPC 库,支持 WebSocket 通信 - 新增 RpcClient 类:connect/disconnect, call/send/on/off/once 方法 - 新增 RpcServer 类:Node.js WebSocket 服务端 - 新增编解码系统:支持 JSON 和 MessagePack - 新增 TextEncoder/TextDecoder polyfill,兼容微信小游戏平台 - 新增 WebSocketAdapter 接口,支持跨平台 WebSocket 抽象 ## @esengine/network (重构) - 重构 NetworkService:拆分为 RpcService 基类和 GameNetworkService - 新增 gameProtocol:类型安全的 API 和消息定义 - 新增类型安全便捷方法:sendInput(), onSync(), onSpawn(), onDespawn() - 更新 NetworkPlugin 使用新的服务架构 - 移除 TSRPC 依赖,改用 @esengine/rpc ## 文档 - 新增 RPC 模块文档(中英文) - 更新 Network 模块文档(中英文) - 更新侧边栏导航 * fix(network,cli): 修复 CI 构建和更新 CLI 适配器 ## 修复 - 在 tsconfig.build.json 添加 rpc 引用,修复类型声明生成 ## CLI 更新 - 更新 nodejs 适配器使用新的 @esengine/rpc - 生成的服务器代码使用 RpcServer 替代旧的 GameServer - 添加 ws 和 @types/ws 依赖 - 更新 README 模板中的客户端连接示例 * chore: 添加 CLI changeset * fix(ci): add @esengine/rpc to build and check scripts - Add rpc package to CI build step (must build before network) - Add rpc to type-check:framework, lint:framework, test:ci:framework * fix(rpc,network): fix tsconfig for declaration generation - Remove composite mode from rpc (not needed, causes CI issues) - Remove rpc from network project references (resolves via node_modules) - Remove unused references from network tsconfig.build.json
2025-12-28 10:54:51 +08:00
resolve()
} else {
wss.on('listening', () => {
options.onStart?.(options.port!)
resolve()
})
}
feat(rpc,network): 新增 RPC 库并迁移网络模块 (#364) * feat(rpc,network): 新增 RPC 库并迁移网络模块 ## @esengine/rpc (新增) - 新增类型安全的 RPC 库,支持 WebSocket 通信 - 新增 RpcClient 类:connect/disconnect, call/send/on/off/once 方法 - 新增 RpcServer 类:Node.js WebSocket 服务端 - 新增编解码系统:支持 JSON 和 MessagePack - 新增 TextEncoder/TextDecoder polyfill,兼容微信小游戏平台 - 新增 WebSocketAdapter 接口,支持跨平台 WebSocket 抽象 ## @esengine/network (重构) - 重构 NetworkService:拆分为 RpcService 基类和 GameNetworkService - 新增 gameProtocol:类型安全的 API 和消息定义 - 新增类型安全便捷方法:sendInput(), onSync(), onSpawn(), onDespawn() - 更新 NetworkPlugin 使用新的服务架构 - 移除 TSRPC 依赖,改用 @esengine/rpc ## 文档 - 新增 RPC 模块文档(中英文) - 更新 Network 模块文档(中英文) - 更新侧边栏导航 * fix(network,cli): 修复 CI 构建和更新 CLI 适配器 ## 修复 - 在 tsconfig.build.json 添加 rpc 引用,修复类型声明生成 ## CLI 更新 - 更新 nodejs 适配器使用新的 @esengine/rpc - 生成的服务器代码使用 RpcServer 替代旧的 GameServer - 添加 ws 和 @types/ws 依赖 - 更新 README 模板中的客户端连接示例 * chore: 添加 CLI changeset * fix(ci): add @esengine/rpc to build and check scripts - Add rpc package to CI build step (must build before network) - Add rpc to type-check:framework, lint:framework, test:ci:framework * fix(rpc,network): fix tsconfig for declaration generation - Remove composite mode from rpc (not needed, causes CI issues) - Remove rpc from network project references (resolves via node_modules) - Remove unused references from network tsconfig.build.json
2025-12-28 10:54:51 +08:00
})
},
async stop() {
return new Promise((resolve, reject) => {
if (!wss) {
resolve()
return
}
for (const conn of connections) {
conn.close('Server shutting down')
}
wss.close((err) => {
if (err) reject(err)
else resolve()
})
})
},
send(conn, name, data) {
const packet: Packet = [PT.Message, name as string, data]
;(conn as ServerConnection<TConnData>).send(codec.encode(packet))
},
broadcast(name, data, opts) {
const packet: Packet = [PT.Message, name as string, data]
const encoded = codec.encode(packet)
const excludeSet = new Set(
Array.isArray(opts?.exclude)
? opts.exclude
: opts?.exclude
? [opts.exclude]
: []
)
for (const conn of connections) {
if (!excludeSet.has(conn)) {
conn.send(encoded)
}
}
},
}
return server
}