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
This commit is contained in:
20
packages/framework/rpc/module.json
Normal file
20
packages/framework/rpc/module.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "rpc",
|
||||
"name": "@esengine/rpc",
|
||||
"globalKey": "rpc",
|
||||
"displayName": "RPC",
|
||||
"description": "类型安全的 RPC 通信库 | Type-safe RPC communication library",
|
||||
"version": "1.0.0",
|
||||
"category": "Network",
|
||||
"icon": "Network",
|
||||
"tags": ["rpc", "websocket", "network", "multiplayer"],
|
||||
"isCore": false,
|
||||
"defaultEnabled": true,
|
||||
"isEngineModule": true,
|
||||
"canContainContent": false,
|
||||
"platforms": ["web", "desktop", "node"],
|
||||
"dependencies": [],
|
||||
"exports": {},
|
||||
"requiresWasm": false,
|
||||
"outputPath": "dist/index.js"
|
||||
}
|
||||
60
packages/framework/rpc/package.json
Normal file
60
packages/framework/rpc/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@esengine/rpc",
|
||||
"version": "1.0.0",
|
||||
"description": "Elegant type-safe RPC library for ESEngine",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./server": {
|
||||
"import": "./dist/server/index.js",
|
||||
"types": "./dist/server/index.d.ts"
|
||||
},
|
||||
"./client": {
|
||||
"import": "./dist/client/index.js",
|
||||
"types": "./dist/client/index.d.ts"
|
||||
},
|
||||
"./codec": {
|
||||
"import": "./dist/codec/index.js",
|
||||
"types": "./dist/codec/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"module.json"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup && tsc --emitDeclarationOnly",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"dependencies": {},
|
||||
"optionalDependencies": {
|
||||
"msgpackr": "^1.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ws": "^8.5.13",
|
||||
"msgpackr": "^1.11.0",
|
||||
"ws": "^8.18.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.0",
|
||||
"ws": ">=8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
461
packages/framework/rpc/src/client/index.ts
Normal file
461
packages/framework/rpc/src/client/index.ts
Normal file
@@ -0,0 +1,461 @@
|
||||
/**
|
||||
* @zh RPC 客户端模块
|
||||
* @en RPC Client Module
|
||||
*/
|
||||
|
||||
import type {
|
||||
ProtocolDef,
|
||||
ApiNames,
|
||||
MsgNames,
|
||||
ApiInput,
|
||||
ApiOutput,
|
||||
MsgData,
|
||||
Packet,
|
||||
ConnectionStatus,
|
||||
} from '../types'
|
||||
import { RpcError, ErrorCode } from '../types'
|
||||
import { json } from '../codec/json'
|
||||
import type { Codec } from '../codec/types'
|
||||
|
||||
// ============================================================================
|
||||
// Re-exports | 类型重导出
|
||||
// ============================================================================
|
||||
|
||||
export type {
|
||||
ProtocolDef,
|
||||
ApiNames,
|
||||
MsgNames,
|
||||
ApiInput,
|
||||
ApiOutput,
|
||||
MsgData,
|
||||
ConnectionStatus,
|
||||
Codec,
|
||||
}
|
||||
export { RpcError, ErrorCode }
|
||||
|
||||
// ============================================================================
|
||||
// Types | 类型定义
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh WebSocket 适配器接口
|
||||
* @en WebSocket adapter interface
|
||||
*
|
||||
* @zh 用于适配不同平台的 WebSocket 实现(浏览器、微信小游戏等)
|
||||
* @en Used to adapt different platform WebSocket implementations (browser, WeChat Mini Games, etc.)
|
||||
*/
|
||||
export interface WebSocketAdapter {
|
||||
readonly readyState: number
|
||||
send(data: string | ArrayBuffer): void
|
||||
close(code?: number, reason?: string): void
|
||||
onopen: ((ev: Event) => void) | null
|
||||
onclose: ((ev: { code: number; reason: string }) => void) | null
|
||||
onerror: ((ev: Event) => void) | null
|
||||
onmessage: ((ev: { data: string | ArrayBuffer }) => void) | null
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh WebSocket 工厂函数类型
|
||||
* @en WebSocket factory function type
|
||||
*/
|
||||
export type WebSocketFactory = (url: string) => WebSocketAdapter
|
||||
|
||||
/**
|
||||
* @zh 客户端配置
|
||||
* @en Client options
|
||||
*/
|
||||
export interface RpcClientOptions {
|
||||
/**
|
||||
* @zh 编解码器
|
||||
* @en Codec
|
||||
* @defaultValue json()
|
||||
*/
|
||||
codec?: Codec
|
||||
|
||||
/**
|
||||
* @zh API 调用超时(毫秒)
|
||||
* @en API call timeout in milliseconds
|
||||
* @defaultValue 30000
|
||||
*/
|
||||
timeout?: number
|
||||
|
||||
/**
|
||||
* @zh 自动重连
|
||||
* @en Auto reconnect
|
||||
* @defaultValue true
|
||||
*/
|
||||
autoReconnect?: boolean
|
||||
|
||||
/**
|
||||
* @zh 重连间隔(毫秒)
|
||||
* @en Reconnect interval in milliseconds
|
||||
* @defaultValue 3000
|
||||
*/
|
||||
reconnectInterval?: number
|
||||
|
||||
/**
|
||||
* @zh WebSocket 工厂函数
|
||||
* @en WebSocket factory function
|
||||
*
|
||||
* @zh 用于自定义 WebSocket 实现,如微信小游戏
|
||||
* @en Used for custom WebSocket implementation, e.g., WeChat Mini Games
|
||||
*/
|
||||
webSocketFactory?: WebSocketFactory
|
||||
|
||||
/**
|
||||
* @zh 连接成功回调
|
||||
* @en Connection established callback
|
||||
*/
|
||||
onConnect?: () => void
|
||||
|
||||
/**
|
||||
* @zh 连接断开回调
|
||||
* @en Connection closed callback
|
||||
*/
|
||||
onDisconnect?: (reason?: string) => void
|
||||
|
||||
/**
|
||||
* @zh 错误回调
|
||||
* @en Error callback
|
||||
*/
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
/** @deprecated Use RpcClientOptions instead */
|
||||
export type ConnectOptions = RpcClientOptions
|
||||
|
||||
// ============================================================================
|
||||
// Constants | 常量
|
||||
// ============================================================================
|
||||
|
||||
const PacketType = {
|
||||
ApiRequest: 0,
|
||||
ApiResponse: 1,
|
||||
ApiError: 2,
|
||||
Message: 3,
|
||||
Heartbeat: 9,
|
||||
} as const
|
||||
|
||||
const defaultWebSocketFactory: WebSocketFactory = (url) =>
|
||||
new WebSocket(url) as unknown as WebSocketAdapter
|
||||
|
||||
// ============================================================================
|
||||
// RpcClient Class | RPC 客户端类
|
||||
// ============================================================================
|
||||
|
||||
interface PendingCall {
|
||||
resolve: (value: unknown) => void
|
||||
reject: (error: Error) => void
|
||||
timer: ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh RPC 客户端
|
||||
* @en RPC Client
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const client = new RpcClient(protocol, 'ws://localhost:3000')
|
||||
* await client.connect()
|
||||
*
|
||||
* const result = await client.call('join', { name: 'Alice' })
|
||||
*
|
||||
* client.on('chat', (msg) => console.log(msg.text))
|
||||
* ```
|
||||
*/
|
||||
export class RpcClient<P extends ProtocolDef> {
|
||||
private readonly _url: string
|
||||
private readonly _codec: Codec
|
||||
private readonly _timeout: number
|
||||
private readonly _reconnectInterval: number
|
||||
private readonly _wsFactory: WebSocketFactory
|
||||
private readonly _options: RpcClientOptions
|
||||
|
||||
private _ws: WebSocketAdapter | null = null
|
||||
private _status: ConnectionStatus = 'closed'
|
||||
private _callIdCounter = 0
|
||||
private _shouldReconnect: boolean
|
||||
private _reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
private readonly _pendingCalls = new Map<number, PendingCall>()
|
||||
private readonly _msgHandlers = new Map<string, Set<(data: unknown) => void>>()
|
||||
|
||||
constructor(
|
||||
_protocol: P,
|
||||
url: string,
|
||||
options: RpcClientOptions = {}
|
||||
) {
|
||||
this._url = url
|
||||
this._options = options
|
||||
this._codec = options.codec ?? json()
|
||||
this._timeout = options.timeout ?? 30000
|
||||
this._shouldReconnect = options.autoReconnect ?? true
|
||||
this._reconnectInterval = options.reconnectInterval ?? 3000
|
||||
this._wsFactory = options.webSocketFactory ?? defaultWebSocketFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 连接状态
|
||||
* @en Connection status
|
||||
*/
|
||||
get status(): ConnectionStatus {
|
||||
return this._status
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 是否已连接
|
||||
* @en Whether connected
|
||||
*/
|
||||
get isConnected(): boolean {
|
||||
return this._status === 'open'
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 连接到服务器
|
||||
* @en Connect to server
|
||||
*/
|
||||
connect(): Promise<this> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this._status === 'open' || this._status === 'connecting') {
|
||||
resolve(this)
|
||||
return
|
||||
}
|
||||
|
||||
this._status = 'connecting'
|
||||
this._ws = this._wsFactory(this._url)
|
||||
|
||||
this._ws.onopen = () => {
|
||||
this._status = 'open'
|
||||
this._options.onConnect?.()
|
||||
resolve(this)
|
||||
}
|
||||
|
||||
this._ws.onclose = (e) => {
|
||||
this._status = 'closed'
|
||||
this._rejectAllPending()
|
||||
this._options.onDisconnect?.(e.reason)
|
||||
this._scheduleReconnect()
|
||||
}
|
||||
|
||||
this._ws.onerror = () => {
|
||||
const err = new Error('WebSocket error')
|
||||
this._options.onError?.(err)
|
||||
if (this._status === 'connecting') {
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
this._ws.onmessage = (e) => {
|
||||
this._handleMessage(e.data as string | ArrayBuffer)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 断开连接
|
||||
* @en Disconnect
|
||||
*/
|
||||
disconnect(): void {
|
||||
this._shouldReconnect = false
|
||||
this._clearReconnectTimer()
|
||||
if (this._ws) {
|
||||
this._status = 'closing'
|
||||
this._ws.close()
|
||||
this._ws = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 调用 API
|
||||
* @en Call API
|
||||
*/
|
||||
call<K extends ApiNames<P>>(
|
||||
name: K,
|
||||
input: ApiInput<P['api'][K]>
|
||||
): Promise<ApiOutput<P['api'][K]>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this._status !== 'open') {
|
||||
reject(new RpcError(ErrorCode.CONNECTION_CLOSED, 'Not connected'))
|
||||
return
|
||||
}
|
||||
|
||||
const id = ++this._callIdCounter
|
||||
const timer = setTimeout(() => {
|
||||
this._pendingCalls.delete(id)
|
||||
reject(new RpcError(ErrorCode.TIMEOUT, 'Request timeout'))
|
||||
}, this._timeout)
|
||||
|
||||
this._pendingCalls.set(id, {
|
||||
resolve: resolve as (v: unknown) => void,
|
||||
reject,
|
||||
timer,
|
||||
})
|
||||
|
||||
const packet: Packet = [PacketType.ApiRequest, id, name as string, input]
|
||||
this._ws!.send(this._codec.encode(packet) as string | ArrayBuffer)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 发送消息
|
||||
* @en Send message
|
||||
*/
|
||||
send<K extends MsgNames<P>>(name: K, data: MsgData<P['msg'][K]>): void {
|
||||
if (this._status !== 'open') return
|
||||
const packet: Packet = [PacketType.Message, name as string, data]
|
||||
this._ws!.send(this._codec.encode(packet) as string | ArrayBuffer)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 监听消息
|
||||
* @en Listen to message
|
||||
*/
|
||||
on<K extends MsgNames<P>>(
|
||||
name: K,
|
||||
handler: (data: MsgData<P['msg'][K]>) => void
|
||||
): this {
|
||||
const key = name as string
|
||||
let handlers = this._msgHandlers.get(key)
|
||||
if (!handlers) {
|
||||
handlers = new Set()
|
||||
this._msgHandlers.set(key, handlers)
|
||||
}
|
||||
handlers.add(handler as (data: unknown) => void)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 取消监听消息
|
||||
* @en Remove message listener
|
||||
*/
|
||||
off<K extends MsgNames<P>>(
|
||||
name: K,
|
||||
handler?: (data: MsgData<P['msg'][K]>) => void
|
||||
): this {
|
||||
const key = name as string
|
||||
if (handler) {
|
||||
this._msgHandlers.get(key)?.delete(handler as (data: unknown) => void)
|
||||
} else {
|
||||
this._msgHandlers.delete(key)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 监听消息(只触发一次)
|
||||
* @en Listen to message (once)
|
||||
*/
|
||||
once<K extends MsgNames<P>>(
|
||||
name: K,
|
||||
handler: (data: MsgData<P['msg'][K]>) => void
|
||||
): this {
|
||||
const wrapper = (data: MsgData<P['msg'][K]>) => {
|
||||
this.off(name, wrapper)
|
||||
handler(data)
|
||||
}
|
||||
return this.on(name, wrapper)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Private Methods | 私有方法
|
||||
// ========================================================================
|
||||
|
||||
private _handleMessage(raw: string | ArrayBuffer): void {
|
||||
try {
|
||||
const data = typeof raw === 'string' ? raw : new Uint8Array(raw)
|
||||
const packet = this._codec.decode(data)
|
||||
const type = packet[0]
|
||||
|
||||
switch (type) {
|
||||
case PacketType.ApiResponse:
|
||||
this._handleApiResponse(packet as [number, number, unknown])
|
||||
break
|
||||
case PacketType.ApiError:
|
||||
this._handleApiError(packet as [number, number, string, string])
|
||||
break
|
||||
case PacketType.Message:
|
||||
this._handleMsg(packet as [number, string, unknown])
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
this._options.onError?.(err as Error)
|
||||
}
|
||||
}
|
||||
|
||||
private _handleApiResponse([, id, result]: [number, number, unknown]): void {
|
||||
const pending = this._pendingCalls.get(id)
|
||||
if (pending) {
|
||||
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)
|
||||
if (pending) {
|
||||
clearTimeout(pending.timer)
|
||||
this._pendingCalls.delete(id)
|
||||
pending.reject(new RpcError(code, message))
|
||||
}
|
||||
}
|
||||
|
||||
private _handleMsg([, path, data]: [number, string, unknown]): void {
|
||||
const handlers = this._msgHandlers.get(path)
|
||||
if (handlers) {
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
handler(data)
|
||||
} catch (err) {
|
||||
this._options.onError?.(err as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _rejectAllPending(): void {
|
||||
for (const [, pending] of this._pendingCalls) {
|
||||
clearTimeout(pending.timer)
|
||||
pending.reject(new RpcError(ErrorCode.CONNECTION_CLOSED, 'Connection closed'))
|
||||
}
|
||||
this._pendingCalls.clear()
|
||||
}
|
||||
|
||||
private _scheduleReconnect(): void {
|
||||
if (this._shouldReconnect && !this._reconnectTimer) {
|
||||
this._reconnectTimer = setTimeout(() => {
|
||||
this._reconnectTimer = null
|
||||
this.connect().catch(() => {})
|
||||
}, this._reconnectInterval)
|
||||
}
|
||||
}
|
||||
|
||||
private _clearReconnectTimer(): void {
|
||||
if (this._reconnectTimer) {
|
||||
clearTimeout(this._reconnectTimer)
|
||||
this._reconnectTimer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Factory Function | 工厂函数
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 连接到 RPC 服务器(便捷函数)
|
||||
* @en Connect to RPC server (convenience function)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const client = await connect(protocol, 'ws://localhost:3000')
|
||||
* const result = await client.call('join', { name: 'Alice' })
|
||||
* ```
|
||||
*/
|
||||
export function connect<P extends ProtocolDef>(
|
||||
protocol: P,
|
||||
url: string,
|
||||
options: RpcClientOptions = {}
|
||||
): Promise<RpcClient<P>> {
|
||||
return new RpcClient(protocol, url, options).connect()
|
||||
}
|
||||
9
packages/framework/rpc/src/codec/index.ts
Normal file
9
packages/framework/rpc/src/codec/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @zh 编解码器模块
|
||||
* @en Codec Module
|
||||
*/
|
||||
|
||||
export type { Codec } from './types'
|
||||
export { json } from './json'
|
||||
export { msgpack } from './msgpack'
|
||||
export { textEncode, textDecode } from './polyfill'
|
||||
30
packages/framework/rpc/src/codec/json.ts
Normal file
30
packages/framework/rpc/src/codec/json.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @zh JSON 编解码器
|
||||
* @en JSON Codec
|
||||
*/
|
||||
|
||||
import type { Packet } from '../types'
|
||||
import type { Codec } from './types'
|
||||
import { textDecode } from './polyfill'
|
||||
|
||||
/**
|
||||
* @zh 创建 JSON 编解码器
|
||||
* @en Create JSON codec
|
||||
*
|
||||
* @zh 适用于开发调试,可读性好
|
||||
* @en Suitable for development, human-readable
|
||||
*/
|
||||
export function json(): Codec {
|
||||
return {
|
||||
encode(packet: Packet): string {
|
||||
return JSON.stringify(packet)
|
||||
},
|
||||
|
||||
decode(data: string | Uint8Array): Packet {
|
||||
const str = typeof data === 'string'
|
||||
? data
|
||||
: textDecode(data)
|
||||
return JSON.parse(str) as Packet
|
||||
},
|
||||
}
|
||||
}
|
||||
34
packages/framework/rpc/src/codec/msgpack.ts
Normal file
34
packages/framework/rpc/src/codec/msgpack.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @zh MessagePack 编解码器
|
||||
* @en MessagePack Codec
|
||||
*/
|
||||
|
||||
import { Packr, Unpackr } from 'msgpackr'
|
||||
import type { Packet } from '../types'
|
||||
import type { Codec } from './types'
|
||||
import { textEncode } from './polyfill'
|
||||
|
||||
/**
|
||||
* @zh 创建 MessagePack 编解码器
|
||||
* @en Create MessagePack codec
|
||||
*
|
||||
* @zh 适用于生产环境,体积更小、速度更快
|
||||
* @en Suitable for production, smaller size and faster speed
|
||||
*/
|
||||
export function msgpack(): Codec {
|
||||
const encoder = new Packr({ structuredClone: true })
|
||||
const decoder = new Unpackr({ structuredClone: true })
|
||||
|
||||
return {
|
||||
encode(packet: Packet): Uint8Array {
|
||||
return encoder.pack(packet)
|
||||
},
|
||||
|
||||
decode(data: string | Uint8Array): Packet {
|
||||
const buf = typeof data === 'string'
|
||||
? textEncode(data)
|
||||
: data
|
||||
return decoder.unpack(buf) as Packet
|
||||
},
|
||||
}
|
||||
}
|
||||
112
packages/framework/rpc/src/codec/polyfill.ts
Normal file
112
packages/framework/rpc/src/codec/polyfill.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @zh 平台兼容性 polyfill
|
||||
* @en Platform compatibility polyfill
|
||||
*
|
||||
* @zh 为微信小游戏等不支持原生 TextEncoder/TextDecoder 的平台提供兼容层
|
||||
* @en Provides compatibility layer for platforms like WeChat Mini Games that don't support native TextEncoder/TextDecoder
|
||||
*/
|
||||
|
||||
/**
|
||||
* @zh 获取全局 TextEncoder 实现
|
||||
* @en Get global TextEncoder implementation
|
||||
*/
|
||||
function getTextEncoder(): { encode(str: string): Uint8Array } {
|
||||
if (typeof TextEncoder !== 'undefined') {
|
||||
return new TextEncoder()
|
||||
}
|
||||
return {
|
||||
encode(str: string): Uint8Array {
|
||||
const utf8: number[] = []
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
let charCode = str.charCodeAt(i)
|
||||
if (charCode < 0x80) {
|
||||
utf8.push(charCode)
|
||||
} else if (charCode < 0x800) {
|
||||
utf8.push(0xc0 | (charCode >> 6), 0x80 | (charCode & 0x3f))
|
||||
} else if (charCode >= 0xd800 && charCode <= 0xdbff) {
|
||||
i++
|
||||
const low = str.charCodeAt(i)
|
||||
charCode = 0x10000 + ((charCode - 0xd800) << 10) + (low - 0xdc00)
|
||||
utf8.push(
|
||||
0xf0 | (charCode >> 18),
|
||||
0x80 | ((charCode >> 12) & 0x3f),
|
||||
0x80 | ((charCode >> 6) & 0x3f),
|
||||
0x80 | (charCode & 0x3f)
|
||||
)
|
||||
} else {
|
||||
utf8.push(
|
||||
0xe0 | (charCode >> 12),
|
||||
0x80 | ((charCode >> 6) & 0x3f),
|
||||
0x80 | (charCode & 0x3f)
|
||||
)
|
||||
}
|
||||
}
|
||||
return new Uint8Array(utf8)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取全局 TextDecoder 实现
|
||||
* @en Get global TextDecoder implementation
|
||||
*/
|
||||
function getTextDecoder(): { decode(data: Uint8Array): string } {
|
||||
if (typeof TextDecoder !== 'undefined') {
|
||||
return new TextDecoder()
|
||||
}
|
||||
return {
|
||||
decode(data: Uint8Array): string {
|
||||
let str = ''
|
||||
let i = 0
|
||||
while (i < data.length) {
|
||||
const byte1 = data[i++]
|
||||
if (byte1 < 0x80) {
|
||||
str += String.fromCharCode(byte1)
|
||||
} else if ((byte1 & 0xe0) === 0xc0) {
|
||||
const byte2 = data[i++]
|
||||
str += String.fromCharCode(((byte1 & 0x1f) << 6) | (byte2 & 0x3f))
|
||||
} else if ((byte1 & 0xf0) === 0xe0) {
|
||||
const byte2 = data[i++]
|
||||
const byte3 = data[i++]
|
||||
str += String.fromCharCode(
|
||||
((byte1 & 0x0f) << 12) | ((byte2 & 0x3f) << 6) | (byte3 & 0x3f)
|
||||
)
|
||||
} else if ((byte1 & 0xf8) === 0xf0) {
|
||||
const byte2 = data[i++]
|
||||
const byte3 = data[i++]
|
||||
const byte4 = data[i++]
|
||||
const codePoint =
|
||||
((byte1 & 0x07) << 18) |
|
||||
((byte2 & 0x3f) << 12) |
|
||||
((byte3 & 0x3f) << 6) |
|
||||
(byte4 & 0x3f)
|
||||
const offset = codePoint - 0x10000
|
||||
str += String.fromCharCode(
|
||||
0xd800 + (offset >> 10),
|
||||
0xdc00 + (offset & 0x3ff)
|
||||
)
|
||||
}
|
||||
}
|
||||
return str
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const encoder = getTextEncoder()
|
||||
const decoder = getTextDecoder()
|
||||
|
||||
/**
|
||||
* @zh 将字符串编码为 UTF-8 字节数组
|
||||
* @en Encode string to UTF-8 byte array
|
||||
*/
|
||||
export function textEncode(str: string): Uint8Array {
|
||||
return encoder.encode(str)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 将 UTF-8 字节数组解码为字符串
|
||||
* @en Decode UTF-8 byte array to string
|
||||
*/
|
||||
export function textDecode(data: Uint8Array): string {
|
||||
return decoder.decode(data)
|
||||
}
|
||||
24
packages/framework/rpc/src/codec/types.ts
Normal file
24
packages/framework/rpc/src/codec/types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @zh 编解码器类型定义
|
||||
* @en Codec Type Definitions
|
||||
*/
|
||||
|
||||
import type { Packet } from '../types'
|
||||
|
||||
/**
|
||||
* @zh 编解码器接口
|
||||
* @en Codec interface
|
||||
*/
|
||||
export interface Codec {
|
||||
/**
|
||||
* @zh 编码数据包
|
||||
* @en Encode packet
|
||||
*/
|
||||
encode(packet: Packet): string | Uint8Array
|
||||
|
||||
/**
|
||||
* @zh 解码数据包
|
||||
* @en Decode packet
|
||||
*/
|
||||
decode(data: string | Uint8Array): Packet
|
||||
}
|
||||
63
packages/framework/rpc/src/define.ts
Normal file
63
packages/framework/rpc/src/define.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @zh 协议定义模块
|
||||
* @en Protocol Definition Module
|
||||
*/
|
||||
|
||||
import type { ApiDef, MsgDef, ProtocolDef } from './types'
|
||||
|
||||
/**
|
||||
* @zh 创建 API 定义
|
||||
* @en Create API definition
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const join = rpc.api<{ name: string }, { id: string }>()
|
||||
* ```
|
||||
*/
|
||||
function api<TInput = void, TOutput = void>(): ApiDef<TInput, TOutput> {
|
||||
return { _type: 'api' } as ApiDef<TInput, TOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建消息定义
|
||||
* @en Create message definition
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const chat = rpc.msg<{ from: string; text: string }>()
|
||||
* ```
|
||||
*/
|
||||
function msg<TData = void>(): MsgDef<TData> {
|
||||
return { _type: 'msg' } as MsgDef<TData>
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 定义协议
|
||||
* @en Define protocol
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export const protocol = rpc.define({
|
||||
* api: {
|
||||
* join: rpc.api<{ name: string }, { id: string }>(),
|
||||
* leave: rpc.api<void, void>(),
|
||||
* },
|
||||
* msg: {
|
||||
* chat: rpc.msg<{ from: string; text: string }>(),
|
||||
* },
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
function define<T extends ProtocolDef>(protocol: T): T {
|
||||
return protocol
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh RPC 协议定义工具
|
||||
* @en RPC protocol definition utilities
|
||||
*/
|
||||
export const rpc = {
|
||||
define,
|
||||
api,
|
||||
msg,
|
||||
} as const
|
||||
42
packages/framework/rpc/src/index.ts
Normal file
42
packages/framework/rpc/src/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @zh ESEngine RPC 库
|
||||
* @en ESEngine RPC Library
|
||||
*
|
||||
* @zh 类型安全的 RPC 通信库,支持 WebSocket 长连接
|
||||
* @en Type-safe RPC communication library with WebSocket support
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 1. 定义协议(共享)
|
||||
* import { rpc } from '@esengine/rpc'
|
||||
*
|
||||
* export const protocol = rpc.define({
|
||||
* api: {
|
||||
* join: rpc.api<{ name: string }, { id: string }>(),
|
||||
* },
|
||||
* msg: {
|
||||
* chat: rpc.msg<{ from: string; text: string }>(),
|
||||
* },
|
||||
* })
|
||||
*
|
||||
* // 2. 服务端
|
||||
* import { serve } from '@esengine/rpc/server'
|
||||
*
|
||||
* const server = serve(protocol, {
|
||||
* port: 3000,
|
||||
* api: {
|
||||
* join: async (input, conn) => ({ id: conn.id }),
|
||||
* },
|
||||
* })
|
||||
* await server.start()
|
||||
*
|
||||
* // 3. 客户端
|
||||
* import { connect } from '@esengine/rpc/client'
|
||||
*
|
||||
* const client = await connect(protocol, 'ws://localhost:3000')
|
||||
* const result = await client.call('join', { name: 'Alice' })
|
||||
* ```
|
||||
*/
|
||||
|
||||
export { rpc } from './define'
|
||||
export * from './types'
|
||||
68
packages/framework/rpc/src/server/connection.ts
Normal file
68
packages/framework/rpc/src/server/connection.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @zh 服务端连接模块
|
||||
* @en Server Connection Module
|
||||
*/
|
||||
|
||||
import type { Connection, ConnectionStatus } from '../types'
|
||||
|
||||
/**
|
||||
* @zh 服务端连接实现
|
||||
* @en Server connection implementation
|
||||
*/
|
||||
export class ServerConnection<TData = unknown> implements Connection<TData> {
|
||||
readonly id: string
|
||||
readonly ip: string
|
||||
data: TData
|
||||
|
||||
private _status: ConnectionStatus = 'open'
|
||||
private _socket: any
|
||||
private _onClose?: () => void
|
||||
|
||||
constructor(options: {
|
||||
id: string
|
||||
ip: string
|
||||
socket: any
|
||||
initialData: TData
|
||||
onClose?: () => void
|
||||
}) {
|
||||
this.id = options.id
|
||||
this.ip = options.ip
|
||||
this.data = options.initialData
|
||||
this._socket = options.socket
|
||||
this._onClose = options.onClose
|
||||
}
|
||||
|
||||
get status(): ConnectionStatus {
|
||||
return this._status
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 发送原始数据
|
||||
* @en Send raw data
|
||||
*/
|
||||
send(data: string | Uint8Array): void {
|
||||
if (this._status !== 'open') return
|
||||
this._socket.send(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 关闭连接
|
||||
* @en Close connection
|
||||
*/
|
||||
close(reason?: string): void {
|
||||
if (this._status !== 'open') return
|
||||
|
||||
this._status = 'closing'
|
||||
this._socket.close(1000, reason)
|
||||
this._status = 'closed'
|
||||
this._onClose?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 标记连接已关闭(内部使用)
|
||||
* @en Mark connection as closed (internal use)
|
||||
*/
|
||||
_markClosed(): void {
|
||||
this._status = 'closed'
|
||||
}
|
||||
}
|
||||
372
packages/framework/rpc/src/server/index.ts
Normal file
372
packages/framework/rpc/src/server/index.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
/**
|
||||
* @zh RPC 服务端模块
|
||||
* @en RPC Server Module
|
||||
*/
|
||||
|
||||
import { WebSocketServer, WebSocket } from 'ws'
|
||||
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 监听端口
|
||||
* @en Listen port
|
||||
*/
|
||||
port: number
|
||||
|
||||
/**
|
||||
* @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) => {
|
||||
wss = new WebSocketServer({ port: options.port })
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
wss.on('listening', () => {
|
||||
options.onStart?.(options.port)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
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
|
||||
}
|
||||
217
packages/framework/rpc/src/types.ts
Normal file
217
packages/framework/rpc/src/types.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* @zh RPC 核心类型定义
|
||||
* @en RPC Core Type Definitions
|
||||
*/
|
||||
|
||||
// ============ Protocol Types ============
|
||||
|
||||
/**
|
||||
* @zh API 定义标记
|
||||
* @en API definition marker
|
||||
*/
|
||||
export interface ApiDef<TInput = unknown, TOutput = unknown> {
|
||||
readonly _type: 'api'
|
||||
readonly _input: TInput
|
||||
readonly _output: TOutput
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 消息定义标记
|
||||
* @en Message definition marker
|
||||
*/
|
||||
export interface MsgDef<TData = unknown> {
|
||||
readonly _type: 'msg'
|
||||
readonly _data: TData
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 协议定义
|
||||
* @en Protocol definition
|
||||
*/
|
||||
export interface ProtocolDef {
|
||||
readonly api: Record<string, ApiDef<any, any>>
|
||||
readonly msg: Record<string, MsgDef<any>>
|
||||
}
|
||||
|
||||
// ============ Type Inference ============
|
||||
|
||||
/**
|
||||
* @zh 提取 API 输入类型
|
||||
* @en Extract API input type
|
||||
*/
|
||||
export type ApiInput<T> = T extends ApiDef<infer I, any> ? I : never
|
||||
|
||||
/**
|
||||
* @zh 提取 API 输出类型
|
||||
* @en Extract API output type
|
||||
*/
|
||||
export type ApiOutput<T> = T extends ApiDef<any, infer O> ? O : never
|
||||
|
||||
/**
|
||||
* @zh 提取消息数据类型
|
||||
* @en Extract message data type
|
||||
*/
|
||||
export type MsgData<T> = T extends MsgDef<infer D> ? D : never
|
||||
|
||||
/**
|
||||
* @zh 提取协议中所有 API 名称
|
||||
* @en Extract all API names from protocol
|
||||
*/
|
||||
export type ApiNames<P extends ProtocolDef> = keyof P['api'] & string
|
||||
|
||||
/**
|
||||
* @zh 提取协议中所有消息名称
|
||||
* @en Extract all message names from protocol
|
||||
*/
|
||||
export type MsgNames<P extends ProtocolDef> = keyof P['msg'] & string
|
||||
|
||||
// ============ Connection Types ============
|
||||
|
||||
/**
|
||||
* @zh 连接状态
|
||||
* @en Connection status
|
||||
*/
|
||||
export type ConnectionStatus = 'connecting' | 'open' | 'closing' | 'closed'
|
||||
|
||||
/**
|
||||
* @zh 连接接口
|
||||
* @en Connection interface
|
||||
*/
|
||||
export interface Connection<TData = unknown> {
|
||||
/**
|
||||
* @zh 连接唯一标识
|
||||
* @en Connection unique identifier
|
||||
*/
|
||||
readonly id: string
|
||||
|
||||
/**
|
||||
* @zh 客户端 IP 地址
|
||||
* @en Client IP address
|
||||
*/
|
||||
readonly ip: string
|
||||
|
||||
/**
|
||||
* @zh 连接状态
|
||||
* @en Connection status
|
||||
*/
|
||||
readonly status: ConnectionStatus
|
||||
|
||||
/**
|
||||
* @zh 用户自定义数据
|
||||
* @en User-defined data
|
||||
*/
|
||||
data: TData
|
||||
|
||||
/**
|
||||
* @zh 关闭连接
|
||||
* @en Close connection
|
||||
*/
|
||||
close(reason?: string): void
|
||||
}
|
||||
|
||||
// ============ Packet Types ============
|
||||
|
||||
/**
|
||||
* @zh 数据包类型
|
||||
* @en Packet types
|
||||
*/
|
||||
export const PacketType = {
|
||||
ApiRequest: 0,
|
||||
ApiResponse: 1,
|
||||
ApiError: 2,
|
||||
Message: 3,
|
||||
Heartbeat: 9,
|
||||
} as const
|
||||
|
||||
export type PacketType = typeof PacketType[keyof typeof PacketType]
|
||||
|
||||
/**
|
||||
* @zh API 请求包
|
||||
* @en API request packet
|
||||
*/
|
||||
export type ApiRequestPacket = [
|
||||
type: typeof PacketType.ApiRequest,
|
||||
id: number,
|
||||
path: string,
|
||||
data: unknown
|
||||
]
|
||||
|
||||
/**
|
||||
* @zh API 响应包(成功)
|
||||
* @en API response packet (success)
|
||||
*/
|
||||
export type ApiResponsePacket = [
|
||||
type: typeof PacketType.ApiResponse,
|
||||
id: number,
|
||||
data: unknown
|
||||
]
|
||||
|
||||
/**
|
||||
* @zh API 错误包
|
||||
* @en API error packet
|
||||
*/
|
||||
export type ApiErrorPacket = [
|
||||
type: typeof PacketType.ApiError,
|
||||
id: number,
|
||||
code: string,
|
||||
message: string
|
||||
]
|
||||
|
||||
/**
|
||||
* @zh 消息包
|
||||
* @en Message packet
|
||||
*/
|
||||
export type MessagePacket = [
|
||||
type: typeof PacketType.Message,
|
||||
path: string,
|
||||
data: unknown
|
||||
]
|
||||
|
||||
/**
|
||||
* @zh 心跳包
|
||||
* @en Heartbeat packet
|
||||
*/
|
||||
export type HeartbeatPacket = [type: typeof PacketType.Heartbeat]
|
||||
|
||||
/**
|
||||
* @zh 所有数据包类型
|
||||
* @en All packet types
|
||||
*/
|
||||
export type Packet =
|
||||
| ApiRequestPacket
|
||||
| ApiResponsePacket
|
||||
| ApiErrorPacket
|
||||
| MessagePacket
|
||||
| HeartbeatPacket
|
||||
|
||||
// ============ Error Types ============
|
||||
|
||||
/**
|
||||
* @zh RPC 错误
|
||||
* @en RPC Error
|
||||
*/
|
||||
export class RpcError extends Error {
|
||||
constructor(
|
||||
public readonly code: string,
|
||||
message: string,
|
||||
public readonly details?: unknown
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'RpcError'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 预定义错误码
|
||||
* @en Predefined error codes
|
||||
*/
|
||||
export const ErrorCode = {
|
||||
INVALID_REQUEST: 'INVALID_REQUEST',
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
UNAUTHORIZED: 'UNAUTHORIZED',
|
||||
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
||||
TIMEOUT: 'TIMEOUT',
|
||||
CONNECTION_CLOSED: 'CONNECTION_CLOSED',
|
||||
} as const
|
||||
|
||||
export type ErrorCode = typeof ErrorCode[keyof typeof ErrorCode]
|
||||
11
packages/framework/rpc/tsconfig.json
Normal file
11
packages/framework/rpc/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
15
packages/framework/rpc/tsup.config.ts
Normal file
15
packages/framework/rpc/tsup.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
index: 'src/index.ts',
|
||||
'server/index': 'src/server/index.ts',
|
||||
'client/index': 'src/client/index.ts',
|
||||
'codec/index': 'src/codec/index.ts',
|
||||
},
|
||||
format: ['esm'],
|
||||
dts: false,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
external: ['ws', 'msgpackr'],
|
||||
})
|
||||
Reference in New Issue
Block a user