事件系统
整棵上下文树共享一条事件总线,作用域化发生在 Context 层:通过上下文订阅的监听器,
在该上下文的作用域回收时一起摘除。对应实现:src/Cordis/Events.cs。
五种派发模式
事件名是字符串,派发方式由调用哪个方法决定。
| 方法 | 返回 | 派发方式 | 返回值处理 |
|---|---|---|---|
EmitAsync | ValueTask | 按注册顺序逐个 await | 忽略 |
ParallelAsync | ValueTask | 全部启动,然后一起等待 | 忽略 |
SerialAsync | ValueTask<object?> | 按注册顺序逐个 await | 遇首个命中值即中断并返回它 |
BailAsync | ValueTask<object?> | 按注册顺序逐个 await | 遇首个命中值即中断并返回它 |
WaterfallAsync<TResult> | ValueTask<TResult> | 监听器嵌套包裹 terminal | 由是否调用 next 决定 |
Serial 与 Bail 在当前实现里行为相同
两者的派发逻辑完全一致:顺序执行,取首个命中值。差别只在
BailAsync<TResult> 多提供一个把结果转成目标类型的重载。
不要依赖它们之间存在语义差异。
bail 判定
判定一个监听器返回值算不算“命中”的规则只有一条:既不是 null 也不是 false。这意味着监听器可以用 false 明确表达“我不处理,继续往下走”,与“没有返回值”等价。
// 谁先给出非 null 且非 false 的结果,谁的结果被采用
var handler = await ctx.BailAsync<IToolHandler>("tool/resolve", name);
ctx.On<string, bool>("tool/resolve", name =>
{
return false; // 不处理,派发继续
});
Waterfall:监听器包在 terminal 外层
waterfall 不是把值依次传递下去,而是把监听器套在一个 terminal 委托的外层,形成洋葱结构。每个监听器拿到原始事件参数和一个续延 next,只有显式调用它才会进入后续监听器和 terminal。
// terminal 是内置行为,监听器可以在它前后加工,也可以完全拦截
var result = await ctx.WaterfallAsync(
"request/send",
() => SendAsync(request),
request);
// 加工型:调用 next 让链条继续
ctx.OnWaterfall<Request, Response>("request/send", async (req, next) =>
{
var response = await next();
response.Headers.Add("x-traced", "1");
return response;
});
// 拦截型:不调用 next,后续监听器与 terminal 都不会执行
ctx.OnWaterfall<Request, Response>("request/send", (req, next) =>
ValueTask.FromResult(Cached[req.Key]));
省略 next 会短路整条剩余链路。反过来,terminal 或内层监听器抛出的异常会先经过外层监听器,让它们有机会处理;未被处理的异常最终向 WaterfallAsync 调用方传播。
订阅与作用域
通过 ctx.On(...) 订阅的监听器自动绑定当前作用域,作用域回收时摘除;返回的 IDisposable 是提前退订的手段,不是必须调用的清理。
On有无参、单参、双参,以及产生结果供 serial / bail 使用的多种重载。OnWaterfall注册环绕式监听器,最多接收两个事件参数外加续延。Once注册一次性监听器,在首次调用前就被摘除;并发 dispatch 下以原子认领保证至多调用一次,输掉认领的 dispatch 视同该 listener 不存在。prepend: true把监听器插到同名监听器之前,用来抢先处理。Off(name, listener)按委托实例移除;HasListeners(name)查询是否有人在听。
绕过作用域的订阅要自己负责
直接用 ctx.Events.On(...) 订阅不会绑定作用域,等于放弃了自动回收。
插件里应当一律走 ctx.On(...)。
异常传播与 contained 通知
固定 rc.8 的普通派发契约是:EmitAsync、SerialAsync、BailAsync 与
WaterfallAsync 的 listener 异常直接使整次 dispatch 失败,顺序模式不再运行后续 listener;
ParallelAsync 等待全部 listener settled 后聚合所有失败。外层 waterfall listener 仍可显式
捕获下游异常并返回替代值。
普通派发不会把 listener failure 隐式改写成错误通知:调用方若需要事务语义,就 await 对应 dispatch 并处理失败;需要通知语义时,必须显式选择下面的 contained 入口。
EmitContained 是独立的 Tether fire-and-forget 扩展:它启动 listener 而不等待完成,异步失败
尽量路由到 internal/error;错误事件自身的失败不会递归路由。调用方必须显式选择 contained,
普通 dispatch 不能隐式切换到这条路径。
内置事件名
ready、dispose、fork、service 和
internal/error 的触发时机与参数列在
插件与 Context 的生命周期事件一节。