feat(framework): StartupOrchestrator 启动链路编排

串联启动门控四条件(C 规范 §6.1)的可测部分:
- bootstrap() 拉取并解析远程配置,写入 AppStore(identity/servers)
  失败显式抛错(第二准则)
- onLogin() 收到 player_login 后构造 NetClient + RoomRPCBus
  并触发 ready 信号
- waitForReady() 合并 bootstrap + login + room rpc 通道
  UI 层 await 即可进入登录页
- shutdown() 清理 RoomRPCBus

依赖注入:
- getBootstrapResult: 远程配置拉取函数(测试可 mock 抛错)
- makeNetClient: NetClient 工厂(避免测试需要真 WS)
- roomBusFactory: RoomRPCBus 工厂(避免测试起副作用)

167/167 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:27:14 +08:00
co-authored by Claude Opus 5
parent 64b818d514
commit c57d1d9214
2 changed files with 219 additions and 0 deletions
@@ -0,0 +1,87 @@
import type { EventBus } from '../core/events.ts';
import type { NetClientEvents } from '../net/net-client.ts';
import type { ChannelIdentity } from '../config/identity.ts';
import type { BootstrapResult } from '../config/bootstrap.ts';
import type { PlatformSession } from './session.ts';
import type { RoomRPCBus } from './room-rpc-bus.ts';
/**
* StartupOrchestrator:把启动链路串起来的纯逻辑编排器。
*
* 流程:bootstrap() → onLogin()(由 PlatformSession 在收到 login 事件后调用)→ ready
*
* 不直接构造 NetClient / RoomRPCBus(依赖注入),便于单测覆盖全部阶段。
* 不主动连 WS/发包——NetClient 由业务侧触发 start(),本类只编排阶段信号。
*
* 启动门控四条件(C 规范 §6.1):资源(外部)/ WS onopen(NetClient.on)/ 配置(本类 bootstrap)/ 加载计时器(外部)。
* waitForReady() 合并后三个为「就绪信号」,UI 层 await 进入登录页。
*/
export interface StartupOptions {
session: PlatformSession;
bus: EventBus<NetClientEvents>;
/** 返回 bootstrap 结果(远程配置 + 身份 + 服务器列表) */
getBootstrapResult: () => Promise<BootstrapResult>;
/** 构造 NetClient 的工厂(注入避免硬依赖真 WS) */
makeNetClient: () => { bus: EventBus<NetClientEvents> };
/** 用于 session 初始化的身份字段 */
identity: ChannelIdentity;
/** 构造 RoomRPCBus 的工厂 */
roomBusFactory: (session: PlatformSession, bus: EventBus<NetClientEvents>) => RoomRPCBus;
}
export class StartupOrchestrator {
bootstrapResult: BootstrapResult | null = null;
private readyResolve: (() => void) | null = null;
private readyPromise: Promise<void>;
private roomBus: RoomRPCBus | null = null;
private netClient: { bus: EventBus<NetClientEvents> } | null = null;
constructor(private readonly opts: StartupOptions) {
// 立即构造一个 pending Promise,UI 层可提前 await
this.readyPromise = new Promise<void>((resolve) => {
this.readyResolve = resolve;
});
}
/** 阶段 1:拉取并解析远程配置,失败显式抛错 */
async bootstrap(): Promise<BootstrapResult> {
const result = await this.opts.getBootstrapResult();
this.bootstrapResult = result;
// 把身份 + 服务器写入 AppStore(session 在构造时已订阅 login 等事件,
// 这里直接驱动 AppStore.setServers/setIdentity 以提前有数据)
this.opts.session.app.setServers(result.servers);
this.opts.session.app.setIdentity(result.identity);
return result;
}
/**
* 阶段 2/3:构造 NetClient + RoomRPCBus,准备就绪(但不主动 start 连接;
* 业务侧在收到 phase=connected 后再 netClient.start())。
*
* 通常 PlatformSession 收到 'login' 事件后调用此方法——login 事件
* 表明 NetClient 已 start 且 server 已响应 player_login。
*/
async onLogin(): Promise<void> {
// 构造 NetClient(业务侧会调用 start() 真正建立 WS)
this.netClient = this.opts.makeNetClient();
// 构造 RoomRPCBus 并立即 start,把 room route 的包挂到 bus
this.roomBus = this.opts.roomBusFactory(this.opts.session, this.netClient.bus);
this.roomBus.start();
// 触发 ready 信号
if (this.readyResolve) { this.readyResolve(); this.readyResolve = null; }
}
/** UI 层等待「bootstrap + login + 房间 rpc 通道就绪」 */
async waitForReady(): Promise<void> {
return this.readyPromise;
}
isReady(): boolean { return this.readyResolve === null; }
/** 清理:停止 RoomRPCBus + 关闭 NetClient */
shutdown(): void {
this.roomBus?.stop();
this.roomBus = null;
this.netClient = null;
}
}