BotOf TechAI / IoT / Full-Stack / 植物养护知识分享
返回首页时空可组合性 ③:Cordis 如何实现可恢复组件与 HMR(中文译文)

时空可组合性 ③: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 / interceptctx[@@store] / ctx[@@isolate] / ctx[@@intercept]
get/set/isolate/interceptctx 上同名操作
fiber 七元组Fiber 实例及其 inject/apply/parent/state/dispose/committed
registryctx.registry
目标视图fiber.target
transition 句柄fiber.inertia

5.1.1 effect 追踪

所有 context 变更最终都归约为 ctx.effectexecute(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 到元数据的 @@interceptctx.get(k) 先经 realm 再取值;ctx.set(k,v) 本身是 effect,安装 binding 并返回删除 binding 的 inverse。安装和删除都会 notify:扫描 live fiber,若其声明了变化 key 且 realm 相同,就重新计算目标视图。

只有 Active fiber 的 binding 算“已提供”。provider 一进入 Unloading,consumer 会先失去满足条件,但旧 binding 仍留到 consumer 清理完成。isolateintercept 都通过派生子 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、组件模块 urlisolateintercept、组件 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 边界,分三阶段:

  1. 模块分类:从已改变文件与不能热替换的 externals 出发,对 import 子图做不动点分类;依赖 accepted 模块者也 accepted,全部依赖 declined 者 declined,留在 import 环中的未决项保守地 declined。
  2. 过期 entry 检测:遍历 entry 的传递依赖树;只要与 accepted 相交就标为 stale,并把整棵受影响树加入失效集合。
  3. 事务式重载:备份并清空模块缓存,逐个恢复旧 fiber、导入新模块并创建新 fiber。任一导入失败就恢复缓存,再用备份模块重建所有 stale entry,避免半新半旧。

5.3 Koishi 案例

Koishi 在四年中形成了超过 4,000 个社区插件,覆盖即时通信适配器、数据库驱动、管理控制台与最终用户功能。服务端本身与浏览器控制台是两个不同领域、不同运行环境的 Cordis 应用,说明元框架固定的是组合方式而不是业务含义。

时间维度上,控制台可原地禁用插件,开发期保存代码即可 HMR,同时保留系统其余缓存与连接。空间维度上,适配器、存储与功能插件由不同作者独立提供;provider 切换只重新激活解析结果实际变化的 consumer,缺依赖的插件静默保持 Inactive。

论文也主动限定证据:这是单一语言、单一生态的观察性案例,只能证明可行性与实际采用,不能分离 TypeScript、Koishi 领域和范式本身各自的贡献;性能开销与开发效率的对照实验仍待完成。此外,当前 Koishi 使用 Cordis v3,论文描述的是语义经过精炼、加载器重写的 v4。