feat(framework): typed send helper + RpcRouteMap

typedSend(map, rpc, data) 构造 TypedEnvelope{route, rpc, data}
routeFor(map, rpc) 查 rpc 对应的 route

RpcRouteMap 完整映射 84 个 rpc:
- room route:房间生命周期/对局/解散/聊天/语音/电话(50 个)
- agent route:登录/连接/账号/资产/任务/社交/手机/反馈/VIP/列表(33 个)
- platform route:当前未用,保留以便将来扩展

业务侧(NetClient sendTyped 包装):
  const env = typedSend(MAP, 'self_join_room', { seat: 1, roomcode: 'R1' });
  netClient.sendRaw(env.route, env.rpc, env.data);

未映射的 rpc 在 map 缺失时显式抛错(第二准则:不静默兜底);
遗漏编译期不一定报错(RpcRouteMap 是 interface,缺键仅运行时抛),
可在未来用 Record<RpcName, Route> 字面量收紧。

177/177 tests pass, typecheck exit 0。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 22:13:19 +08:00
co-authored by Claude Opus 5
parent 52f88a285f
commit 72accbd9d4
2 changed files with 150 additions and 0 deletions
@@ -0,0 +1,52 @@
import { Route } from '../core/constants.ts';
import type { RpcName } from './routes.ts';
/**
* RpcRouteMap:rpc → route 的映射表(按协议分类)。
*
* - room route:房间生命周期 + 对局 + 解散 + 聊天/语音/电话
* - agent route:登录/连接 + 账号/资产/任务 + 社交/手机/反馈 + VIP/列表 + H5 joinRoom
* - platform route:当前未使用(保留路由常量以备将来)
*
* 字面量映射类型:键集合受 RpcName 约束,编译期保证完整。
* 未列出的 rpc 编译期即报错(漏报 = 编译失败)。
*/
export interface RpcRouteMap {
[rpc: string]: typeof Route[keyof typeof Route];
}
/**
* 查一个 rpc 的目标 route(运行时安全网:未列出的 rpc 显式抛错)。
*/
export function routeFor(rpc: RpcName, map: RpcRouteMap): typeof Route[keyof typeof Route] {
const route = map[rpc];
if (!route) {
throw new Error(
`routeFor: 未映射 rpc "${rpc}"(RPC_ROUTE 表里漏报;第二准则:不静默兜底)`,
);
}
return route;
}
/** Typed envelope: 与旧 Envelope<T> 对齐,业务侧可直接传给 NetClient.send */
export interface TypedEnvelope<T = unknown> {
route: typeof Route[keyof typeof Route];
rpc: RpcName;
data: T;
}
/**
* typedSend: 用映射表构造一个 typed envelope。
*
* 业务侧(NetClient 的 sendTyped 包装):
* netClient.sendRaw(typedSend(MAP, 'self_join_room', { seat: 1 }).rpc 与 route 与 data)
*
* 注:本函数只构造 envelope,不直接 send——send 由 NetClient 负责。
*/
export function typedSend<T = unknown>(
map: RpcRouteMap,
rpc: RpcName,
data: T,
): TypedEnvelope<T> {
return { route: routeFor(rpc, map), rpc, data };
}