02-具体观 — Cordis 算法与实现剖析

阅读对象:e:\code\deepseek-harness\cordis(Cordis v4)

方法:三观递进之第二观「具体观」------潜入水下,把"看见"变成"看懂"。

结构:共 7 个核心技术点,每节按「原理(论文引用)→ 代码实现 → 具体例子」三段式推进;论文引用格式统一为 [标题](URL) --- 核心贡献一句话概括


总述:七把解剖刀

整体观告诉我们 Cordis 的四件套:Context、Fiber、Reflect、Loader。但"知道它们是什么"和"知道它们怎么工作"之间隔着整整一层。本文用七把解剖刀,从最底层的可逆效果剥起,一路剥到最外层的热替换:

  1. 可逆效果系统(revertible effects)------一切可组合性的地基
  2. 响应式服务依赖(reactive coeffects)------空间维度的实现
  3. Context 的 Proxy 语义------一切读取的运行时路由
  4. 事件分发与内部总线------五种分发模式的算法
  5. 错误与栈追踪工程------长栈拼接的极致细节
  6. 配置声明与加载管道------声明到运行的双向桥
  7. 热模块替换------把时间组合性变成开发体验

顺序即递进:1 定义"时间可逆",2 定义"空间响应",3 是二者的统一入口,4-5 是其上的工程语义,6-7 是把理论推向生产的两座桥。每节独立可读,但连起来读,你会看到一台完整的机器。


一分 · 可逆效果系统(revertible effects)

1.1 原理:效果的代数结构

可逆效果是 Cordis 论文的核心构造之一。

A Programming Paradigm for Spatiotemporal Composability(cordiverse,2026-08-13 preprint,under active revision)--- 形式化定义 revertible effects:每个上下文变换携带逆变换,由运行时追踪并在组件移除时完全回滚,这是时间可组合性的语义基础。

这一思想有深厚的学术谱系。可计算效果(effect)的抽象最早由代数效应与处理器(algebraic effects and handlers)理论给出:

Programming with Algebraic Effects and Handlers(Andrej Bauer & Matija Pretnar)--- 把效果建模为代数运算 、处理器建模为商(homomorphisms),运行时可拦截与改写效果,构成可组合效果系统的理论底座。
What is algebraic about algebraic effects and handlers?(Andrej Bauer)--- 澄清 effect 的代数学本质:effect 是自由代数上的运算(action),handler 是其余代数(comodel)上的解释,二者互为对偶------这正是 Cordis 中"效果上下文(写)与余效果上下文(读)"二元结构的哲学来源。

Cordis 的工程化洞见在于:理论上的"逆变换"落地为一条朴素纪律------任何注册都要返回一个 disposer;运行时把这些 disposer 收进栈式结构,卸载即逆序出栈。

1.2 实现解剖:fiber.ts

效果的四重归一化ctx.effect(execute, label) 返回的 wrapper 会立即被执行,执行结果归一到四种形态(_execute 方法):函数/可舍弃值直接收集;Promise 用 .then 收集;同步迭代器逐项收集;异步迭代器逐项 await(且每轮检查 epoch 是否已失效,失效即中止)。四种形态在回收时统一转换为一串 disposer:

ts 复制代码
// fiber.ts 中 _execute 的四分支(略述)
const result = runner.execute()  // 用户 effect 回调
if (typeof result === 'function') {
  safeCollect(result)            // 形态①:函数直接是 disposer
} else if (Array.isArray(result)) {
  for (const item of result) safeCollect(item)  // 形态②:数组逐个收集
} else if (result && Symbol.asyncIterator in result) {
  for await (const item of result) {            // 形态③:异步迭代器
    if (this.epoch === INACTIVE) break
    safeCollect(item)
  }
} else if (result && Symbol.iterator in result) {
  for (const item of result) safeCollect(item)  // 形态④:同步迭代器
}

注意这套统一是有代价的正确性:无论你返回函数、可迭代、异步可迭代,还是什么都不返回(undefined 静默跳过),框架都无差别地兑现"卸载时逐个回滚"。

epoch:依赖指纹 。Fiber 维护两个 epoch:this.epoch(目标值)与 runner.epoch(已执行值)。_refresh()inject 列表里每个服务的提供者 uid 拼接成指纹字符串:

ts 复制代码
protected _refresh() {
  let epoch = ''
  for (const name of this.inject) {
    const impl = this._checkImpl(name)
    epoch += ':' + impl?.fiber.uid
  }
  this.epoch = epoch
  if (this.epoch === INACTIVE) this._setEpoch(INACTIVE)
}

_checkImpl 返回 undefined(服务未就绪)时该段拼接为空------epoch 变成 INACTIVE 哨兵,Fiber 立即走 _unload 回 PENDING 等待。服务就绪后下次 notify 重算 epoch,Fiber 再重启。epoch 相等 = 世界未变,这是所有增量行为的判据。

惯性锁this.inertia 是一个"加载中/卸载中"的一次性标记。_reload_unload 的 finally 分支里都有同一个判断:如果 epoch 已经漂移,就反向再走一遍;否则清除惯性。这保证频繁抖动时不会并发执行两轮卸载/重载,最终收敛到当前 epoch 对应的状态:

ts 复制代码
protected async _unload() {
  await Promise.all(this._disposables.clear().map((dispose) => composeError(dispose)()))
  this.store = undefined
  if (this.epoch === INACTIVE) {
    this.inertia = undefined   // 无人再用,安静归位
  } else {
    this._updateState(FiberState.LOADING)
    this._reload()             // 依赖又活了,急切重载
  }
}

effect 的双重身份ctx.effect() 的返回值既是"可以再调用的解除器"(返回 () => this.dispose()),又是 Thenable/AsyncDisposable------await dispose 等价于 await dispose.disposeAsync()。这让"等待一个效果终结"(例如插件启动完成)成为一等表达式,loader 的 fiber.await() 即建立其上。

1.3 具体例子

例子 A:ctx.on() 是 effect 的语法糖events.tson()ctx.reflect.bind(listener) 包装后调用 register,而 register 内部最终执行的是:

ts 复制代码
this.fiber.effect(() => {...})   // 监听器登记为可逆效果,卸载自动解绑

所以"插件被卸载后事件监听器神秘残留"这类 bug,在 Cordis 里从机制上不可能发生。

例子 B:ctx.timeout() 把定时器装进可逆封装timer 包):

ts 复制代码
timeout(callback, delay) {
  const dispose = this.ctx.effect(() => {
    const timer = setTimeout(() => { dispose(); callback() }, delay)
    return () => clearTimeout(timer)   // 卸载时清定时器
  }, 'ctx.timeout()')
  return dispose
}

