时空可组合性 ③:Cordis 如何实现可恢复组件与 HMR(中文译文)
译文系列(3/4):① 问题、预备知识与上下文范式 · ② 动态组合演算 · ③ Cordis 实现与 Koishi · ④ 系统边界、相关工作与结论
本篇进入实现层:理论中的 context、inverse、committed view 与惯性状态机,怎样变成 Cordis 的真实运行时对象。
5. 实现与案例
Cordis 是时空可组合性的元框架:它不规定 Web、ORM 或 UI 等具体领域,只提供动态组合语义。实现分三层:核心库直接实现 effects/coeffects;组件加载器增加配置协调与 HMR;Koishi 等应用框架在其上提供领域能力。
5.1 核心库
理论与运行时的主要对应如下:
| 理论对象 | Cordis 运行时 |
|---|---|
Γ∞ | 一等对象 ctx |
| 可逆 effect / iterator | 返回或 yield disposer 的回调 |
effect_Γ(e) | ctx.effect(callback) |
| store / isolate / intercept | ctx[@@store] / ctx[@@isolate] / ctx[@@intercept] |
get/set/isolate/intercept | ctx 上同名操作 |
| fiber 七元组 | Fiber 实例及其 inject/apply/parent/state/dispose/committed |
| registry | ctx.registry |
| 目标视图 | fiber.target |
| transition 句柄 | fiber.inertia |
5.1.1 effect 追踪
所有 context 变更最终都归约为 ctx.effect。execute(callback, guard) 驱动 effect iterator,每次 yield 的 inverse 都组合到累加器最前面;guard 失效便停止推进。effect 再加入两项语义:返回的 disposer 只能触发一次;这个 disposer 也被前插到父 context 的 dispose,于是子 effect 自动进入父生命周期。
运行时不验证 inverse 的数学见证。回调声明“这个 closure 能恢复刚才的动作”,正确性仍是原子 effect 作者的义务;框架负责的是后续组合、顺序与调用不再出错。
5.1.2 coeffect 操作
每个 context 携带三个 symbol-keyed 槽位:值仓库 @@store、key 到 realm 的 @@isolate、key 到元数据的 @@intercept。ctx.get(k) 先经 realm 再取值;ctx.set(k,v) 本身是 effect,安装 binding 并返回删除 binding 的 inverse。安装和删除都会 notify:扫描 live fiber,若其声明了变化 key 且 realm 相同,就重新计算目标视图。
只有 Active fiber 的 binding 算“已提供”。provider 一进入 Unloading,consumer 会先失去满足条件,但旧 binding 仍留到 consumer 清理完成。isolate 与 intercept 都通过派生子 context 生效:前者改变解析到哪个 binding,后者改变 binding 被怎样使用;丢弃子 context 即完成恢复。
5.1.3 生命周期
ctx.use(component, config) 创建 fiber,把组件配置绑定成 fiber.apply,并将“创建 fiber”登记为父 context 的 effect。实际状态机围绕 refresh/reload/unload:
refresh: 重算 target;空闲时按 target 启动 reload 或 unload
reload: 提交依赖视图 → 迭代执行 apply → Active,或串接 unload
unload: 通知并等待所有 consumer → 执行 dispose → Inactive,或串接 reload
target 比较 provider 的新鲜 uid,而不是值相等;即便新旧 provider 提供等值对象,替换仍会被识别。算法同时在 transition 完成处检查 target,并在 iterator 每一步检查 target,分别处理跨 transition 的惯性链与 transition 内的过期前缀。
5.1.4 context 属性访问
除了反射式 ctx.get(key),TypeScript 实现还用 Proxy 支持 ctx[key]。解析从当前 fiber 沿父链向上:committed view 中有 key 就返回;声明了 key 却尚未提交,抛出 inactive access;一路到根仍未声明,抛出 undeclared access。它把依赖声明变成运行时 capability 检查,也保证 teardown 读取的是已提交视图而不是正在变化的 store。
5.2 组件加载器
核心 API 面向组件作者;加载器面向编排者,把声明式持久配置协调成 fiber 操作。
定义 74(entry)。 每个配置 entry 记录:稳定 id、组件模块 url、isolate、intercept、组件 config 和管理开关 disabled。entry 组成配置树;@cordisjs/group 管理子 entry,@cordisjs/include 把 YAML/JSON 外部配置接入树中。
协调按字段采用最小扰动策略:id/url 改变则重建;isolate 重新分配 realm;intercept 原地更新;config 交给组件做细粒度 diff;disabled 控制卸载/加载。定理 73 保证最终 quiet 状态取决于最终配置,而不是中间协调顺序;定理 63 允许模块并行加载,依赖只约束激活时刻。
移动 entry 时,本地 realm 应随 entry 走,共享 realm 则应保持全局归属。加载器为每个 key 维护 delimiter tag,比较 entry 与 provider 是否源于同一 isolate scope,只移动真正属于该 entry 的 binding,并只通知“可见性确实发生改变”的 consumer。
5.2.2 热模块替换
HMR 把模块替换视作 fiber 的恢复与重建,不要求业务作者写 accept 边界,分三阶段:
- 模块分类:从已改变文件与不能热替换的 externals 出发,对 import 子图做不动点分类;依赖 accepted 模块者也 accepted,全部依赖 declined 者 declined,留在 import 环中的未决项保守地 declined。
- 过期 entry 检测:遍历 entry 的传递依赖树;只要与 accepted 相交就标为 stale,并把整棵受影响树加入失效集合。
- 事务式重载:备份并清空模块缓存,逐个恢复旧 fiber、导入新模块并创建新 fiber。任一导入失败就恢复缓存,再用备份模块重建所有 stale entry,避免半新半旧。
5.3 Koishi 案例
Koishi 在四年中形成了超过 4,000 个社区插件,覆盖即时通信适配器、数据库驱动、管理控制台与最终用户功能。服务端本身与浏览器控制台是两个不同领域、不同运行环境的 Cordis 应用,说明元框架固定的是组合方式而不是业务含义。
时间维度上,控制台可原地禁用插件,开发期保存代码即可 HMR,同时保留系统其余缓存与连接。空间维度上,适配器、存储与功能插件由不同作者独立提供;provider 切换只重新激活解析结果实际变化的 consumer,缺依赖的插件静默保持 Inactive。
论文也主动限定证据:这是单一语言、单一生态的观察性案例,只能证明可行性与实际采用,不能分离 TypeScript、Koishi 领域和范式本身各自的贡献;性能开销与开发效率的对照实验仍待完成。此外,当前 Koishi 使用 Cordis v3,论文描述的是语义经过精炼、加载器重写的 v4。