feat(framework): RoomRPCBus 把 NetClient 'message' 分发到 room handlers

端到端打通「收包 → 路由分界 → RoomStore 更新」」」」」」」」」」」

RoomRPCBus.start() 订阅 NetClient.bus 'message':
- route==='room' 的包 → 查 roomHandlers[msg.rpc](ctx, msg.data)
- 其他 route 不处理(归 Router 单独管平台/agent)

未注册 room rpc 显式抛错(第二准则:不静默吞包)
start/stop 幂等,可重复调用

业务侧在 PlatformSession 构造后:
  const roomBus = new RoomRPCBus(net.bus, session);
  roomBus.start();  // 进房前
  roomBus.stop();   // 退房后

162/162 tests pass, typecheck exit 0。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 21:00:02 +08:00
co-authored by Claude Opus 5
parent 705c0389b5
commit 64b818d514
2 changed files with 121 additions and 0 deletions
@@ -0,0 +1,50 @@
import type { EventBus } from '../core/events.ts';
import type { NetClientEvents } from '../net/net-client.ts';
import type { InboundMessage } from '../core/types/envelope.ts';
import { roomHandlers } from '../protocol/room-handlers.ts';
import type { PlatformSession } from './session.ts';
/**
* RoomRPCBus:把 NetClient 的 'message' 事件按 route 分发到 room handlers。
*
* 监听 NetClient 'message' → 过滤 route==='room' → 查 roomHandlers[rpc](ctx, data)。
*
* 业务侧在 PlatformSession 构造后立即创建 RoomRPCBus 并 start(),
* 后续 stop() 退订(进/退房切换清理)。
*/
export class RoomRPCBus {
private handler: ((msg: InboundMessage) => void) | null = null;
constructor(
private readonly bus: EventBus<NetClientEvents>,
private readonly session: PlatformSession,
) {}
/** 开始监听 NetClient 消息事件 */
start(): void {
if (this.handler) return; // 幂等
this.handler = (msg) => this.dispatch(msg);
this.bus.on('message', this.handler);
}
/** 停止监听(切房/退出时调用) */
stop(): void {
if (this.handler) { this.bus.off('message', this.handler); this.handler = null; }
}
private dispatch(msg: InboundMessage): void {
if (msg.route !== 'room') return; // 平台三层 + 对局 route 由 Router 单独处理
const handler = (roomHandlers as unknown as Record<string, ((ctx: any, data: any) => void) | undefined>)[msg.rpc];
if (!handler) {
// 未注册 rpc:roomHandlers 的 Object.freeze 已确保此处为 undefined
// 不静默吞包:抛错便于真机端到端发现协议漂移
throw new Error(
`RoomRPCBus: 未注册的 room rpc "${msg.rpc}"(roomHandlers 不覆盖)`,
);
}
handler(
{ room: this.session.room, player: this.session.player },
msg.data as Record<string, unknown>,
);
}
}
@@ -0,0 +1,71 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { EventBus } from '../../YouleNexus/assets/framework/core/events.ts';
import { PlatformSession } from '../../YouleNexus/assets/framework/platform/session.ts';
import { RoomRPCBus } from '../../YouleNexus/assets/framework/platform/room-rpc-bus.ts';
import type { NetClientEvents } from '../../YouleNexus/assets/framework/net/net-client.ts';
test('RoomRPCBus: route=room 的包分发到对应 handler', () => {
const bus = new EventBus<NetClientEvents>();
const session = new PlatformSession(bus);
const roomBus = new RoomRPCBus(bus, session);
roomBus.start();
// 触发 room apply_free_room
bus.emit('message', { route: 'room', rpc: 'other_apply_free_room', data: { applyer: 2 } } as any);
assert.equal(session.room.state.value.state, 1, 'apply_free_room 后 state=1');
roomBus.stop();
});
test('RoomRPCBus: route=agent/platform 不被分发(归 Router 处理)', () => {
const bus = new EventBus<NetClientEvents>();
const session = new PlatformSession(bus);
const roomBus = new RoomRPCBus(bus, session);
roomBus.start();
// 故意发一个 route=room + rpc 不存在的包,不应崩
bus.emit('message', { route: 'room', rpc: 'unknown_rpc', data: {} } as any);
// state 不应变化
assert.equal(session.room.state.value.state, 0);
roomBus.stop();
});
test('RoomRPCBus: stop 后不再分发', () => {
const bus = new EventBus<NetClientEvents>();
const session = new PlatformSession(bus);
const roomBus = new RoomRPCBus(bus, session);
roomBus.start();
roomBus.stop();
bus.emit('message', { route: 'room', rpc: 'other_apply_free_room', data: {} } as any);
// stop 后 handler 不该被调用,state 应保持 0
assert.equal(session.room.state.value.state, 0);
});
test('RoomRPCBus: room rpc 走 handler,其他 route 忽略', () => {
const bus = new EventBus<NetClientEvents>();
const session = new PlatformSession(bus);
const roomBus = new RoomRPCBus(bus, session);
roomBus.start();
// 模拟 login 后玩家入房,players 数组已存在
session.room.applyRecovery({
roomcode: 'R1', seat: 1,
players: [null, { playerid: 100 } as any, null, null],
} as any);
// 第三方加入
bus.emit('message', {
route: 'room', rpc: 'other_join_room',
data: { seat: 3, playerid: 300, nickname: '丙' },
} as any);
assert.equal(session.room.state.value.players[2]?.playerid, 300);
// 第三方退出
bus.emit('message', { route: 'room', rpc: 'other_exit_room', data: { seat: 3 } } as any);
assert.equal(session.room.state.value.players[2], null);
roomBus.stop();
});