回调版触发后自动解除自身 ;Promise 版则在卸载时 reject(new Error('Context has been disposed'))------定时器是时间的占用,而 Cordis 让时间的占用也可以回滚。
图 2-1 · effect 生命周期:注册 → 执行 → 逆序回滚 :本图展示 ctx.effect() 的结果如何被 _execute 归一化为 disposer 链,以及 Fiber 卸载时如何逆序执行。看懂它,就理解了"可逆效果"的运行时全貌。
#mermaid-svg-Wn0AhjGGSPmYm3gz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Wn0AhjGGSPmYm3gz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Wn0AhjGGSPmYm3gz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Wn0AhjGGSPmYm3gz .error-icon{fill:#552222;}#mermaid-svg-Wn0AhjGGSPmYm3gz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Wn0AhjGGSPmYm3gz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Wn0AhjGGSPmYm3gz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Wn0AhjGGSPmYm3gz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Wn0AhjGGSPmYm3gz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Wn0AhjGGSPmYm3gz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Wn0AhjGGSPmYm3gz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Wn0AhjGGSPmYm3gz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Wn0AhjGGSPmYm3gz .marker.cross{stroke:#333333;}#mermaid-svg-Wn0AhjGGSPmYm3gz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Wn0AhjGGSPmYm3gz p{margin:0;}#mermaid-svg-Wn0AhjGGSPmYm3gz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Wn0AhjGGSPmYm3gz .cluster-label text{fill:#333;}#mermaid-svg-Wn0AhjGGSPmYm3gz .cluster-label span{color:#333;}#mermaid-svg-Wn0AhjGGSPmYm3gz .cluster-label span p{background-color:transparent;}#mermaid-svg-Wn0AhjGGSPmYm3gz .label text,#mermaid-svg-Wn0AhjGGSPmYm3gz span{fill:#333;color:#333;}#mermaid-svg-Wn0AhjGGSPmYm3gz .node rect,#mermaid-svg-Wn0AhjGGSPmYm3gz .node circle,#mermaid-svg-Wn0AhjGGSPmYm3gz .node ellipse,#mermaid-svg-Wn0AhjGGSPmYm3gz .node polygon,#mermaid-svg-Wn0AhjGGSPmYm3gz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Wn0AhjGGSPmYm3gz .rough-node .label text,#mermaid-svg-Wn0AhjGGSPmYm3gz .node .label text,#mermaid-svg-Wn0AhjGGSPmYm3gz .image-shape .label,#mermaid-svg-Wn0AhjGGSPmYm3gz .icon-shape .label{text-anchor:middle;}#mermaid-svg-Wn0AhjGGSPmYm3gz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Wn0AhjGGSPmYm3gz .rough-node .label,#mermaid-svg-Wn0AhjGGSPmYm3gz .node .label,#mermaid-svg-Wn0AhjGGSPmYm3gz .image-shape .label,#mermaid-svg-Wn0AhjGGSPmYm3gz .icon-shape .label{text-align:center;}#mermaid-svg-Wn0AhjGGSPmYm3gz .node.clickable{cursor:pointer;}#mermaid-svg-Wn0AhjGGSPmYm3gz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Wn0AhjGGSPmYm3gz .arrowheadPath{fill:#333333;}#mermaid-svg-Wn0AhjGGSPmYm3gz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Wn0AhjGGSPmYm3gz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Wn0AhjGGSPmYm3gz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Wn0AhjGGSPmYm3gz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Wn0AhjGGSPmYm3gz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Wn0AhjGGSPmYm3gz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Wn0AhjGGSPmYm3gz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Wn0AhjGGSPmYm3gz .cluster text{fill:#333;}#mermaid-svg-Wn0AhjGGSPmYm3gz .cluster span{color:#333;}#mermaid-svg-Wn0AhjGGSPmYm3gz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Wn0AhjGGSPmYm3gz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Wn0AhjGGSPmYm3gz rect.text{fill:none;stroke-width:0;}#mermaid-svg-Wn0AhjGGSPmYm3gz .icon-shape,#mermaid-svg-Wn0AhjGGSPmYm3gz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Wn0AhjGGSPmYm3gz .icon-shape p,#mermaid-svg-Wn0AhjGGSPmYm3gz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Wn0AhjGGSPmYm3gz .icon-shape .label rect,#mermaid-svg-Wn0AhjGGSPmYm3gz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Wn0AhjGGSPmYm3gz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Wn0AhjGGSPmYm3gz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Wn0AhjGGSPmYm3gz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 函数 / 可舍弃值
thenable

Symbol.iterator

Symbol.asyncIterator

undefined/null
ctx.effect(execute, label)
_execute 归一化
execute() 返回?
直接收集 disposer
then 后收集
同步迭代收集
异步迭代收集

逐轮校验 epoch
静默跳过
eff 入栈 DisposableList
Fiber 卸载 _unload
clear() 逆序出栈
逐个执行 disposer

(后再卸载依赖方)


二分 · 响应式服务依赖(reactive coeffects)

2.1 原理:余效果与依赖的声明式应答

如果说可逆效果回答"组件如何写世界",那响应式余效果回答"世界变化如何通知组件"。这在学术上对应 coeffects(余效果):

Coeffects: A Calculus of Context-Dependent Computation(Tomas Petricek, Dominic Orchard, Alan Mycroft)--- 提出 coeffect 系统:把"计算所依赖的环境之形状"结构化为可组合的类型,与 effect(计算改写环境)构成对偶。

Cordis 论文在此基础上要求更强的运行时承诺:

A Programming Paradigm for Spatiotemporal Composability --- 形式化 reactive coeffects :上下文每次变化,都要按组件声明的 coeffect 规格(即 inject)通知组件,使空间可组合性(声明依赖 + 自动响应)成为一等语义。

2.2 实现解剖:reflect.ts + fiber.ts

Impl 三元组 。服务仓库 store 不是名字 → 值,而是名字 → symbol → Impl

ts 复制代码
interface Impl {
  name: string      // 服务名
  value: any        // 服务实例
  fiber: Fiber      // 谁提供的
  check?: (value) => boolean   // 可选就绪检查
}

isolate 键:空间的隔离provide('timer', impl) 先取 isolate['timer']------一个为当前隔离域专属生成的 Symbol------再用它做 store 键。于是"同一个服务名在不同 isolate 域里是不同的事实",两个不相干的宇宙可以各管各的 ctx.timer。loader 的 isolate 插件正是靠这套机制实现配置隔离域。

provide 的注册与撤销要对偶

ts 复制代码
provide(name, value, check) {
  this.ctx.fiber.effect(() => {
    // 注册:分配 isolate symbol、写 store、ACTIVE 时通知
    const key = this.isolate[name] ??= Symbol(name)
    this.store[key] = { name, value, fiber: this.ctx.fiber, check }
    ...
    return () => {
      delete this.store[key]                     // 撤销
      return this.notify([name])                 // 并广播"世界变了"
    }
  })
}

注意返回值里那个 notify------服务卸载也是一次空间地震,依赖方必须立刻知道。

notify 双通道

ts 复制代码
notify(names, filter?) {
  for (const runtime of this.registry...) {
    for (const fiber of runtime.fibers) {
      if (!names.some((name) => fiber.inject.includes(name))) continue
      if (filter && !filter(fiber)) continue
      let hasUpdate = false
      for (const name of names) {
        if (!fiber.inject.includes(name)) continue
        fiber._checkImpl(name)          // 通道①:直接重查每个依赖方
        hasUpdate = true
      }
      if (hasUpdate) fiber._refresh()   // 变化 → 重算 epoch → 决定重载
    }
  }
  for (const name of names) {
    this.ctx.emit('internal/service', name, ...)  // 通道②:事件广播给监听者
  }
}

通道①是图内通知 (直接驱动依赖方刷新),通道②是图外观察 (任何插件可以 ctx.on('internal/service') 观察服务变化)。_checkImpl 顺带把就绪的服务写进 fiber._store[name]------依赖方启动后直接读 _store 就能拿到实现,无需再次查表。

inject 解析Inject.resolve 处理三种声明形态:字符串数组 ['timer'] → 全部为必需依赖;含 checkProto 的对象 → 先并原型链再并自有键;普通对象 → 自有键。@Inject 装饰器在类插件的原型上登记注入,运行时 InitHookthis.ctx.inject(inject, callback) 包装构造函数。

2.3 具体例子

hmr 包就是一个教科书级的依赖消费者:

ts 复制代码
@Inject('loader')
@Inject('timer')
class Hmr extends Service { ... }

