helix_core/module_host.rs
1//! # module_host.rs
2//!
3//! `Module` trait 和 `ModuleHost`——模块注册、路由、生命周期。
4//!
5//! ## Module trait 设计(审查意见2 P1 + S1 落实)
6//!
7//! - 统一 `&Tick<'_>` 入参,消除原草稿 `InboundFrame` 中间层
8//! - 删除 `tick()` 方法:周期行为通过 `ScheduleTimer` self-arm 表达
9//! - `accepts()` 仅用于 `Inbound` / `Command` 路由;`PortReply` / `Timer` 走 corr_map 定向路由
10//!
11//! ## 路由策略(审查意见2 P3 落实)
12//!
13//! - `Inbound` / `Command`:遍历 modules,找第一个 `accepts()` 返回 true 的模块
14//! - `PortReply` / `PortProgress`:通过 `corr_map`(Correlation → module_index)O(1) 定向投递
15//! - `Timer`:通过 `timer_map`(TimerId → module_index)O(1) 定向投递
16//!
17//! 预期模块数 < 10,Inbound 的线性扫描实践 O(1),可接受。
18
19use crate::effect::{Correlation, EffectSink, TimerId};
20use crate::error::CoreError;
21use crate::platform::MaybeSend;
22use crate::tick::Tick;
23use std::collections::HashMap;
24
25/// 确定性业务模块的完整契约。
26///
27/// ## 实现约束(不可违反)
28///
29/// - `accepts` 和 `handle` 必须是纯函数(无 I/O,无 await,无 spawn)
30/// - `handle` 通过 `&mut EffectSink` 输出 Effect,不直接调用任何 port
31/// - 周期心跳:在 `on_start` 吐 `ScheduleTimer{id, after_ms}`,
32/// 在 `handle(Tick::Timer{id})` 重新 arm,实现无限循环心跳
33pub trait Module: MaybeSend + 'static {
34 /// 模块唯一名称(用于日志 / 调试 / error 报告)
35 fn name(&self) -> &'static str;
36
37 /// 路由判定:此模块是否处理该 Tick。
38 ///
39 /// ## 约束
40 ///
41 /// - 仅用于 `Tick::Inbound` 和 `Tick::Command` 的路由
42 /// - `Tick::PortReply` / `Tick::PortProgress` 和 `Tick::Timer` **不调用此方法**,走映射定向路由
43 /// - 必须是纯函数(不修改 self)
44 fn accepts(&self, tick: &Tick) -> bool;
45
46 /// 处理一个 Tick,将 Effect 写入 `out`。
47 ///
48 /// ## 不变量(不可违反)
49 ///
50 /// - 严格同步:零 await,零 I/O,零 spawn
51 /// - 不直接调用任何 port trait
52 /// - 所有副作用通过 `out.push(Effect::...)` 表达
53 fn handle(&mut self, tick: &Tick, now_ms: u64, out: &mut EffectSink) -> Result<(), CoreError>;
54
55 /// 模块启动钩子。
56 ///
57 /// 在 `engine.start()` 时调用,输出初始 Effect:
58 /// - `Effect::Send{hello 帧}`(WS 握手)
59 /// - `Effect::Persist{get cursor, corr}`(读取持久化状态)
60 /// - `Effect::ScheduleTimer{ping, 8s}`(arm 初始心跳)
61 fn on_start(&mut self, out: &mut EffectSink) -> Result<(), CoreError> {
62 let _ = out;
63 Ok(())
64 }
65
66 /// 模块停止钩子。
67 ///
68 /// 输出清理 Effect:
69 /// - `Effect::CancelTimer` 所有 timer
70 /// - `Effect::Send{close 帧}`
71 fn on_stop(&mut self, out: &mut EffectSink) -> Result<(), CoreError> {
72 let _ = out;
73 Ok(())
74 }
75}
76
77// ─── CorrelationMap ──────────────────────────────────────────────────────────
78
79/// `Correlation → module_index` 映射,供 PortReply 定向路由。
80///
81/// 每次 `step()` 产出含 Correlation 的 Effect(Persist/Http/UploadFile),
82/// `ExecutionShell` 调用 `register()` 记录映射。
83/// `PortProgress` 到达时 `peek()`,`PortReply` 到达时 `consume()`。
84#[derive(Default)]
85pub(crate) struct CorrelationMap {
86 map: HashMap<Correlation, usize>,
87}
88
89impl CorrelationMap {
90 pub fn register(&mut self, corr: Correlation, module_index: usize) {
91 self.map.insert(corr, module_index);
92 }
93
94 /// 消费(移除):PortReply 处理完毕后移除,避免 stale corr 积累。
95 /// MIN-8:原名 `lookup` 语义不明,改为 `consume` 体现"取出并移除"。
96 pub fn consume(&mut self, corr: &Correlation) -> Option<usize> {
97 self.map.remove(corr)
98 }
99
100 /// 非终态进度只读定位,不消费;同 corr 的最终 PortReply 必须仍可路由。
101 pub fn peek(&self, corr: &Correlation) -> Option<usize> {
102 self.map.get(corr).copied()
103 }
104}
105
106/// `TimerId → module_index` 映射,供 Timer 定向路由。
107///
108/// ## 语义真源:timer 一律 one-shot-on-fire(HIGH 修复,2026-06-22)
109///
110/// 全仓**无 recurring timer**——所有 timer 触发即作废,"周期心跳"由模块在
111/// `handle(Tick::Timer)` 内吐**全新 id** 的 `ScheduleTimer` re-arm 实现(如 IM ping),
112/// 新 id 经 `register_effects_to_maps` 注册新条目,**绝不复用旧 id**。
113/// 因此 fire 后旧条目永远应回收:`engine` 在 `dispatch_reply` 后统一 `remove(fired_id)`。
114/// 不存在"同 id 复用且 fire 后不重新 ScheduleTimer"的模式(如未来要引入,须新增
115/// 显式 RecurringTimer 语义,而非偷偷复用 id;回放测试 + drift-review 守护)。
116#[derive(Default)]
117pub(crate) struct TimerMap {
118 map: HashMap<TimerId, usize>,
119}
120
121impl TimerMap {
122 pub fn register(&mut self, id: TimerId, module_index: usize) {
123 self.map.insert(id, module_index);
124 }
125
126 /// 只读查路由(不移除):派发前定位 module_index。
127 /// fire 后由 `engine` 在 `dispatch_reply` 之后统一 `remove(fired_id)` 回收
128 /// (one-shot-on-fire 语义,见 struct doc),故此处只读,移除职责归 engine。
129 pub fn peek(&self, id: &TimerId) -> Option<usize> {
130 self.map.get(id).copied()
131 }
132
133 /// 移除一条路由条目。两个调用点:
134 /// - `engine` 在 Timer 命中、派发完毕后回收已触发的 one-shot id(防永久泄漏)
135 /// - `register_effects_to_maps` 处理模块吐出的 `Effect::CancelTimer`
136 pub fn remove(&mut self, id: &TimerId) {
137 self.map.remove(id);
138 }
139
140 /// 测试探针:当前路由条目数。仅用于回放测试断言 timer_map 不随触发次数线性增长,
141 /// `#[cfg(test)]` 隔离不污染生产 API(不破坏 public-api 闸门)。
142 #[cfg(test)]
143 pub fn len(&self) -> usize {
144 self.map.len()
145 }
146}
147
148// ─── ModuleHost ──────────────────────────────────────────────────────────────
149
150/// 模块注册与路由中枢。
151///
152/// ## 开闭原则(轴④)
153///
154/// 加新业务模块 = 调 `register()`,不改 `dispatch_*` 函数一行。
155pub struct ModuleHost {
156 modules: Vec<Box<dyn Module>>,
157}
158
159impl ModuleHost {
160 pub fn new() -> Self {
161 Self {
162 modules: Vec::new(),
163 }
164 }
165
166 /// 注册新业务模块(加模块不改 dispatch 逻辑,开闭原则)
167 pub fn register(&mut self, m: impl Module + 'static) {
168 self.modules.push(Box::new(m));
169 }
170
171 /// 返回已注册的模块数量(供 ExecutionShell::start() 按 index 逐个调用)
172 pub fn module_count(&self) -> usize {
173 self.modules.len()
174 }
175
176 /// 路由 Inbound/Command:线性扫描 accepts()
177 /// 返回命中的 module_index(用于 corr_map 注册)
178 pub fn dispatch_inbound(
179 &mut self,
180 tick: &Tick,
181 now_ms: u64,
182 out: &mut EffectSink,
183 ) -> Result<Option<usize>, CoreError> {
184 for (idx, m) in self.modules.iter_mut().enumerate() {
185 if m.accepts(tick) {
186 m.handle(tick, now_ms, out)
187 .map_err(|e| CoreError::ModuleError {
188 module: m.name(),
189 source: Box::new(e),
190 })?;
191 return Ok(Some(idx));
192 }
193 }
194 Ok(None)
195 }
196
197 /// 定向投递 PortReply(通过 corr_map 查找 module_index)
198 pub fn dispatch_reply(
199 &mut self,
200 module_index: usize,
201 tick: &Tick,
202 now_ms: u64,
203 out: &mut EffectSink,
204 ) -> Result<(), CoreError> {
205 let m = self
206 .modules
207 .get_mut(module_index)
208 .ok_or(CoreError::NoHandler("module_index out of bounds"))?;
209 m.handle(tick, now_ms, out)
210 .map_err(|e| CoreError::ModuleError {
211 module: m.name(),
212 source: Box::new(e),
213 })
214 }
215
216 /// 调用单个模块的 on_start(BLK-1a:供 ExecutionShell 按 index 逐个调用,
217 /// 以便每个模块的 Correlation/TimerId 注册到正确的 module_index)
218 pub fn start_one(&mut self, idx: usize, out: &mut EffectSink) -> Result<(), CoreError> {
219 let m = self.modules.get_mut(idx).ok_or(CoreError::NoHandler(
220 "start_one: module_index out of bounds",
221 ))?;
222 m.on_start(out).map_err(|e| CoreError::ModuleError {
223 module: m.name(),
224 source: Box::new(e),
225 })
226 }
227
228 /// 调用所有模块的 on_start(按注册顺序)
229 ///
230 /// 注意:此方法不区分各模块的 Correlation;若需要精确路由,
231 /// 使用 `start_one(idx)` 逐个调用(ExecutionShell::start 已改为逐个调用)。
232 pub fn start_all(&mut self, out: &mut EffectSink) -> Result<(), CoreError> {
233 for m in &mut self.modules {
234 m.on_start(out).map_err(|e| CoreError::ModuleError {
235 module: m.name(),
236 source: Box::new(e),
237 })?;
238 }
239 Ok(())
240 }
241
242 /// 调用所有模块的 on_stop(按注册逆序,last-in first-out)
243 pub fn stop_all(&mut self, out: &mut EffectSink) -> Result<(), CoreError> {
244 for m in self.modules.iter_mut().rev() {
245 m.on_stop(out).map_err(|e| CoreError::ModuleError {
246 module: m.name(),
247 source: Box::new(e),
248 })?;
249 }
250 Ok(())
251 }
252}
253
254impl Default for ModuleHost {
255 fn default() -> Self {
256 Self::new()
257 }
258}