Skip to content
My Blog
Go back

第 03 课 · 五种事件分发模式:协作的语法

前置:第 02 课 | 预计时长:45–60 分钟 | 动手环节:必须完成

你将学会

问题引入

第 02 课解决了「谁先激活」。这一课解决「事件发生时,多个监听器如何协作」。 dsh 的事件不是一种泛型广播,而是五种语义——同一个 on() 注册,按事件名声明的模式被调度。选错模式的后果从「副作用顺序错乱」到「整条链被意外否决」不等,所以这是写插件的语法课。

正文

3.1 统一的短路判定

一切从 isBailed 开始(vendor/cordis/src/events.ts:13-15):

function isBailed(value) {
  return value !== null && value !== false && value !== undefined
}

返回值非 null / false / undefined 即视为「短路」。serial 与 bail 模式用它决定停不停。这个闭集要背下来:返回 0'' 都算短路,返回 null 不算。

3.2 五种模式语义总表

所有模式经统一 dispatch()events.ts:165-175),支持 thisArg 与上下文过滤。语义差异只在调度:

模式调度返回值处理典型用途
emit同步循环,不 await忽略纯通知:session/eventturn/start
parallelPromise.allSettled 并发任一拒绝 → AggregateError互不依赖的副作用
serial逐个 await遇到 bail 值即停,返回该值顺序敏感、可否决的异步决策
bail同步逐个同 serial同步版本的可否决决策
waterfall按注册序由外向内包装末参 next,链式委托拦截 / 改写 / 否决(around 中间件)

对应的伪代码(events.ts:194-243,节选):

emit(name, ...args) {
  for (listener of dispatch('emit', args)) listener(...args)
  // 陷阱:某监听器同步抛错会饿死后续监听器
}

async serial(name, ...args) {
  for (listener of listeners) {
    value = await listener(...args)
    if (isBailed(value)) return value     // 停止,返回该值
  }
}

3.3 waterfall:由外向内的中间件链

这是 dsh 最重要的模式。末位参数被替换为最内层的 next,监听器按注册序从外向内逐层包裹真正的实现(events.ts:234-243):

waterfall(...args) {
  cbs = dispatch('waterfall', args)   // 注册的监听器,按序
  inner = args.pop()                  // 最内层:事件发起方给的实现
  next = () => {
    cb = cbs.shift() ?? inner         // 取下一环;没有监听器了就落到实现
    return cb(...args)
  }
  args.push(next)                     // next 作为末位参数传给外层
  return next()
}

仓库级强制约定(根 AGENTS.md):waterfall 监听器必须调用 next() 来委托;不调用即短路否决整条链。 两个典型用法:

// 1) 包裹校验:调 next() 并检查其结果(packages/llm/llm/src/invariant.ts:88)
ctx.on('llm/stream', (_options, next) => validateStream(next(), fail),
       { global: true, prepend: true })   // prepend=插到队首(最外层)

// 2) 否决/改写:不调 next(),直接返回自己的决策
//    如 tools/pre-execute 返回 {kind:'deny'},见第 09 课

3.4 注册选项

3.5 事件词表靠声明合并扩展

dsh 的事件名都有类型(如 SessionEventMap、LLM 事件),通过 TypeScript declaration merging 扩展(第 02 课的 Proxy 访问同理)。仓库约定 JSDoc 必须标 @mode(用了五种中的哪种)与 payload @param。语义总表的权威是 docs/cordis-primer.md:19-25,waterfall 细则在 :31

动手环节

  1. 五模式实验scratch/lesson03.ts):对同一个事件名分别用五种模式各挂 3 个监听器(其中 serial/bail 用例里让第 2 个返回真值;waterfall 用例里让第 2 个不调 next()),运行并记录每种模式的输出顺序与短路行为。
  2. 找真实用法:在仓库里 grep -rn "prepend: true" packages/ | head,解释每个命中处为什么需要插队。
  3. 找否决用法:读 packages/core/tools/src/index.ts:1474 附近的 tools/pre-execute 瀑布,找出「不调 next()」的监听器长什么样(第 09 课会展开)。

自检清单

常见误解

延伸阅读


Share this post:

Previous Post
声明即授权:dsh Web 客户端的 slot 体系
Next Post
压缩自己也进日志:dsh 的 Compaction 设计