Hmr 类在实例化时声明需要 loadertimer 两个服务。时序是:loader 先 ACTIVE(提供 internal ModuleLoader 与 entries())→ timer ACTIVE → notify 重查 Hmr 的注入 → _refresh() 算出完整 epoch → _reload() 执行构造函数体。Hmr 从未关心自己排在谁后面,它只声明自己需要谁------这就是"依赖图求解"替代"启动脚本排序"。
图 2-2 · 依赖响应链:provide → store → notify → refresh:本图展示空间可组合性的完整一次心跳------A 提供服务、B 声明依赖、A 变化时 B 自动重启。绿色路径是图内通知(fiber 直达),橙色路径是图外观察(事件广播)。
EventsService 插件 B(依赖方) ReflectService 插件 A(提供方) EventsService 插件 B(依赖方) ReflectService 插件 A(提供方) #mermaid-svg-bdWD6qiPGiEVri6e{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bdWD6qiPGiEVri6e .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bdWD6qiPGiEVri6e .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bdWD6qiPGiEVri6e .error-icon{fill:#552222;}#mermaid-svg-bdWD6qiPGiEVri6e .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bdWD6qiPGiEVri6e .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bdWD6qiPGiEVri6e .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bdWD6qiPGiEVri6e .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bdWD6qiPGiEVri6e .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bdWD6qiPGiEVri6e .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bdWD6qiPGiEVri6e .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bdWD6qiPGiEVri6e .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bdWD6qiPGiEVri6e .marker.cross{stroke:#333333;}#mermaid-svg-bdWD6qiPGiEVri6e svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bdWD6qiPGiEVri6e p{margin:0;}#mermaid-svg-bdWD6qiPGiEVri6e .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bdWD6qiPGiEVri6e text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-bdWD6qiPGiEVri6e .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bdWD6qiPGiEVri6e .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-bdWD6qiPGiEVri6e .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-bdWD6qiPGiEVri6e .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-bdWD6qiPGiEVri6e #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-bdWD6qiPGiEVri6e .sequenceNumber{fill:white;}#mermaid-svg-bdWD6qiPGiEVri6e #sequencenumber{fill:#333;}#mermaid-svg-bdWD6qiPGiEVri6e #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-bdWD6qiPGiEVri6e .messageText{fill:#333;stroke:none;}#mermaid-svg-bdWD6qiPGiEVri6e .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bdWD6qiPGiEVri6e .labelText,#mermaid-svg-bdWD6qiPGiEVri6e .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-bdWD6qiPGiEVri6e .loopText,#mermaid-svg-bdWD6qiPGiEVri6e .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-bdWD6qiPGiEVri6e .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bdWD6qiPGiEVri6e .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-bdWD6qiPGiEVri6e .noteText,#mermaid-svg-bdWD6qiPGiEVri6e .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-bdWD6qiPGiEVri6e .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bdWD6qiPGiEVri6e .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bdWD6qiPGiEVri6e .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bdWD6qiPGiEVri6e .actorPopupMenu{position:absolute;}#mermaid-svg-bdWD6qiPGiEVri6e .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-bdWD6qiPGiEVri6e .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bdWD6qiPGiEVri6e .actor-man circle,#mermaid-svg-bdWD6qiPGiEVri6e line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-bdWD6qiPGiEVri6e :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 图外观察者同步得知服务变化 provide('timer', impl) storekey = Impl{fiber, value, check} 运行中提供服务 dispose(服务卸载) delete storekey _checkImpl('timer') 检查依赖 缺失 → _refresh() epoch = INACTIVE → _unload → PENDING provide('timer', impl2) 再提供 _checkImpl('timer') 就绪 新 epoch → _reload → ACTIVE emit('internal/service', 'timer', value)



三分 · Context 的 Proxy 语义:一切读取的运行时路由

3.1 原理:把"上下文类型"做成可代理对象

Context 在 Cordis 论文中是统一效果上下文与余效果上下文的关键抽象------读写两个世界同一个对象

A Programming Paradigm for Spatiotemporal Composability --- 统一 effect 上下文与 coeffect 上下文为单一 context type:组件的一切计算要么读取该上下文(依赖),要么改写该上下文(效果),二者共享同一份"世界句柄"。

要让 ctx.xxx 具有"读即路由、错即抛错"的语义,靠普通对象做不到------必须用 Proxy。而 Proxy 的正确性本身就是一个被严格研究过的问题:

Trustworthy Proxies: Virtualizing Objects with Invariants(Tom Van Cutsem & Mark S. Miller)--- 论证 Proxy 必须满足不变量(透明的 getPrototypeOf/getOwnPropertyDescriptor 转发、Symbol.iterator 一致性等)才能作为语言的可靠扩展点,这些不变量正是 createTraceable 守护的东西。

3.2 实现解剖:reflect.ts handler 与 utils.ts 追踪代理

特殊属性的分类isSpecialProperty(prop) 判定:symbol、prototype/then、数字字符串、_ 前缀------这些要么是语言元操作,要么是 Cordis 私有效(symbols.*),一律直通 Reflect.get,不参与路由。

属性解析链handler.get):

  1. 特殊属性 → 直通
  2. 自有属性(如 ctx.timer 快速方法被 accessor 定义)→ 走 accessor
  3. 无 accessor → 走 internal/get 瀑布(补丁方可在 internal/get 上提供自定义值)
  4. 仍未命中 → 沿 fiber.parent 链冒泡查 fiber.store(每层都校验 isolate 键)
  5. 冒泡尽头的 root 上要 @Inject 声明才可读------未声明即抛 cannot get property ... without inject

这个顺序意味着:任何一次 ctx.xxx 读取都不能静默失败。要么拿到值,要么拿到带指引的错误。

createTraceable:让被拿出去的对象"认家" 。服务对象本身不是代理,直接透传会丢失上下文。getTraceable(ctx, value) 在取用时把服务包装成追踪代理:

ts 复制代码
export function createTraceable(ctx, value, tracker?) {
  // 关键设计:代理是一个"方法调用即注入 ctx 的壳"
  get(target, property, receiver) {
    if (property === symbols.caller) return caller      // 找到当前调用方
    ...
    const descriptor = getPropertyDescriptor(target, property)
    // 取方法时:若对象方法体内使用 this.ctx,this 会被替换为 shadow 上下文
    const def = descriptor?.get ?? descriptor?.value
    if (typeof def === 'function') return createShadowMethod(ctx, ..., receiver)
  }
  apply(thisArg, args) { ... }   // 函数调用也可追踪
}

配合 createShadow------调用被追踪对象的方法时,this 被换成一个"影子上下文"(只在调用期间存在的扩展),this.ctx 因此总能指回发起调用的那一个 Fiber。日志、derive 数据的归属判定都靠它。

createCallable:可调用服务Service 基类若带 [symbols.invoke](如 Logger、部分网关),构造时用 createCallable 生成"既是函数又是对象"的身份:ctx.logger('name')ctx.logger.info(...) 两种写法都合法。Service 还重写了 Symbol.hasInstance,让 plugin instanceof MyService 穿过代理仍然成立。

3.3 具体例子

例子 A:ctx.logger 的两次读取返回不同能力handler.get 命中的是 accessor:ctx.logger(对象)→ 可再 .info();而 Logger.prototype.info 的实现是 this()().info(...)------this() 是 callable 自身,内部经 [symbols.invoke](name) 解析出"当前调用方 Fiber 的名字"作为日志名。同一个 ctx.logger,打出来的日志天然按插件归属分门别类。

