Skip to main content

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}