例子 B:accessor/mixin 注入 12 个快速方法ReflectService 构造时 mixin('events', ['on','once','parallel','emit','serial','bail','waterfall'])mixin('timer', ['timeout','interval','throttle','debounce',...])mixin 本质是一个 generator 效果,为每个键定义 accessor:ctx.on(...)ctx.events.on(...) 完全等价,都是 accessor→服务方法→withProps(receiver, service) 绑定 this 后的调用。
图 2-3 · ctx.xxx 属性解析决策树:每一次属性读取都是一次运行时路由。本图展示五种分支,路由终点要么是有保障的值,要么是带指引的错误------这是"上下文类型"在 JavaScript 里的具体形态。
#mermaid-svg-4cjl7XOwoTbLOEXp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4cjl7XOwoTbLOEXp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4cjl7XOwoTbLOEXp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4cjl7XOwoTbLOEXp .error-icon{fill:#552222;}#mermaid-svg-4cjl7XOwoTbLOEXp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4cjl7XOwoTbLOEXp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4cjl7XOwoTbLOEXp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4cjl7XOwoTbLOEXp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4cjl7XOwoTbLOEXp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4cjl7XOwoTbLOEXp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4cjl7XOwoTbLOEXp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4cjl7XOwoTbLOEXp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4cjl7XOwoTbLOEXp .marker.cross{stroke:#333333;}#mermaid-svg-4cjl7XOwoTbLOEXp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4cjl7XOwoTbLOEXp p{margin:0;}#mermaid-svg-4cjl7XOwoTbLOEXp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4cjl7XOwoTbLOEXp .cluster-label text{fill:#333;}#mermaid-svg-4cjl7XOwoTbLOEXp .cluster-label span{color:#333;}#mermaid-svg-4cjl7XOwoTbLOEXp .cluster-label span p{background-color:transparent;}#mermaid-svg-4cjl7XOwoTbLOEXp .label text,#mermaid-svg-4cjl7XOwoTbLOEXp span{fill:#333;color:#333;}#mermaid-svg-4cjl7XOwoTbLOEXp .node rect,#mermaid-svg-4cjl7XOwoTbLOEXp .node circle,#mermaid-svg-4cjl7XOwoTbLOEXp .node ellipse,#mermaid-svg-4cjl7XOwoTbLOEXp .node polygon,#mermaid-svg-4cjl7XOwoTbLOEXp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4cjl7XOwoTbLOEXp .rough-node .label text,#mermaid-svg-4cjl7XOwoTbLOEXp .node .label text,#mermaid-svg-4cjl7XOwoTbLOEXp .image-shape .label,#mermaid-svg-4cjl7XOwoTbLOEXp .icon-shape .label{text-anchor:middle;}#mermaid-svg-4cjl7XOwoTbLOEXp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4cjl7XOwoTbLOEXp .rough-node .label,#mermaid-svg-4cjl7XOwoTbLOEXp .node .label,#mermaid-svg-4cjl7XOwoTbLOEXp .image-shape .label,#mermaid-svg-4cjl7XOwoTbLOEXp .icon-shape .label{text-align:center;}#mermaid-svg-4cjl7XOwoTbLOEXp .node.clickable{cursor:pointer;}#mermaid-svg-4cjl7XOwoTbLOEXp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4cjl7XOwoTbLOEXp .arrowheadPath{fill:#333333;}#mermaid-svg-4cjl7XOwoTbLOEXp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4cjl7XOwoTbLOEXp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4cjl7XOwoTbLOEXp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4cjl7XOwoTbLOEXp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4cjl7XOwoTbLOEXp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4cjl7XOwoTbLOEXp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4cjl7XOwoTbLOEXp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4cjl7XOwoTbLOEXp .cluster text{fill:#333;}#mermaid-svg-4cjl7XOwoTbLOEXp .cluster span{color:#333;}#mermaid-svg-4cjl7XOwoTbLOEXp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4cjl7XOwoTbLOEXp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4cjl7XOwoTbLOEXp rect.text{fill:none;stroke-width:0;}#mermaid-svg-4cjl7XOwoTbLOEXp .icon-shape,#mermaid-svg-4cjl7XOwoTbLOEXp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4cjl7XOwoTbLOEXp .icon-shape p,#mermaid-svg-4cjl7XOwoTbLOEXp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4cjl7XOwoTbLOEXp .icon-shape .label rect,#mermaid-svg-4cjl7XOwoTbLOEXp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4cjl7XOwoTbLOEXp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4cjl7XOwoTbLOEXp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4cjl7XOwoTbLOEXp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

symbol/prototype/_ 前缀



命中
未命中
命中且隔离匹配
隔离不匹配
root 仍未命中


读取 ctx.xxx
isSpecialProperty?
Reflect.get 直通
自有属性 / accessor?
def.get.call(ctx, receiver)
waterfall('internal/get')

补丁方提供值?
使用补丁值
沿 parent 链

检查 fiber.store
getTraceable(impl.value)
抛错 isolate 冲突
该键已 @Inject 声明?
抛错 依赖未就绪
抛错 未声明读取


四分 · 事件分发与内部总线

4.1 原理:观察者模式之后

事件系统是 Cordis 的"业务公共面",但它的设计立场恰恰源于对经典观察者模式的反思:

Deprecating the Observer Pattern with Scala.React(Ingo Maier, Tiark Rompf, Martin Odersky)--- 系统化批评观察者模式的回调地狱与难以组合,主张用响应式抽象表达事件流。

Cordis 的回应不是"驱逐回调",而是给回调加上可组合的控制结构 :五种分发模式 = 观察 × 扇出 × 顺序 × 短路 × 环绕,并配套统一的可逆注册。事件在这一层仍是函数的组合,但在更高层(internal/* 事件族)它同时是框架自身的主干逻辑------加载、配置、服务变化都经事件总线,无一例外。

4.2 实现解剖:events.ts

分发模式的骨架 。所有模式共享 _resolve(type, args):探测第一个参数是否为对象(作为 thisArg),剥离事件名,处理 internal/ 前缀的静默(不触发 internal/dispatch 观察钩子),按 global/filter 过滤候选监听器。之后五种模式的循环各不相同:

ts 复制代码
// emit ------ 同步、注册序、无返回值
for (const hook of callbacks) hook.callback(...args)

// parallel ------ Promise.allSettled,有败则 AggregateError
await Promise.allSettled(callbacks.map(async (h) => h.callback(...args)))

// serial ------ 顺序 await,遇 isBailed 短路
for (const hook of callbacks) {
  const value = await hook.callback(...args)
  if (isBailed(value)) return value
}

// bail ------ 同步、首个非假值即停
for (const hook of callbacks) {
  const value = hook.callback(...args)
  if (isBailed(value)) return value
}

// waterfall ------ CPS 环绕中间件
const next = function (...args) { /* 弹栈递归 */ }
const inner = args.pop()
callbacks[0](...args, inner ? next : callback)

isBailed(value) 的定义是 value !== null && value !== false && value !== undefined------返回任何"真值"都视作中断信号。这一定义让串行管道既可用 return 短路,又可显式 return null 放行。

internal/update 是一场自我实现的 waterfall 。事件系统在构造时就注册了 internal/update 的 global 处理器:它是一个手写的 _next 链 + DisposableList。loader 与 hmr 都通过 ctx.on('internal/update', ...) 挂钩它,于是"配置更新 → 回写文件 / 重载 Fiber"变成了对同一瀑布的两个观察者------框架的更新语义与业务事件的语义共用同一份实现

注册的可逆性register() 内部:

ts 复制代码
const disposer = this.fiber.effect(() => {
  // 按 prepend 选择 push 或 unshift 进 hooks
  return () => { /* 从 hooks 中移除 */ }
})

监听器登记进当前 Fiber 的效果链,卸载自动解绑------事件在这一层也是可逆效果的特例(呼应第一分)。

4.3 具体例子:waterfall 是 Express 中间件的结构化版本

Node 生态熟悉的 app.use(mw) 链与 ctx.waterfall 异曲同工:Koa 的 next() 向下游委托并回传包装结果;Cordis 的 next() 同样"委托 + 值回传"。差别在两点:其一,waterfall 的注册方定义了参数契约 (事件名 + 负载类型),短路与包装都是契约内行为,而非 middleware 的自由协议;其二,waterfall 监听器天然可卸载 ,热替换时整条链可以原子地重建。若需要"顺序但可 await",用 serial;需要并行扇出,用 parallel;纯通知用 emit;单决策用 bail
图 2-4 · 五种分发模式的决策地图:五种模式在"同步/异步 × 是否短路 × 是否传递返回值"三维度上各占其位。事件作者先选模式再写监听器,模式即协议。
#mermaid-svg-uFvWvuw9KkebNeU2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uFvWvuw9KkebNeU2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uFvWvuw9KkebNeU2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uFvWvuw9KkebNeU2 .error-icon{fill:#552222;}#mermaid-svg-uFvWvuw9KkebNeU2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uFvWvuw9KkebNeU2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uFvWvuw9KkebNeU2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uFvWvuw9KkebNeU2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uFvWvuw9KkebNeU2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uFvWvuw9KkebNeU2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uFvWvuw9KkebNeU2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uFvWvuw9KkebNeU2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uFvWvuw9KkebNeU2 .marker.cross{stroke:#333333;}#mermaid-svg-uFvWvuw9KkebNeU2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uFvWvuw9KkebNeU2 p{margin:0;}#mermaid-svg-uFvWvuw9KkebNeU2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-uFvWvuw9KkebNeU2 .cluster-label text{fill:#333;}#mermaid-svg-uFvWvuw9KkebNeU2 .cluster-label span{color:#333;}#mermaid-svg-uFvWvuw9KkebNeU2 .cluster-label span p{background-color:transparent;}#mermaid-svg-uFvWvuw9KkebNeU2 .label text,#mermaid-svg-uFvWvuw9KkebNeU2 span{fill:#333;color:#333;}#mermaid-svg-uFvWvuw9KkebNeU2 .node rect,#mermaid-svg-uFvWvuw9KkebNeU2 .node circle,#mermaid-svg-uFvWvuw9KkebNeU2 .node ellipse,#mermaid-svg-uFvWvuw9KkebNeU2 .node polygon,#mermaid-svg-uFvWvuw9KkebNeU2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uFvWvuw9KkebNeU2 .rough-node .label text,#mermaid-svg-uFvWvuw9KkebNeU2 .node .label text,#mermaid-svg-uFvWvuw9KkebNeU2 .image-shape .label,#mermaid-svg-uFvWvuw9KkebNeU2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-uFvWvuw9KkebNeU2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uFvWvuw9KkebNeU2 .rough-node .label,#mermaid-svg-uFvWvuw9KkebNeU2 .node .label,#mermaid-svg-uFvWvuw9KkebNeU2 .image-shape .label,#mermaid-svg-uFvWvuw9KkebNeU2 .icon-shape .label{text-align:center;}#mermaid-svg-uFvWvuw9KkebNeU2 .node.clickable{cursor:pointer;}#mermaid-svg-uFvWvuw9KkebNeU2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uFvWvuw9KkebNeU2 .arrowheadPath{fill:#333333;}#mermaid-svg-uFvWvuw9KkebNeU2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uFvWvuw9KkebNeU2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uFvWvuw9KkebNeU2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uFvWvuw9KkebNeU2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uFvWvuw9KkebNeU2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uFvWvuw9KkebNeU2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uFvWvuw9KkebNeU2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uFvWvuw9KkebNeU2 .cluster text{fill:#333;}#mermaid-svg-uFvWvuw9KkebNeU2 .cluster span{color:#333;}#mermaid-svg-uFvWvuw9KkebNeU2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uFvWvuw9KkebNeU2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uFvWvuw9KkebNeU2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-uFvWvuw9KkebNeU2 .icon-shape,#mermaid-svg-uFvWvuw9KkebNeU2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uFvWvuw9KkebNeU2 .icon-shape p,#mermaid-svg-uFvWvuw9KkebNeU2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uFvWvuw9KkebNeU2 .icon-shape .label rect,#mermaid-svg-uFvWvuw9KkebNeU2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uFvWvuw9KkebNeU2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uFvWvuw9KkebNeU2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uFvWvuw9KkebNeU2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 观察
环绕
扇出
顺序
短路


事件派发
选择模式
emit

同步 · 注册序 · 无返回值
waterfall

next() 链 · 值传递 · 可短路
parallel

allSettled · 聚合错误
serial

顺序 await · isBailed 短路
bail

首个非假值即停
isBailed(value)?
立即返回 value
继续下一个监听器



五分 · 错误与栈追踪工程

5.1 原理:异步世界的"案发还原"

长栈拼接(long stack trace)解决的是异步编程的老大难:错误发生在十层回调之下,堆栈却只显示最近一帧。Cordis 的元理论承诺"每个上下文变换可逆",若错误日志无法告诉你是哪一次变换、在哪一个组件、由谁触发,"可逆"就只是一句口号------错误追踪是组合性的可观测性基础(对应论文中 context type 的运行时踪迹思想)。工程上这等价于把"调用方链"显式化:每层加载/执行都记录自己的外层栈,错误时按层拼接。

A Programming Paradigm for Spatiotemporal Composability --- context type 显式承载变换踪迹;Cordis 将这一理论原则工程化为 composeError 的调用方栈记录机制。

5.2 实现解剖:utils.tscomposeError / handleError

核心算法是锚点定位 + 外层栈拼接 ,且只对最内层(reason)做一次包裹,避免嵌套膨胀:

ts 复制代码
function handleError(info, reason, getOuterStack?) {
  if (!(reason instanceof Error)) {
    reason = new Error(String(reason))          // 非 Error 先包装
  }
  if (getOuterStack && reason.stack) {
    const lines = reason.stack.split('\n')
    const index = lines.indexOf(info.error.stack.split('\n')[2])  // 锚点:采集帧
    if (index >= 0) {
      const offset = (index - info.offset) + 3 + 2   // 帧偏移校准
      const stack = lines.slice(0, index - info.offset)
        .concat(lines.slice(index + offset).filter((line) => {
          return !/at.*\(<anonymous>\)/.test(line)  // 跳过匿名帧
        }))
        .concat(getOuterStack())
      try { reason.stack = stack.join('\n') } catch {}
    }
  }
  return reason
}

三个细节(全部来自源码实证)------定位用「采集点错误栈的第二行」作为锚点;eval 或辅助函数产生的无意义帧用正则剥离;getOuterStack() 由调用方注入(loader 的 Entry.getOuterStack 生成 at <baseUrl>#<id> 标记帧,hmr 传空数组主动隐藏 HMR 内部帧)。composeError(callback, getOuterStack) 则同步/异步两态统一:同步抛错、Promise 拒绝,都会被套上这一层"案发还原"。

ts 复制代码
// fiber.ts 里每个 disposer 都被 composeError 包裹:
await Promise.all(this._disposables.clear().map((dispose) => {
  return composeError(() => dispose(), () => this.runner.getOuterStack())()
}))

如此,任何卸载阶段抛出的异常,都会带着"谁在卸载、从哪里开始卸载"的栈迹浮出水面。

5.3 具体例子:loader 的 baseUrl 标记帧

Entry.getOuterStack 沿 entry 链逐层产出形如 at <baseUrl>#<id> 的帧。当 cordis.ymlagent-spine 的插件初始化抛错,日志里的栈会呈现:

复制代码
Error: persona 模板解析失败
    at DemoPlugin.apply (.../agent-spine/demo.ts:42:3)
    at <file:///path/to/deepseek-harness/examples/jsonrpc-agent/cordis.yml>#agent-spine
    at <file:///path/to/deepseek-harness/examples/jsonrpc-agent/cordis.yml>#<root>

最后一个帧就是 getOuterStack 注入的------错误从代码世界映射回了配置文件 ,查错时你的视线会直接被牵到 cordis.yml 的哪一行。
图 2-5 · 长栈拼接流水线:composeError 的五个步骤------采集锚点帧、定位内层位置、校准偏移、剥离匿名帧、拼接外层栈。每一步都在消除"噪音帧"、注入"信息帧"。
#mermaid-svg-c01BDfvDu5jaFkBy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-c01BDfvDu5jaFkBy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-c01BDfvDu5jaFkBy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-c01BDfvDu5jaFkBy .error-icon{fill:#552222;}#mermaid-svg-c01BDfvDu5jaFkBy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-c01BDfvDu5jaFkBy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-c01BDfvDu5jaFkBy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-c01BDfvDu5jaFkBy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-c01BDfvDu5jaFkBy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-c01BDfvDu5jaFkBy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-c01BDfvDu5jaFkBy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-c01BDfvDu5jaFkBy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-c01BDfvDu5jaFkBy .marker.cross{stroke:#333333;}#mermaid-svg-c01BDfvDu5jaFkBy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-c01BDfvDu5jaFkBy p{margin:0;}#mermaid-svg-c01BDfvDu5jaFkBy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-c01BDfvDu5jaFkBy .cluster-label text{fill:#333;}#mermaid-svg-c01BDfvDu5jaFkBy .cluster-label span{color:#333;}#mermaid-svg-c01BDfvDu5jaFkBy .cluster-label span p{background-color:transparent;}#mermaid-svg-c01BDfvDu5jaFkBy .label text,#mermaid-svg-c01BDfvDu5jaFkBy span{fill:#333;color:#333;}#mermaid-svg-c01BDfvDu5jaFkBy .node rect,#mermaid-svg-c01BDfvDu5jaFkBy .node circle,#mermaid-svg-c01BDfvDu5jaFkBy .node ellipse,#mermaid-svg-c01BDfvDu5jaFkBy .node polygon,#mermaid-svg-c01BDfvDu5jaFkBy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-c01BDfvDu5jaFkBy .rough-node .label text,#mermaid-svg-c01BDfvDu5jaFkBy .node .label text,#mermaid-svg-c01BDfvDu5jaFkBy .image-shape .label,#mermaid-svg-c01BDfvDu5jaFkBy .icon-shape .label{text-anchor:middle;}#mermaid-svg-c01BDfvDu5jaFkBy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-c01BDfvDu5jaFkBy .rough-node .label,#mermaid-svg-c01BDfvDu5jaFkBy .node .label,#mermaid-svg-c01BDfvDu5jaFkBy .image-shape .label,#mermaid-svg-c01BDfvDu5jaFkBy .icon-shape .label{text-align:center;}#mermaid-svg-c01BDfvDu5jaFkBy .node.clickable{cursor:pointer;}#mermaid-svg-c01BDfvDu5jaFkBy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-c01BDfvDu5jaFkBy .arrowheadPath{fill:#333333;}#mermaid-svg-c01BDfvDu5jaFkBy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-c01BDfvDu5jaFkBy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-c01BDfvDu5jaFkBy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-c01BDfvDu5jaFkBy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-c01BDfvDu5jaFkBy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-c01BDfvDu5jaFkBy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-c01BDfvDu5jaFkBy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-c01BDfvDu5jaFkBy .cluster text{fill:#333;}#mermaid-svg-c01BDfvDu5jaFkBy .cluster span{color:#333;}#mermaid-svg-c01BDfvDu5jaFkBy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-c01BDfvDu5jaFkBy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-c01BDfvDu5jaFkBy rect.text{fill:none;stroke-width:0;}#mermaid-svg-c01BDfvDu5jaFkBy .icon-shape,#mermaid-svg-c01BDfvDu5jaFkBy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-c01BDfvDu5jaFkBy .icon-shape p,#mermaid-svg-c01BDfvDu5jaFkBy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-c01BDfvDu5jaFkBy .icon-shape .label rect,#mermaid-svg-c01BDfvDu5jaFkBy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-c01BDfvDu5jaFkBy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-c01BDfvDu5jaFkBy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-c01BDfvDu5jaFkBy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否

reason 错误对象
是 Error?
包装为 Error
以采集点栈第 2 行为锚点

在内层栈中定位 index
index 校准偏移

(eval/辅助函数帧)
剥离 /at.*()/ 帧
拼接 getOuterStack() 注入的

baseUrl#id 标记帧
回写 reason.stack

六分 · 配置声明与加载管道:双向的桥

6.1 原理:声明式配置与差量调和

配置系统是 Cordis "空间可组合性"的声明面:组件不再被命令式地"按顺序安装",而是被声明式地"按图装配"。这一思想的源头可追溯至纯函数式部署模型:

The Purely Functional Software Deployment Model(Eelco Dolstra,Nix 博士论文)--- 提出用纯函数式、可复现、可回滚的部署模型替代命令式安装,配置即求值,系统状态可从声明完全重建。

而"改声明 → 最小化应用差异"正是增量计算(self-adjusting computation)的经典问题:

Adaptive Functional Programming(Umut A. Acar, Guy E. Blelloch, Robert Harper)--- 建立自适应函数式编程:程序自动追踪依赖并增量重算变化部分,无需全量重跑------Cordis 的 group.update 差量调和正是这一思想的运行时实践。

6.2 实现解剖:include + loader/config/*

!!js 表达式节点include 用 js-yaml 注册自定义类型 ! js(YAML tag tag:yaml.org,2002:js):标量被构造为 { __jsExpr: 'process.env.X ?? "default"' } 形状的节点;求值函数(loader/config/utils.ts)是:

ts 复制代码
export const evaluate = new Function('ctx', 'expr',
  'with (ctx) { return eval(expr) }')

interpolate(ctx, value) 递归展开:表达式节点求值,数组逐项、对象逐值。注意 with (ctx) 的语义边界------表达式环境是当前插件的 ctx ,所以 process.envctx.timer 都可访问;代价是配置文件必须来自可信来源(本仓库的定位是"被信任的部署环境",代码注释亦如此声明,这是需要如实陈述的取舍)。

读-补丁-写三段Include 装载时:读文件(YAML schema 或 JSON 或 import)→ applyPatches 应用 overlay → root.update(data)。写回时 _writeFile 先写 .tmprename------原子写 保证半途断电不会留下半个配置;writeFile 还会被 setTimeout 0 去抖,合并同批改动。

group.update:oldMap/newMap 差量调和loader/config/group.ts):

ts 复制代码
async update(config) {
  const oldMap = new Map(this.data.map((item) => [item.id, item]))
  const newMap = new Map(config.map((item) => [item.id, item]))
  for (const item of config) {
    if (!oldMap.has(item.id)) this.create(item)   // 新增
  }
  for (const item of this.data) {
    if (!newMap.has(item.id)) this.remove(item.id)  // 消失即移除
  }
  ...Promise.all(其余以列表形式做整体更新)
}

这段代码是"声明式组合"的心脏:新增补上、消失拆掉、重叠微调 ,从不重建整棵树。Entry.update 更细一层:用 deepEqual 对比新旧配置,无变化直接返回(幂等),有变化才 fiber.update(config)restart()

isolate 隔离域loader/config/isolate.ts)。条目可声明 interceptisolateisolate[serviceName]true 时用 LocalRealmSymbol('name#' + id),域随条目 id 唯一),为字符串时用 GlobalRealmSymbol('name@' + label),多组共享)。七步 patch 流程的核心是 swap(target, source):整体替换代理对象原型、迁移 isolate 符号、notify 隔离域专属的服务(.filter 限定 fiber),让同名服务在同一进程里各自为政

patch 机制 (include 的 applyPatches)。- insert: [...] 定位目标 group 追加条目;- { id, name, config 覆盖 } 为 override------id 定位、name 校验冲突、其余字段合并进原条目。补丁文件的 config 依 6.1 的求值规则在目标激活时求值。

6.3 具体例子:web-schedule overlay 的执行路径

  1. include 读入 web-schedule/cordis.yml,得到 [{ insert: [时间上下文与调度条目] }]
  2. applyPatches 遍历 patch:insert 未指定 group → 插到根 group 数据尾部;
  3. root.updategroup.update 的 oldMap/newMap 调和发现两个新 id → create 两条 Entry;
  4. 各自 Entry._init()importctx.plugin → Fiber ACTIVE,被调度插件 injectctx.timer 恰好已就绪,立即启动。

另一个角落的例子是 disabled 祖先传播Entry.disabled getter 沿 parent 链逐层检查------父组 disabled,子孙全部静默跳过装载;取消父组 disabled,子孙的状态会按自己的 disabled 重新裁决。声明的一个字节,字面意义上控制着一整棵子树
图 2-6 · 配置管道:声明 → 运行的双向流 :上行为启动装配(文件 → 条目 → Fiber),下行为运行期回写(插件改配置 → 文件)。!!js 求值发生在插件上下文激活之前,是连接"文件世界"与"运行时世界"的闸门。
渲染错误: Mermaid 渲染失败: Lexical error on line 19. Unrecognized text. ... --> AP FB -.ctx.fiber.update.-> CF ----------------------^



七分 · 热模块替换(hmr):把时间组合性变成开发体验

7.1 原理:运行中的软件更新

HMR 的本质是"动态软件更新"(Dynamic Software Updating, DSU)------在进程不重启的前提下替换运行中的代码,是一个有三十余年历史的研究领域:

Dynamic Software Updating(Michael Hicks, Jonathan T. Moore, Scott Nettles)--- 早期 DSU 系统(PLDI 2001):运行中替换二进制,通过版本化状态与转换函数保持语义一致,为"不停机打补丁"奠定理论框架。

Cordis 把 DSU 的目标放回组件语义:换代码 = 旧 Fiber 回滚 + 新 Fiber 装载 ,而回滚/装载的正确性早已由可逆效果系统保证。于是 HMR 唯一要解决的是"改动了什么、哪些插件必须重启"------这是一张模块依赖图上的可达性问题;其"局部重算、最小重建"的思路与 React Fiber 的增量调度同源:

React Fiber Architecture(Dan Abramov)--- 解释 React 的 Fiber 调度:把渲染拆成可中断、可恢复、可优先级的增量单元------Cordis 的 Fiber 命名与"可中断的装配"精神一脉相承。

7.2 实现解剖:hmr/src/index.ts(analyzeChanges / partialReload)

三通道分流 。文件事件 change 到达后按以下顺序裁决:

ts 复制代码
watcher.on('change', (path) => {
  if (externals.has(path)) return this.loader.exit()      // 通道①:外部依赖 → 全量重启
  if (this.loader.loadCache.has(url)) {                    // 通道②:受管 ESM 模块 → 增量重载
    stashed.push(url)
    this.partialReload()                                   // 去抖合并
  } else if (this.loader.entries().some((e) => e.name === relative)) {
    this.loader.entries().find(...).update()               // 通道③:include 配置文件 → diff 更新
  } else {
    this.ctx.emit('hmr/change', path)                      // 其余 → 广播给外部观察者
  }
})

analyzeChanges:accepted/declined 不动点。对 stashed 变更集合做 BFS 反向可达分析,循环直到集合不再增长:

ts 复制代码
let changed = true
while (changed) {
  changed = false
  for (const key of pending) {
    for (const [depUrl, parents] of dependencies) {   // 依赖图反向边
      if (!parents.has(key)) continue
      if (accepted.has(depUrl)) continue
      if (declined.has(depUrl)) continue
      if (allDescendantsDeclined(depUrl)) {
        declined.add(depUrl)          // 整棵子树都死了 → 标记 declined
      } else {
        accepted.add(depUrl)          // 有后代要重载 → 标记 accepted
      }
      pending.push(depUrl)
      changed = true
    }
  }
}

语义:accepted = 必须重载的集合 (变更本身及受影响的父模块),declined = 禁止单独重载、需要整进程重启的边界 (例如 node_modules 或依赖图顶端未归属任何插件的模块)。两个集合皆从 stashed 起点向外扩张、直至不动点收敛。

partialReload:双缓存清理 + 失败回滚。载入新模块前,必须先让 Node 忘记旧模块:

ts 复制代码
// ESM 侧:Loader.loadCache 是内部 Map,直接调用 Map 原语清表
this.loader.loadCache.delete(key)   
require.cache[key]?.delete()           // CJS 侧:require.cache 同步清理

清理后重新 importunwrapExports,对每个受影响插件 registry.delete(old) + 用相同 config 重注册。整个流程被 try/catch 包裹:任何一步失败,就用先前备份的 cache 快照(esmBackup/cjsBackup)恢复并重注册旧插件------热更失败不是灾难,而是回滚(时间可组合性在开发期的第二次兑现)。

7.3 具体例子:改了一个被多处引用的工具文件

假设 packages/tools/src/misc.ts 被插件 agent-spinetool-fs 同时 import:

  1. 文件保存 → watcher 触发 changemisc.ts 在 loadCache 中 → 进入 stashed,partialReload 去抖启动;
  2. analyzeChanges:stashed={misc.ts} → BFS 反向边找到两个直接 import 方 → 标记 accepted 并加入 pending;继续上溯它们的父模块(若有)直至收敛;落入 node_modulesnode: 的边被 loadDependencies 的排除规则跳过,绝不越界;
  3. 清理 misc.ts 及整条 accepted 子树的缓存 → 重新 import → agent-spinetool-fs 各自的旧 Fiber dispose、新 Fiber 装载(配置保持不变,epoch 重算);
  4. 若任一插件在装载期抛错 → 快照回滚,两个插件恢复旧版本,日志中留下完整的长栈(第五分)。
  5. include 文件呢?它走通道③的 entry.update------配置 diff 极小,DSU 在这里表现为"只重启配置变了的插件"。

图 2-7 · HMR 三通道决策:一次文件变化按"外部依赖 / 受管模块 / 配置文件"三个通道分流。通道②的精细度最高,也最常被踩到。
#mermaid-svg-n4bHYWywmtQah9XF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-n4bHYWywmtQah9XF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-n4bHYWywmtQah9XF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-n4bHYWywmtQah9XF .error-icon{fill:#552222;}#mermaid-svg-n4bHYWywmtQah9XF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-n4bHYWywmtQah9XF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-n4bHYWywmtQah9XF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-n4bHYWywmtQah9XF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-n4bHYWywmtQah9XF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-n4bHYWywmtQah9XF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-n4bHYWywmtQah9XF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-n4bHYWywmtQah9XF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-n4bHYWywmtQah9XF .marker.cross{stroke:#333333;}#mermaid-svg-n4bHYWywmtQah9XF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-n4bHYWywmtQah9XF p{margin:0;}#mermaid-svg-n4bHYWywmtQah9XF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-n4bHYWywmtQah9XF .cluster-label text{fill:#333;}#mermaid-svg-n4bHYWywmtQah9XF .cluster-label span{color:#333;}#mermaid-svg-n4bHYWywmtQah9XF .cluster-label span p{background-color:transparent;}#mermaid-svg-n4bHYWywmtQah9XF .label text,#mermaid-svg-n4bHYWywmtQah9XF span{fill:#333;color:#333;}#mermaid-svg-n4bHYWywmtQah9XF .node rect,#mermaid-svg-n4bHYWywmtQah9XF .node circle,#mermaid-svg-n4bHYWywmtQah9XF .node ellipse,#mermaid-svg-n4bHYWywmtQah9XF .node polygon,#mermaid-svg-n4bHYWywmtQah9XF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-n4bHYWywmtQah9XF .rough-node .label text,#mermaid-svg-n4bHYWywmtQah9XF .node .label text,#mermaid-svg-n4bHYWywmtQah9XF .image-shape .label,#mermaid-svg-n4bHYWywmtQah9XF .icon-shape .label{text-anchor:middle;}#mermaid-svg-n4bHYWywmtQah9XF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-n4bHYWywmtQah9XF .rough-node .label,#mermaid-svg-n4bHYWywmtQah9XF .node .label,#mermaid-svg-n4bHYWywmtQah9XF .image-shape .label,#mermaid-svg-n4bHYWywmtQah9XF .icon-shape .label{text-align:center;}#mermaid-svg-n4bHYWywmtQah9XF .node.clickable{cursor:pointer;}#mermaid-svg-n4bHYWywmtQah9XF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-n4bHYWywmtQah9XF .arrowheadPath{fill:#333333;}#mermaid-svg-n4bHYWywmtQah9XF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-n4bHYWywmtQah9XF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-n4bHYWywmtQah9XF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-n4bHYWywmtQah9XF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-n4bHYWywmtQah9XF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-n4bHYWywmtQah9XF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-n4bHYWywmtQah9XF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-n4bHYWywmtQah9XF .cluster text{fill:#333;}#mermaid-svg-n4bHYWywmtQah9XF .cluster span{color:#333;}#mermaid-svg-n4bHYWywmtQah9XF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-n4bHYWywmtQah9XF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-n4bHYWywmtQah9XF rect.text{fill:none;stroke-width:0;}#mermaid-svg-n4bHYWywmtQah9XF .icon-shape,#mermaid-svg-n4bHYWywmtQah9XF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-n4bHYWywmtQah9XF .icon-shape p,#mermaid-svg-n4bHYWywmtQah9XF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-n4bHYWywmtQah9XF .icon-shape .label rect,#mermaid-svg-n4bHYWywmtQah9XF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-n4bHYWywmtQah9XF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-n4bHYWywmtQah9XF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-n4bHYWywmtQah9XF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是





文件 change 事件
在 externals 集合?
loader.exit()

整进程重启
在 loadCache?
stashed 入队

partialReload 去抖合并
匹配 include 配置路径?
entry.update()

配置 diff 更新
emit hmr/change

广播给观察者
图 2-8 · accepted/declined 不动点算法:从 stashed 起点做反向 BFS,标记必须重载(accepted)与必须重启的边界(declined),循环至集合不再增长。这是 HMR 正确性的枢纽。
#mermaid-svg-Gxv2iEKBWzM4fCKP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Gxv2iEKBWzM4fCKP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Gxv2iEKBWzM4fCKP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Gxv2iEKBWzM4fCKP .error-icon{fill:#552222;}#mermaid-svg-Gxv2iEKBWzM4fCKP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Gxv2iEKBWzM4fCKP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Gxv2iEKBWzM4fCKP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Gxv2iEKBWzM4fCKP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Gxv2iEKBWzM4fCKP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Gxv2iEKBWzM4fCKP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Gxv2iEKBWzM4fCKP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Gxv2iEKBWzM4fCKP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Gxv2iEKBWzM4fCKP .marker.cross{stroke:#333333;}#mermaid-svg-Gxv2iEKBWzM4fCKP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Gxv2iEKBWzM4fCKP p{margin:0;}#mermaid-svg-Gxv2iEKBWzM4fCKP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Gxv2iEKBWzM4fCKP .cluster-label text{fill:#333;}#mermaid-svg-Gxv2iEKBWzM4fCKP .cluster-label span{color:#333;}#mermaid-svg-Gxv2iEKBWzM4fCKP .cluster-label span p{background-color:transparent;}#mermaid-svg-Gxv2iEKBWzM4fCKP .label text,#mermaid-svg-Gxv2iEKBWzM4fCKP span{fill:#333;color:#333;}#mermaid-svg-Gxv2iEKBWzM4fCKP .node rect,#mermaid-svg-Gxv2iEKBWzM4fCKP .node circle,#mermaid-svg-Gxv2iEKBWzM4fCKP .node ellipse,#mermaid-svg-Gxv2iEKBWzM4fCKP .node polygon,#mermaid-svg-Gxv2iEKBWzM4fCKP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Gxv2iEKBWzM4fCKP .rough-node .label text,#mermaid-svg-Gxv2iEKBWzM4fCKP .node .label text,#mermaid-svg-Gxv2iEKBWzM4fCKP .image-shape .label,#mermaid-svg-Gxv2iEKBWzM4fCKP .icon-shape .label{text-anchor:middle;}#mermaid-svg-Gxv2iEKBWzM4fCKP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Gxv2iEKBWzM4fCKP .rough-node .label,#mermaid-svg-Gxv2iEKBWzM4fCKP .node .label,#mermaid-svg-Gxv2iEKBWzM4fCKP .image-shape .label,#mermaid-svg-Gxv2iEKBWzM4fCKP .icon-shape .label{text-align:center;}#mermaid-svg-Gxv2iEKBWzM4fCKP .node.clickable{cursor:pointer;}#mermaid-svg-Gxv2iEKBWzM4fCKP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Gxv2iEKBWzM4fCKP .arrowheadPath{fill:#333333;}#mermaid-svg-Gxv2iEKBWzM4fCKP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Gxv2iEKBWzM4fCKP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Gxv2iEKBWzM4fCKP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Gxv2iEKBWzM4fCKP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Gxv2iEKBWzM4fCKP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Gxv2iEKBWzM4fCKP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Gxv2iEKBWzM4fCKP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Gxv2iEKBWzM4fCKP .cluster text{fill:#333;}#mermaid-svg-Gxv2iEKBWzM4fCKP .cluster span{color:#333;}#mermaid-svg-Gxv2iEKBWzM4fCKP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Gxv2iEKBWzM4fCKP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Gxv2iEKBWzM4fCKP rect.text{fill:none;stroke-width:0;}#mermaid-svg-Gxv2iEKBWzM4fCKP .icon-shape,#mermaid-svg-Gxv2iEKBWzM4fCKP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Gxv2iEKBWzM4fCKP .icon-shape p,#mermaid-svg-Gxv2iEKBWzM4fCKP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Gxv2iEKBWzM4fCKP .icon-shape .label rect,#mermaid-svg-Gxv2iEKBWzM4fCKP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Gxv2iEKBWzM4fCKP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Gxv2iEKBWzM4fCKP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Gxv2iEKBWzM4fCKP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是




stashed 变更集合
pending 队列初始化
有未处理模块?
取出一模块,遍历其反向依赖边
父模块已被分类?
整棵后代子树皆 declined?
declined.add(父模块)
accepted.add(父模块)
pending.push(父模块)
收敛:accepted→重载集合

declined→重启边界


总述 · 收束:七把刀如何合成一把手术刀

七节看似独立,实则是一条连续的论证链:可逆效果(1)给出"卸载即回滚"的时间原语;响应式余效果(2)给出"声明即通知"的空间原语;Context 代理(3)把两个原语收进同一个可路由对象;事件分发(4)与长栈工程(5)让原语之上的业务与排错都结构清晰;配置调和(6)把空间组合性推向声明文件;热替换(7)再把时间组合性推向开发循环。越靠外的机制越新,但全部建立在第 1、2 节的两块基石上。

技术点 论文 核心文件 关键符号
1 可逆效果 Spatiotemporal Composability / Bauer & Pretnar fiber.ts _executeepochDisposableListinertia
2 响应式依赖 Coeffects reflect.ts providenotify_checkImplInject.resolve
3 Proxy 语义 Trustworthy Proxies reflect.ts + utils.ts handlergetTraceablecreateCallablemixin
4 事件分发 Deprecating the Observer Pattern events.ts 五种分发、isBailedinternal/update
5 长栈工程 Spatiotemporal Composability utils.ts composeErrorhandleErrorgetOuterStack
6 配置调和 Nix 部署模型 / Adaptive FP loader/config/* + include group.updateinterpolateapplyPatches、Realm
7 热替换 Dynamic Software Updating / React Fiber hmr/index.ts analyzeChangespartialReload、快照回滚

下一篇《深刻观》将跃出水面:七把刀背后的那把刀是什么?为什么这些机制凑在一起,值得一篇论文?

下一篇预告:可逆性是第一原则、context type 的优雅暴力、元框架的克制、"时空即组合"的坐标系。

相关推荐
烬羽1 小时前
求第 K 大,为什么反而要用最小堆?
javascript·数据结构·算法
没落之王2 小时前
易学使者郑氏正脉郑冰推演马航事件
大数据·人工智能·算法·机器学习·可用性测试
阿部多瑞 ABU2 小时前
告别手工核图:基于 .NET + MuPDFCore 的 CAD 等轴测 PDF 材料表提取与新旧版本对比实战
后端·算法·ui·pdf·c#
郝学胜-神的一滴2 小时前
并查集深度入门:从玄学抽象到 QuickFind & QuickUnion 源码实战
数据结构·c++·python·程序人生·算法·软件开发
比奇堡裤头村2 小时前
统计学习方法——决策树
算法·决策树·学习方法
灵晔君2 小时前
【Linux】进程控制(一)——进程创建、进程终止与进程等待
linux·c语言·算法
数模竞赛Paid answer11 小时前
2026年中青杯数学建模A题数学建模论文智能评估系统与多智能体优化方法求解全过程论文及程序
算法·数学建模·中青杯
银-豆豆12 小时前
数据结构与算法-动态规划、回溯与贪心
算法·动态规划
想吃火锅100513 小时前
【leetcode】200. 岛屿数量
算法·leetcode·职场和发展